sync public allowlist from private myoffice
This commit is contained in:
@@ -1,335 +0,0 @@
|
||||
namespace MyOffice.DbContext;
|
||||
|
||||
using Data.Models.Accounts;
|
||||
using Data.Models.Currencies;
|
||||
using Data.Models.Items;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Data.Models.Users;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using MyOffice.Data.Models.Verifications;
|
||||
using MyOffice.Data.Models.Notifications;
|
||||
|
||||
public enum AppDbContextProvidersEnum
|
||||
{
|
||||
sqlite,
|
||||
mssql,
|
||||
npgsql
|
||||
}
|
||||
|
||||
public class AppDbContext : DbContext
|
||||
{
|
||||
private readonly AppDbContextProvidersEnum _provider;
|
||||
|
||||
private readonly Dictionary<AppDbContextProvidersEnum, string> _noCaseCollation = new()
|
||||
{
|
||||
{ AppDbContextProvidersEnum.sqlite, "NOCASE" },
|
||||
{ AppDbContextProvidersEnum.npgsql, "my_ci_collation" }
|
||||
};
|
||||
|
||||
public AppDbContext(AppDbContextProvidersEnum provider, DbContextOptions<AppDbContext> options) : base(options)
|
||||
{
|
||||
_provider = provider;
|
||||
ConfigureChangeTracker();
|
||||
}
|
||||
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options, ConnectionConfiguration connection) : base(options)
|
||||
{
|
||||
_provider = Enum.Parse<AppDbContextProvidersEnum>(connection.Provider, ignoreCase: true);
|
||||
ConfigureChangeTracker();
|
||||
}
|
||||
|
||||
private void ConfigureChangeTracker()
|
||||
{
|
||||
// Match historical repository behavior (manual EntityState updates).
|
||||
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
ChangeTracker.AutoDetectChangesEnabled = false;
|
||||
}
|
||||
|
||||
public DbSet<User> Users { get; set; } = null!;
|
||||
public DbSet<UserExternal> UserClaims { get; set; } = null!;
|
||||
|
||||
public DbSet<CurrencyGlobal> CurrencyGlobals { get; set; } = null!;
|
||||
public DbSet<Currency> Currencies { get; set; } = null!;
|
||||
public DbSet<CurrencyRate> CurrencyRates { get; set; } = null!;
|
||||
public DbSet<AccountCategory> AccountCategories { get; set; } = null!;
|
||||
public DbSet<Account> Accounts { get; set; } = null!;
|
||||
public DbSet<AccountAccountCategory> AccountAccountCategories { get; set; } = null!;
|
||||
public DbSet<AccountAccess> AccountAccesses { get; set; } = null!;
|
||||
public DbSet<AccountAccessInvite> AccountAccessInvites { get; set; } = null!;
|
||||
|
||||
public DbSet<ItemCategory> ItemCategories { get; set; } = null!;
|
||||
public DbSet<ItemGlobal> ItemGlobals { get; set; } = null!;
|
||||
public DbSet<Item> Items { get; set; } = null!;
|
||||
public DbSet<Motion> Motions { get; set; } = null!;
|
||||
|
||||
public DbSet<VerificationCode> Verifications { get; set; } = null!;
|
||||
public DbSet<EmailTemplate> EmailTemplates { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
var noCaseCollation = _noCaseCollation[_provider];
|
||||
|
||||
if (_provider == AppDbContextProvidersEnum.npgsql)
|
||||
{
|
||||
modelBuilder.HasCollation("my_ci_collation", "en-u-ks-primary", "icu", false);
|
||||
}
|
||||
|
||||
/* NOCASE PROPERTIES */
|
||||
modelBuilder.Entity<User>()
|
||||
.Property(x => x.UserName)
|
||||
.UseCollation(noCaseCollation);
|
||||
|
||||
modelBuilder.Entity<User>()
|
||||
.Property(x => x.Email)
|
||||
.UseCollation(noCaseCollation);
|
||||
|
||||
modelBuilder.Entity<UserExternal>()
|
||||
.Property(x => x.Provider)
|
||||
.UseCollation(noCaseCollation);
|
||||
|
||||
modelBuilder.Entity<UserExternal>()
|
||||
.Property(x => x.Email)
|
||||
.UseCollation(noCaseCollation);
|
||||
|
||||
|
||||
modelBuilder.Entity<UserExternal>()
|
||||
.HasOne(x => x.User)
|
||||
.WithMany(x => x.UserClaims)
|
||||
.HasForeignKey(x => x.UserId);
|
||||
|
||||
UserCreating(modelBuilder);
|
||||
CurrencyCreating(modelBuilder);
|
||||
AccountCreating(modelBuilder);
|
||||
MotionsCreating(modelBuilder);
|
||||
VerificationsCreating(modelBuilder);
|
||||
EmailTemplateCreating(modelBuilder);
|
||||
|
||||
modelBuilder.UseOpenIddict();
|
||||
|
||||
base.OnModelCreating(modelBuilder);
|
||||
}
|
||||
|
||||
private void VerificationsCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<VerificationCode>()
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
modelBuilder.Entity<VerificationCode>()
|
||||
.HasOne(x => x.User)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.UserId);
|
||||
}
|
||||
|
||||
private void EmailTemplateCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<EmailTemplate>()
|
||||
.HasKey(x => x.Id);
|
||||
}
|
||||
|
||||
private void UserCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<User>()
|
||||
.HasOne(x => x.Currency)
|
||||
.WithMany()
|
||||
.HasForeignKey(x => x.CurrencyId);
|
||||
}
|
||||
|
||||
private void CurrencyCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<CurrencyGlobal>()
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
modelBuilder.Entity<Currency>()
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
modelBuilder.Entity<CurrencyRate>()
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
modelBuilder.Entity<Currency>()
|
||||
.HasOne(x => x.CurrencyGlobal)
|
||||
.WithMany(x => x.Currencies)
|
||||
.HasForeignKey(x => x.CurrencyGlobalId);
|
||||
|
||||
modelBuilder.Entity<Currency>()
|
||||
.HasOne(x => x.User)
|
||||
.WithMany(x => x.Currencies)
|
||||
.HasForeignKey(x => x.UserId);
|
||||
|
||||
modelBuilder.Entity<CurrencyRate>()
|
||||
.HasOne(x => x.Currency)
|
||||
.WithMany(x => x.Rates)
|
||||
.HasForeignKey(x => x.CurrencyId);
|
||||
|
||||
modelBuilder.Entity<Currency>()
|
||||
.HasOne(x => x.CurrentRate)
|
||||
.WithMany(x => x.Currencies)
|
||||
.HasForeignKey(x => x.CurrentRateId);
|
||||
}
|
||||
|
||||
private void AccountCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
#region Account
|
||||
|
||||
modelBuilder.Entity<Account>()
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
modelBuilder.Entity<Account>()
|
||||
.HasOne(x => x.CurrencyGlobal)
|
||||
.WithMany(x => x.Accounts)
|
||||
.HasForeignKey(x => x.CurrencyGlobalId);
|
||||
|
||||
modelBuilder.Entity<Account>()
|
||||
.HasOne(x => x.Owner)
|
||||
.WithMany(x => x.Accounts)
|
||||
.HasForeignKey(x => x.OwnerId);
|
||||
|
||||
#endregion Account
|
||||
|
||||
#region AccountAccess
|
||||
|
||||
modelBuilder.Entity<AccountAccess>()
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
modelBuilder.Entity<AccountAccess>()
|
||||
.HasOne(x => x.Account)
|
||||
.WithMany(x => x.AccessRights)
|
||||
.HasForeignKey(x => x.AccountId);
|
||||
|
||||
modelBuilder.Entity<AccountAccess>()
|
||||
.HasOne(x => x.User)
|
||||
.WithMany(x => x.AccountAccess)
|
||||
.HasForeignKey(x => x.UserId);
|
||||
|
||||
modelBuilder.Entity<AccountAccess>()
|
||||
.HasOne(x => x.Owner)
|
||||
.WithMany(x => x.AccountAccessOwners)
|
||||
.HasForeignKey(x => x.OwnerId);
|
||||
|
||||
modelBuilder
|
||||
.Entity<AccountAccess>()
|
||||
.Property(d => d.Type)
|
||||
.HasConversion(new EnumToStringConverter<AccountAccessTypeEnum>());
|
||||
|
||||
modelBuilder.Entity<AccountAccess>()
|
||||
.HasIndex(p => new { p.AccountId, p.UserId })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<AccountAccess>()
|
||||
.HasIndex(p => new { p.AccountId, p.OwnerId })
|
||||
.IsUnique();
|
||||
|
||||
#endregion AccountAccess
|
||||
|
||||
#region AccountAccessInvite
|
||||
|
||||
modelBuilder.Entity<AccountAccessInvite>()
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
modelBuilder.Entity<AccountAccessInvite>()
|
||||
.HasOne(x => x.User)
|
||||
.WithMany(x => x.AccountAccessInvites)
|
||||
.HasForeignKey(x => x.UserId);
|
||||
|
||||
modelBuilder.Entity<AccountAccessInvite>()
|
||||
.HasOne(x => x.Account)
|
||||
.WithMany(x => x.Invites)
|
||||
.HasForeignKey(x => x.AccountId);
|
||||
|
||||
#endregion AccountAccessInvite
|
||||
|
||||
#region Motion
|
||||
|
||||
modelBuilder.Entity<Motion>()
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
modelBuilder.Entity<Motion>()
|
||||
.HasOne(x => x.Account)
|
||||
.WithMany(x => x.Motions)
|
||||
.HasForeignKey(x => x.AccountId);
|
||||
|
||||
#endregion Motion
|
||||
|
||||
#region AccountCategory
|
||||
|
||||
modelBuilder.Entity<AccountCategory>()
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
modelBuilder.Entity<AccountCategory>()
|
||||
.HasOne(x => x.User)
|
||||
.WithMany(x => x.AccountCategories)
|
||||
.HasForeignKey(x => x.UserId);
|
||||
|
||||
#endregion AccountCategory
|
||||
|
||||
#region AccountAccountCategory
|
||||
|
||||
modelBuilder.Entity<AccountAccountCategory>()
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
modelBuilder.Entity<AccountAccountCategory>()
|
||||
.HasOne(x => x.Account)
|
||||
.WithMany(x => x.Categories)
|
||||
.HasForeignKey(x => x.AccountId);
|
||||
|
||||
modelBuilder.Entity<AccountAccountCategory>()
|
||||
.HasOne(x => x.Category)
|
||||
.WithMany(x => x.Accounts)
|
||||
.HasForeignKey(x => x.CategoryId);
|
||||
|
||||
modelBuilder.Entity<AccountAccountCategory>()
|
||||
.HasIndex(p => new { p.AccountId, p.CategoryId })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<AccountAccountCategory>()
|
||||
.HasOne(x => x.Category)
|
||||
.WithMany(x => x.Accounts)
|
||||
.HasForeignKey(x => x.CategoryId);
|
||||
|
||||
#endregion AccountAccountCategory
|
||||
}
|
||||
|
||||
private void MotionsCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<ItemGlobal>()
|
||||
.HasKey(x => x.Id);
|
||||
modelBuilder.Entity<ItemCategory>()
|
||||
.HasKey(x => x.Id);
|
||||
modelBuilder.Entity<Item>()
|
||||
.HasKey(x => x.Id);
|
||||
modelBuilder.Entity<Motion>()
|
||||
.HasKey(x => x.Id);
|
||||
|
||||
modelBuilder.Entity<ItemCategory>()
|
||||
.HasOne(x => x.User)
|
||||
.WithMany(x => x.ItemCategories)
|
||||
.HasForeignKey(x => x.UserId);
|
||||
|
||||
modelBuilder.Entity<Item>()
|
||||
.HasOne(x => x.Category)
|
||||
.WithMany(x => x.Items)
|
||||
.HasForeignKey(x => x.CategoryId);
|
||||
|
||||
modelBuilder.Entity<Item>()
|
||||
.HasOne(x => x.ItemGlobal)
|
||||
.WithMany(x => x.Items)
|
||||
.HasForeignKey(x => x.ItemGlobalId);
|
||||
|
||||
modelBuilder.Entity<Motion>()
|
||||
.HasOne(x => x.Item)
|
||||
.WithMany(x => x.Motions)
|
||||
.HasForeignKey(x => x.ItemId);
|
||||
|
||||
modelBuilder.Entity<Motion>()
|
||||
.HasOne(x => x.Account)
|
||||
.WithMany(x => x.Motions)
|
||||
.HasForeignKey(x => x.AccountId);
|
||||
|
||||
modelBuilder.Entity<Motion>()
|
||||
.Property(x => x.AmountMinus)
|
||||
.HasPrecision(18, 6);
|
||||
|
||||
modelBuilder.Entity<Motion>()
|
||||
.Property(x => x.AmountPlus)
|
||||
.HasPrecision(18, 6);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
namespace MyOffice.DbContext;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
|
||||
{
|
||||
public static AppDbContextFactory Instance = new();
|
||||
|
||||
public AppDbContext CreateDbContext()
|
||||
{
|
||||
return CreateDbContext(null);
|
||||
}
|
||||
|
||||
public AppDbContext CreateDbContext(string[]? args)
|
||||
{
|
||||
var connection = RepositoryInitializer.ConnectionString;
|
||||
if (connection is null)
|
||||
{
|
||||
connection = new ConnectionConfiguration(
|
||||
args?[0] ?? "npgsql",
|
||||
args?[1] ?? string.Empty);
|
||||
}
|
||||
|
||||
var builder = new DbContextOptionsBuilder<AppDbContext>();
|
||||
DbContextServiceCollectionExtensions.ConfigureDbContextOptions(builder, connection);
|
||||
|
||||
if (!Enum.TryParse<AppDbContextProvidersEnum>(connection?.Provider ?? args?[0] ?? "", ignoreCase: true, out var providerEnum))
|
||||
throw new InvalidOperationException($"No such provider: [{connection?.Provider}]");
|
||||
|
||||
var db = new AppDbContext(providerEnum, builder.Options);
|
||||
db.ChangeTracker.AutoDetectChangesEnabled = false;
|
||||
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
|
||||
|
||||
return db;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
namespace MyOffice.DbContext;
|
||||
|
||||
using Data.Models.Currencies;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Migrate and seed using a DI-scoped <see cref="AppDbContext"/> (not the design-time factory).
|
||||
/// </summary>
|
||||
public static class DatabaseBootstrapper
|
||||
{
|
||||
public static async Task InitializeAsync(AppDbContext db, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await db.Database.MigrateAsync(cancellationToken);
|
||||
await PrefillAsync(db, cancellationToken);
|
||||
await DemoDataSeeder.SeedIfEmptyAsync(db, cancellationToken);
|
||||
await UpdateCurrentRatesAsync(db, cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task PrefillAsync(AppDbContext dbContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var predefinedCurrencyGlobals = new List<CurrencyGlobal>
|
||||
{
|
||||
new() { Id = CurrencyGlobalIdEnum.UAH.ToString(), DefaultQuantity = 1, Symbol = "₴", Name = "Ukrainian hryvnias" },
|
||||
new() { Id = CurrencyGlobalIdEnum.USD.ToString(), DefaultQuantity = 1, Symbol = "$", Name = "US Dollar" },
|
||||
new() { Id = CurrencyGlobalIdEnum.EUR.ToString(), DefaultQuantity = 1, Symbol = "€", Name = "Euros" },
|
||||
new() { Id = CurrencyGlobalIdEnum.GBP.ToString(), DefaultQuantity = 1, Symbol = "£", Name = "British pounds sterling" },
|
||||
new() { Id = CurrencyGlobalIdEnum.RUB.ToString(), DefaultQuantity = 10, Symbol = "₽", Name = "Russia Ruble" },
|
||||
new() { Id = CurrencyGlobalIdEnum.BTC.ToString(), DefaultQuantity = 10, Symbol = "btc", Name = "Bitcoin" },
|
||||
new() { Id = CurrencyGlobalIdEnum.ETH.ToString(), DefaultQuantity = 10, Symbol = "eth", Name = "Ethereum" },
|
||||
new() { Id = CurrencyGlobalIdEnum.TON.ToString(), DefaultQuantity = 10, Symbol = "ton", Name = "TON" },
|
||||
new() { Id = CurrencyGlobalIdEnum.OTHER.ToString(), DefaultQuantity = 1, Symbol = "", Name = "Other" },
|
||||
};
|
||||
|
||||
var currencyGlobals = await dbContext.CurrencyGlobals.ToListAsync(cancellationToken);
|
||||
foreach (var predefined in predefinedCurrencyGlobals)
|
||||
{
|
||||
if (currencyGlobals.All(x => x.Id != predefined.Id))
|
||||
{
|
||||
dbContext.CurrencyGlobals.Add(predefined);
|
||||
}
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static async Task UpdateCurrentRatesAsync(AppDbContext dbContext, CancellationToken cancellationToken)
|
||||
{
|
||||
var lastRates = await dbContext.CurrencyRates
|
||||
.Include(x => x.Currency)
|
||||
.GroupBy(x => new { x.CurrencyId },
|
||||
(key, g) => g.Where(x => x.DateTime <= DateTime.UtcNow).OrderByDescending(x => x.DateTime).First())
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var currencies = await dbContext.Currencies.ToListAsync(cancellationToken);
|
||||
foreach (var currency in currencies)
|
||||
{
|
||||
var rate = lastRates.FirstOrDefault(x => x.CurrencyId == currency.Id);
|
||||
if (rate != null)
|
||||
{
|
||||
currency.CurrentRateId = rate.Id;
|
||||
dbContext.Attach(currency).State = EntityState.Modified;
|
||||
}
|
||||
}
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
namespace MyOffice.DbContext;
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public static class DbContextServiceCollectionExtensions
|
||||
{
|
||||
public static IServiceCollection AddAppDbContext(
|
||||
this IServiceCollection services,
|
||||
ConnectionConfiguration connection
|
||||
)
|
||||
{
|
||||
services.AddSingleton(connection);
|
||||
|
||||
services.AddDbContext<AppDbContext>((_, options) =>
|
||||
{
|
||||
ConfigureDbContextOptions(options, connection);
|
||||
});
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
public static void ConfigureDbContextOptions(
|
||||
DbContextOptionsBuilder options,
|
||||
ConnectionConfiguration connection
|
||||
)
|
||||
{
|
||||
if (!Enum.TryParse<AppDbContextProvidersEnum>(connection.Provider, ignoreCase: true, out var provider))
|
||||
throw new InvalidOperationException($"No such provider: [{connection.Provider}]");
|
||||
|
||||
switch (provider)
|
||||
{
|
||||
case AppDbContextProvidersEnum.npgsql:
|
||||
options.UseNpgsql(connection.ConnectionString, x =>
|
||||
x.MigrationsAssembly("MyOffice.Migrations.Postgres"));
|
||||
break;
|
||||
case AppDbContextProvidersEnum.sqlite:
|
||||
options.UseSqlite(connection.ConnectionString, x =>
|
||||
x.MigrationsAssembly("MyOffice.Migrations.Sqlite"));
|
||||
break;
|
||||
default:
|
||||
throw new NotSupportedException($"No such provider: [{connection.Provider}]");
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
options.EnableSensitiveDataLogging();
|
||||
options.EnableDetailedErrors();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1,292 +0,0 @@
|
||||
namespace MyOffice.DbContext;
|
||||
|
||||
using Data.Models.Accounts;
|
||||
using Data.Models.Currencies;
|
||||
using Data.Models.Items;
|
||||
using Data.Models.Users;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
/// <summary>
|
||||
/// Fills an empty database with three demo users (UAH/USD/EUR) and sample catalog data.
|
||||
/// </summary>
|
||||
public static class DemoDataSeeder
|
||||
{
|
||||
private static readonly PasswordHasher<object> PasswordHasher = new();
|
||||
|
||||
private static readonly string[] CurrencyCodes = ["UAH", "USD", "EUR"];
|
||||
|
||||
private static readonly (string Code, string Email, string Password, string FirstName)[] Users =
|
||||
[
|
||||
("UAH", "user_UAH@user_UAH.userUAH", "user_UAH", "User UAH"),
|
||||
("USD", "user_USD@user_USD.userUSD", "user_USD", "User USD"),
|
||||
("EUR", "user_EUR@user_EUR.userEUR", "user_EUR", "User EUR"),
|
||||
];
|
||||
|
||||
/// <summary>1 USD = 42 UAH, 1 EUR = 50 UAH.</summary>
|
||||
private const decimal UsdInUah = 42m;
|
||||
private const decimal EurInUah = 50m;
|
||||
|
||||
public static async Task SeedIfEmptyAsync(AppDbContext db, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (await db.Users.AnyAsync(cancellationToken))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// AppDbContext defaults to NoTracking; seeding needs identity values and graph inserts.
|
||||
var previousTracking = db.ChangeTracker.QueryTrackingBehavior;
|
||||
var previousDetect = db.ChangeTracker.AutoDetectChangesEnabled;
|
||||
db.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.TrackAll;
|
||||
db.ChangeTracker.AutoDetectChangesEnabled = true;
|
||||
|
||||
try
|
||||
{
|
||||
var yearStart = new DateTime(DateTime.UtcNow.Year, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
var rng = new Random(2026);
|
||||
|
||||
foreach (var (code, email, password, firstName) in Users)
|
||||
{
|
||||
await SeedUserAsync(db, code, email, password, firstName, yearStart, rng, cancellationToken);
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
db.ChangeTracker.QueryTrackingBehavior = previousTracking;
|
||||
db.ChangeTracker.AutoDetectChangesEnabled = previousDetect;
|
||||
db.ChangeTracker.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SeedUserAsync(
|
||||
AppDbContext db,
|
||||
string homeCurrency,
|
||||
string email,
|
||||
string password,
|
||||
string firstName,
|
||||
DateTime rateDate,
|
||||
Random rng,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var ua = homeCurrency == "UAH";
|
||||
var userId = Guid.NewGuid();
|
||||
|
||||
var user = new User
|
||||
{
|
||||
Id = userId,
|
||||
UserName = email,
|
||||
Email = email,
|
||||
PasswordHash = PasswordHasher.HashPassword(new object(), password),
|
||||
FirstName = firstName,
|
||||
FullName = firstName,
|
||||
IsEmailConfirmed = true,
|
||||
CurrencyId = homeCurrency,
|
||||
};
|
||||
db.Users.Add(user);
|
||||
|
||||
var currencies = new Dictionary<string, Currency>(StringComparer.Ordinal);
|
||||
foreach (var code in CurrencyCodes)
|
||||
{
|
||||
var currency = new Currency
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
CurrencyGlobalId = code,
|
||||
Name = CurrencyDisplayName(code, ua),
|
||||
ShortName = code,
|
||||
IsPrimary = code == homeCurrency,
|
||||
};
|
||||
db.Currencies.Add(currency);
|
||||
db.CurrencyRates.Add(new CurrencyRate
|
||||
{
|
||||
CurrencyId = currency.Id,
|
||||
DateTime = rateDate,
|
||||
Quantity = 1,
|
||||
Rate = RateInHome(code, homeCurrency),
|
||||
});
|
||||
currencies[code] = currency;
|
||||
}
|
||||
|
||||
// UnCategorized convention: category Id == user Id
|
||||
db.ItemCategories.Add(new ItemCategory
|
||||
{
|
||||
Id = userId,
|
||||
UserId = userId,
|
||||
Name = ua ? "Без категорії" : "UnCategorized",
|
||||
Items = [],
|
||||
IsInternal = true,
|
||||
});
|
||||
|
||||
var accountCategoryDefs = ua
|
||||
? new (string Key, string Name)[] { ("CASH", "Готівка"), ("BANK", "Банк"), ("DEPOSIT", "Депозит") }
|
||||
: [("CASH", "CASH"), ("BANK", "BANK"), ("DEPOSIT", "DEPOSIT")];
|
||||
|
||||
var accounts = new List<Account>();
|
||||
foreach (var (key, name) in accountCategoryDefs)
|
||||
{
|
||||
var category = new AccountCategory
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Name = name,
|
||||
};
|
||||
db.AccountCategories.Add(category);
|
||||
|
||||
foreach (var code in CurrencyCodes)
|
||||
{
|
||||
var account = new Account
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
OwnerId = userId,
|
||||
CurrencyGlobalId = code,
|
||||
Name = $"{name} {code}",
|
||||
};
|
||||
db.Accounts.Add(account);
|
||||
db.AccountAccountCategories.Add(new AccountAccountCategory
|
||||
{
|
||||
AccountId = account.Id,
|
||||
CategoryId = category.Id,
|
||||
});
|
||||
db.AccountAccesses.Add(new AccountAccess
|
||||
{
|
||||
AccountId = account.Id,
|
||||
UserId = userId,
|
||||
OwnerId = userId,
|
||||
IsAllowRead = true,
|
||||
IsAllowWrite = true,
|
||||
IsAllowManage = true,
|
||||
Type = AccountAccessTypeEnum.balance,
|
||||
});
|
||||
accounts.Add(account);
|
||||
}
|
||||
}
|
||||
|
||||
var incomeCategory = new ItemCategory
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Name = ua ? "Доходи" : "INCOME",
|
||||
Items = [],
|
||||
};
|
||||
var outcomeCategory = new ItemCategory
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
Name = ua ? "Витрати" : "OUTCOME",
|
||||
Items = [],
|
||||
};
|
||||
db.ItemCategories.AddRange(incomeCategory, outcomeCategory);
|
||||
|
||||
var incomeNames = ua
|
||||
? new[] { "Готівка", "Офіс" }
|
||||
: ["CASH", "OFFICE"];
|
||||
var outcomeNames = ua
|
||||
? new[]
|
||||
{
|
||||
"Товари", "Бензин", "Steam", "Продукти", "Кафе",
|
||||
"Оренда", "Комунальні", "Інтернет", "Зв'язок", "Розваги",
|
||||
}
|
||||
: [
|
||||
"Goods", "Petrol", "Steam", "Groceries", "Cafe",
|
||||
"Rent", "Utilities", "Internet", "Mobile", "Entertainment",
|
||||
];
|
||||
|
||||
var incomeItems = await AddItemsAsync(db, incomeCategory.Id, incomeNames, cancellationToken);
|
||||
var outcomeItems = await AddItemsAsync(db, outcomeCategory.Id, outcomeNames, cancellationToken);
|
||||
|
||||
await db.SaveChangesAsync(cancellationToken);
|
||||
|
||||
SeedMotions(db, accounts, incomeItems, outcomeItems, rng, count: 100);
|
||||
}
|
||||
|
||||
private static async Task<List<Item>> AddItemsAsync(
|
||||
AppDbContext db,
|
||||
Guid categoryId,
|
||||
IEnumerable<string> names,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var items = new List<Item>();
|
||||
foreach (var name in names)
|
||||
{
|
||||
var global = db.ItemGlobals.Local.FirstOrDefault(x => x.Name == name)
|
||||
?? await db.ItemGlobals.FirstOrDefaultAsync(x => x.Name == name, cancellationToken);
|
||||
if (global == null)
|
||||
{
|
||||
global = new ItemGlobal { Id = Guid.NewGuid(), Name = name };
|
||||
db.ItemGlobals.Add(global);
|
||||
}
|
||||
|
||||
var item = new Item
|
||||
{
|
||||
CategoryId = categoryId,
|
||||
ItemGlobalId = global.Id,
|
||||
};
|
||||
db.Items.Add(item);
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
private static void SeedMotions(
|
||||
AppDbContext db,
|
||||
List<Account> accounts,
|
||||
List<Item> incomeItems,
|
||||
List<Item> outcomeItems,
|
||||
Random rng,
|
||||
int count)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var from = now.AddMonths(-3);
|
||||
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var isIncome = rng.Next(2) == 0;
|
||||
var item = isIncome
|
||||
? incomeItems[rng.Next(incomeItems.Count)]
|
||||
: outcomeItems[rng.Next(outcomeItems.Count)];
|
||||
var account = accounts[rng.Next(accounts.Count)];
|
||||
var days = (now - from).TotalDays;
|
||||
var when = from.AddDays(rng.NextDouble() * days);
|
||||
var amount = Math.Round((decimal)(rng.NextDouble() * (isIncome ? 4500 : 700) + (isIncome ? 200 : 20)), 2);
|
||||
|
||||
db.Motions.Add(new Motion
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AccountId = account.Id,
|
||||
ItemId = item.Id,
|
||||
DateTime = when,
|
||||
CreatedOn = when,
|
||||
Description = isIncome ? "Demo income" : "Demo expense",
|
||||
AmountPlus = isIncome ? amount : 0,
|
||||
AmountMinus = isIncome ? 0 : amount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static decimal RateInHome(string currencyCode, string homeCurrency)
|
||||
{
|
||||
static decimal InUah(string code) => code switch
|
||||
{
|
||||
"UAH" => 1m,
|
||||
"USD" => UsdInUah,
|
||||
"EUR" => EurInUah,
|
||||
_ => 1m,
|
||||
};
|
||||
|
||||
return InUah(currencyCode) / InUah(homeCurrency);
|
||||
}
|
||||
|
||||
private static string CurrencyDisplayName(string code, bool ua) => (code, ua) switch
|
||||
{
|
||||
("UAH", true) => "Гривня",
|
||||
("USD", true) => "Долар США",
|
||||
("EUR", true) => "Євро",
|
||||
("UAH", _) => "Ukrainian hryvnia",
|
||||
("USD", _) => "US Dollar",
|
||||
("EUR", _) => "Euro",
|
||||
_ => code,
|
||||
};
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.10">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="10.0.10" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.3" />
|
||||
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="7.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MyOffice.Data.Models\MyOffice.Data.Models.csproj" />
|
||||
<ProjectReference Include="..\MyOffice.Shared\MyOffice.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,23 +0,0 @@
|
||||
namespace MyOffice.DbContext;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
public record ConnectionConfiguration(string Provider, string ConnectionString);
|
||||
|
||||
public static class RepositoryInitializer
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers DbContext DI. Migrate/seed runs via <c>DatabaseInitializerHostedService</c>.
|
||||
/// <see cref="ConnectionString"/> remains for design-time <see cref="AppDbContextFactory"/>.
|
||||
/// </summary>
|
||||
public static void Initialize(
|
||||
IServiceCollection services,
|
||||
ConnectionConfiguration connectionString
|
||||
)
|
||||
{
|
||||
ConnectionString = connectionString;
|
||||
services.AddAppDbContext(connectionString);
|
||||
}
|
||||
|
||||
public static ConnectionConfiguration ConnectionString { get; internal set; } = null!;
|
||||
}
|
||||
Reference in New Issue
Block a user