Main расходился с БД стейджа (Phase2c3_MsStrict в history, но код ещё ссылался на VatRate etc.) — деплой ломался. Реплицирую удаление сущностей вручную, чтобы код совпадал с таблицами. Убрано (нет в OtherSystem — не выдумываем): - Domain: VatRate сущность целиком; Counterparty.Kind + enum CounterpartyKind; Store.Kind + enum StoreKind; Product.IsAlcohol; UnitOfMeasure.Symbol/DecimalPlaces/IsBase. - EF: DbSet<VatRate>, ConfigureVatRate, Product.VatRate navigation, индекс Counterparty.Kind. - DTO/Input: соответствующие поля и VatRateDto/Input. - API: VatRatesController удалён; references в Products/Counterparties/Stores/UoM/Supplies/Retail/Stock. Добавлено как в OtherSystem: - Product.Vat (int) + Product.VatEnabled — OtherSystem держит НДС числом на товаре. - KZ default VAT 16% — applied в сидерах и в OtherSystemImportService когда товар не принёс свой vat. OtherSystemImportService: - ResolveKind убран; CompanyType=entrepreneur→Individual (как и было). - VatRates lookup → прямой p.Vat ?? 16 + p.Vat > 0 для VatEnabled. - baseUnit ищется по code="796" вместо IsBase. Web: - types.ts: убраны CounterpartyKind/StoreKind/VatRate/Product.vatRateId/vatPercent/isAlcohol/UoM.symbol/decimalPlaces/isBase; добавлено Product.vat/vatEnabled; унифицировано unitSymbol→unitName. - VatRatesPage удалён, роут из App.tsx тоже. - CounterpartiesPage/StoresPage/UnitsOfMeasurePage: убраны соответствующие поля в формах. - ProductEditPage: select "Ставка НДС" теперь с фиксированными 0/10/12/16/20 + чекбокс VatEnabled. - Stock/RetailSale/Supply pages: unitSymbol → unitName. deploy-stage unguarded — теперь код соответствует DB, авто-deploy безопасен.
134 lines
5 KiB
C#
134 lines
5 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 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 = "упаковка" }
|
||
);
|
||
}
|
||
|
||
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",
|
||
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;
|
||
}
|