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:
soroush.asadi
2026-05-27 21:33:48 +03:30
parent 03376b3ea1
commit ef15fd6247
472 changed files with 120358 additions and 0 deletions
@@ -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}";
}