Files
meezi/src/Meezi.API/Services/DemoSeedService.cs
T
soroush.asadi 72f95aa0db
CI/CD / CI · API (dotnet build + test) (push) Successful in 1m9s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 47s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m6s
CI/CD / CI · Admin Web (tsc) (push) Successful in 36s
CI/CD / CI · Website (tsc) (push) Successful in 44s
CI/CD / CI · Koja (tsc) (push) Successful in 50s
CI/CD / Deploy · all services (push) Successful in 1m24s
fix(demo-seed): stop truncating ingredient/table ids to 36 chars
BuildDemoIngredients/BuildDemoTables built ids as
"{cafeId}_ing_{guid}"[..36]. For a real cafe (32-char hex id) the
first 36 chars are just "{cafeId}_ing" — the unique guid is cut off,
so all 15 ingredients (and all 10 tables) get the SAME id, causing a
primary-key collision on SaveChanges -> 500. cafe_demo_001 has a short
id so the guid survived, which is why the bug only hit real cafes.

The Id columns are text (no length limit), so the truncation served no
purpose. Removed [..36] from both so the full unique id is kept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 11:55:06 +03:30

180 lines
7.1 KiB
C#

using Meezi.Core.Entities;
using Meezi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Meezi.API.Services;
public record DemoSeedResult(
int CategoriesAdded,
int ItemsAdded,
int TablesAdded,
int IngredientsAdded,
bool TaxCreated);
public interface IDemoSeedService
{
Task<DemoSeedResult> SeedAsync(string cafeId, CancellationToken ct = default);
}
public class DemoSeedService : IDemoSeedService
{
private readonly AppDbContext _db;
private readonly ILogger<DemoSeedService> _logger;
public DemoSeedService(AppDbContext db, ILogger<DemoSeedService> logger)
{
_db = db;
_logger = logger;
}
public async Task<DemoSeedResult> SeedAsync(string cafeId, CancellationToken ct = default)
{
// 1. Ensure 9% default tax
var taxId = $"{cafeId}_demo_tax";
var taxCreated = false;
if (!await _db.Taxes.AnyAsync(t => t.CafeId == cafeId && t.IsDefault, ct))
{
_db.Taxes.Add(new Tax
{
Id = taxId,
CafeId = cafeId,
Name = "مالیات ارزش افزوده",
Rate = 9,
IsDefault = true,
IsRequired = true,
IsCompound = false
});
await _db.SaveChangesAsync(ct);
taxCreated = true;
}
else
{
taxId = await _db.Taxes
.Where(t => t.CafeId == cafeId && t.IsDefault)
.Select(t => t.Id)
.FirstAsync(ct);
}
// 2. Seed menu (categories + items) using café-agnostic seeder.
// useScopedIds=true prefixes all IDs with cafeId so multiple cafés
// can each have their own demo menu without primary-key collisions.
var beforeCats = await _db.MenuCategories.CountAsync(c => c.CafeId == cafeId, ct);
var beforeItems = await _db.MenuItems.CountAsync(i => i.CafeId == cafeId, ct);
await DemoMenuSeeder.EnsureMenuAsync(_db, cafeId, taxId, _logger, useScopedIds: true);
var afterCats = await _db.MenuCategories.CountAsync(c => c.CafeId == cafeId, ct);
var afterItems = await _db.MenuItems.CountAsync(i => i.CafeId == cafeId, ct);
// 3. Seed ingredients if warehouse is empty
var ingredientsAdded = 0;
if (!await _db.Ingredients.AnyAsync(i => i.CafeId == cafeId, ct))
{
var demoIngredients = BuildDemoIngredients(cafeId);
_db.Ingredients.AddRange(demoIngredients);
await _db.SaveChangesAsync(ct);
ingredientsAdded = demoIngredients.Count;
}
// 4. Seed 10 tables if no tables exist for this café's first active branch
var tablesAdded = 0;
if (!await _db.Tables.AnyAsync(t => t.CafeId == cafeId, ct))
{
var branchId = await _db.Branches
.Where(b => b.CafeId == cafeId && b.IsActive && b.DeletedAt == null)
.OrderBy(b => b.Id)
.Select(b => b.Id)
.FirstOrDefaultAsync(ct);
if (branchId is not null)
{
var tables = BuildDemoTables(cafeId, branchId);
_db.Tables.AddRange(tables);
await _db.SaveChangesAsync(ct);
tablesAdded = tables.Count;
}
}
_logger.LogInformation(
"Demo seed complete for cafe {CafeId}: +{Cats} cats, +{Items} items, +{Tables} tables, +{Ing} ingredients, tax={TaxCreated}",
cafeId, afterCats - beforeCats, afterItems - beforeItems, tablesAdded, ingredientsAdded, taxCreated);
return new DemoSeedResult(
CategoriesAdded: afterCats - beforeCats,
ItemsAdded: afterItems - beforeItems,
TablesAdded: tablesAdded,
IngredientsAdded: ingredientsAdded,
TaxCreated: taxCreated);
}
private static List<Ingredient> BuildDemoIngredients(string cafeId) =>
[
Ingredient(cafeId, "قهوه اسپرسو", "گرم", 2000, 500, 80, 2000),
Ingredient(cafeId, "شیر", "میلی‌لیتر", 10000, 2000, 15, 10000),
Ingredient(cafeId, "شکر", "گرم", 5000, 1000, 5, 5000),
Ingredient(cafeId, "وانیل", "میلی‌لیتر", 500, 100, 50, 500),
Ingredient(cafeId, "شکلات تلخ", "گرم", 1000, 200, 120, 1000),
Ingredient(cafeId, "خامه", "میلی‌لیتر", 2000, 500, 30, 2000),
Ingredient(cafeId, "دارچین", "گرم", 300, 50, 40, 300),
Ingredient(cafeId, "چای سیاه", "گرم", 1000, 200, 60, 1000),
Ingredient(cafeId, "آب معدنی", "میلی‌لیتر", 20000, 5000, 3, 20000),
Ingredient(cafeId, "نان تست", "عدد", 100, 20, 8000, 100),
Ingredient(cafeId, "تخم‌مرغ", "عدد", 60, 12, 6000, 60),
Ingredient(cafeId, "کره", "گرم", 500, 100, 80, 500),
Ingredient(cafeId, "پنیر", "گرم", 1000, 200, 90, 1000),
Ingredient(cafeId, "اسپاتولا یخ", "عدد", 200, 50, 2000, 200),
Ingredient(cafeId, "سس کارامل", "میلی‌لیتر", 1000, 200, 60, 1000),
];
private static Ingredient Ingredient(
string cafeId, string name, string unit,
decimal qty, decimal reorder, decimal cost, decimal par) =>
new()
{
// No [..36] truncation: Id is a text column, and truncating to 36 chars
// cuts off the unique guid for real (32-char) café ids → every row gets
// the same id → PK collision → 500. Keep the full unique id.
Id = $"{cafeId}_ing_{Guid.NewGuid():N}",
CafeId = cafeId,
Name = name,
Unit = unit,
QuantityOnHand = qty,
ReorderLevel = reorder,
UnitCost = cost,
ParLevel = par,
LowStockWarningPercent = 20m
};
private static List<Table> BuildDemoTables(string cafeId, string branchId)
{
var tables = new List<Table>();
// Floor 1: tables 1-4
for (var i = 1; i <= 4; i++)
tables.Add(Table(cafeId, branchId, i.ToString(), 4, "طبقه اول", i));
// Floor 2: tables 5-8
for (var i = 5; i <= 8; i++)
tables.Add(Table(cafeId, branchId, i.ToString(), 4, "طبقه دوم", i));
// VIP: tables 9-10
for (var i = 9; i <= 10; i++)
tables.Add(Table(cafeId, branchId, i.ToString(), 6, "VIP", i));
return tables;
}
private static Table Table(
string cafeId, string branchId, string number, int capacity, string floor, int sortOrder) =>
new()
{
// No [..36] truncation (see Ingredient above): truncating cuts the guid
// for real 32-char café ids → identical ids → PK collision → 500.
Id = $"{cafeId}_tbl_{Guid.NewGuid():N}",
CafeId = cafeId,
BranchId = branchId,
Number = number,
Capacity = capacity,
Floor = floor,
SortOrder = sortOrder,
QrCode = Guid.NewGuid().ToString("N"),
IsActive = true,
IsCleaning = false
};
}