665e3ca279
CI/CD / CI · API (dotnet build + test) (push) Successful in 56s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 52s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m3s
CI/CD / CI · Admin Web (tsc) (push) Successful in 35s
CI/CD / CI · Website (tsc) (push) Successful in 43s
CI/CD / CI · Koja (tsc) (push) Successful in 48s
CI/CD / Deploy · all services (push) Successful in 1m31s
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 <noreply@anthropic.com>
220 lines
8.3 KiB
C#
220 lines
8.3 KiB
C#
using Meezi.Core.Entities;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace Meezi.Infrastructure.Data;
|
|
|
|
public static class DemoMenuSeeder
|
|
{
|
|
/// <param name="useScopedIds">
|
|
/// When true, category and item IDs are prefixed with <paramref name="cafeId"/> 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.
|
|
/// </param>
|
|
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
|
|
{
|
|
Id = taxId,
|
|
CafeId = cafeId,
|
|
Name = "مالیات",
|
|
Rate = 9,
|
|
IsDefault = true,
|
|
IsRequired = true,
|
|
IsCompound = false
|
|
});
|
|
}
|
|
|
|
var existingCategoryIds = await db.MenuCategories
|
|
.Where(c => c.CafeId == cafeId)
|
|
.ToDictionaryAsync(c => c.Id, StringComparer.Ordinal);
|
|
|
|
var categoriesAdded = 0;
|
|
foreach (var cat in DemoMenuCatalog.Categories)
|
|
{
|
|
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;
|
|
if (string.IsNullOrWhiteSpace(row.IconPresetId) && !string.IsNullOrWhiteSpace(cat.IconPresetId))
|
|
row.IconPresetId = cat.IconPresetId;
|
|
if (string.IsNullOrWhiteSpace(row.IconStyle) && !string.IsNullOrWhiteSpace(cat.IconStyle))
|
|
row.IconStyle = cat.IconStyle;
|
|
if (string.IsNullOrWhiteSpace(row.NameEn) && !string.IsNullOrWhiteSpace(cat.NameEn))
|
|
row.NameEn = cat.NameEn;
|
|
if (string.IsNullOrWhiteSpace(row.NameAr) && cat.NameAr is not null)
|
|
row.NameAr = cat.NameAr;
|
|
continue;
|
|
}
|
|
|
|
db.MenuCategories.Add(new MenuCategory
|
|
{
|
|
Id = catId,
|
|
CafeId = cafeId,
|
|
Name = cat.Name,
|
|
NameEn = cat.NameEn,
|
|
NameAr = cat.NameAr,
|
|
Icon = cat.Icon,
|
|
IconPresetId = cat.IconPresetId,
|
|
IconStyle = cat.IconStyle,
|
|
SortOrder = cat.SortOrder,
|
|
TaxId = taxId,
|
|
IsActive = true
|
|
});
|
|
categoriesAdded++;
|
|
}
|
|
|
|
var existingItemIds = await db.MenuItems
|
|
.Where(i => i.CafeId == cafeId)
|
|
.Select(i => i.Id)
|
|
.ToListAsync();
|
|
|
|
var itemsAdded = 0;
|
|
foreach (var item in DemoMenuCatalog.Items)
|
|
{
|
|
var itemId = Scoped(item.Id);
|
|
if (existingItemIds.Contains(itemId))
|
|
continue;
|
|
|
|
db.MenuItems.Add(new MenuItem
|
|
{
|
|
Id = itemId,
|
|
CafeId = cafeId,
|
|
CategoryId = Scoped(item.CategoryId), // FK must point at scoped category ID
|
|
Name = item.Name,
|
|
NameEn = item.NameEn,
|
|
NameAr = item.NameAr,
|
|
Description = item.Description,
|
|
Price = item.PriceToman,
|
|
DiscountPercent = item.DiscountPercent,
|
|
ImageUrl = DemoMenuCatalog.ResolveItemImageUrl(item),
|
|
IsAvailable = true
|
|
});
|
|
itemsAdded++;
|
|
}
|
|
|
|
if (categoriesAdded > 0 || itemsAdded > 0)
|
|
await db.SaveChangesAsync();
|
|
|
|
if (categoriesAdded > 0 || itemsAdded > 0)
|
|
{
|
|
logger.LogInformation(
|
|
"Demo menu seed: +{Categories} categories, +{Items} items (catalog total {Total}) for cafe {CafeId}",
|
|
categoriesAdded,
|
|
itemsAdded,
|
|
DemoMenuCatalog.Items.Count,
|
|
cafeId);
|
|
}
|
|
|
|
await EnsureMenuImagesAsync(db, cafeId, logger);
|
|
await EnsureMenuTranslationsAsync(db, cafeId, logger);
|
|
}
|
|
|
|
/// <summary>Upserts ImageUrl from catalog/manifest/Food-101 fallbacks.</summary>
|
|
public static async Task EnsureMenuImagesAsync(AppDbContext db, string cafeId, ILogger logger)
|
|
{
|
|
var catalogById = DemoMenuCatalog.Items.ToDictionary(i => i.Id, StringComparer.Ordinal);
|
|
var items = await db.MenuItems
|
|
.Include(i => i.Category)
|
|
.Where(i => i.CafeId == cafeId)
|
|
.ToListAsync();
|
|
|
|
var updated = 0;
|
|
foreach (var row in items)
|
|
{
|
|
var resolved = catalogById.TryGetValue(row.Id, out var seed)
|
|
? DemoMenuCatalog.ResolveItemImageUrl(seed)
|
|
: MenuItemImageDefaults.ResolveImageUrl(row.Id, row.CategoryId, row.Category?.Name);
|
|
|
|
if (string.IsNullOrWhiteSpace(resolved)) continue;
|
|
|
|
var inCatalog = catalogById.ContainsKey(row.Id);
|
|
var shouldUpdate = MenuItemImageDefaults.NeedsImageRepair(row.ImageUrl) || inCatalog;
|
|
if (!shouldUpdate || string.Equals(row.ImageUrl, resolved, StringComparison.Ordinal))
|
|
continue;
|
|
|
|
row.ImageUrl = resolved;
|
|
updated++;
|
|
}
|
|
|
|
if (updated > 0)
|
|
{
|
|
await db.SaveChangesAsync();
|
|
logger.LogInformation("Menu image upsert: {Count} items updated for cafe {CafeId}", updated, cafeId);
|
|
}
|
|
}
|
|
|
|
/// <summary>Upserts NameEn/NameAr from catalog for demo menu rows.</summary>
|
|
public static async Task EnsureMenuTranslationsAsync(AppDbContext db, string cafeId, ILogger logger)
|
|
{
|
|
var catalogItems = DemoMenuCatalog.Items.ToDictionary(i => i.Id, StringComparer.Ordinal);
|
|
var catalogCats = DemoMenuCatalog.Categories.ToDictionary(c => c.Id, StringComparer.Ordinal);
|
|
|
|
var items = await db.MenuItems.Where(i => i.CafeId == cafeId && catalogItems.Keys.Contains(i.Id)).ToListAsync();
|
|
var itemUpdated = 0;
|
|
foreach (var row in items)
|
|
{
|
|
if (!catalogItems.TryGetValue(row.Id, out var seed)) continue;
|
|
var changed = false;
|
|
if (string.IsNullOrWhiteSpace(row.NameEn) && !string.IsNullOrWhiteSpace(seed.NameEn))
|
|
{
|
|
row.NameEn = seed.NameEn;
|
|
changed = true;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(row.NameAr) && seed.NameAr is not null)
|
|
{
|
|
row.NameAr = seed.NameAr;
|
|
changed = true;
|
|
}
|
|
if (changed) itemUpdated++;
|
|
}
|
|
|
|
var categories = await db.MenuCategories.Where(c => c.CafeId == cafeId && catalogCats.Keys.Contains(c.Id)).ToListAsync();
|
|
var catUpdated = 0;
|
|
foreach (var row in categories)
|
|
{
|
|
if (!catalogCats.TryGetValue(row.Id, out var seed)) continue;
|
|
var changed = false;
|
|
if (string.IsNullOrWhiteSpace(row.NameEn) && !string.IsNullOrWhiteSpace(seed.NameEn))
|
|
{
|
|
row.NameEn = seed.NameEn;
|
|
changed = true;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(row.NameAr) && seed.NameAr is not null)
|
|
{
|
|
row.NameAr = seed.NameAr;
|
|
changed = true;
|
|
}
|
|
if (string.IsNullOrWhiteSpace(row.Icon) && !string.IsNullOrWhiteSpace(seed.Icon))
|
|
{
|
|
row.Icon = seed.Icon;
|
|
changed = true;
|
|
}
|
|
if (changed) catUpdated++;
|
|
}
|
|
|
|
if (itemUpdated > 0 || catUpdated > 0)
|
|
{
|
|
await db.SaveChangesAsync();
|
|
logger.LogInformation(
|
|
"Menu translation upsert: {Items} items, {Cats} categories for cafe {CafeId}",
|
|
itemUpdated,
|
|
catUpdated,
|
|
cafeId);
|
|
}
|
|
}
|
|
}
|