Login on https://food-market.zat.kz failed because DevDataSeeder skipped in non-Dev envs, so the demo admin account never existed on stage. Seeder is idempotent — checks-then-creates for every entity. Safe to run on every startup in any env. Once a real org/admin replaces the seeded demo, this seeder is a no-op. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
144 lines
5.7 KiB
C#
144 lines
5.7 KiB
C#
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<AppDbContext>();
|
||
var userMgr = scope.ServiceProvider.GetRequiredService<UserManager<User>>();
|
||
var roleMgr = scope.ServiceProvider.GetRequiredService<RoleManager<Role>>();
|
||
|
||
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 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"
|
||
};
|
||
db.Organizations.Add(demoOrg);
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
private static async Task SeedTenantReferencesAsync(AppDbContext db, Guid orgId, CancellationToken ct)
|
||
{
|
||
var anyVat = await db.VatRates.IgnoreQueryFilters().AnyAsync(v => v.OrganizationId == orgId, ct);
|
||
if (!anyVat)
|
||
{
|
||
db.VatRates.AddRange(
|
||
new VatRate { OrganizationId = orgId, Name = "Без НДС", Percent = 0m, IsDefault = false },
|
||
new VatRate { OrganizationId = orgId, Name = "НДС 12%", Percent = 12m, IsIncludedInPrice = true, IsDefault = true }
|
||
);
|
||
}
|
||
|
||
var anyUnit = await db.UnitsOfMeasure.IgnoreQueryFilters().AnyAsync(u => u.OrganizationId == orgId, ct);
|
||
if (!anyUnit)
|
||
{
|
||
db.UnitsOfMeasure.AddRange(
|
||
new UnitOfMeasure { OrganizationId = orgId, Code = "796", Symbol = "шт", Name = "штука", DecimalPlaces = 0, IsBase = true },
|
||
new UnitOfMeasure { OrganizationId = orgId, Code = "166", Symbol = "кг", Name = "килограмм", DecimalPlaces = 3 },
|
||
new UnitOfMeasure { OrganizationId = orgId, Code = "112", Symbol = "л", Name = "литр", DecimalPlaces = 3 },
|
||
new UnitOfMeasure { OrganizationId = orgId, Code = "006", Symbol = "м", Name = "метр", DecimalPlaces = 3 },
|
||
new UnitOfMeasure { OrganizationId = orgId, Code = "625", Symbol = "уп", Name = "упаковка", DecimalPlaces = 0 }
|
||
);
|
||
}
|
||
|
||
var anyPriceType = await db.PriceTypes.IgnoreQueryFilters().AnyAsync(p => p.OrganizationId == orgId, ct);
|
||
if (!anyPriceType)
|
||
{
|
||
db.PriceTypes.AddRange(
|
||
new PriceType { OrganizationId = orgId, Name = "Розничная", IsDefault = true, IsRetail = true, SortOrder = 1 },
|
||
new PriceType { OrganizationId = orgId, Name = "Оптовая", SortOrder = 2 }
|
||
);
|
||
}
|
||
|
||
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",
|
||
Kind = StoreKind.Warehouse,
|
||
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);
|
||
}
|
||
|
||
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
|
||
}
|