using foodmarket.Domain.Catalog; using foodmarket.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; namespace foodmarket.Api.Seed; // Populates a starter catalog so the system is usable immediately after first run. // Runs only in Development and only if the tenant has no products yet — safe to run multiple times. public class DemoCatalogSeeder : IHostedService { private readonly IServiceProvider _services; private readonly IHostEnvironment _env; public DemoCatalogSeeder(IServiceProvider services, IHostEnvironment env) { _services = services; _env = env; } public async Task StartAsync(CancellationToken ct) { if (!_env.IsDevelopment()) return; using var scope = _services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var demoOrg = await db.Organizations.FirstOrDefaultAsync(o => o.Name == "Demo Market", ct); if (demoOrg is null) return; var orgId = demoOrg.Id; // Skip if products already present — don't re-seed on every restart. var hasProducts = await db.Products.IgnoreQueryFilters().AnyAsync(p => p.OrganizationId == orgId, ct); if (hasProducts) return; // KZ дефолт — 16% (из Country.VatRate), льготные категории (хлеб) — 0%. const int vatDefault = 16; const int vat0 = 0; var unitSht = await db.UnitsOfMeasure.IgnoreQueryFilters() .FirstOrDefaultAsync(u => u.OrganizationId == orgId && u.Code == "796", ct); var unitKg = await db.UnitsOfMeasure.IgnoreQueryFilters() .FirstOrDefaultAsync(u => u.OrganizationId == orgId && u.Code == "166", ct); var unitL = await db.UnitsOfMeasure.IgnoreQueryFilters() .FirstOrDefaultAsync(u => u.OrganizationId == orgId && u.Code == "112", ct); if (unitSht is null) return; var retailPriceType = await db.PriceTypes.IgnoreQueryFilters() .FirstOrDefaultAsync(p => p.OrganizationId == orgId && p.IsRetail, ct); if (retailPriceType is null) return; var kzt = await db.Currencies.FirstOrDefaultAsync(c => c.Code == "KZT", ct); if (kzt is null) return; var kz = await db.Countries.FirstOrDefaultAsync(c => c.Code == "KZ", ct); var ru = await db.Countries.FirstOrDefaultAsync(c => c.Code == "RU", ct); // Product groups (hierarchy — Напитки/Безалкогольные, Напитки/Алкогольные, etc.) var groups = new Dictionary(); Guid AddGroup(string name, Guid? parentId) { var id = Guid.NewGuid(); var path = parentId is null ? name : $"{groups.First(g => g.Value == parentId).Key}/{name}"; db.ProductGroups.Add(new ProductGroup { Id = id, OrganizationId = orgId, Name = name, ParentId = parentId, Path = path, SortOrder = groups.Count, IsActive = true, }); groups[path] = id; return id; } var gDrinks = AddGroup("Напитки", null); var gDrinksNon = AddGroup("Безалкогольные", gDrinks); AddGroup("Алкогольные", gDrinks); var gDairy = AddGroup("Молочные продукты", null); var gBakery = AddGroup("Хлеб и выпечка", null); var gSweets = AddGroup("Кондитерские", null); var gGrocery = AddGroup("Бакалея", null); var gSnacks = AddGroup("Снеки", null); // Demo suppliers var supplier1 = new Counterparty { OrganizationId = orgId, Name = "ТОО «Продтрейд»", LegalName = "Товарищество с ограниченной ответственностью «Продтрейд»", Type = CounterpartyType.LegalEntity, Bin = "100140005678", CountryId = kz?.Id, Address = "Алматы, ул. Абая 15", Phone = "+7 (727) 100-00-01", Email = "order@prodtrade.kz", BankName = "Kaspi Bank", BankAccount = "KZ000000000000000001", Bik = "CASPKZKA", IsActive = true, }; var supplier2 = new Counterparty { OrganizationId = orgId, Name = "ИП Иванов А.С.", Type = CounterpartyType.Individual, Iin = "850101300000", CountryId = kz?.Id, Phone = "+7 (777) 100-00-02", ContactPerson = "Иванов Алексей", IsActive = true, }; db.Counterparties.AddRange(supplier1, supplier2); // Barcodes use generic internal range (2xxxxxxxxxxxx) to avoid colliding with real products. // When user does real приёмка, real barcodes will overwrite. var demo = new (string Name, Guid Group, decimal RetailPrice, string Article, string Barcode, Guid? Country, bool IsWeighed, Guid Unit)[] { // Напитки — безалкогольные ("Вода питьевая «Тассай» 0.5л", gDrinksNon, 220, "DR-WAT-001", "2000000000018", kz?.Id, false, unitSht.Id), ("Вода питьевая «Тассай» 1.5л", gDrinksNon, 380, "DR-WAT-002", "2000000000025", kz?.Id, false, unitSht.Id), ("Сок «Да-Да» яблоко 1л", gDrinksNon, 650, "DR-JUC-001", "2000000000032", kz?.Id, false, unitSht.Id), ("Сок «Да-Да» апельсин 1л", gDrinksNon, 650, "DR-JUC-002", "2000000000049", kz?.Id, false, unitSht.Id), ("Кока-кола 0.5л", gDrinksNon, 420, "DR-SOD-001", "2000000000056", ru?.Id, false, unitSht.Id), ("Пепси 0.5л", gDrinksNon, 420, "DR-SOD-002", "2000000000063", ru?.Id, false, unitSht.Id), ("Спрайт 0.5л", gDrinksNon, 420, "DR-SOD-003", "2000000000070", ru?.Id, false, unitSht.Id), ("Чай чёрный «Пиала» пакетированный, 100шт", gDrinksNon, 1250, "DR-TEA-001", "2000000000087", kz?.Id, false, unitSht.Id), // Молочные ("Молоко «Простоквашино» 2.5% 930мл", gDairy, 720, "DAI-MLK-001", "2000000000094", ru?.Id, false, unitSht.Id), ("Молоко «Food Master» 3.2% 1л", gDairy, 620, "DAI-MLK-002", "2000000000100", kz?.Id, false, unitSht.Id), ("Кефир «Food Master» 2.5% 1л", gDairy, 590, "DAI-KEF-001", "2000000000117", kz?.Id, false, unitSht.Id), ("Йогурт «Актимель» клубника 100мл", gDairy, 180, "DAI-YOG-001", "2000000000124", null, false, unitSht.Id), ("Творог «Простоквашино» 5% 200г", gDairy, 590, "DAI-CRD-001", "2000000000131", ru?.Id, false, unitSht.Id), ("Сметана «Food Master» 20% 400г", gDairy, 850, "DAI-SCR-001", "2000000000148", kz?.Id, false, unitSht.Id), ("Сыр «Российский» 45%", gDairy, 3200, "DAI-CHE-001", "2000000000155", kz?.Id, true, unitKg?.Id ?? unitSht.Id), // Хлеб и выпечка ("Хлеб «Семейный» нарезной 500г", gBakery, 180, "BAK-BRD-001", "2000000000162", kz?.Id, false, unitSht.Id), ("Батон «Столичный» 400г", gBakery, 170, "BAK-BRD-002", "2000000000179", kz?.Id, false, unitSht.Id), ("Лепёшка домашняя", gBakery, 220, "BAK-LEP-001", "2000000000186", kz?.Id, false, unitSht.Id), ("Круассан шоколадный", gBakery, 320, "BAK-CRO-001", "2000000000193", kz?.Id, false, unitSht.Id), // Кондитерские ("Шоколад «Рахат» молочный 100г", gSweets, 650, "SW-CHO-001", "2000000000209", kz?.Id, false, unitSht.Id), ("Шоколад «Алёнка» 100г", gSweets, 620, "SW-CHO-002", "2000000000216", ru?.Id, false, unitSht.Id), ("Конфеты «Казахстанские» 200г", gSweets, 890, "SW-CND-001", "2000000000223", kz?.Id, false, unitSht.Id), ("Печенье «Юбилейное» 250г", gSweets, 540, "SW-CKE-001", "2000000000230", ru?.Id, false, unitSht.Id), ("Вафли «Артек» 250г", gSweets, 480, "SW-WAF-001", "2000000000247", kz?.Id, false, unitSht.Id), // Бакалея ("Сахар-песок 1кг", gGrocery, 480, "GRO-SUG-001", "2000000000254", kz?.Id, false, unitSht.Id), ("Соль поваренная 1кг", gGrocery, 180, "GRO-SLT-001", "2000000000261", kz?.Id, false, unitSht.Id), ("Рис круглозёрный 1кг", gGrocery, 850, "GRO-RCE-001", "2000000000278", kz?.Id, false, unitSht.Id), ("Гречка «Мистраль» 900г", gGrocery, 990, "GRO-GRE-001", "2000000000285", ru?.Id, false, unitSht.Id), ("Макароны «Corona» 500г", gGrocery, 420, "GRO-PAS-001", "2000000000292", kz?.Id, false, unitSht.Id), ("Масло подсолнечное «Шедевр» 1л", gGrocery, 920, "GRO-OIL-001", "2000000000308", kz?.Id, false, unitL?.Id ?? unitSht.Id), ("Кофе «Якобс Монарх» 95г растворимый", gGrocery, 2890, "GRO-COF-001", "2000000000315", null, false, unitSht.Id), // Снеки ("Чипсы «Lays» сметана-лук 80г", gSnacks, 380, "SN-CHP-001", "2000000000322", kz?.Id, false, unitSht.Id), ("Сухарики «3 корочки» ржаные 40г", gSnacks, 190, "SN-SBR-001", "2000000000339", ru?.Id, false, unitSht.Id), ("Орешки фисташки 100г", gSnacks, 1190, "SN-NUT-001", "2000000000346", kz?.Id, false, unitSht.Id), ("Семечки «Мартин» 100г", gSnacks, 290, "SN-SED-001", "2000000000353", ru?.Id, false, unitSht.Id), }; var products = demo.Select(d => { var p = new Product { OrganizationId = orgId, Name = d.Name, Article = d.Article, UnitOfMeasureId = d.Unit, // Хлеб/батон/лепёшка — 0% (льготная категория в KZ). Vat = d.Name.Contains("Хлеб") || d.Name.Contains("Батон") || d.Name.Contains("Лепёшка") ? vat0 : vatDefault, VatEnabled = !(d.Name.Contains("Хлеб") || d.Name.Contains("Батон") || d.Name.Contains("Лепёшка")), ProductGroupId = d.Group, CountryOfOriginId = d.Country, Packaging = d.IsWeighed ? Packaging.Weight : Packaging.Piece, IsActive = true, PurchasePrice = Math.Round(d.RetailPrice * 0.72m, 2), PurchaseCurrencyId = kzt.Id, Prices = [ new ProductPrice { OrganizationId = orgId, PriceTypeId = retailPriceType.Id, Amount = d.RetailPrice, CurrencyId = kzt.Id, }, ], Barcodes = [ new ProductBarcode { OrganizationId = orgId, Code = d.Barcode, Type = BarcodeType.Ean13, IsPrimary = true, }, ], }; return p; }).ToList(); db.Products.AddRange(products); await db.SaveChangesAsync(ct); } public Task StopAsync(CancellationToken ct) => Task.CompletedTask; }