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,239 @@
|
||||
using Meezi.Core.Branding;
|
||||
using Meezi.Core.Entities;
|
||||
using Meezi.Core.Enums;
|
||||
using Meezi.Infrastructure.Branding;
|
||||
using Meezi.Infrastructure.Discover;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Meezi.Infrastructure.Data;
|
||||
|
||||
/// <summary>Seeds 30 Persian showcase cafés for public discover (development only).</summary>
|
||||
public static class DiscoverShowcaseSeeder
|
||||
{
|
||||
private static readonly string[] ReviewAuthors = ["سارا", "علی", "مینا", "رضا", "نازنین"];
|
||||
private static readonly string[] ReviewComments =
|
||||
[
|
||||
"فضا و نوشیدنی عالی بود.",
|
||||
"سرویس سریع، پیشنهاد میکنم.",
|
||||
"مناسب قرار و کار.",
|
||||
"قیمت مناسب برای کیفیت.",
|
||||
"دسر و قهوه خوشمزه بود.",
|
||||
];
|
||||
|
||||
public static async Task SeedAsync(AppDbContext db, ILogger logger)
|
||||
{
|
||||
var addedCafes = 0;
|
||||
foreach (var spec in DiscoverShowcaseCatalog.Cafes)
|
||||
{
|
||||
var cafe = await db.Cafes.FirstOrDefaultAsync(c => c.Id == spec.Id);
|
||||
if (cafe is null)
|
||||
{
|
||||
cafe = new Cafe
|
||||
{
|
||||
Id = spec.Id,
|
||||
Name = spec.Name,
|
||||
NameEn = spec.Slug.Replace('-', ' '),
|
||||
Slug = spec.Slug,
|
||||
City = spec.City,
|
||||
Address = spec.Address,
|
||||
Description = spec.Description,
|
||||
PlanTier = spec.PlanTier,
|
||||
PreferredLanguage = "fa",
|
||||
IsVerified = true,
|
||||
DiscoverProfileJson = CafeDiscoverProfileSerializer.Serialize(spec.Profile),
|
||||
DiscoverBadgesJson = DiscoverBadgesSerializer.Serialize(spec.Badges),
|
||||
ThemeJson = CafeThemeSerializer.Serialize(new CafeTheme
|
||||
{
|
||||
PaletteId = CafeThemeDefaults.PaletteMeeziGreen,
|
||||
PanelStyle = CafeThemeDefaults.PanelModern,
|
||||
MenuStyle = CafeThemeDefaults.MenuCards,
|
||||
Density = CafeThemeDefaults.DensityComfortable,
|
||||
Radius = CafeThemeDefaults.RadiusMd
|
||||
})
|
||||
};
|
||||
db.Cafes.Add(cafe);
|
||||
|
||||
var branchId = BranchId(spec.Id);
|
||||
db.Branches.Add(new Branch
|
||||
{
|
||||
Id = branchId,
|
||||
CafeId = spec.Id,
|
||||
Name = "شعبه اصلی",
|
||||
City = spec.City,
|
||||
Address = spec.Address,
|
||||
Phone = "02100000000",
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
UpdatedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
db.Employees.Add(new Employee
|
||||
{
|
||||
Id = OwnerId(spec.Id),
|
||||
CafeId = spec.Id,
|
||||
BranchId = branchId,
|
||||
Name = $"مدیر {spec.Name}",
|
||||
Phone = spec.OwnerPhone,
|
||||
Role = EmployeeRole.Owner,
|
||||
BaseSalary = 0
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
addedCafes++;
|
||||
}
|
||||
else
|
||||
{
|
||||
var changed = false;
|
||||
if (string.IsNullOrEmpty(cafe.DiscoverProfileJson))
|
||||
{
|
||||
cafe.DiscoverProfileJson = CafeDiscoverProfileSerializer.Serialize(spec.Profile);
|
||||
changed = true;
|
||||
}
|
||||
if (string.IsNullOrEmpty(cafe.DiscoverBadgesJson) && spec.Badges is { Count: > 0 })
|
||||
{
|
||||
cafe.DiscoverBadgesJson = DiscoverBadgesSerializer.Serialize(spec.Badges);
|
||||
changed = true;
|
||||
}
|
||||
if (!cafe.IsVerified)
|
||||
{
|
||||
cafe.IsVerified = true;
|
||||
changed = true;
|
||||
}
|
||||
if (changed)
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await EnsureShowcaseMenuAsync(db, spec, logger);
|
||||
await EnsureShowcaseReviewsAsync(db, spec.Id, logger);
|
||||
}
|
||||
|
||||
if (addedCafes > 0)
|
||||
logger.LogInformation("Discover showcase seed: {Count} new cafés", addedCafes);
|
||||
}
|
||||
|
||||
private static string BranchId(string cafeId) => $"branch_{cafeId}_main";
|
||||
private static string OwnerId(string cafeId) => $"emp_{cafeId}_owner";
|
||||
|
||||
private static async Task EnsureShowcaseMenuAsync(
|
||||
AppDbContext db,
|
||||
DiscoverShowcaseCatalog.ShowcaseCafe spec,
|
||||
ILogger logger)
|
||||
{
|
||||
var taxId = $"tax_{spec.Id}";
|
||||
if (!await db.Taxes.AnyAsync(t => t.Id == taxId))
|
||||
{
|
||||
db.Taxes.Add(new Tax
|
||||
{
|
||||
Id = taxId,
|
||||
CafeId = spec.Id,
|
||||
Name = "مالیات",
|
||||
Rate = 9,
|
||||
IsDefault = true,
|
||||
IsRequired = true,
|
||||
IsCompound = false
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var templateIndex = spec.MenuTemplateIndex % DiscoverShowcaseMenus.Templates.Count;
|
||||
var template = DiscoverShowcaseMenus.Templates[templateIndex];
|
||||
|
||||
var existingCats = await db.MenuCategories
|
||||
.Where(c => c.CafeId == spec.Id)
|
||||
.Select(c => c.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var catsAdded = 0;
|
||||
foreach (var cat in template.Categories)
|
||||
{
|
||||
var catId = Prefixed(spec.Id, cat.Id);
|
||||
if (existingCats.Contains(catId))
|
||||
continue;
|
||||
|
||||
db.MenuCategories.Add(new MenuCategory
|
||||
{
|
||||
Id = catId,
|
||||
CafeId = spec.Id,
|
||||
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
|
||||
});
|
||||
catsAdded++;
|
||||
}
|
||||
|
||||
if (catsAdded > 0)
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var existingItems = await db.MenuItems
|
||||
.Where(i => i.CafeId == spec.Id)
|
||||
.Select(i => i.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var itemsAdded = 0;
|
||||
foreach (var item in template.Items)
|
||||
{
|
||||
var itemId = Prefixed(spec.Id, item.Id);
|
||||
if (existingItems.Contains(itemId))
|
||||
continue;
|
||||
|
||||
var catId = Prefixed(spec.Id, item.CategoryId);
|
||||
db.MenuItems.Add(new MenuItem
|
||||
{
|
||||
Id = itemId,
|
||||
CafeId = spec.Id,
|
||||
CategoryId = catId,
|
||||
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 (itemsAdded > 0)
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation(
|
||||
"Showcase menu: cafe {Slug} +{Cats} cats +{Items} items",
|
||||
spec.Slug,
|
||||
catsAdded,
|
||||
itemsAdded);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task EnsureShowcaseReviewsAsync(AppDbContext db, string cafeId, ILogger logger)
|
||||
{
|
||||
if (await db.CafeReviews.CountAsync(r => r.CafeId == cafeId) >= 2)
|
||||
return;
|
||||
|
||||
var rng = new Random(cafeId.GetHashCode(StringComparison.Ordinal));
|
||||
var count = 2 - await db.CafeReviews.CountAsync(r => r.CafeId == cafeId);
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
db.CafeReviews.Add(new CafeReview
|
||||
{
|
||||
CafeId = cafeId,
|
||||
AuthorName = ReviewAuthors[rng.Next(ReviewAuthors.Length)],
|
||||
Rating = rng.Next(4, 6),
|
||||
Comment = ReviewComments[rng.Next(ReviewComments.Length)],
|
||||
CreatedAt = DateTime.UtcNow.AddDays(-rng.Next(1, 30))
|
||||
});
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogDebug("Showcase reviews seeded for {CafeId}", cafeId);
|
||||
}
|
||||
|
||||
private static string Prefixed(string cafeId, string seedId) => $"{cafeId}_{seedId}";
|
||||
}
|
||||
Reference in New Issue
Block a user