feat(api): .NET 10 multi-tenant REST API
Full backend implementation: - Multi-tenant cafe/restaurant management (menus, orders, tables, staff) - POS order flow with ZarinPal and Snappfood payment integration - OTP authentication via Kavenegar SMS - QR digital menu with public discover/finder endpoints - Customer loyalty, coupons, CRM - PostgreSQL via EF Core, Redis for caching/sessions - Background jobs, webhook handlers - Full migration history Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
using Meezi.Core.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Meezi.Infrastructure.Data;
|
||||
|
||||
public static class DemoMenuSeeder
|
||||
{
|
||||
public static async Task EnsureMenuAsync(AppDbContext db, string cafeId, string taxId, ILogger logger)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (existingCategoryIds.TryGetValue(cat.Id, 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 = cat.Id,
|
||||
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)
|
||||
{
|
||||
if (existingItemIds.Contains(item.Id))
|
||||
continue;
|
||||
|
||||
db.MenuItems.Add(new MenuItem
|
||||
{
|
||||
Id = item.Id,
|
||||
CafeId = cafeId,
|
||||
CategoryId = item.CategoryId,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user