From 665e3ca279b9e24b02f2abab487fcb900ef85700 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Mon, 1 Jun 2026 13:59:48 +0330 Subject: [PATCH] =?UTF-8?q?fix(demo):=20scope=20category/item=20IDs=20per?= =?UTF-8?q?=20caf=C3=A9=20to=20prevent=20PK=20collisions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DemoMenuSeeder used hardcoded IDs like cat_demo_coffee for every café. If the dev seeder (runs when ASPNETCORE_ENVIRONMENT=Development) already inserted those IDs for cafe_demo_001, a production café clicking "Add demo data" hit a primary-key constraint violation. Fix: EnsureMenuAsync now accepts useScopedIds=true which prefixes every category and item ID with cafeId (e.g. cafe_abc_cat_demo_coffee). CategoryId FKs on items are remapped through the same function. DemoSeedService (the API endpoint handler) always passes useScopedIds=true. DevelopmentDataSeeder keeps useScopedIds=false (default) so the existing cafe_demo_001 rows in dev databases are not touched. Co-Authored-By: Claude Sonnet 4.6 --- src/Meezi.API/Services/DemoSeedService.cs | 6 ++-- .../Data/DemoMenuSeeder.cs | 29 +++++++++++++++---- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/Meezi.API/Services/DemoSeedService.cs b/src/Meezi.API/Services/DemoSeedService.cs index c3021b5..60e875d 100644 --- a/src/Meezi.API/Services/DemoSeedService.cs +++ b/src/Meezi.API/Services/DemoSeedService.cs @@ -56,10 +56,12 @@ public class DemoSeedService : IDemoSeedService .FirstAsync(ct); } - // 2. Seed menu (categories + items) using café-agnostic seeder + // 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); + 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); diff --git a/src/Meezi.Infrastructure/Data/DemoMenuSeeder.cs b/src/Meezi.Infrastructure/Data/DemoMenuSeeder.cs index 9e2aaf7..d105eed 100644 --- a/src/Meezi.Infrastructure/Data/DemoMenuSeeder.cs +++ b/src/Meezi.Infrastructure/Data/DemoMenuSeeder.cs @@ -6,8 +6,23 @@ namespace Meezi.Infrastructure.Data; public static class DemoMenuSeeder { - public static async Task EnsureMenuAsync(AppDbContext db, string cafeId, string taxId, ILogger logger) + /// + /// When true, category and item IDs are prefixed with so + /// multiple cafés can each have their own copy of the demo menu without a primary-key + /// collision. Pass false only for the legacy demo café (cafe_demo_001) whose IDs are + /// already in the database without a café prefix. + /// + public static async Task EnsureMenuAsync( + AppDbContext db, string cafeId, string taxId, ILogger logger, + bool useScopedIds = false) { + // When useScopedIds=true every row gets a deterministic ID that is unique per café: + // category → "{cafeId}_{catalogId}" + // item → "{cafeId}_{catalogId}" + // The catalog item's CategoryId is remapped through the same function. + string Scoped(string catalogId) => + useScopedIds ? $"{cafeId}_{catalogId}" : catalogId; + if (!await db.Taxes.AnyAsync(t => t.Id == taxId && t.CafeId == cafeId)) { db.Taxes.Add(new Tax @@ -29,7 +44,8 @@ public static class DemoMenuSeeder var categoriesAdded = 0; foreach (var cat in DemoMenuCatalog.Categories) { - if (existingCategoryIds.TryGetValue(cat.Id, out var row)) + var catId = Scoped(cat.Id); + if (existingCategoryIds.TryGetValue(catId, out var row)) { if (string.IsNullOrWhiteSpace(row.Icon) && !string.IsNullOrWhiteSpace(cat.Icon)) row.Icon = cat.Icon; @@ -46,7 +62,7 @@ public static class DemoMenuSeeder db.MenuCategories.Add(new MenuCategory { - Id = cat.Id, + Id = catId, CafeId = cafeId, Name = cat.Name, NameEn = cat.NameEn, @@ -69,14 +85,15 @@ public static class DemoMenuSeeder var itemsAdded = 0; foreach (var item in DemoMenuCatalog.Items) { - if (existingItemIds.Contains(item.Id)) + var itemId = Scoped(item.Id); + if (existingItemIds.Contains(itemId)) continue; db.MenuItems.Add(new MenuItem { - Id = item.Id, + Id = itemId, CafeId = cafeId, - CategoryId = item.CategoryId, + CategoryId = Scoped(item.CategoryId), // FK must point at scoped category ID Name = item.Name, NameEn = item.NameEn, NameAr = item.NameAr,