using foodmarket.Domain.Catalog; using foodmarket.Domain.Organizations; using foodmarket.Infrastructure.Identity; using foodmarket.Infrastructure.Persistence; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace foodmarket.Api.Seed; public class DevDataSeeder : IHostedService { private readonly IServiceProvider _services; private readonly IHostEnvironment _env; public DevDataSeeder(IServiceProvider services, IHostEnvironment env) { _services = services; _env = env; } public async Task StartAsync(CancellationToken ct) { // Idempotent — runs in all envs to bootstrap a usable admin + demo org. // Once first real user/org is set up via UI, rename/disable demo. // (Wired regardless of env so stage/prod first-deploy lands a working // admin, otherwise nobody can log in.) using var scope = _services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var userMgr = scope.ServiceProvider.GetRequiredService>(); var roleMgr = scope.ServiceProvider.GetRequiredService>(); foreach (var role in new[] { SystemRoles.SuperAdmin, SystemRoles.Admin, SystemRoles.Manager, SystemRoles.Cashier, SystemRoles.Storekeeper }) { if (!await roleMgr.RoleExistsAsync(role)) { await roleMgr.CreateAsync(new Role { Name = role }); } } var kzt = await db.Currencies.FirstOrDefaultAsync(c => c.Code == "KZT", ct); var demoOrg = await db.Organizations.FirstOrDefaultAsync(o => o.Name == "Demo Market", ct); if (demoOrg is null) { demoOrg = new Organization { Name = "Demo Market", CountryCode = "KZ", Bin = "000000000000", Address = "Алматы, ул. Пример 1", Phone = "+7 (777) 000-00-00", Email = "demo@food-market.local", DefaultCurrencyId = kzt?.Id, }; db.Organizations.Add(demoOrg); await db.SaveChangesAsync(ct); } else if (demoOrg.DefaultCurrencyId is null && kzt is not null) { demoOrg.DefaultCurrencyId = kzt.Id; await db.SaveChangesAsync(ct); } await SeedTenantReferencesAsync(db, demoOrg.Id, ct); const string adminEmail = "admin@food-market.local"; var admin = await userMgr.FindByEmailAsync(adminEmail); if (admin is null) { admin = new User { UserName = adminEmail, Email = adminEmail, EmailConfirmed = true, FullName = "System Admin", OrganizationId = demoOrg.Id, }; var result = await userMgr.CreateAsync(admin, "Admin12345!"); if (result.Succeeded) { await userMgr.AddToRoleAsync(admin, SystemRoles.Admin); } } await SeedAdminEmployeeAsync(db, demoOrg.Id, admin?.Id, ct); } /// Привязывает существующего admin@food-market.local к /// Employee-записи с системной ролью «Администратор» — чтобы UI «Сотрудники» /// сразу показывал учётку с правильной ролью, а не пустой список. private static async Task SeedAdminEmployeeAsync(AppDbContext db, Guid orgId, Guid? adminUserId, CancellationToken ct) { if (adminUserId is null) return; var existing = await db.Employees.IgnoreQueryFilters() .FirstOrDefaultAsync(e => e.OrganizationId == orgId && e.UserId == adminUserId, ct); if (existing is not null) return; var adminRole = await db.EmployeeRoles.IgnoreQueryFilters() .FirstOrDefaultAsync(r => r.OrganizationId == orgId && r.IsSystem && r.Name == "Администратор", ct); if (adminRole is null) return; db.Employees.Add(new Employee { OrganizationId = orgId, UserId = adminUserId, LastName = "Admin", FirstName = "System", Position = "Администратор", Email = "admin@food-market.local", RoleId = adminRole.Id, IsActive = true, }); await db.SaveChangesAsync(ct); } private static async Task SeedTenantReferencesAsync(AppDbContext db, Guid orgId, CancellationToken ct) { var anyUnit = await db.UnitsOfMeasure.IgnoreQueryFilters().AnyAsync(u => u.OrganizationId == orgId, ct); if (!anyUnit) { db.UnitsOfMeasure.AddRange( new UnitOfMeasure { OrganizationId = orgId, Code = "796", Name = "штука" }, new UnitOfMeasure { OrganizationId = orgId, Code = "166", Name = "килограмм" }, new UnitOfMeasure { OrganizationId = orgId, Code = "112", Name = "литр" }, new UnitOfMeasure { OrganizationId = orgId, Code = "006", Name = "метр" }, new UnitOfMeasure { OrganizationId = orgId, Code = "625", Name = "упаковка" } ); } // Сидируем PriceType ТОЛЬКО если у организации не было ни одной записи. // Если есть — никогда не создаём «системную копию», корректность IsSystem // обеспечивает миграция Phase3b_FixPriceTypeIsSystem (выбор по факту: // запись с максимумом ProductPrice). var anyPriceType = await db.PriceTypes.IgnoreQueryFilters().AnyAsync(p => p.OrganizationId == orgId, ct); if (!anyPriceType) { db.PriceTypes.AddRange( new PriceType { OrganizationId = orgId, Name = "Розничная цена", IsSystem = true, IsRequired = true, IsRetail = true, SortOrder = 0 }, new PriceType { OrganizationId = orgId, Name = "Оптовая", SortOrder = 1 } ); } var mainStore = await db.Stores.IgnoreQueryFilters().FirstOrDefaultAsync(s => s.OrganizationId == orgId && s.IsMain, ct); if (mainStore is null) { mainStore = new Store { OrganizationId = orgId, Name = "Основной склад", Code = "MAIN", IsMain = true, Address = "Алматы, ул. Пример 1", }; db.Stores.Add(mainStore); await db.SaveChangesAsync(ct); } var anyRetail = await db.RetailPoints.IgnoreQueryFilters().AnyAsync(r => r.OrganizationId == orgId, ct); if (!anyRetail) { db.RetailPoints.Add(new RetailPoint { OrganizationId = orgId, Name = "Касса 1", Code = "POS-1", StoreId = mainStore.Id, }); } await db.SaveChangesAsync(ct); await SeedEmployeeRolesAsync(db, orgId, ct); } /// Системные роли (IsSystem=true): Администратор / Менеджер / /// Кладовщик / Кассир / Закупщик / Бухгалтер. Сидируется один раз /// per организацию; обновлять не пытаемся, чтобы не сбросить кастомные /// правки галок которые админ мог сделать. private static async Task SeedEmployeeRolesAsync(AppDbContext db, Guid orgId, CancellationToken ct) { var anyRole = await db.EmployeeRoles.IgnoreQueryFilters().AnyAsync(r => r.OrganizationId == orgId, ct); if (anyRole) return; var admin = new EmployeeRole { OrganizationId = orgId, Name = "Администратор", Description = "Полный доступ ко всем разделам организации", IsSystem = true, SortOrder = 0, Permissions = RolePermissions.All(), }; // Менеджер/Кладовщик/Закупщик/Бухгалтер — кастомные шаблоны (IsSystem=false), // юзер может удалить или подкрутить под себя. Системные только Администратор + Кассир. var manager = new EmployeeRole { OrganizationId = orgId, Name = "Менеджер", Description = "Управление каталогом, документами и контрагентами", IsSystem = false, SortOrder = 10, Permissions = new RolePermissions { ProductsView = true, ProductsEdit = true, ProductGroupsManage = true, PriceTypesManage = true, SuppliesView = true, SuppliesEdit = true, SuppliesPost = true, CounterpartiesView = true, CounterpartiesEdit = true, ReportsView = true, StocksView = true, }, }; var keeper = new EmployeeRole { OrganizationId = orgId, Name = "Кладовщик", Description = "Приёмки, инвентаризация, остатки", IsSystem = false, SortOrder = 20, Permissions = new RolePermissions { ProductsView = true, SuppliesView = true, SuppliesEdit = true, SuppliesPost = true, StocksView = true, }, }; var cashier = new EmployeeRole { OrganizationId = orgId, Name = "Кассир", Description = "Только работа на кассе. Без доступа к веб-админке.", IsSystem = true, SortOrder = 30, Permissions = new RolePermissions { ProductsView = true, StocksView = true, RetailSalesOperate = true, // RetailSalesRefund по умолчанию false — админ включит при необходимости }, }; var buyer = new EmployeeRole { OrganizationId = orgId, Name = "Закупщик", Description = "Заказы поставщикам и приёмка товара", IsSystem = false, SortOrder = 40, Permissions = new RolePermissions { ProductsView = true, SuppliesView = true, SuppliesEdit = true, CounterpartiesView = true, CounterpartiesEdit = true, }, }; var accountant = new EmployeeRole { OrganizationId = orgId, Name = "Бухгалтер", Description = "Просмотр всех данных и отчётов, без редактирования", IsSystem = false, SortOrder = 50, Permissions = new RolePermissions { ProductsView = true, SuppliesView = true, CounterpartiesView = true, ReportsView = true, StocksView = true, }, }; db.EmployeeRoles.AddRange(admin, manager, keeper, cashier, buyer, accountant); await db.SaveChangesAsync(ct); } public Task StopAsync(CancellationToken ct) => Task.CompletedTask; }