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,401 @@
|
||||
using Meezi.Core.Branding;
|
||||
using Meezi.Core.Discover;
|
||||
using Meezi.Core.Entities;
|
||||
using Meezi.Core.Enums;
|
||||
using Meezi.Infrastructure.Branding;
|
||||
using Meezi.Infrastructure.Discover;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Meezi.Infrastructure.Data;
|
||||
|
||||
public static class DevelopmentDataSeeder
|
||||
{
|
||||
public static async Task SeedAsync(IServiceProvider services)
|
||||
{
|
||||
var env = services.GetRequiredService<IHostEnvironment>();
|
||||
if (!env.IsDevelopment())
|
||||
return;
|
||||
|
||||
var logger = services.GetRequiredService<ILoggerFactory>().CreateLogger("DevelopmentDataSeeder");
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
var cafe = await db.Cafes.FirstOrDefaultAsync(c => c.Slug == "demo-cafe");
|
||||
if (cafe is null)
|
||||
{
|
||||
cafe = new Cafe
|
||||
{
|
||||
Id = "cafe_demo_001",
|
||||
Name = "کافه دمو",
|
||||
NameEn = "Demo Cafe",
|
||||
Slug = "demo-cafe",
|
||||
City = "تهران",
|
||||
Address = "تهران، خیابان ولیعصر",
|
||||
Description = "کافه دمو میزی — مناسب کار، میهمانی و قهوه تخصصی.",
|
||||
PlanTier = PlanTier.Pro,
|
||||
PreferredLanguage = "fa",
|
||||
IsVerified = true,
|
||||
SnappfoodVendorId = "demo_vendor"
|
||||
};
|
||||
|
||||
var owner = new Employee
|
||||
{
|
||||
Id = "emp_demo_owner",
|
||||
CafeId = cafe.Id,
|
||||
BranchId = "branch_demo_main",
|
||||
Name = "مدیر دمو",
|
||||
Phone = "09121234567",
|
||||
Role = EmployeeRole.Owner,
|
||||
BaseSalary = 0
|
||||
};
|
||||
|
||||
db.Cafes.Add(cafe);
|
||||
db.Employees.Add(owner);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
logger.LogInformation("Development seed: cafe slug={Slug}, owner phone={Phone}", cafe.Slug, owner.Phone);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(cafe.SnappfoodVendorId))
|
||||
{
|
||||
cafe.SnappfoodVendorId = "demo_vendor";
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(cafe.ThemeJson))
|
||||
{
|
||||
cafe.ThemeJson = CafeThemeSerializer.Serialize(new CafeTheme
|
||||
{
|
||||
PaletteId = CafeThemeDefaults.PaletteMeeziGreen,
|
||||
PanelStyle = CafeThemeDefaults.PanelModern,
|
||||
MenuStyle = CafeThemeDefaults.MenuCards,
|
||||
Density = CafeThemeDefaults.DensityComfortable,
|
||||
Radius = CafeThemeDefaults.RadiusMd
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(cafe.DiscoverProfileJson))
|
||||
{
|
||||
cafe.DiscoverProfileJson = CafeDiscoverProfileSerializer.Serialize(new CafeDiscoverProfile
|
||||
{
|
||||
Themes = ["modern", "plants_heavy"],
|
||||
Size = "cozy",
|
||||
Vibes = ["quiet", "cozy"],
|
||||
Occasions = ["date", "friends", "study_work"],
|
||||
SpaceFeatures = ["indoor", "wifi", "plants"],
|
||||
NoiseLevel = "quiet",
|
||||
PriceTier = "mid"
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await SeedDemoReviewsAsync(db, cafe.Id, logger);
|
||||
await SeedDemoBranchAsync(db, cafe.Id, logger);
|
||||
await SeedDemoOpenShiftsAsync(db, cafe.Id, logger);
|
||||
|
||||
var ownerEmp = await db.Employees.FirstOrDefaultAsync(e => e.Id == "emp_demo_owner");
|
||||
if (ownerEmp is not null && ownerEmp.BranchId is null)
|
||||
{
|
||||
ownerEmp.BranchId = "branch_demo_main";
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
await DemoEmployeesSeeder.EnsureEmployeesAsync(db, cafe.Id, logger);
|
||||
|
||||
const string taxId = "tax_demo_vat";
|
||||
await DemoMenuSeeder.EnsureMenuAsync(db, cafe.Id, taxId, logger);
|
||||
await SeedDemoInventoryAsync(db, cafe.Id, logger);
|
||||
await DemoCouponSeeder.EnsureCouponsAsync(db, cafe.Id, logger);
|
||||
await SeedDemoTablesAsync(db, cafe.Id, logger);
|
||||
|
||||
if (!await db.EmployeeSchedules.AnyAsync(s => s.EmployeeId == "emp_demo_owner"))
|
||||
{
|
||||
for (var day = 0; day <= 6; day++)
|
||||
{
|
||||
db.EmployeeSchedules.Add(new EmployeeSchedule
|
||||
{
|
||||
EmployeeId = "emp_demo_owner",
|
||||
DayOfWeek = day,
|
||||
ShiftType = day is 5 ? ShiftType.DayOff : ShiftType.Morning
|
||||
});
|
||||
}
|
||||
|
||||
var monthYear = DateTime.UtcNow.ToString("yyyy-MM");
|
||||
db.EmployeeSalaries.Add(new EmployeeSalary
|
||||
{
|
||||
EmployeeId = "emp_demo_owner",
|
||||
MonthYear = monthYear,
|
||||
BaseSalary = 25_000_000,
|
||||
OvertimePay = 0,
|
||||
Deductions = 0,
|
||||
NetSalary = 25_000_000,
|
||||
IsPaid = false
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Development HR seed for cafe {CafeId}", cafe.Id);
|
||||
}
|
||||
|
||||
await SeedDemoOrdersAsync(db, cafe.Id, logger);
|
||||
await DiscoverShowcaseSeeder.SeedAsync(db, logger);
|
||||
}
|
||||
|
||||
private static async Task SeedDemoBranchAsync(AppDbContext db, string cafeId, ILogger logger)
|
||||
{
|
||||
const string branchId = "branch_demo_main";
|
||||
if (!await db.Branches.AnyAsync(b => b.Id == branchId))
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
db.Branches.Add(new Branch
|
||||
{
|
||||
Id = branchId,
|
||||
CafeId = cafeId,
|
||||
Name = "شعبه اصلی",
|
||||
City = "تهران",
|
||||
Address = "تهران، خیابان ولیعصر",
|
||||
Phone = "02112345678",
|
||||
IsActive = true,
|
||||
CreatedAt = now,
|
||||
UpdatedAt = now
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Development branch seed: {BranchId}", branchId);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SeedDemoOpenShiftsAsync(AppDbContext db, string cafeId, ILogger logger)
|
||||
{
|
||||
const string ownerId = "emp_demo_owner";
|
||||
var branchIds = await db.Branches
|
||||
.Where(b => b.CafeId == cafeId && b.IsActive)
|
||||
.Select(b => b.Id)
|
||||
.ToListAsync();
|
||||
|
||||
var added = false;
|
||||
foreach (var branchId in branchIds)
|
||||
{
|
||||
var hasOpen = await db.RegisterShifts.AnyAsync(
|
||||
s => s.CafeId == cafeId && s.BranchId == branchId && s.Status == ShiftStatus.Open);
|
||||
if (hasOpen)
|
||||
continue;
|
||||
|
||||
db.RegisterShifts.Add(new Shift
|
||||
{
|
||||
Id = $"shift_open_{branchId}",
|
||||
CafeId = cafeId,
|
||||
BranchId = branchId,
|
||||
OpenedByUserId = ownerId,
|
||||
OpenedAt = DateTime.UtcNow,
|
||||
OpeningCash = 0,
|
||||
ExpectedCash = 0,
|
||||
Status = ShiftStatus.Open
|
||||
});
|
||||
added = true;
|
||||
}
|
||||
|
||||
if (added)
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Development open-shift seed for cafe {CafeId}", cafeId);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task SeedDemoInventoryAsync(AppDbContext db, string cafeId, ILogger logger)
|
||||
{
|
||||
if (await db.Ingredients.AnyAsync(i => i.CafeId == cafeId))
|
||||
return;
|
||||
|
||||
db.Ingredients.AddRange(
|
||||
new Ingredient
|
||||
{
|
||||
Id = "ing_demo_milk",
|
||||
CafeId = cafeId,
|
||||
Name = "شیر",
|
||||
Unit = "میلیلیتر",
|
||||
QuantityOnHand = 12000,
|
||||
ReorderLevel = 2000,
|
||||
ParLevel = 12000,
|
||||
UnitCost = 80,
|
||||
LowStockWarningPercent = 20
|
||||
},
|
||||
new Ingredient
|
||||
{
|
||||
Id = "ing_demo_coffee",
|
||||
CafeId = cafeId,
|
||||
Name = "پودر قهوه",
|
||||
Unit = "گرم",
|
||||
QuantityOnHand = 500,
|
||||
ReorderLevel = 100,
|
||||
ParLevel = 500,
|
||||
UnitCost = 12,
|
||||
LowStockWarningPercent = 20
|
||||
},
|
||||
new Ingredient
|
||||
{
|
||||
Id = "ing_demo_cups",
|
||||
CafeId = cafeId,
|
||||
Name = "لیوان یکبارمصرف",
|
||||
Unit = "عدد",
|
||||
QuantityOnHand = 80,
|
||||
ReorderLevel = 20,
|
||||
ParLevel = 100,
|
||||
UnitCost = 500,
|
||||
LowStockWarningPercent = 20
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var espresso = await db.MenuItems
|
||||
.FirstOrDefaultAsync(m => m.CafeId == cafeId && m.Name.Contains("اسپرسو"));
|
||||
if (espresso is not null)
|
||||
{
|
||||
db.MenuItemIngredients.AddRange(
|
||||
new MenuItemIngredient
|
||||
{
|
||||
Id = "mii_demo_espresso_coffee",
|
||||
CafeId = cafeId,
|
||||
MenuItemId = espresso.Id,
|
||||
IngredientId = "ing_demo_coffee",
|
||||
QuantityPerUnit = 10
|
||||
},
|
||||
new MenuItemIngredient
|
||||
{
|
||||
Id = "mii_demo_espresso_cup",
|
||||
CafeId = cafeId,
|
||||
MenuItemId = espresso.Id,
|
||||
IngredientId = "ing_demo_cups",
|
||||
QuantityPerUnit = 1
|
||||
});
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
logger.LogInformation("Development inventory seed for cafe {CafeId}", cafeId);
|
||||
}
|
||||
|
||||
private static async Task SeedDemoTablesAsync(AppDbContext db, string cafeId, ILogger logger)
|
||||
{
|
||||
var specs = new (string Id, string Number, int Capacity, string? Floor, string? QrCode)[]
|
||||
{
|
||||
("table_demo_1", "1", 4, "همکف", "demo_table_01"),
|
||||
("table_demo_2", "2", 2, "همکف", null),
|
||||
("table_demo_3", "3", 4, "همکف", null),
|
||||
("table_demo_4", "4", 6, "بالکن", null),
|
||||
("table_demo_5", "5", 4, "بالکن", null),
|
||||
("table_demo_6", "6", 2, "بالکن", null),
|
||||
("table_demo_7", "7", 8, "سالن VIP", null),
|
||||
("table_demo_8", "8", 4, "سالن VIP", null),
|
||||
};
|
||||
|
||||
foreach (var s in specs)
|
||||
{
|
||||
if (await db.Tables.AnyAsync(t => t.Id == s.Id))
|
||||
continue;
|
||||
|
||||
db.Tables.Add(new Table
|
||||
{
|
||||
Id = s.Id,
|
||||
CafeId = cafeId,
|
||||
BranchId = "branch_demo_main",
|
||||
Number = s.Number,
|
||||
Capacity = s.Capacity,
|
||||
Floor = s.Floor,
|
||||
QrCode = s.QrCode ?? Guid.NewGuid().ToString("N"),
|
||||
IsActive = true
|
||||
});
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Development tables seed (8 tables, QR: demo_table_01) for cafe {CafeId}", cafeId);
|
||||
}
|
||||
|
||||
private static async Task SeedDemoReviewsAsync(AppDbContext db, string cafeId, ILogger logger)
|
||||
{
|
||||
if (await db.CafeReviews.AnyAsync(r => r.CafeId == cafeId))
|
||||
return;
|
||||
|
||||
db.CafeReviews.AddRange(
|
||||
new CafeReview
|
||||
{
|
||||
CafeId = cafeId,
|
||||
AuthorName = "سارا",
|
||||
Rating = 5,
|
||||
Comment = "قهوه و فضا عالی بود.",
|
||||
CreatedAt = DateTime.UtcNow.AddDays(-3)
|
||||
},
|
||||
new CafeReview
|
||||
{
|
||||
CafeId = cafeId,
|
||||
AuthorName = "علی",
|
||||
Rating = 4,
|
||||
Comment = "سرویس سریع، کیک خوشمزه.",
|
||||
CreatedAt = DateTime.UtcNow.AddDays(-1)
|
||||
});
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Development reviews seed for cafe {CafeId}", cafeId);
|
||||
}
|
||||
|
||||
private static async Task SeedDemoOrdersAsync(AppDbContext db, string cafeId, ILogger logger)
|
||||
{
|
||||
if (await db.Orders.AnyAsync(o => o.CafeId == cafeId))
|
||||
return;
|
||||
|
||||
var latte = await db.MenuItems.FirstOrDefaultAsync(m => m.Id == "item_demo_latte");
|
||||
var cake = await db.MenuItems.FirstOrDefaultAsync(m => m.Id == "item_demo_cake");
|
||||
if (latte is null || cake is null)
|
||||
return;
|
||||
|
||||
var customer = new Customer
|
||||
{
|
||||
Id = "cust_demo_reports",
|
||||
CafeId = cafeId,
|
||||
Name = "مشتری گزارش",
|
||||
Phone = "09120000001",
|
||||
Group = CustomerGroup.Regular,
|
||||
CreatedAt = DateTime.UtcNow.AddDays(-10)
|
||||
};
|
||||
db.Customers.Add(customer);
|
||||
|
||||
for (var i = 0; i < 7; i++)
|
||||
{
|
||||
var createdAt = DateTime.UtcNow.AddDays(-i).AddHours(-2);
|
||||
var subtotal = 215_000m;
|
||||
var tax = Math.Round(subtotal * 0.09m, 0);
|
||||
var order = new Order
|
||||
{
|
||||
Id = $"order_demo_{i}",
|
||||
CafeId = cafeId,
|
||||
CustomerId = i % 2 == 0 ? customer.Id : null,
|
||||
OrderType = OrderType.DineIn,
|
||||
Status = OrderStatus.Delivered,
|
||||
DisplayNumber = i + 1,
|
||||
Subtotal = subtotal,
|
||||
TaxTotal = tax,
|
||||
DiscountAmount = i == 0 ? 15_000m : 0,
|
||||
Total = subtotal + tax - (i == 0 ? 15_000m : 0),
|
||||
CreatedAt = createdAt
|
||||
};
|
||||
db.Orders.Add(order);
|
||||
db.OrderItems.AddRange(
|
||||
new OrderItem
|
||||
{
|
||||
OrderId = order.Id,
|
||||
MenuItemId = latte.Id,
|
||||
Quantity = 1,
|
||||
UnitPrice = latte.Price
|
||||
},
|
||||
new OrderItem
|
||||
{
|
||||
OrderId = order.Id,
|
||||
MenuItemId = cake.Id,
|
||||
Quantity = 1,
|
||||
UnitPrice = cake.Price
|
||||
});
|
||||
}
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Development reports seed: 7 demo orders for cafe {CafeId}", cafeId);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user