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,153 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Meezi.Core.Branding;
namespace Meezi.Infrastructure.Branding;
public static partial class CafeThemeSerializer
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true
};
private static readonly HashSet<string> ValidPalettes =
[
"meezi-green", "ocean-blue", "royal-purple", "sunset-orange", "rose-blush",
"charcoal-gold", "espresso", "forest", "midnight", "coral", "gold-luxury",
"mint-fresh", "wine-bar", "slate-modern", "cherry", "teal-wave", "sand-cafe"
];
private static readonly HashSet<string> ValidPanelStyles =
[
CafeThemeDefaults.PanelFlat, CafeThemeDefaults.PanelModern, CafeThemeDefaults.PanelGlass,
CafeThemeDefaults.PanelMinimal, CafeThemeDefaults.PanelBold, CafeThemeDefaults.PanelSoft,
CafeThemeDefaults.PanelElevated, CafeThemeDefaults.PanelOutline
];
private static readonly HashSet<string> ValidMenuStyles =
[
CafeThemeDefaults.MenuCards, CafeThemeDefaults.MenuCompact, CafeThemeDefaults.MenuGrid,
CafeThemeDefaults.MenuList, CafeThemeDefaults.MenuMagazine, CafeThemeDefaults.MenuClassic
];
private static readonly HashSet<string> ValidMenuTextures =
[
CafeThemeDefaults.MenuTextureNone, CafeThemeDefaults.MenuTexturePaper,
CafeThemeDefaults.MenuTextureLinen, CafeThemeDefaults.MenuTextureDots,
CafeThemeDefaults.MenuTextureGrid, CafeThemeDefaults.MenuTextureMarble,
CafeThemeDefaults.MenuTextureWood, CafeThemeDefaults.MenuTextureWarm
];
private static readonly HashSet<string> ValidDensities =
[
CafeThemeDefaults.DensityCompact, CafeThemeDefaults.DensityComfortable, CafeThemeDefaults.DensitySpacious
];
private static readonly HashSet<string> ValidRadius =
[
CafeThemeDefaults.RadiusNone, CafeThemeDefaults.RadiusSm, CafeThemeDefaults.RadiusMd,
CafeThemeDefaults.RadiusLg, CafeThemeDefaults.RadiusFull
];
public static CafeTheme Parse(string? json)
{
if (string.IsNullOrWhiteSpace(json))
return new CafeTheme();
try
{
var theme = JsonSerializer.Deserialize<CafeTheme>(json, JsonOptions) ?? new CafeTheme();
return Normalize(theme);
}
catch
{
return new CafeTheme();
}
}
public static string Serialize(CafeTheme theme) =>
JsonSerializer.Serialize(Normalize(theme), JsonOptions);
public static CafeTheme Normalize(CafeTheme theme)
{
theme.PaletteId = ValidPalettes.Contains(theme.PaletteId) ? theme.PaletteId : CafeThemeDefaults.PaletteMeeziGreen;
theme.PanelStyle = ValidPanelStyles.Contains(theme.PanelStyle) ? theme.PanelStyle : CafeThemeDefaults.PanelModern;
theme.MenuStyle = ValidMenuStyles.Contains(theme.MenuStyle) ? theme.MenuStyle : CafeThemeDefaults.MenuCards;
theme.MenuTexture = ValidMenuTextures.Contains(theme.MenuTexture)
? theme.MenuTexture
: CafeThemeDefaults.MenuTextureNone;
theme.Density = ValidDensities.Contains(theme.Density) ? theme.Density : CafeThemeDefaults.DensityComfortable;
theme.Radius = ValidRadius.Contains(theme.Radius) ? theme.Radius : CafeThemeDefaults.RadiusMd;
theme.Custom = NormalizeCustom(theme.Custom);
return theme;
}
private static CafeThemeCustomColors? NormalizeCustom(CafeThemeCustomColors? custom)
{
if (custom is null) return null;
var normalized = new CafeThemeCustomColors
{
Primary = NormalizeHex(custom.Primary),
Secondary = NormalizeHex(custom.Secondary),
Accent = NormalizeHex(custom.Accent),
Background = NormalizeHex(custom.Background),
Surface = NormalizeHex(custom.Surface),
Text = NormalizeHex(custom.Text),
TextMuted = NormalizeHex(custom.TextMuted),
Destructive = NormalizeHex(custom.Destructive),
Success = NormalizeHex(custom.Success),
PrimaryOpacity = NormalizeOpacity(custom.PrimaryOpacity),
SecondaryOpacity = NormalizeOpacity(custom.SecondaryOpacity),
AccentOpacity = NormalizeOpacity(custom.AccentOpacity),
BackgroundOpacity = NormalizeOpacity(custom.BackgroundOpacity),
SurfaceOpacity = NormalizeOpacity(custom.SurfaceOpacity),
TextOpacity = NormalizeOpacity(custom.TextOpacity),
TextMutedOpacity = NormalizeOpacity(custom.TextMutedOpacity),
DestructiveOpacity = NormalizeOpacity(custom.DestructiveOpacity),
SuccessOpacity = NormalizeOpacity(custom.SuccessOpacity)
};
return normalized.Primary is null
&& normalized.Secondary is null
&& normalized.Accent is null
&& normalized.Background is null
&& normalized.Surface is null
&& normalized.Text is null
&& normalized.TextMuted is null
&& normalized.Destructive is null
&& normalized.Success is null
&& normalized.PrimaryOpacity is null
&& normalized.SecondaryOpacity is null
&& normalized.AccentOpacity is null
&& normalized.BackgroundOpacity is null
&& normalized.SurfaceOpacity is null
&& normalized.TextOpacity is null
&& normalized.TextMutedOpacity is null
&& normalized.DestructiveOpacity is null
&& normalized.SuccessOpacity is null
? null
: normalized;
}
private static string? NormalizeHex(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
var v = value.Trim();
if (!HexColorRegex().IsMatch(v)) return null;
return v.StartsWith('#') ? v.ToUpperInvariant() : $"#{v.ToUpperInvariant()}";
}
private static int? NormalizeOpacity(int? value)
{
if (value is null) return null;
return Math.Clamp(value.Value, 0, 100);
}
[GeneratedRegex(@"^#?[0-9A-Fa-f]{6}$", RegexOptions.Compiled)]
private static partial Regex HexColorRegex();
}
@@ -0,0 +1,605 @@
using System.Text.Json;
using Meezi.Core.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Meezi.Infrastructure.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<Cafe> Cafes => Set<Cafe>();
public DbSet<Branch> Branches => Set<Branch>();
public DbSet<Table> Tables => Set<Table>();
public DbSet<TableSection> TableSections => Set<TableSection>();
public DbSet<Employee> Employees => Set<Employee>();
public DbSet<MenuCategory> MenuCategories => Set<MenuCategory>();
public DbSet<MenuItem> MenuItems => Set<MenuItem>();
public DbSet<BranchMenuItemOverride> BranchMenuItemOverrides => Set<BranchMenuItemOverride>();
public DbSet<Order> Orders => Set<Order>();
public DbSet<OrderItem> OrderItems => Set<OrderItem>();
public DbSet<Payment> Payments => Set<Payment>();
public DbSet<Customer> Customers => Set<Customer>();
public DbSet<Coupon> Coupons => Set<Coupon>();
public DbSet<Tax> Taxes => Set<Tax>();
public DbSet<EmployeeSalary> EmployeeSalaries => Set<EmployeeSalary>();
public DbSet<Attendance> Attendances => Set<Attendance>();
public DbSet<EmployeeSchedule> EmployeeSchedules => Set<EmployeeSchedule>();
public DbSet<Shift> RegisterShifts => Set<Shift>();
public DbSet<CashTransaction> CashTransactions => Set<CashTransaction>();
public DbSet<LeaveRequest> LeaveRequests => Set<LeaveRequest>();
public DbSet<TableReservation> TableReservations => Set<TableReservation>();
public DbSet<CafeReview> CafeReviews => Set<CafeReview>();
public DbSet<CafeReviewPhoto> CafeReviewPhotos => Set<CafeReviewPhoto>();
public DbSet<ConsumerAccount> ConsumerAccounts => Set<ConsumerAccount>();
public DbSet<KitchenStation> KitchenStations => Set<KitchenStation>();
public DbSet<SubscriptionPayment> SubscriptionPayments => Set<SubscriptionPayment>();
public DbSet<Ingredient> Ingredients => Set<Ingredient>();
public DbSet<MenuItemIngredient> MenuItemIngredients => Set<MenuItemIngredient>();
public DbSet<StockMovement> StockMovements => Set<StockMovement>();
public DbSet<QueueTicket> QueueTickets => Set<QueueTicket>();
public DbSet<DailyReport> DailyReports => Set<DailyReport>();
public DbSet<Expense> Expenses => Set<Expense>();
public DbSet<WebhookLog> WebhookLogs => Set<WebhookLog>();
public DbSet<DeliveryCommissionRate> DeliveryCommissionRates => Set<DeliveryCommissionRate>();
public DbSet<SystemAdmin> SystemAdmins => Set<SystemAdmin>();
public DbSet<PlatformPlanDefinition> PlatformPlanDefinitions => Set<PlatformPlanDefinition>();
public DbSet<PlatformSetting> PlatformSettings => Set<PlatformSetting>();
public DbSet<PlatformFeature> PlatformFeatures => Set<PlatformFeature>();
public DbSet<CafeFeatureOverride> CafeFeatureOverrides => Set<CafeFeatureOverride>();
public DbSet<SupportTicket> SupportTickets => Set<SupportTicket>();
public DbSet<SupportTicketMessage> SupportTicketMessages => Set<SupportTicketMessage>();
public DbSet<CafeNotification> CafeNotifications => Set<CafeNotification>();
// Website CMS
public DbSet<WebsiteBlogPost> WebsiteBlogPosts => Set<WebsiteBlogPost>();
public DbSet<WebsiteComment> WebsiteComments => Set<WebsiteComment>();
public DbSet<DemoRequest> DemoRequests => Set<DemoRequest>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Cafe>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => x.Slug).IsUnique();
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
e.Property(x => x.Slug).HasMaxLength(100).IsRequired();
e.Property(x => x.SnappfoodVendorId).HasMaxLength(100);
e.Property(x => x.Tap30VendorId).HasMaxLength(100);
e.Property(x => x.DigikalaVendorId).HasMaxLength(100);
e.Property(x => x.ThemeJson).HasMaxLength(8000);
e.Property(x => x.DiscoverProfileJson).HasMaxLength(8000);
e.Property(x => x.DiscoverBadgesJson).HasMaxLength(2000);
e.Property(x => x.DefaultTaxRate).HasPrecision(5, 2);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Branch>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
e.Property(x => x.Address).HasMaxLength(500);
e.Property(x => x.City).HasMaxLength(100);
e.Property(x => x.Phone).HasMaxLength(20);
e.Property(x => x.ReceiptPrinterIp).HasMaxLength(45);
e.Property(x => x.KitchenPrinterIp).HasMaxLength(45);
e.Property(x => x.PosDeviceIp).HasMaxLength(45);
e.Property(x => x.ReceiptHeader).HasMaxLength(500);
e.Property(x => x.ReceiptFooter).HasMaxLength(500);
e.Property(x => x.WifiPassword).HasMaxLength(100);
e.HasIndex(x => new { x.CafeId, x.IsActive });
e.HasOne(x => x.Cafe).WithMany(c => c.Branches).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<TableSection>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Name).HasMaxLength(100).IsRequired();
e.HasIndex(x => new { x.BranchId, x.Name });
e.HasIndex(x => x.CafeId);
e.HasOne(x => x.Branch).WithMany(b => b.Sections).HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Table>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Number).HasMaxLength(50).IsRequired();
e.Property(x => x.BranchId).IsRequired();
e.Property(x => x.SortOrder).HasDefaultValue(0);
e.HasIndex(x => x.QrCode).IsUnique();
e.HasIndex(x => new { x.BranchId, x.SectionId, x.SortOrder });
e.HasOne(x => x.Cafe).WithMany(c => c.Tables).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Branch).WithMany(b => b.Tables).HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Restrict);
e.HasOne(x => x.Section).WithMany(s => s.Tables).HasForeignKey(x => x.SectionId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Employee>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => new { x.CafeId, x.Phone })
.IsUnique()
.HasFilter("\"DeletedAt\" IS NULL");
e.HasIndex(x => x.BranchId);
e.HasOne(x => x.Cafe).WithMany(c => c.Employees).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Branch).WithMany(b => b.Staff).HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<MenuCategory>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Icon).HasMaxLength(32);
e.Property(x => x.IconPresetId).HasMaxLength(48);
e.Property(x => x.IconStyle).HasMaxLength(16);
e.Property(x => x.ImageUrl).HasMaxLength(500);
e.HasOne(x => x.Cafe).WithMany(c => c.MenuCategories).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Tax).WithMany(t => t.MenuCategories).HasForeignKey(x => x.TaxId).OnDelete(DeleteBehavior.SetNull);
e.HasOne(x => x.KitchenStation).WithMany(s => s.Categories).HasForeignKey(x => x.KitchenStationId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<MenuItem>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Model3dUrl).HasMaxLength(500);
e.Property(x => x.Price).HasPrecision(18, 2);
e.HasOne(x => x.Cafe).WithMany(c => c.MenuItems).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Category).WithMany(c => c.MenuItems).HasForeignKey(x => x.CategoryId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<BranchMenuItemOverride>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.PriceOverride).HasPrecision(18, 2);
e.HasIndex(x => new { x.BranchId, x.MenuItemId }).IsUnique();
e.HasIndex(x => x.CafeId);
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.MenuItem).WithMany(m => m.BranchOverrides).HasForeignKey(x => x.MenuItemId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Order>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Subtotal).HasPrecision(18, 2);
e.Property(x => x.TaxTotal).HasPrecision(18, 2);
e.Property(x => x.Total).HasPrecision(18, 2);
e.Property(x => x.DiscountAmount).HasPrecision(18, 2);
e.Property(x => x.PlatformCommission).HasPrecision(18, 2);
e.Property(x => x.ExternalOrderId).HasMaxLength(120);
e.Property(x => x.DeliveryMetaJson).HasMaxLength(4000);
e.Property(x => x.GuestTrackingToken).HasMaxLength(64);
e.HasIndex(x => x.GuestTrackingToken);
e.HasIndex(x => new { x.CafeId, x.DisplayNumber }).IsUnique();
e.HasIndex(x => new { x.CafeId, x.DeliveryPlatform, x.ExternalOrderId });
e.HasOne(x => x.Cafe).WithMany(c => c.Orders).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Branch).WithMany(b => b.Orders).HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull);
e.HasOne(x => x.Table).WithMany(t => t.Orders).HasForeignKey(x => x.TableId).OnDelete(DeleteBehavior.SetNull);
e.HasOne(x => x.Reservation).WithMany().HasForeignKey(x => x.ReservationId).OnDelete(DeleteBehavior.SetNull);
e.HasOne(x => x.Customer).WithMany(c => c.Orders).HasForeignKey(x => x.CustomerId).OnDelete(DeleteBehavior.SetNull);
e.HasOne(x => x.Employee).WithMany(emp => emp.Orders).HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.SetNull);
e.HasOne(x => x.Coupon).WithMany(c => c.Orders).HasForeignKey(x => x.CouponId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<OrderItem>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.UnitPrice).HasPrecision(18, 2);
e.HasOne(x => x.Order).WithMany(o => o.Items).HasForeignKey(x => x.OrderId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.MenuItem).WithMany(m => m.OrderItems).HasForeignKey(x => x.MenuItemId).OnDelete(DeleteBehavior.Restrict);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Payment>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Amount).HasPrecision(18, 2);
e.HasOne(x => x.Order).WithMany(o => o.Payments).HasForeignKey(x => x.OrderId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Customer>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => new { x.CafeId, x.Phone });
e.HasOne(x => x.Cafe).WithMany(c => c.Customers).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Coupon>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => new { x.CafeId, x.Code }).IsUnique();
e.Property(x => x.Value).HasPrecision(18, 2);
e.HasOne(x => x.Cafe).WithMany(c => c.Coupons).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Tax>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Rate).HasPrecision(5, 2);
e.HasOne(x => x.Cafe).WithMany(c => c.Taxes).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<EmployeeSalary>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => new { x.EmployeeId, x.MonthYear }).IsUnique();
e.HasOne(x => x.Employee).WithMany(e => e.Salaries).HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Attendance>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => new { x.EmployeeId, x.Date }).IsUnique();
e.HasOne(x => x.Employee).WithMany(e => e.Attendances).HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<EmployeeSchedule>(e =>
{
e.HasKey(x => x.Id);
e.ToTable("EmployeeSchedules");
e.HasIndex(x => new { x.EmployeeId, x.DayOfWeek }).IsUnique();
e.HasOne(x => x.Employee).WithMany(e => e.Schedules).HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Shift>(e =>
{
e.HasKey(x => x.Id);
e.ToTable("RegisterShifts");
e.Property(x => x.OpeningCash).HasPrecision(18, 2);
e.Property(x => x.ClosingCash).HasPrecision(18, 2);
e.Property(x => x.ExpectedCash).HasPrecision(18, 2);
e.Property(x => x.Discrepancy).HasPrecision(18, 2);
e.HasIndex(x => new { x.BranchId, x.Status });
e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.OpenedBy).WithMany().HasForeignKey(x => x.OpenedByUserId).OnDelete(DeleteBehavior.Restrict);
e.HasOne(x => x.ClosedBy).WithMany().HasForeignKey(x => x.ClosedByUserId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<CashTransaction>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Amount).HasPrecision(18, 2);
e.HasIndex(x => x.ShiftId);
e.HasIndex(x => new { x.CafeId, x.BranchId });
e.HasOne(x => x.Shift).WithMany(s => s.Transactions).HasForeignKey(x => x.ShiftId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<LeaveRequest>(e =>
{
e.HasKey(x => x.Id);
e.HasOne(x => x.Employee).WithMany(e => e.LeaveRequests).HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<TableReservation>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => new { x.CafeId, x.Date, x.Time });
e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Table).WithMany().HasForeignKey(x => x.TableId).OnDelete(DeleteBehavior.SetNull);
e.HasOne(x => x.Customer).WithMany().HasForeignKey(x => x.CustomerId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<CafeReview>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => new { x.CafeId, x.CreatedAt });
e.Property(x => x.AuthorName).HasMaxLength(200).IsRequired();
e.HasOne(x => x.Cafe).WithMany(c => c.Reviews).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<CafeReviewPhoto>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Url).HasMaxLength(500).IsRequired();
e.HasIndex(x => x.ReviewId);
e.HasOne(x => x.Review).WithMany(r => r.Photos).HasForeignKey(x => x.ReviewId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<ConsumerAccount>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Phone).HasMaxLength(20).IsRequired();
e.HasIndex(x => x.Phone).IsUnique();
e.Property(x => x.Name).HasMaxLength(200);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<KitchenStation>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Name).HasMaxLength(100).IsRequired();
e.Property(x => x.PrinterIp).HasMaxLength(45);
e.HasIndex(x => new { x.CafeId, x.SortOrder });
e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<SubscriptionPayment>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => x.Authority);
e.Property(x => x.AmountToman).HasPrecision(18, 2);
e.HasOne(x => x.Cafe).WithMany(c => c.SubscriptionPayments).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Ingredient>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
e.Property(x => x.Unit).HasMaxLength(50).IsRequired();
e.Property(x => x.QuantityOnHand).HasPrecision(18, 3);
e.Property(x => x.ReorderLevel).HasPrecision(18, 3);
e.Property(x => x.UnitCost).HasPrecision(18, 2);
e.Property(x => x.ParLevel).HasPrecision(18, 3);
e.Property(x => x.LowStockWarningPercent).HasPrecision(5, 2);
e.HasOne(x => x.Cafe).WithMany(c => c.Ingredients).HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<MenuItemIngredient>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.QuantityPerUnit).HasPrecision(18, 3);
e.HasIndex(x => new { x.CafeId, x.MenuItemId, x.IngredientId }).IsUnique();
e.HasOne(x => x.MenuItem).WithMany(m => m.RecipeIngredients).HasForeignKey(x => x.MenuItemId)
.OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Ingredient).WithMany(i => i.MenuItemRecipes).HasForeignKey(x => x.IngredientId)
.OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<StockMovement>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Delta).HasPrecision(18, 3);
e.Property(x => x.TotalCostToman).HasPrecision(18, 2);
e.Property(x => x.ExpenseId).HasMaxLength(64);
e.Property(x => x.BranchId).HasMaxLength(64);
e.Property(x => x.Kind).HasConversion<string>().HasMaxLength(30);
e.Property(x => x.OrderId).HasMaxLength(64);
e.HasIndex(x => new { x.CafeId, x.OrderId });
e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Ingredient).WithMany(i => i.Movements).HasForeignKey(x => x.IngredientId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<QueueTicket>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => new { x.CafeId, x.BranchId, x.ServiceDate, x.Number }).IsUnique();
e.Property(x => x.CustomerLabel).HasMaxLength(200);
e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.SetNull);
e.HasOne(x => x.Order).WithMany().HasForeignKey(x => x.OrderId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<Expense>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Amount).HasPrecision(18, 2);
e.Property(x => x.Note).HasMaxLength(500);
e.Property(x => x.ReceiptImageUrl).HasMaxLength(500);
e.HasIndex(x => new { x.CafeId, x.BranchId, x.CreatedAt });
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.Shift).WithMany().HasForeignKey(x => x.ShiftId).OnDelete(DeleteBehavior.SetNull);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<DailyReport>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => new { x.CafeId, x.BranchId, x.Date }).IsUnique();
e.Property(x => x.TotalRevenue).HasPrecision(18, 2);
e.Property(x => x.CashRevenue).HasPrecision(18, 2);
e.Property(x => x.CardRevenue).HasPrecision(18, 2);
e.Property(x => x.CreditRevenue).HasPrecision(18, 2);
e.Property(x => x.AvgOrderValue).HasPrecision(18, 2);
e.Property(x => x.VoidAmount).HasPrecision(18, 2);
e.Property(x => x.TotalExpenses).HasPrecision(18, 2);
e.Property(x => x.NetIncome).HasPrecision(18, 2);
var topProductsConverter = new ValueConverter<List<TopProductEntry>, string>(
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
v => JsonSerializer.Deserialize<List<TopProductEntry>>(v, JsonSerializerOptions.Default)
?? new List<TopProductEntry>());
var topProductsComparer = new ValueComparer<List<TopProductEntry>>(
(a, b) => JsonSerializer.Serialize(a, JsonSerializerOptions.Default)
== JsonSerializer.Serialize(b, JsonSerializerOptions.Default),
v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default).GetHashCode(),
v => JsonSerializer.Deserialize<List<TopProductEntry>>(
JsonSerializer.Serialize(v, JsonSerializerOptions.Default),
JsonSerializerOptions.Default)!);
e.Property(x => x.TopProducts)
.HasConversion(topProductsConverter, topProductsComparer)
.HasColumnType("jsonb");
e.HasOne(x => x.Branch).WithMany().HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<WebhookLog>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.RawBody).IsRequired();
e.Property(x => x.SignatureHeader).HasMaxLength(256);
e.Property(x => x.ErrorMessage).HasMaxLength(2000);
e.Property(x => x.ExternalOrderId).HasMaxLength(120);
e.Property(x => x.MeeziOrderId).HasMaxLength(50);
e.HasIndex(x => new { x.Platform, x.CreatedAt });
e.HasIndex(x => x.CafeId);
});
modelBuilder.Entity<DeliveryCommissionRate>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.RatePercent).HasPrecision(5, 2);
e.HasIndex(x => new { x.CafeId, x.Platform }).IsUnique();
e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<SystemAdmin>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
e.Property(x => x.Phone).HasMaxLength(20).IsRequired();
e.HasIndex(x => x.Phone).IsUnique();
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<PlatformPlanDefinition>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.DisplayNameFa).HasMaxLength(200).IsRequired();
e.Property(x => x.DisplayNameEn).HasMaxLength(200);
e.Property(x => x.MonthlyPriceToman).HasPrecision(18, 0);
e.Property(x => x.LimitsJson).HasMaxLength(4000).IsRequired();
e.Property(x => x.FeaturesJson).HasMaxLength(4000);
e.HasIndex(x => x.Tier).IsUnique();
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<PlatformSetting>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Key).HasMaxLength(120).IsRequired();
e.Property(x => x.Value).HasMaxLength(8000).IsRequired();
e.Property(x => x.Category).HasMaxLength(60).IsRequired();
e.Property(x => x.DescriptionFa).HasMaxLength(500);
e.HasIndex(x => x.Key).IsUnique();
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<PlatformFeature>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Key).HasMaxLength(80).IsRequired();
e.Property(x => x.DisplayNameFa).HasMaxLength(200).IsRequired();
e.Property(x => x.DisplayNameEn).HasMaxLength(200);
e.Property(x => x.ModuleGroup).HasMaxLength(60).IsRequired();
e.HasIndex(x => x.Key).IsUnique();
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<CafeFeatureOverride>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.FeatureKey).HasMaxLength(80).IsRequired();
e.HasIndex(x => new { x.CafeId, x.FeatureKey }).IsUnique();
e.HasOne<Cafe>().WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<SupportTicket>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Subject).HasMaxLength(300).IsRequired();
e.HasIndex(x => new { x.CafeId, x.Status, x.UpdatedAt });
e.HasOne(x => x.Cafe).WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade);
e.HasOne(x => x.CreatedByEmployee).WithMany().HasForeignKey(x => x.CreatedByEmployeeId)
.OnDelete(DeleteBehavior.Restrict);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<SupportTicketMessage>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Body).HasMaxLength(8000).IsRequired();
e.HasIndex(x => new { x.TicketId, x.CreatedAt });
e.HasOne(x => x.Ticket).WithMany(t => t.Messages).HasForeignKey(x => x.TicketId)
.OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<CafeNotification>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Type).HasMaxLength(60).IsRequired();
e.Property(x => x.Title).HasMaxLength(300).IsRequired();
e.Property(x => x.Body).HasMaxLength(1000);
e.Property(x => x.ReferenceId).HasMaxLength(64);
e.Property(x => x.TableNumber).HasMaxLength(40);
e.HasIndex(x => new { x.CafeId, x.IsRead, x.CreatedAt });
e.HasQueryFilter(x => x.DeletedAt == null);
});
// ── Website CMS ──────────────────────────────────────────────────────
modelBuilder.Entity<WebsiteBlogPost>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.Slug).HasMaxLength(200).IsRequired();
e.Property(x => x.TitleFa).HasMaxLength(400).IsRequired();
e.Property(x => x.TitleEn).HasMaxLength(400);
e.Property(x => x.ExcerptFa).HasMaxLength(1000);
e.Property(x => x.ExcerptEn).HasMaxLength(1000);
e.Property(x => x.ContentFa).HasColumnType("text");
e.Property(x => x.ContentEn).HasColumnType("text");
e.Property(x => x.Author).HasMaxLength(200);
e.Property(x => x.CategoryFa).HasMaxLength(100);
e.Property(x => x.CategoryEn).HasMaxLength(100);
e.Property(x => x.TagsJson).HasMaxLength(2000).HasDefaultValue("[]");
e.Property(x => x.CoverImage).HasMaxLength(500);
e.HasIndex(x => x.Slug).IsUnique();
e.HasIndex(x => new { x.IsPublished, x.PublishedAt });
e.HasMany(x => x.Comments).WithOne(c => c.Post).HasForeignKey(c => c.PostSlug)
.HasPrincipalKey(p => p.Slug).OnDelete(DeleteBehavior.Cascade);
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<WebsiteComment>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.PostSlug).HasMaxLength(200).IsRequired();
e.Property(x => x.AuthorName).HasMaxLength(100).IsRequired();
e.Property(x => x.AuthorEmail).HasMaxLength(200);
e.Property(x => x.Content).HasMaxLength(3000).IsRequired();
e.Property(x => x.IpAddress).HasMaxLength(50);
e.HasIndex(x => new { x.PostSlug, x.IsApproved, x.CreatedAt });
e.HasQueryFilter(x => x.DeletedAt == null);
});
modelBuilder.Entity<DemoRequest>(e =>
{
e.HasKey(x => x.Id);
e.Property(x => x.ContactName).HasMaxLength(200).IsRequired();
e.Property(x => x.BusinessName).HasMaxLength(300).IsRequired();
e.Property(x => x.Phone).HasMaxLength(20).IsRequired();
e.Property(x => x.Email).HasMaxLength(200);
e.Property(x => x.BranchCount).HasMaxLength(20);
e.Property(x => x.Notes).HasMaxLength(2000);
e.Property(x => x.Source).HasMaxLength(50).HasDefaultValue("website");
e.Property(x => x.AdminNotes).HasMaxLength(2000);
e.HasIndex(x => new { x.Status, x.CreatedAt });
e.HasQueryFilter(x => x.DeletedAt == null);
});
}
}
@@ -0,0 +1,10 @@
using Meezi.Core.Interfaces;
namespace Meezi.Infrastructure.Data;
public class BranchContext : IBranchContext
{
public string? CafeId { get; set; }
public string? BranchId { get; set; }
public bool HasBranch => !string.IsNullOrEmpty(BranchId);
}
@@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
namespace Meezi.Infrastructure.Data;
/// <summary>Idempotent SQL fixes for databases where EF history and schema diverged.</summary>
public static class DatabaseSchemaPatches
{
public static async Task ApplyAsync(AppDbContext db, CancellationToken cancellationToken = default)
{
await db.Database.ExecuteSqlRawAsync(
"""
ALTER TABLE "OrderItems" ADD COLUMN IF NOT EXISTS "IsVoided" boolean NOT NULL DEFAULT false;
ALTER TABLE "OrderItems" ADD COLUMN IF NOT EXISTS "VoidedAt" timestamp with time zone;
ALTER TABLE "OrderItems" ADD COLUMN IF NOT EXISTS "VoidedByUserId" text;
""",
cancellationToken);
}
}
@@ -0,0 +1,43 @@
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Meezi.Infrastructure.Data;
public static class DemoCouponSeeder
{
public static async Task EnsureCouponsAsync(AppDbContext db, string cafeId, ILogger logger)
{
if (await db.Coupons.AnyAsync(c => c.CafeId == cafeId && c.Code == "WELCOME10"))
return;
db.Coupons.AddRange(
new Coupon
{
Id = "coupon_demo_welcome10",
CafeId = cafeId,
Code = "WELCOME10",
Type = CouponType.Percentage,
Value = 10,
MaxDiscount = 50_000,
MinOrderAmount = 100_000,
UsageLimit = 100,
IsActive = true
},
new Coupon
{
Id = "coupon_demo_save20k",
CafeId = cafeId,
Code = "SAVE20",
Type = CouponType.FixedAmount,
Value = 20_000,
MinOrderAmount = 150_000,
UsageLimit = 50,
IsActive = true
});
await db.SaveChangesAsync();
logger.LogInformation("Demo coupons seeded: WELCOME10, SAVE20 for cafe {CafeId}", cafeId);
}
}
@@ -0,0 +1,28 @@
using Meezi.Core.Enums;
namespace Meezi.Infrastructure.Data;
/// <summary>Demo staff for development OTP login (see data/demo-credentials.json).</summary>
public static class DemoEmployeesCatalog
{
public const string DefaultBranchId = "branch_demo_main";
public sealed record EmployeeSeed(
string Id,
string Name,
string Phone,
EmployeeRole Role,
string BranchId = DefaultBranchId,
decimal BaseSalary = 0);
public static IReadOnlyList<EmployeeSeed> Employees { get; } =
[
new("emp_demo_owner", "مدیر دمو", "09121234567", EmployeeRole.Owner),
new("emp_demo_manager", "مدیر شعبه", "09121111111", EmployeeRole.Manager),
new("emp_demo_cashier", "صندوقدار", "09122222222", EmployeeRole.Cashier),
new("emp_demo_waiter", "گارسون", "09123333333", EmployeeRole.Waiter),
new("emp_demo_waiter2", "گارسون ۲", "09124444444", EmployeeRole.Waiter),
new("emp_demo_chef", "آشپز", "09125555555", EmployeeRole.Chef),
new("emp_demo_delivery", "پیک", "09126666666", EmployeeRole.Delivery),
];
}
@@ -0,0 +1,44 @@
using Meezi.Core.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Meezi.Infrastructure.Data;
public static class DemoEmployeesSeeder
{
public static async Task EnsureEmployeesAsync(AppDbContext db, string cafeId, ILogger logger)
{
var existingPhones = await db.Employees
.Where(e => e.CafeId == cafeId && e.DeletedAt == null)
.Select(e => e.Phone)
.ToListAsync();
var added = 0;
foreach (var seed in DemoEmployeesCatalog.Employees)
{
if (existingPhones.Contains(seed.Phone))
continue;
if (await db.Employees.AnyAsync(e => e.Id == seed.Id, cancellationToken: default))
continue;
db.Employees.Add(new Employee
{
Id = seed.Id,
CafeId = cafeId,
BranchId = seed.BranchId,
Name = seed.Name,
Phone = seed.Phone,
Role = seed.Role,
BaseSalary = seed.BaseSalary
});
added++;
}
if (added > 0)
{
await db.SaveChangesAsync();
logger.LogInformation("Demo employees seed: {Count} added for cafe {CafeId}", added, cafeId);
}
}
}
@@ -0,0 +1,149 @@
namespace Meezi.Infrastructure.Data;
/// <summary>
/// Café demo menu aligned with Kaggle Food-101 class names (see Food101ImageFallbacks + data/menu-image-manifest.json).
/// </summary>
public static class DemoMenuCatalog
{
public sealed record CategorySeed(
string Id,
string Name,
string NameEn,
string? NameAr,
int SortOrder,
string? Icon = null,
string? IconPresetId = null,
string? IconStyle = "flat");
public sealed record ItemSeed(
string Id,
string CategoryId,
string Name,
string NameEn,
string? NameAr,
string? Description,
decimal PriceToman,
decimal DiscountPercent,
string Food101Class);
private static string Img(string itemId, string food101Class)
{
var kind = DemoMenuCategoryKinds.KindFor(itemId, food101Class);
var local = MenuImageManifest.GetLocalImageOverride(itemId);
if (!string.IsNullOrWhiteSpace(local))
return local;
return Food101ImageFallbacks.Resolve(food101Class, kind);
}
public static IReadOnlyList<CategorySeed> Categories { get; } =
[
new("cat_demo_drinks", "نوشیدنی گرم", "Hot drinks", "مشروبات ساخنة", 1, IconPresetId: "drinks-hot"),
new("cat_demo_cold", "نوشیدنی سرد", "Cold drinks", "مشروبات باردة", 2, IconPresetId: "drinks-cold"),
new("cat_demo_breakfast", "صبحانه", "Breakfast", "فطور", 3, IconPresetId: "breakfast"),
new("cat_demo_food", "غذا و پیش‌غذا", "Food & snacks", "طعام", 4, IconPresetId: "food-mains"),
new("cat_demo_pasta", "پاستا و پیتزا", "Pasta & pizza", "معكرونة وبيتزا", 5, IconPresetId: "pasta-pizza"),
new("cat_demo_dessert", "دسر", "Desserts", "حلويات", 6, IconPresetId: "dessert"),
];
public static IReadOnlyList<ItemSeed> Items { get; } =
[
// Hot drinks
new("item_demo_espresso", "cat_demo_drinks", "اسپرسو", "Espresso", "إسبريسو", "دوبل یا سینگل", 65_000, 0, "espresso"),
new("item_demo_americano", "cat_demo_drinks", "آمریکانو", "Americano", "أمريكانو", null, 75_000, 0, "cappuccino"),
new("item_demo_latte", "cat_demo_drinks", "لاته", "Latte", "لاتيه", "شیر بخار گرفته", 120_000, 0, "latte"),
new("item_demo_cappuccino", "cat_demo_drinks", "کاپوچینو", "Cappuccino", "كابتشينو", null, 110_000, 10, "cappuccino"),
new("item_demo_mocha", "cat_demo_drinks", "موکا", "Mocha", "موكا", "شکلات و قهوه", 135_000, 0, "mocha"),
new("item_demo_tea", "cat_demo_drinks", "چای ماسالا", "Masala tea", "شاي ماسالا", null, 85_000, 0, "miso_soup"),
// Cold drinks
new("item_demo_iced_latte", "cat_demo_cold", "آیس لاته", "Iced latte", "آيس لاتيه", null, 130_000, 0, "iced_coffee"),
new("item_demo_cold_brew", "cat_demo_cold", "کولد برو", "Cold brew", "كولد برو", null, 140_000, 0, "iced_coffee"),
new("item_demo_lemonade", "cat_demo_cold", "لیموناد", "Lemonade", "ليمونادة", "تازه", 95_000, 0, "lemonade"),
new("item_demo_smoothie", "cat_demo_cold", "اسموتی توت", "Berry smoothie", "سموذي", null, 150_000, 15, "smoothie"),
// Breakfast
new("item_demo_croissant", "cat_demo_breakfast", "کروسان", "Croissant", "كرواسان", "کره‌ای", 75_000, 0, "croque_madame"),
new("item_demo_omelette", "cat_demo_breakfast", "املت", "Omelette", "أومليت", "نان سنگک", 145_000, 0, "omelette"),
new("item_demo_avocado", "cat_demo_breakfast", "توست آووکادو", "Avocado toast", "توست أفوكادو", null, 185_000, 0, "avocado_toast"),
new("item_demo_pancakes", "cat_demo_breakfast", "پنکیک", "Pancakes", "فطائر", "عسل و کره", 165_000, 0, "pancakes"),
new("item_demo_waffles", "cat_demo_breakfast", "وافل", "Waffles", "وافل", null, 175_000, 0, "waffles"),
new("item_demo_french_toast", "cat_demo_breakfast", "فرنچ تست", "French toast", "توست فرنسي", null, 155_000, 0, "french_toast"),
new("item_demo_eggs_benedict", "cat_demo_breakfast", "اگ بندیکت", "Eggs Benedict", "بيض بنديكت", null, 195_000, 0, "eggs_benedict"),
// Food & snacks
new("item_demo_sandwich", "cat_demo_food", "ساندویچ مرغ", "Chicken sandwich", "ساندويتش دجاج", null, 195_000, 0, "club_sandwich"),
new("item_demo_salad", "cat_demo_food", "سالاد سزار", "Caesar salad", "سلطة سيزر", null, 175_000, 0, "caesar_salad"),
new("item_demo_greek_salad", "cat_demo_food", "سالاد یونانی", "Greek salad", "سلطة يونانية", null, 168_000, 0, "greek_salad"),
new("item_demo_burger", "cat_demo_food", "همبرگر", "Burger", "برجر", "۱۵۰ گرم گوشت", 245_000, 0, "hamburger"),
new("item_demo_steak", "cat_demo_food", "استیک", "Steak", "ستيك", "medium", 385_000, 0, "steak"),
new("item_demo_salmon", "cat_demo_food", "سالمون گریل", "Grilled salmon", "سلمون مشوي", null, 320_000, 0, "grilled_salmon"),
new("item_demo_tacos", "cat_demo_food", "تاکو", "Tacos", "تاكو", "سه عدد", 210_000, 0, "tacos"),
new("item_demo_shawarma", "cat_demo_food", "شاورما", "Shawarma", "شاورما", null, 185_000, 0, "shawarma"),
new("item_demo_falafel", "cat_demo_food", "فلافل", "Falafel", "فلافل", "۶ عدد", 125_000, 0, "falafel"),
new("item_demo_hummus", "cat_demo_food", "حمص", "Hummus", "حمص", "نان پیتا", 95_000, 0, "hummus"),
new("item_demo_fries", "cat_demo_food", "سیب‌زمینی سرخ‌کرده", "French fries", "بطاطس مقلية", null, 85_000, 0, "french_fries"),
new("item_demo_spring_rolls", "cat_demo_food", "اسپرینگ رول", "Spring rolls", "سبرينغ رول", null, 115_000, 0, "spring_rolls"),
new("item_demo_ramen", "cat_demo_food", "رامن", "Ramen", "رامن", null, 235_000, 0, "ramen"),
new("item_demo_pho", "cat_demo_food", "فو", "Pho", "فو", null, 225_000, 0, "pho"),
new("item_demo_sushi", "cat_demo_food", "سوشی", "Sushi", "سوشي", "۸ تکه", 290_000, 0, "sushi"),
// Pasta & pizza
new("item_demo_pasta", "cat_demo_pasta", "پاستا آلفردو", "Alfredo pasta", "باستا", null, 220_000, 0, "pasta_carbonara"),
new("item_demo_carbonara", "cat_demo_pasta", "کاربونارا", "Carbonara", "كاربونارا", null, 228_000, 0, "spaghetti_carbonara"),
new("item_demo_bolognese", "cat_demo_pasta", "بولونز", "Bolognese", "بولونيز", null, 215_000, 0, "spaghetti_bolognese"),
new("item_demo_lasagna", "cat_demo_pasta", "لازانیا", "Lasagna", "لازانيا", null, 240_000, 0, "lasagna"),
new("item_demo_gnocchi", "cat_demo_pasta", "نیوکی", "Gnocchi", "جنوكي", null, 232_000, 0, "gnocchi"),
new("item_demo_pizza", "cat_demo_pasta", "پیتزا مارگاریتا", "Margherita pizza", "بيتزا", null, 265_000, 10, "pizza"),
new("item_demo_risotto", "cat_demo_pasta", "ریزوتو قارچ", "Mushroom risotto", "ريزوتو", null, 238_000, 0, "mushroom_risotto"),
// Desserts
new("item_demo_cake", "cat_demo_dessert", "کیک شکلاتی", "Chocolate cake", "كيك شوكولاتة", "برشی", 95_000, 15, "chocolate_cake"),
new("item_demo_cheesecake", "cat_demo_dessert", "چیزکیک", "Cheesecake", "تشيز كيك", null, 115_000, 0, "cheesecake"),
new("item_demo_brownie", "cat_demo_dessert", "براونی", "Brownie", "براوني", "بستنی وانیلی", 105_000, 0, "brownie"),
new("item_demo_icecream", "cat_demo_dessert", "بستنی", "Ice cream", "آيس كريم", "دو اسکوپ", 88_000, 0, "ice_cream"),
new("item_demo_tiramisu", "cat_demo_dessert", "تیرامیسو", "Tiramisu", "تيراميسو", null, 125_000, 0, "tiramisu"),
new("item_demo_donuts", "cat_demo_dessert", "دونات", "Donuts", "دونات", null, 78_000, 0, "donuts"),
new("item_demo_churros", "cat_demo_dessert", "چوروس", "Churros", "تشورو", "شکلات", 92_000, 0, "churros"),
new("item_demo_baklava", "cat_demo_dessert", "باقلوا", "Baklava", "بقلاوة", null, 98_000, 0, "baklava"),
new("item_demo_creme_brulee", "cat_demo_dessert", "کرم بروله", "Crème brûlée", "كريم بروليه", null, 118_000, 0, "creme_brulee"),
];
/// <summary>Resolved image URL for catalog seed (manifest → Food-101 fallback → category default).</summary>
public static string ResolveItemImageUrl(ItemSeed item) =>
Img(item.Id, item.Food101Class);
}
/// <summary>Maps item/category to drink vs food default images.</summary>
file static class DemoMenuCategoryKinds
{
private static readonly HashSet<string> DrinkCategoryIds = new(StringComparer.Ordinal)
{
"cat_demo_drinks",
"cat_demo_cold"
};
private static readonly HashSet<string> DrinkFood101Classes = new(StringComparer.OrdinalIgnoreCase)
{
"espresso", "latte", "cappuccino", "mocha", "iced_coffee", "lemonade", "smoothie", "miso_soup"
};
public static MenuItemVisualKind KindFor(string itemId, string food101Class)
{
if (itemId.Contains("demo_iced", StringComparison.Ordinal)
|| itemId.Contains("demo_cold", StringComparison.Ordinal)
|| itemId.Contains("demo_lemonade", StringComparison.Ordinal)
|| itemId.Contains("demo_smoothie", StringComparison.Ordinal)
|| itemId.Contains("demo_espresso", StringComparison.Ordinal)
|| itemId.Contains("demo_latte", StringComparison.Ordinal)
|| itemId.Contains("demo_cappuccino", StringComparison.Ordinal)
|| itemId.Contains("demo_mocha", StringComparison.Ordinal)
|| itemId.Contains("demo_americano", StringComparison.Ordinal)
|| itemId.Contains("demo_tea", StringComparison.Ordinal))
return MenuItemVisualKind.Drink;
if (DrinkFood101Classes.Contains(food101Class))
return MenuItemVisualKind.Drink;
return MenuItemVisualKind.Food;
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -0,0 +1,146 @@
using Meezi.Core.Discover;
using Meezi.Core.Enums;
namespace Meezi.Infrastructure.Data;
public static class DiscoverShowcaseCatalog
{
public sealed record ShowcaseCafe(
string Id,
string Slug,
string Name,
string City,
string Address,
string Description,
CafeDiscoverProfile Profile,
PlanTier PlanTier,
int MenuTemplateIndex,
string OwnerPhone,
IReadOnlyList<string>? Badges = null);
public static IReadOnlyList<ShowcaseCafe> Cafes { get; } =
[
Cafe("cafe_sc_01", "cafe-mokhteh-tehran", "کافه مخته", "تهران", "تهران، ولیعصر، نرسیده به پارک ساعی",
"رستری تخصصی قهوه و دسر؛ فضای آرام برای کار و قرار دو نفره. اسپرسو، لاته، چیزکیک.",
Profile(["roastery", "modern"], "cozy", ["quiet", "cozy"], ["study_work", "date"], ["indoor", "wifi"], "quiet", "mid"), 0, "09131000001"),
Cafe("cafe_sc_02", "cafe-sibo-karaj", "کافه سیب", "کرج", "کرج، گوهردشت، بلوار موذن‌نیا",
"کافه دنج کرج با قهوه دمی و بیکری؛ مناسب خانواده و صبحانه آخر هفته.",
Profile(["brunch", "plants_heavy"], "medium", ["casual", "cozy"], ["family", "friends"], ["indoor", "kids_friendly"], "moderate", "budget"), 1, "09131000002"),
Cafe("cafe_sc_03", "cafe-ava-valiasr", "کافه آوا", "تهران", "تهران، ولیعصر، بالاتر از میدان ونک",
"کافه مدرن با نور طبیعی؛ لاته آرت و نوشیدنی‌های فصل. جستجو: قهوه تخصصی ولیعصر.",
Profile(["modern", "instagrammable"], "medium", ["trendy", "lively"], ["friends", "quick_coffee"], ["indoor", "terrace"], "moderate", "mid"), 0, "09131000003"),
Cafe("cafe_sc_04", "restaurant-sang-tehran", "رستوران سنگ", "تهران", "تهران، جردن، خیابان ناهید شرقی",
"غذای ایرانی و بین‌المللی؛ فضای لوکس برای شام و جشن. استیک، پاستا، سالاد.",
Profile(["luxury", "heritage"], "large", ["luxury", "romantic"], ["celebration", "business_meeting"], ["indoor", "private_room"], "quiet", "premium"), 2, "09131000004", PlanTier.Business),
Cafe("cafe_sc_05", "cafe-ketab-niavaran", "کافه کتاب نیاوران", "تهران", "تهران، نیاوران، خیابان باهنر",
"بوفه کتاب با قهوه و چای؛ فضای ساکت برای درس و کار. دمنوش، کیک، ساندویچ.",
Profile(["book_cafe", "quiet"], "cozy", ["quiet", "study_friendly"], ["study_work", "solo"], ["indoor", "wifi"], "quiet", "mid"), 0, "09131000005"),
Cafe("cafe_sc_06", "cafe-rooz-karaj", "کافه روز", "کرج", "کرج، عظیمیه، میدان جهاد",
"کافه روز کرج؛ اسموتی، قهوه سرد، صبحانه کامل. مناسب جوانان و دورهمی.",
Profile(["modern", "brunch"], "medium", ["lively", "casual"], ["friends", "breakfast"], ["outdoor", "wifi"], "moderate", "budget"), 1, "09131000006"),
Cafe("cafe_sc_07", "cafe-gol-reza", "کافه گل رضا", "تهران", "تهران، سعادت‌آباد، میدان کاج",
"کافه گل‌دار و دکور سنتی مدرن؛ عالی برای عکاسی و قرار عاشقانه.",
Profile(["persian_traditional", "instagrammable"], "cozy", ["romantic", "cozy"], ["date"], ["indoor", "plants"], "quiet", "mid"), 4, "09131000007"),
Cafe("cafe_sc_08", "cafe-shab-enghelab", "کافه شب", "تهران", "تهران، انقلاب، خیابان ۱۲ فروردین",
"کافه دیروقت؛ قهوه و دسر تا نیمه‌شب. موسیقی ملایم، فضای شب‌گاه.",
Profile(["late_night", "industrial"], "medium", ["lively", "trendy"], ["friends", "finding_someone"], ["indoor", "live_music"], "lively", "mid"), 0, "09131000008"),
Cafe("cafe_sc_09", "cafe-darya-karaj", "کافه دریا", "کرج", "کرج، مارلیک، بلوار ارم",
"تراس باز با نوشیدنی سرد؛ مناسب تابستان و دورهمی دوستانه در کرج.",
Profile(["modern"], "spacious", ["casual", "lively"], ["friends", "family"], ["outdoor", "terrace"], "moderate", "mid"), 0, "09131000009"),
Cafe("cafe_sc_10", "cafe-nan-o-nam", "نان و نمک", "تهران", "تهران، پاسداران، خیابان گلستان هفتم",
"بیکری-کافه با نان تازه و صبحانه؛ کره محلی، مربا، قهوه فیلتر.",
Profile(["brunch", "heritage"], "cozy", ["cozy", "traditional"], ["breakfast", "family"], ["indoor"], "moderate", "budget"), 1, "09131000010"),
Cafe("cafe_sc_11", "cafe-terrace-farmanieh", "تراس فرمانیه", "تهران", "تهران، فرمانیه، خیابان کلاهدوز",
"کافه تراس با ویوی شهر؛ قهوه ویژه و دسر فصل. مناسب قرار و عکاسی.",
Profile(["luxury", "instagrammable"], "large", ["romantic", "luxury"], ["date", "celebration"], ["terrace", "outdoor"], "quiet", "premium"), 3, "09131000011", PlanTier.Enterprise, ["award_winner", "roastery"]),
Cafe("cafe_sc_12", "cafe-minimal-karaj", "مینیمال کرج", "کرج", "کرج، گلشهر، بلوار امیرکبیر",
"کافه مینیمال کرج؛ اسپرسو دقیق و فضای سفید آرام.",
Profile(["minimal", "scandi"], "tiny", ["quiet", "study_friendly"], ["solo", "study_work"], ["indoor", "wifi"], "quiet", "mid"), 0, "09131000012"),
Cafe("cafe_sc_13", "cafe-chocolate-tehran", "خانه شکلات", "تهران", "تهران، زعفرانیه، خیابان مقدس اردبیلی",
"دسرخانه شکلات و قهوه؛ تارت، شکلات دست‌ساز، هات چاکلت.",
Profile(["dessert_focus", "luxury"], "cozy", ["romantic", "cozy"], ["date"], ["indoor"], "quiet", "premium"), 3, "09131000013"),
Cafe("cafe_sc_14", "cafe-vintage-tajrish", "کافه وینتیج تجریش", "تهران", "تهران، تجریش، میدان قدس",
"دکور قدیمی تهران؛ قهوه ترک و کیک خانگی. فضای نوستالژیک.",
Profile(["vintage", "heritage"], "cozy", ["traditional", "cozy"], ["friends", "solo"], ["indoor"], "moderate", "mid"), 0, "09131000014"),
Cafe("cafe_sc_15", "cafe-work-hub-karaj", "هاب کار کرج", "کرج", "کرج، جهانشهر، بلوار شهید مطهری",
"فضای کار اشتراکی با کافه؛ وای‌فای قوی، قهوه نامحدود، ساندویچ.",
Profile(["modern", "book_cafe"], "medium", ["study_friendly", "quiet"], ["study_work", "business_meeting"], ["indoor", "wifi"], "quiet", "budget"), 0, "09131000015"),
Cafe("cafe_sc_16", "cafe-pet-pardis", "کافه پت پاردیس", "تهران", "تهران، پردیس، فاز ۱",
"کافه پت‌فرندلی؛ نوشیدنی گیاهی و فضای باز برای صاحبان سگ.",
Profile(["plants_heavy", "modern"], "spacious", ["casual", "lively"], ["friends", "family"], ["outdoor", "pet_friendly"], "moderate", "mid"), 0, "09131000016", badges: ["pet_friendly"]),
Cafe("cafe_sc_17", "restaurant-sabz-karaj", "رستوران سبز", "کرج", "کرج، مشکین‌دشت، بلوار آزادی",
"رستوران گیاهی و سالم کرج؛ بول، سالاد، آبمیوه تازه.",
Profile(["modern", "plants_heavy"], "medium", ["casual"], ["family", "friends"], ["indoor", "outdoor"], "moderate", "mid"), 2, "09131000017"),
Cafe("cafe_sc_18", "cafe-roastery-darabad", "رستری دارآباد", "تهران", "تهران، دارآباد، خیابان اصلی",
"رستری قهوه تخصصی با دم‌آوری دستی؛ تست پروفایل‌های مختلف.",
Profile(["roastery", "industrial"], "medium", ["trendy"], ["quick_coffee", "friends"], ["indoor"], "moderate", "mid"), 0, "09131000018"),
Cafe("cafe_sc_19", "cafe-family-mehri", "کافه مهری", "تهران", "تهران، تهرانپارس، فلکه اول",
"کافه خانوادگی با فضای بازی کودک؛ صبحانه و ناهار سبک.",
Profile(["brunch"], "large", ["casual", "cozy"], ["family"], ["indoor", "kids_friendly"], "moderate", "budget"), 1, "09131000019"),
Cafe("cafe_sc_20", "cafe-laleh-karaj", "کافه لاله", "کرج", "کرج، باغستان، خیابان شهید بهشتی",
"کافه گل لاله کرج؛ دمنوش گل و قهوه ترک. فضای زنانه دوستانه.",
Profile(["plants_heavy", "persian_traditional"], "cozy", ["cozy", "quiet"], ["friends", "solo"], ["indoor", "plants"], "quiet", "budget"), 4, "09131000020"),
Cafe("cafe_sc_21", "cafe-business-iran", "کافه ایران‌زمین", "تهران", "تهران، آفریقا، برج میلاد نزدیک",
"مناسب جلسه کاری؛ قهوه سریع، صبحانه executive، سالاد.",
Profile(["modern", "luxury"], "medium", ["luxury", "quiet"], ["business_meeting"], ["indoor", "wifi", "private_room"], "quiet", "premium"), 2, "09131000021"),
Cafe("cafe_sc_22", "cafe-sunset-karaj", "غروب کرج", "کرج", "کرج، حصارک، جاده چالوس",
"ویوی کوه و غروب؛ قهوه دمی و کیک هویج. مناسب آخر هفته.",
Profile(["instagrammable"], "cozy", ["romantic", "casual"], ["date", "friends"], ["outdoor", "terrace"], "quiet", "mid"), 0, "09131000022"),
Cafe("cafe_sc_23", "cafe-gaming-tehran", "کافه گیم", "تهران", "تهران، یوسف‌آباد، خیابان جهان‌آرا",
"کافه گیمینگ؛ نوشیدنی انرژی‌زا و اسنک؛ فضای پرانرژی جوانان.",
Profile(["modern", "late_night"], "medium", ["lively", "trendy"], ["friends"], ["indoor"], "lively", "budget"), 0, "09131000023"),
Cafe("cafe_sc_24", "cafe-honey-karaj", "عسل کرج", "کرج", "کرج، طالقانی، میدان شهدا",
"کافه با عسل محلی و چای؛ کیک عسل، دمنوش گیاهی کرج.",
Profile(["heritage", "persian_traditional"], "cozy", ["traditional", "cozy"], ["family"], ["indoor"], "quiet", "budget"), 4, "09131000024"),
Cafe("cafe_sc_25", "cafe-art-tehran", "کافه هنر", "تهران", "تهران، ایرانشهر، خیابان نواب",
"گالری-کافه؛ قهوه و نمایشگاه موقت. فضای هنری و خلاق.",
Profile(["artistic", "vintage"], "medium", ["artistic", "quiet"], ["solo", "friends"], ["indoor"], "quiet", "mid"), 0, "09131000025"),
Cafe("cafe_sc_26", "cafe-quick-karaj", "ایستگاه قهوه کرج", "کرج", "کرج، کرج مرکزی، میدان آزادگان",
"قهوه سریع کرج؛ مناسب مسیر کار و دانشجو. ساندویچ و اسپرسو.",
Profile(["modern"], "tiny", ["casual"], ["quick_coffee", "study_work"], ["indoor"], "moderate", "budget"), 0, "09131000026"),
Cafe("cafe_sc_27", "restaurant-caspian", "رستوران کاسپین", "تهران", "تهران، الهیه، خیابان فرشته",
"غذای دریایی و فیش؛ پاستا دریایی، سالاد، mocktail.",
Profile(["luxury", "modern"], "large", ["luxury", "romantic"], ["celebration", "date"], ["indoor"], "quiet", "premium"), 2, "09131000027", PlanTier.Enterprise, ["verified_partner"]),
Cafe("cafe_sc_28", "cafe-mountain-karaj", "کافه کوهستان", "کرج", "کرج، محمدشهر، انتهای بلوار ارم",
"فضای کوهستانی کرج؛ چای کوهی و سوخاری. هوای خنک تراس.",
Profile(["modern"], "spacious", ["casual", "cozy"], ["family", "friends"], ["outdoor", "terrace"], "moderate", "mid"), 2, "09131000028"),
Cafe("cafe_sc_29", "cafe-vegan-tehran", "کافه وگان", "تهران", "تهران، جمالزاده، خیابان مخبر",
"منوی وگان و گیاهی؛ لاته جو، شیر بادام، دسر وگان.",
Profile(["modern", "plants_heavy"], "cozy", ["trendy", "casual"], ["friends", "solo"], ["indoor", "wifi"], "quiet", "mid"), 0, "09131000029", badges: ["eco_friendly"]),
Cafe("cafe_sc_30", "cafe-royal-karaj", "کافه رویال کرج", "کرج", "کرج، عظیمیه، برج بلور",
"کافه لوکس کرج؛ دسر فرانسوی و قهوه اسپشیالتی. مناسب جشن.",
Profile(["luxury", "dessert_focus"], "large", ["luxury", "romantic"], ["celebration", "date"], ["indoor", "private_room"], "quiet", "premium"), 3, "09131000030", PlanTier.Enterprise, ["award_winner", "verified_partner"]),
];
private static ShowcaseCafe Cafe(
string id,
string slug,
string name,
string city,
string address,
string description,
CafeDiscoverProfile profile,
int menuTemplate,
string phone,
PlanTier plan = PlanTier.Pro,
IReadOnlyList<string>? badges = null) =>
new(id, slug, name, city, address, description, profile, plan, menuTemplate, phone, badges);
private static CafeDiscoverProfile Profile(
string[] themes,
string size,
string[] vibes,
string[] occasions,
string[] space,
string noise,
string price) => new()
{
Themes = themes.ToList(),
Size = size,
Vibes = vibes.ToList(),
Occasions = occasions.ToList(),
SpaceFeatures = space.ToList(),
NoiseLevel = noise,
PriceTier = price,
};
}
@@ -0,0 +1,78 @@
namespace Meezi.Infrastructure.Data;
/// <summary>Rotating menu templates for discover showcase cafés.</summary>
public static class DiscoverShowcaseMenus
{
public sealed record MenuTemplate(
IReadOnlyList<DemoMenuCatalog.CategorySeed> Categories,
IReadOnlyList<DemoMenuCatalog.ItemSeed> Items);
public static IReadOnlyList<MenuTemplate> Templates { get; } =
[
CoffeeShopTemplate(),
BrunchTemplate(),
RestaurantTemplate(),
DessertTemplate(),
TraditionalTemplate(),
];
private static MenuTemplate CoffeeShopTemplate() => new(
[
new("cat_sc_hot", "قهوه تخصصی", "Specialty coffee", null, 1, IconPresetId: "drinks-hot", IconStyle: "gradient"),
new("cat_sc_cold", "نوشیدنی سرد", "Cold drinks", null, 2, IconPresetId: "drinks-cold", IconStyle: "modern"),
new("cat_sc_snack", "میان‌وعده", "Snacks", null, 3, IconPresetId: "food-mains", IconStyle: "flat"),
],
[
new("item_sc_espresso", "cat_sc_hot", "اسپرسو", "Espresso", null, "دوبل سینگل", 70_000, 0, "espresso"),
new("item_sc_latte", "cat_sc_hot", "لاته", "Latte", null, "شیر بخار", 125_000, 0, "latte"),
new("item_sc_mocha", "cat_sc_hot", "موکا", "Mocha", null, null, 140_000, 0, "mocha"),
new("item_sc_iced", "cat_sc_cold", "آیس آمریکانو", "Iced americano", null, null, 115_000, 0, "iced_coffee"),
new("item_sc_cake", "cat_sc_snack", "چیزکیک", "Cheesecake", null, null, 165_000, 10, "cheesecake"),
]);
private static MenuTemplate BrunchTemplate() => new(
[
new("cat_sc_br", "صبحانه", "Breakfast", null, 1, IconPresetId: "breakfast", IconStyle: "pastel"),
new("cat_sc_br_d", "نوشیدنی", "Drinks", null, 2, IconPresetId: "drinks-hot"),
],
[
new("item_sc_omelette", "cat_sc_br", "املت", "Omelette", null, "نان سنگک", 155_000, 0, "omelette"),
new("item_sc_avocado", "cat_sc_br", "توست آووکادو", "Avocado toast", null, null, 195_000, 0, "avocado_toast"),
new("item_sc_tea", "cat_sc_br_d", "چای", "Tea", null, null, 65_000, 0, "miso_soup"),
]);
private static MenuTemplate RestaurantTemplate() => new(
[
new("cat_sc_main", "غذای اصلی", "Mains", null, 1, IconPresetId: "food-mains", IconStyle: "bold"),
new("cat_sc_salad", "سالاد", "Salads", null, 2, IconPresetId: "food-mains"),
new("cat_sc_drink", "نوشیدنی", "Drinks", null, 3, IconPresetId: "drinks-cold"),
],
[
new("item_sc_burger", "cat_sc_main", "همبرگر", "Burger", null, null, 265_000, 0, "hamburger"),
new("item_sc_pasta", "cat_sc_main", "پاستا", "Pasta", null, null, 245_000, 0, "spaghetti_bolognese"),
new("item_sc_salad", "cat_sc_salad", "سالاد سزار", "Caesar salad", null, null, 185_000, 0, "caesar_salad"),
new("item_sc_lemon", "cat_sc_drink", "لیموناد", "Lemonade", null, null, 95_000, 0, "lemonade"),
]);
private static MenuTemplate DessertTemplate() => new(
[
new("cat_sc_des", "دسر", "Desserts", null, 1, IconPresetId: "dessert", IconStyle: "soft"),
new("cat_sc_des_d", "قهوه", "Coffee", null, 2, IconPresetId: "drinks-hot"),
],
[
new("item_sc_tiramisu", "cat_sc_des", "تیرامیسو", "Tiramisu", null, null, 175_000, 0, "tiramisu"),
new("item_sc_macaron", "cat_sc_des", "ماکارون", "Macaron", null, null, 95_000, 0, "macarons"),
new("item_sc_capp", "cat_sc_des_d", "کاپوچینو", "Cappuccino", null, null, 110_000, 0, "cappuccino"),
]);
private static MenuTemplate TraditionalTemplate() => new(
[
new("cat_sc_tr", "نوشیدنی سنتی", "Traditional drinks", null, 1, IconPresetId: "drinks-hot", IconStyle: "duotone"),
new("cat_sc_tr_f", "غذا", "Food", null, 2, IconPresetId: "food-mains"),
],
[
new("item_sc_tea_tr", "cat_sc_tr", "چای ایرانی", "Persian tea", null, null, 55_000, 0, "miso_soup"),
new("item_sc_kebab", "cat_sc_tr_f", "چلوکباب", "Kebab plate", null, null, 320_000, 0, "steak"),
new("item_sc_soup", "cat_sc_tr_f", "سوپ", "Soup", null, null, 120_000, 0, "miso_soup"),
]);
}
@@ -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}";
}
@@ -0,0 +1,109 @@
namespace Meezi.Infrastructure.Data;
/// <summary>
/// Unsplash fallbacks per Food-101 class folder name (Kaggle dataset layout).
/// Used when menu-image-manifest.json has no entry or local JPEG was not imported.
/// </summary>
public static class Food101ImageFallbacks
{
private static readonly Dictionary<string, string> ClassUrls = new(StringComparer.OrdinalIgnoreCase)
{
["apple_pie"] = "https://images.unsplash.com/photo-1535920527002-b43e668c3097?w=600",
["avocado_toast"] = "https://images.unsplash.com/photo-1541519221064-49632fb3380e?w=600",
["baklava"] = "https://images.unsplash.com/photo-1598110756419-84b30681f41f?w=600",
["beef_carpaccio"] = "https://images.unsplash.com/photo-1600891963295-d66a269b9202?w=600",
["beef_tartare"] = "https://images.unsplash.com/photo-1600891963295-d66a269b9202?w=600",
["beet_salad"] = "https://images.unsplash.com/photo-1512621776951-a57141f2eefd?w=600",
["bruschetta"] = "https://images.unsplash.com/photo-1572695157366-8486c5880882?w=600",
["brownie"] = "https://images.unsplash.com/photo-1606313564200-e75d5e30476e?w=600",
["caesar_salad"] = "https://images.unsplash.com/photo-1546793665-c74683f339c1?w=600",
["caprese_salad"] = "https://images.unsplash.com/photo-1592417817098-8fd3d9eb14a5?w=600",
["cappuccino"] = "https://images.unsplash.com/photo-1572442388796-11668a67e3d0?w=600",
["carrot_cake"] = "https://images.unsplash.com/photo-1621303837534-610466b814da?w=600",
["cheesecake"] = "https://images.unsplash.com/photo-1524351199678-941a58cfcc36?w=600",
["chocolate_cake"] = "https://images.unsplash.com/photo-1578985545062-69928b1d9587?w=600",
["churros"] = "https://images.unsplash.com/photo-1627482299165-0883a056a48f?w=600",
["club_sandwich"] = "https://images.unsplash.com/photo-1528735602780-2552fd46c7af?w=600",
["creme_brulee"] = "https://images.unsplash.com/photo-1470309864661-683be0ef7eaf?w=600",
["croque_madame"] = "https://images.unsplash.com/photo-1555507036342-9231d37c10f3?w=600",
["cup_cakes"] = "https://images.unsplash.com/photo-1614707267537-b85a1e38f271?w=600",
["donuts"] = "https://images.unsplash.com/photo-1551024506-0bccd28d3071?w=600",
["dumplings"] = "https://images.unsplash.com/photo-1496116218417-1a781a1c08c2?w=600",
["edamame"] = "https://images.unsplash.com/photo-1459411621453-7b03977d4a4d?w=600",
["eggs_benedict"] = "https://images.unsplash.com/photo-1608039819502-3d5a2a2e0b0e?w=600",
["espresso"] = "https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=600&auto=format&fit=crop",
["falafel"] = "https://images.unsplash.com/photo-1601050690597-df5748fb5cee?w=600",
["french_fries"] = "https://images.unsplash.com/photo-1573080496219-76b9e6909700?w=600",
["french_toast"] = "https://images.unsplash.com/photo-1484723091739-30a329e1f0c4?w=600",
["fried_calamari"] = "https://images.unsplash.com/photo-1599487488170-d11ec9c172f0?w=600",
["garlic_bread"] = "https://images.unsplash.com/photo-1619535852122-9f0362cc8b0e?w=600",
["gnocchi"] = "https://images.unsplash.com/photo-1551183053-bf33a48c970a?w=600",
["greek_salad"] = "https://images.unsplash.com/photo-1540189549336-e6e99c3679fe?w=600",
["grilled_cheese_sandwich"] = "https://images.unsplash.com/photo-1528735602780-2552fd46c7af?w=600",
["grilled_salmon"] = "https://images.unsplash.com/photo-1467003909585-2f8a72700288?w=600",
["guacamole"] = "https://images.unsplash.com/photo-1529084963126-48f862a7038c?w=600",
["hamburger"] = "https://images.unsplash.com/photo-1568901346375-23c9450c58cd?w=600",
["hot_and_sour_soup"] = "https://images.unsplash.com/photo-1547592160-23ac45744acd?w=600",
["hot_dog"] = "https://images.unsplash.com/photo-1612392062631-94de6c5a533f?w=600",
["hummus"] = "https://images.unsplash.com/photo-1626208082043-e6319abfeec2?w=600",
["ice_cream"] = "https://images.unsplash.com/photo-1563805042-7684c019e1cb?w=600",
["iced_coffee"] = "https://images.unsplash.com/photo-1517487881594-2787aeee8f58?w=600&auto=format&fit=crop",
["lasagna"] = "https://images.unsplash.com/photo-1574894709920-11b28e7367e3?w=600",
["latte"] = "https://images.unsplash.com/photo-1461023058943-07fcbe16d735?w=600",
["lemonade"] = "https://images.unsplash.com/photo-1523672990561-64c16245f769?w=600",
["lobster_bisque"] = "https://images.unsplash.com/photo-1547592160-23ac45744acd?w=600",
["macaroni_and_cheese"] = "https://images.unsplash.com/photo-1543339493-18da843d4cb4?w=600",
["miso_soup"] = "https://images.unsplash.com/photo-1556679343-c7306c1976bc?w=600",
["mocha"] = "https://images.unsplash.com/photo-1577887233537-a81b387b125e?w=600",
["mushroom_risotto"] = "https://images.unsplash.com/photo-1476124362071-b9f2c96ef2db?w=600",
["nachos"] = "https://images.unsplash.com/photo-1513459032971-41fd1e48ff3c?w=600",
["omelette"] = "https://images.unsplash.com/photo-1525351484343-752d43d363f1?w=600",
["onion_rings"] = "https://images.unsplash.com/photo-1630431341973-02b95b531aa4?w=600",
["oysters"] = "https://images.unsplash.com/photo-1514392043407-4f468d2f5c9e?w=600",
["paella"] = "https://images.unsplash.com/photo-1534084650011-4c4d81e8ca4b?w=600",
["pancakes"] = "https://images.unsplash.com/photo-1567620905732-2d1ec7ab7440?w=600",
["panna_cotta"] = "https://images.unsplash.com/photo-1488477181946-6428a029177a?w=600",
["pasta_carbonara"] = "https://images.unsplash.com/photo-1621996346565-e3dbc646d9a9?w=600",
["pho"] = "https://images.unsplash.com/photo-1591814468924-caf36d123dd6?w=600",
["pizza"] = "https://images.unsplash.com/photo-1513104890138-7c749659a591?w=600",
["pork_chop"] = "https://images.unsplash.com/photo-1432139558640-143f293648c5?w=600",
["prime_rib"] = "https://images.unsplash.com/photo-1546833999-b9f581a1996d?w=600",
["ramen"] = "https://images.unsplash.com/photo-1569718212165-3a8278d5f624?w=600",
["risotto"] = "https://images.unsplash.com/photo-1476124362071-b9f2c96ef2db?w=600",
["samosa"] = "https://images.unsplash.com/photo-1601050690597-df5748fb5cee?w=600",
["sashimi"] = "https://images.unsplash.com/photo-1579584425555-c3ce17fd4351?w=600",
["scallops"] = "https://images.unsplash.com/photo-1599021452684-36e82be5777f?w=600",
["shawarma"] = "https://images.unsplash.com/photo-1529006557810-274adbcb39d8?w=600",
["smoothie"] = "https://images.unsplash.com/photo-1505252585463-0433371f7f6b?w=600",
["spaghetti_bolognese"] = "https://images.unsplash.com/photo-1622973536968-77544a8a4e0e?w=600",
["spaghetti_carbonara"] = "https://images.unsplash.com/photo-1612874741227-866d1aeeecd1?w=600",
["spring_rolls"] = "https://images.unsplash.com/photo-1526318896985-4d29c0903299?w=600",
["steak"] = "https://images.unsplash.com/photo-1600891963295-d66a269b9202?w=600",
["strawberry_shortcake"] = "https://images.unsplash.com/photo-1464349095436-e59b7d591c4f?w=600",
["sushi"] = "https://images.unsplash.com/photo-1579584425555-c3ce17fd4351?w=600",
["tacos"] = "https://images.unsplash.com/photo-1565299585323-38174c4aab1e?w=600",
["tiramisu"] = "https://images.unsplash.com/photo-1571877227200-a0d98ea607e9?w=600",
["tuna_tartare"] = "https://images.unsplash.com/photo-1544025162-d766942659778?w=600",
["waffles"] = "https://images.unsplash.com/photo-1567818735240-7acbb4b7e34c?w=600",
};
public static bool TryGetUrl(string food101Class, out string url)
{
if (ClassUrls.TryGetValue(food101Class, out var found) && !string.IsNullOrWhiteSpace(found))
{
url = found;
return true;
}
url = "";
return false;
}
public static string Resolve(string? food101Class, MenuItemVisualKind kind)
{
if (!string.IsNullOrWhiteSpace(food101Class) && TryGetUrl(food101Class, out var url))
return url;
return MenuItemImageDefaults.GetDefaultImageUrl(kind);
}
}
@@ -0,0 +1,101 @@
using System.Text.Json;
namespace Meezi.Infrastructure.Data;
public static class MenuImageManifest
{
private static IReadOnlyDictionary<string, string>? _urls;
private static string? _defaultDrinkUrl;
private static string? _defaultFoodUrl;
private const string FallbackDrinkUrl =
"https://images.unsplash.com/photo-1509042239860-f550ce710b93?w=600&auto=format&fit=crop";
private const string FallbackFoodUrl =
"https://images.unsplash.com/photo-1546793665-c74683f339c1?w=600";
public static string? GetImageUrl(string itemId)
{
EnsureLoaded();
return _urls!.TryGetValue(itemId, out var url) ? url : null;
}
public static string GetDefaultDrinkImageUrl()
{
EnsureLoaded();
return _defaultDrinkUrl ?? FallbackDrinkUrl;
}
public static string GetDefaultFoodImageUrl()
{
EnsureLoaded();
return _defaultFoodUrl ?? FallbackFoodUrl;
}
public static string ResolveImageUrl(string itemId, string fallback)
=> GetLocalImageOverride(itemId) ?? fallback;
/// <summary>Only imported Kaggle/upload paths override Food-101 fallbacks (not stale CDN URLs in manifest).</summary>
public static string? GetLocalImageOverride(string itemId)
{
var url = GetImageUrl(itemId);
if (string.IsNullOrWhiteSpace(url)) return null;
if (url.StartsWith("/uploads/", StringComparison.OrdinalIgnoreCase)
|| url.StartsWith("uploads/", StringComparison.OrdinalIgnoreCase))
return url.StartsWith('/') ? url : $"/{url}";
return null;
}
private static void EnsureLoaded()
{
if (_urls is not null) return;
LoadFromDisk();
}
private static void LoadFromDisk()
{
var paths = new[]
{
Path.Combine(AppContext.BaseDirectory, "data", "menu-image-manifest.json"),
Path.Combine(Directory.GetCurrentDirectory(), "data", "menu-image-manifest.json"),
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "data", "menu-image-manifest.json")),
Path.GetFullPath(Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "data", "menu-image-manifest.json")),
};
foreach (var path in paths)
{
if (!File.Exists(path)) continue;
try
{
var json = File.ReadAllText(path);
var doc = JsonDocument.Parse(json);
if (doc.RootElement.TryGetProperty("defaults", out var defaults))
{
if (defaults.TryGetProperty("drink", out var drink))
_defaultDrinkUrl = drink.GetString();
if (defaults.TryGetProperty("food", out var food))
_defaultFoodUrl = food.GetString();
}
var map = new Dictionary<string, string>(StringComparer.Ordinal);
if (doc.RootElement.TryGetProperty("items", out var items))
{
foreach (var prop in items.EnumerateObject())
{
if (prop.Value.TryGetProperty("imageUrl", out var url))
map[prop.Name] = url.GetString() ?? "";
}
}
_urls = map;
return;
}
catch
{
// try next path
}
}
_urls = new Dictionary<string, string>(StringComparer.Ordinal);
}
}
@@ -0,0 +1,67 @@
using Meezi.Core.Entities;
namespace Meezi.Infrastructure.Data;
public enum MenuItemVisualKind
{
Food,
Drink
}
public static class MenuItemImageDefaults
{
private static readonly HashSet<string> DrinkCategoryIds = new(StringComparer.Ordinal)
{
"cat_demo_drinks",
"cat_demo_cold"
};
private static readonly string[] DrinkCategoryHints =
[
"drink", "cold", "coffee", "tea", "juice", "smoothie", "beverage", "bar",
"نوشیدنی", "سرد", "گرم", "قهوه", "چای", "آبمیوه", "اسموتی", "مشروب", "بار"
];
public static MenuItemVisualKind InferKind(string categoryId, string? categoryName = null)
{
if (DrinkCategoryIds.Contains(categoryId))
return MenuItemVisualKind.Drink;
var haystack = $"{categoryId} {categoryName}".ToLowerInvariant();
if (DrinkCategoryHints.Any(h => haystack.Contains(h, StringComparison.Ordinal)))
return MenuItemVisualKind.Drink;
return MenuItemVisualKind.Food;
}
public static string GetDefaultImageUrl(MenuItemVisualKind kind)
=> kind == MenuItemVisualKind.Drink
? MenuImageManifest.GetDefaultDrinkImageUrl()
: MenuImageManifest.GetDefaultFoodImageUrl();
/// <summary>Remote https URL suitable for &lt;img src&gt; (excludes missing local uploads).</summary>
public static bool IsUsableImageUrl(string? imageUrl) =>
!string.IsNullOrWhiteSpace(imageUrl)
&& (imageUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
|| imageUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase));
public static bool NeedsImageRepair(string? imageUrl) => !IsUsableImageUrl(imageUrl);
public static string ResolveImageUrl(string itemId, string categoryId, string? categoryName)
{
var localOverride = MenuImageManifest.GetLocalImageOverride(itemId);
if (!string.IsNullOrWhiteSpace(localOverride))
return localOverride;
var catalog = DemoMenuCatalog.Items.FirstOrDefault(i => i.Id == itemId);
if (catalog is not null)
return DemoMenuCatalog.ResolveItemImageUrl(catalog);
return GetDefaultImageUrl(InferKind(categoryId, categoryName));
}
public static string ResolveDisplayImageUrl(MenuItem item) =>
IsUsableImageUrl(item.ImageUrl)
? item.ImageUrl!
: ResolveImageUrl(item.Id, item.CategoryId, item.Category?.Name);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,672 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Cafes",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
NameAr = table.Column<string>(type: "text", nullable: true),
NameEn = table.Column<string>(type: "text", nullable: true),
Slug = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
Phone = table.Column<string>(type: "text", nullable: true),
Address = table.Column<string>(type: "text", nullable: true),
City = table.Column<string>(type: "text", nullable: true),
LogoUrl = table.Column<string>(type: "text", nullable: true),
PlanTier = table.Column<int>(type: "integer", nullable: false),
PlanExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
IsVerified = table.Column<bool>(type: "boolean", nullable: false),
PreferredLanguage = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Cafes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Branches",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Address = table.Column<string>(type: "text", nullable: true),
City = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Branches", x => x.Id);
table.ForeignKey(
name: "FK_Branches_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Coupons",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Code = table.Column<string>(type: "text", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false),
Value = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
MinOrderAmount = table.Column<decimal>(type: "numeric", nullable: true),
MaxDiscount = table.Column<decimal>(type: "numeric", nullable: true),
UsageLimit = table.Column<int>(type: "integer", nullable: true),
UsedCount = table.Column<int>(type: "integer", nullable: false),
TargetGroup = table.Column<int>(type: "integer", nullable: true),
StartsAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ExpiresAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
IsActive = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Coupons", x => x.Id);
table.ForeignKey(
name: "FK_Coupons_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Customers",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Phone = table.Column<string>(type: "text", nullable: false),
NationalId = table.Column<string>(type: "text", nullable: true),
BirthDateJalali = table.Column<string>(type: "text", nullable: true),
Group = table.Column<int>(type: "integer", nullable: false),
LoyaltyPoints = table.Column<int>(type: "integer", nullable: false),
ReferredBy = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Customers", x => x.Id);
table.ForeignKey(
name: "FK_Customers_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Employees",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Phone = table.Column<string>(type: "text", nullable: false),
NationalId = table.Column<string>(type: "text", nullable: true),
Role = table.Column<int>(type: "integer", nullable: false),
BaseSalary = table.Column<decimal>(type: "numeric", nullable: false),
PinCode = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Employees", x => x.Id);
table.ForeignKey(
name: "FK_Employees_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Taxes",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
Rate = table.Column<decimal>(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false),
IsDefault = table.Column<bool>(type: "boolean", nullable: false),
IsRequired = table.Column<bool>(type: "boolean", nullable: false),
IsCompound = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Taxes", x => x.Id);
table.ForeignKey(
name: "FK_Taxes_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Tables",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
BranchId = table.Column<string>(type: "text", nullable: true),
Number = table.Column<int>(type: "integer", nullable: false),
Capacity = table.Column<int>(type: "integer", nullable: false),
Floor = table.Column<string>(type: "text", nullable: true),
QrCode = table.Column<string>(type: "text", nullable: false),
IsActive = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Tables", x => x.Id);
table.ForeignKey(
name: "FK_Tables_Branches_BranchId",
column: x => x.BranchId,
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_Tables_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Attendances",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
EmployeeId = table.Column<string>(type: "text", nullable: false),
Date = table.Column<DateOnly>(type: "date", nullable: false),
ClockIn = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ClockOut = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Notes = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Attendances", x => x.Id);
table.ForeignKey(
name: "FK_Attendances_Employees_EmployeeId",
column: x => x.EmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "EmployeeSalaries",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
EmployeeId = table.Column<string>(type: "text", nullable: false),
MonthYear = table.Column<string>(type: "text", nullable: false),
BaseSalary = table.Column<decimal>(type: "numeric", nullable: false),
OvertimePay = table.Column<decimal>(type: "numeric", nullable: false),
Deductions = table.Column<decimal>(type: "numeric", nullable: false),
NetSalary = table.Column<decimal>(type: "numeric", nullable: false),
IsPaid = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_EmployeeSalaries", x => x.Id);
table.ForeignKey(
name: "FK_EmployeeSalaries_Employees_EmployeeId",
column: x => x.EmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "LeaveRequests",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
EmployeeId = table.Column<string>(type: "text", nullable: false),
StartDate = table.Column<DateOnly>(type: "date", nullable: false),
EndDate = table.Column<DateOnly>(type: "date", nullable: false),
Reason = table.Column<string>(type: "text", nullable: true),
Status = table.Column<int>(type: "integer", nullable: false),
ReviewedBy = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_LeaveRequests", x => x.Id);
table.ForeignKey(
name: "FK_LeaveRequests_Employees_EmployeeId",
column: x => x.EmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Shifts",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
EmployeeId = table.Column<string>(type: "text", nullable: false),
DayOfWeek = table.Column<int>(type: "integer", nullable: false),
ShiftType = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Shifts", x => x.Id);
table.ForeignKey(
name: "FK_Shifts_Employees_EmployeeId",
column: x => x.EmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "MenuCategories",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
NameAr = table.Column<string>(type: "text", nullable: true),
NameEn = table.Column<string>(type: "text", nullable: true),
SortOrder = table.Column<int>(type: "integer", nullable: false),
TaxId = table.Column<string>(type: "text", nullable: true),
DiscountPercent = table.Column<decimal>(type: "numeric", nullable: false),
IsActive = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MenuCategories", x => x.Id);
table.ForeignKey(
name: "FK_MenuCategories_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MenuCategories_Taxes_TaxId",
column: x => x.TaxId,
principalTable: "Taxes",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "Orders",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
BranchId = table.Column<string>(type: "text", nullable: true),
TableId = table.Column<string>(type: "text", nullable: true),
CustomerId = table.Column<string>(type: "text", nullable: true),
EmployeeId = table.Column<string>(type: "text", nullable: true),
OrderType = table.Column<int>(type: "integer", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
CouponId = table.Column<string>(type: "text", nullable: true),
DiscountAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Subtotal = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
TaxTotal = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Total = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
SnappfoodOrderId = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Orders", x => x.Id);
table.ForeignKey(
name: "FK_Orders_Branches_BranchId",
column: x => x.BranchId,
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_Orders_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Orders_Coupons_CouponId",
column: x => x.CouponId,
principalTable: "Coupons",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_Orders_Customers_CustomerId",
column: x => x.CustomerId,
principalTable: "Customers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_Orders_Employees_EmployeeId",
column: x => x.EmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_Orders_Tables_TableId",
column: x => x.TableId,
principalTable: "Tables",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateTable(
name: "MenuItems",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
CategoryId = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "text", nullable: false),
NameAr = table.Column<string>(type: "text", nullable: true),
NameEn = table.Column<string>(type: "text", nullable: true),
Description = table.Column<string>(type: "text", nullable: true),
Price = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
ImageUrl = table.Column<string>(type: "text", nullable: true),
IsAvailable = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MenuItems", x => x.Id);
table.ForeignKey(
name: "FK_MenuItems_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MenuItems_MenuCategories_CategoryId",
column: x => x.CategoryId,
principalTable: "MenuCategories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Payments",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
OrderId = table.Column<string>(type: "text", nullable: false),
Method = table.Column<int>(type: "integer", nullable: false),
Amount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
Reference = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Payments", x => x.Id);
table.ForeignKey(
name: "FK_Payments_Orders_OrderId",
column: x => x.OrderId,
principalTable: "Orders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "OrderItems",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
OrderId = table.Column<string>(type: "text", nullable: false),
MenuItemId = table.Column<string>(type: "text", nullable: false),
Quantity = table.Column<int>(type: "integer", nullable: false),
UnitPrice = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Notes = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_OrderItems", x => x.Id);
table.ForeignKey(
name: "FK_OrderItems_MenuItems_MenuItemId",
column: x => x.MenuItemId,
principalTable: "MenuItems",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_OrderItems_Orders_OrderId",
column: x => x.OrderId,
principalTable: "Orders",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Attendances_EmployeeId_Date",
table: "Attendances",
columns: new[] { "EmployeeId", "Date" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Branches_CafeId",
table: "Branches",
column: "CafeId");
migrationBuilder.CreateIndex(
name: "IX_Cafes_Slug",
table: "Cafes",
column: "Slug",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Coupons_CafeId_Code",
table: "Coupons",
columns: new[] { "CafeId", "Code" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Customers_CafeId_Phone",
table: "Customers",
columns: new[] { "CafeId", "Phone" });
migrationBuilder.CreateIndex(
name: "IX_Employees_CafeId_Phone",
table: "Employees",
columns: new[] { "CafeId", "Phone" });
migrationBuilder.CreateIndex(
name: "IX_EmployeeSalaries_EmployeeId_MonthYear",
table: "EmployeeSalaries",
columns: new[] { "EmployeeId", "MonthYear" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_LeaveRequests_EmployeeId",
table: "LeaveRequests",
column: "EmployeeId");
migrationBuilder.CreateIndex(
name: "IX_MenuCategories_CafeId",
table: "MenuCategories",
column: "CafeId");
migrationBuilder.CreateIndex(
name: "IX_MenuCategories_TaxId",
table: "MenuCategories",
column: "TaxId");
migrationBuilder.CreateIndex(
name: "IX_MenuItems_CafeId",
table: "MenuItems",
column: "CafeId");
migrationBuilder.CreateIndex(
name: "IX_MenuItems_CategoryId",
table: "MenuItems",
column: "CategoryId");
migrationBuilder.CreateIndex(
name: "IX_OrderItems_MenuItemId",
table: "OrderItems",
column: "MenuItemId");
migrationBuilder.CreateIndex(
name: "IX_OrderItems_OrderId",
table: "OrderItems",
column: "OrderId");
migrationBuilder.CreateIndex(
name: "IX_Orders_BranchId",
table: "Orders",
column: "BranchId");
migrationBuilder.CreateIndex(
name: "IX_Orders_CafeId",
table: "Orders",
column: "CafeId");
migrationBuilder.CreateIndex(
name: "IX_Orders_CouponId",
table: "Orders",
column: "CouponId");
migrationBuilder.CreateIndex(
name: "IX_Orders_CustomerId",
table: "Orders",
column: "CustomerId");
migrationBuilder.CreateIndex(
name: "IX_Orders_EmployeeId",
table: "Orders",
column: "EmployeeId");
migrationBuilder.CreateIndex(
name: "IX_Orders_TableId",
table: "Orders",
column: "TableId");
migrationBuilder.CreateIndex(
name: "IX_Payments_OrderId",
table: "Payments",
column: "OrderId");
migrationBuilder.CreateIndex(
name: "IX_Shifts_EmployeeId_DayOfWeek",
table: "Shifts",
columns: new[] { "EmployeeId", "DayOfWeek" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Tables_BranchId",
table: "Tables",
column: "BranchId");
migrationBuilder.CreateIndex(
name: "IX_Tables_CafeId",
table: "Tables",
column: "CafeId");
migrationBuilder.CreateIndex(
name: "IX_Tables_QrCode",
table: "Tables",
column: "QrCode",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Taxes_CafeId",
table: "Taxes",
column: "CafeId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Attendances");
migrationBuilder.DropTable(
name: "EmployeeSalaries");
migrationBuilder.DropTable(
name: "LeaveRequests");
migrationBuilder.DropTable(
name: "OrderItems");
migrationBuilder.DropTable(
name: "Payments");
migrationBuilder.DropTable(
name: "Shifts");
migrationBuilder.DropTable(
name: "MenuItems");
migrationBuilder.DropTable(
name: "Orders");
migrationBuilder.DropTable(
name: "MenuCategories");
migrationBuilder.DropTable(
name: "Coupons");
migrationBuilder.DropTable(
name: "Customers");
migrationBuilder.DropTable(
name: "Employees");
migrationBuilder.DropTable(
name: "Tables");
migrationBuilder.DropTable(
name: "Taxes");
migrationBuilder.DropTable(
name: "Branches");
migrationBuilder.DropTable(
name: "Cafes");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,66 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddTableReservations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TableReservations",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
CafeId = table.Column<string>(type: "text", nullable: false),
CustomerId = table.Column<string>(type: "text", nullable: true),
GuestName = table.Column<string>(type: "text", nullable: false),
GuestPhone = table.Column<string>(type: "text", nullable: false),
Date = table.Column<DateOnly>(type: "date", nullable: false),
Time = table.Column<TimeOnly>(type: "time without time zone", nullable: false),
PartySize = table.Column<int>(type: "integer", nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
Notes = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_TableReservations", x => x.Id);
table.ForeignKey(
name: "FK_TableReservations_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_TableReservations_Customers_CustomerId",
column: x => x.CustomerId,
principalTable: "Customers",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateIndex(
name: "IX_TableReservations_CafeId_Date_Time",
table: "TableReservations",
columns: new[] { "CafeId", "Date", "Time" });
migrationBuilder.CreateIndex(
name: "IX_TableReservations_CustomerId",
table: "TableReservations",
column: "CustomerId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TableReservations");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,73 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddCafeReviews : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CoverImageUrl",
table: "Cafes",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Description",
table: "Cafes",
type: "text",
nullable: true);
migrationBuilder.CreateTable(
name: "CafeReviews",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
CafeId = table.Column<string>(type: "text", nullable: false),
AuthorName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
AuthorPhone = table.Column<string>(type: "text", nullable: true),
Rating = table.Column<int>(type: "integer", nullable: false),
Comment = table.Column<string>(type: "text", nullable: true),
OwnerReply = table.Column<string>(type: "text", nullable: true),
OwnerRepliedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_CafeReviews", x => x.Id);
table.ForeignKey(
name: "FK_CafeReviews_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_CafeReviews_CafeId_CreatedAt",
table: "CafeReviews",
columns: new[] { "CafeId", "CreatedAt" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CafeReviews");
migrationBuilder.DropColumn(
name: "CoverImageUrl",
table: "Cafes");
migrationBuilder.DropColumn(
name: "Description",
table: "Cafes");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddSubscriptionBilling : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SnappfoodVendorId",
table: "Cafes",
type: "character varying(100)",
maxLength: 100,
nullable: true);
migrationBuilder.CreateTable(
name: "SubscriptionPayments",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
PlanTier = table.Column<int>(type: "integer", nullable: false),
Months = table.Column<int>(type: "integer", nullable: false),
AmountToman = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
AmountRials = table.Column<long>(type: "bigint", nullable: false),
Authority = table.Column<string>(type: "text", nullable: true),
RefId = table.Column<string>(type: "text", nullable: true),
Status = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SubscriptionPayments", x => x.Id);
table.ForeignKey(
name: "FK_SubscriptionPayments_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_SubscriptionPayments_Authority",
table: "SubscriptionPayments",
column: "Authority");
migrationBuilder.CreateIndex(
name: "IX_SubscriptionPayments_CafeId",
table: "SubscriptionPayments",
column: "CafeId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "SubscriptionPayments");
migrationBuilder.DropColumn(
name: "SnappfoodVendorId",
table: "Cafes");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,67 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class TableManagementEnhancements : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "Number",
table: "Tables",
type: "character varying(50)",
maxLength: 50,
nullable: false,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.AddColumn<string>(
name: "TableId",
table: "TableReservations",
type: "text",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_TableReservations_TableId",
table: "TableReservations",
column: "TableId");
migrationBuilder.AddForeignKey(
name: "FK_TableReservations_Tables_TableId",
table: "TableReservations",
column: "TableId",
principalTable: "Tables",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_TableReservations_Tables_TableId",
table: "TableReservations");
migrationBuilder.DropIndex(
name: "IX_TableReservations_TableId",
table: "TableReservations");
migrationBuilder.DropColumn(
name: "TableId",
table: "TableReservations");
migrationBuilder.AlterColumn<int>(
name: "Number",
table: "Tables",
type: "integer",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(50)",
oldMaxLength: 50);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddMenuItemDiscountAndMedia : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "DiscountPercent",
table: "MenuItems",
type: "numeric",
nullable: false,
defaultValue: 0m);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DiscountPercent",
table: "MenuItems");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,93 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddInventoryEntities : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Ingredients",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Unit = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
QuantityOnHand = table.Column<decimal>(type: "numeric(18,3)", precision: 18, scale: 3, nullable: false),
ReorderLevel = table.Column<decimal>(type: "numeric(18,3)", precision: 18, scale: 3, nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Ingredients", x => x.Id);
table.ForeignKey(
name: "FK_Ingredients_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "StockMovements",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
IngredientId = table.Column<string>(type: "text", nullable: false),
Delta = table.Column<decimal>(type: "numeric(18,3)", precision: 18, scale: 3, nullable: false),
Note = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_StockMovements", x => x.Id);
table.ForeignKey(
name: "FK_StockMovements_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_StockMovements_Ingredients_IngredientId",
column: x => x.IngredientId,
principalTable: "Ingredients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Ingredients_CafeId",
table: "Ingredients",
column: "CafeId");
migrationBuilder.CreateIndex(
name: "IX_StockMovements_CafeId",
table: "StockMovements",
column: "CafeId");
migrationBuilder.CreateIndex(
name: "IX_StockMovements_IngredientId",
table: "StockMovements",
column: "IngredientId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "StockMovements");
migrationBuilder.DropTable(
name: "Ingredients");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,48 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddMenuAndTableMedia : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ImageUrl",
table: "Tables",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "VideoUrl",
table: "Tables",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "VideoUrl",
table: "MenuItems",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ImageUrl",
table: "Tables");
migrationBuilder.DropColumn(
name: "VideoUrl",
table: "Tables");
migrationBuilder.DropColumn(
name: "VideoUrl",
table: "MenuItems");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddOrderReservationLink : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ReservationId",
table: "Orders",
type: "text",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Orders_ReservationId",
table: "Orders",
column: "ReservationId");
migrationBuilder.AddForeignKey(
name: "FK_Orders_TableReservations_ReservationId",
table: "Orders",
column: "ReservationId",
principalTable: "TableReservations",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Orders_TableReservations_ReservationId",
table: "Orders");
migrationBuilder.DropIndex(
name: "IX_Orders_ReservationId",
table: "Orders");
migrationBuilder.DropColumn(
name: "ReservationId",
table: "Orders");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddOrderGuestName : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "GuestName",
table: "Orders",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "GuestName",
table: "Orders");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class PosTableSessionFields : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsCleaning",
table: "Tables",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "GuestPhone",
table: "Orders",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IsCleaning",
table: "Tables");
migrationBuilder.DropColumn(
name: "GuestPhone",
table: "Orders");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,41 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class VoidOrderLine : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsVoided",
table: "OrderItems",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<DateTime>(
name: "VoidedAt",
table: "OrderItems",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "VoidedByUserId",
table: "OrderItems",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(name: "VoidedByUserId", table: "OrderItems");
migrationBuilder.DropColumn(name: "VoidedAt", table: "OrderItems");
migrationBuilder.DropColumn(name: "IsVoided", table: "OrderItems");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class EmployeeBranchId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "BranchId",
table: "Employees",
type: "text",
nullable: true);
migrationBuilder.CreateIndex(
name: "IX_Employees_BranchId",
table: "Employees",
column: "BranchId");
migrationBuilder.AddForeignKey(
name: "FK_Employees_Branches_BranchId",
table: "Employees",
column: "BranchId",
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Employees_Branches_BranchId",
table: "Employees");
migrationBuilder.DropIndex(
name: "IX_Employees_BranchId",
table: "Employees");
migrationBuilder.DropColumn(
name: "BranchId",
table: "Employees");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,78 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class DailyQueueTickets : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "QueueTickets",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
BranchId = table.Column<string>(type: "text", nullable: true),
ServiceDate = table.Column<DateOnly>(type: "date", nullable: false),
Number = table.Column<int>(type: "integer", nullable: false),
CustomerLabel = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
IssuedByUserId = table.Column<string>(type: "text", nullable: true),
Status = table.Column<int>(type: "integer", nullable: false),
OrderId = table.Column<string>(type: "text", nullable: true),
IssuedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_QueueTickets", x => x.Id);
table.ForeignKey(
name: "FK_QueueTickets_Branches_BranchId",
column: x => x.BranchId,
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_QueueTickets_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_QueueTickets_Orders_OrderId",
column: x => x.OrderId,
principalTable: "Orders",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateIndex(
name: "IX_QueueTickets_BranchId",
table: "QueueTickets",
column: "BranchId");
migrationBuilder.CreateIndex(
name: "IX_QueueTickets_CafeId_BranchId_ServiceDate_Number",
table: "QueueTickets",
columns: new[] { "CafeId", "BranchId", "ServiceDate", "Number" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_QueueTickets_OrderId",
table: "QueueTickets",
column: "OrderId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "QueueTickets");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,128 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddBranchEntity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Branches_CafeId",
table: "Branches");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Branches",
type: "character varying(200)",
maxLength: 200,
nullable: false,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "City",
table: "Branches",
type: "character varying(100)",
maxLength: 100,
nullable: true,
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Address",
table: "Branches",
type: "character varying(500)",
maxLength: 500,
nullable: true,
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AddColumn<bool>(
name: "IsActive",
table: "Branches",
type: "boolean",
nullable: false,
defaultValue: true);
migrationBuilder.AddColumn<string>(
name: "Phone",
table: "Branches",
type: "character varying(20)",
maxLength: 20,
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "UpdatedAt",
table: "Branches",
type: "timestamp with time zone",
nullable: false,
defaultValueSql: "NOW() AT TIME ZONE 'UTC'");
migrationBuilder.CreateIndex(
name: "IX_Branches_CafeId_IsActive",
table: "Branches",
columns: new[] { "CafeId", "IsActive" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Branches_CafeId_IsActive",
table: "Branches");
migrationBuilder.DropColumn(
name: "IsActive",
table: "Branches");
migrationBuilder.DropColumn(
name: "Phone",
table: "Branches");
migrationBuilder.DropColumn(
name: "UpdatedAt",
table: "Branches");
migrationBuilder.AlterColumn<string>(
name: "Name",
table: "Branches",
type: "text",
nullable: false,
oldClrType: typeof(string),
oldType: "character varying(200)",
oldMaxLength: 200);
migrationBuilder.AlterColumn<string>(
name: "City",
table: "Branches",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(100)",
oldMaxLength: 100,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Address",
table: "Branches",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(500)",
oldMaxLength: 500,
oldNullable: true);
migrationBuilder.CreateIndex(
name: "IX_Branches_CafeId",
table: "Branches",
column: "CafeId");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,184 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddShiftAndCashTransaction : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Shifts_Employees_EmployeeId",
table: "Shifts");
migrationBuilder.RenameTable(
name: "Shifts",
newName: "EmployeeSchedules");
migrationBuilder.RenameIndex(
name: "IX_Shifts_EmployeeId_DayOfWeek",
table: "EmployeeSchedules",
newName: "IX_EmployeeSchedules_EmployeeId_DayOfWeek");
migrationBuilder.AddForeignKey(
name: "FK_EmployeeSchedules_Employees_EmployeeId",
table: "EmployeeSchedules",
column: "EmployeeId",
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
migrationBuilder.CreateTable(
name: "RegisterShifts",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
CafeId = table.Column<string>(type: "text", nullable: false),
BranchId = table.Column<string>(type: "text", nullable: false),
OpenedByUserId = table.Column<string>(type: "text", nullable: false),
ClosedByUserId = table.Column<string>(type: "text", nullable: true),
OpenedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
ClosedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
OpeningCash = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
ClosingCash = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true),
ExpectedCash = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Discrepancy = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true),
Status = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RegisterShifts", x => x.Id);
table.ForeignKey(
name: "FK_RegisterShifts_Branches_BranchId",
column: x => x.BranchId,
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_RegisterShifts_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_RegisterShifts_Employees_ClosedByUserId",
column: x => x.ClosedByUserId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_RegisterShifts_Employees_OpenedByUserId",
column: x => x.OpenedByUserId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "CashTransactions",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
ShiftId = table.Column<string>(type: "text", nullable: false),
BranchId = table.Column<string>(type: "text", nullable: true),
Type = table.Column<int>(type: "integer", nullable: false),
Method = table.Column<int>(type: "integer", nullable: false),
Amount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
ReferenceId = table.Column<string>(type: "text", nullable: true),
Note = table.Column<string>(type: "text", nullable: true),
CreatedByUserId = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CashTransactions", x => x.Id);
table.ForeignKey(
name: "FK_CashTransactions_Branches_BranchId",
column: x => x.BranchId,
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_CashTransactions_RegisterShifts_ShiftId",
column: x => x.ShiftId,
principalTable: "RegisterShifts",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_RegisterShifts_BranchId_Status",
table: "RegisterShifts",
columns: new[] { "BranchId", "Status" });
migrationBuilder.CreateIndex(
name: "IX_RegisterShifts_CafeId",
table: "RegisterShifts",
column: "CafeId");
migrationBuilder.CreateIndex(
name: "IX_RegisterShifts_ClosedByUserId",
table: "RegisterShifts",
column: "ClosedByUserId");
migrationBuilder.CreateIndex(
name: "IX_RegisterShifts_OpenedByUserId",
table: "RegisterShifts",
column: "OpenedByUserId");
migrationBuilder.CreateIndex(
name: "IX_CashTransactions_BranchId",
table: "CashTransactions",
column: "BranchId");
migrationBuilder.CreateIndex(
name: "IX_CashTransactions_CafeId_BranchId",
table: "CashTransactions",
columns: new[] { "CafeId", "BranchId" });
migrationBuilder.CreateIndex(
name: "IX_CashTransactions_ShiftId",
table: "CashTransactions",
column: "ShiftId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CashTransactions");
migrationBuilder.DropTable(
name: "RegisterShifts");
migrationBuilder.DropForeignKey(
name: "FK_EmployeeSchedules_Employees_EmployeeId",
table: "EmployeeSchedules");
migrationBuilder.RenameTable(
name: "EmployeeSchedules",
newName: "Shifts");
migrationBuilder.RenameIndex(
name: "IX_EmployeeSchedules_EmployeeId_DayOfWeek",
table: "Shifts",
newName: "IX_Shifts_EmployeeId_DayOfWeek");
migrationBuilder.AddForeignKey(
name: "FK_Shifts_Employees_EmployeeId",
table: "Shifts",
column: "EmployeeId",
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using Meezi.Core.Entities;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddDailyReport : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DailyReports",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
BranchId = table.Column<string>(type: "text", nullable: false),
Date = table.Column<DateOnly>(type: "date", nullable: false),
TotalRevenue = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
CashRevenue = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
CardRevenue = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
CreditRevenue = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
TotalOrders = table.Column<int>(type: "integer", nullable: false),
AvgOrderValue = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
TotalVoids = table.Column<int>(type: "integer", nullable: false),
VoidAmount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
TotalExpenses = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
NetIncome = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
TopProducts = table.Column<List<TopProductEntry>>(type: "jsonb", nullable: false),
GeneratedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DailyReports", x => x.Id);
table.ForeignKey(
name: "FK_DailyReports_Branches_BranchId",
column: x => x.BranchId,
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_DailyReports_BranchId",
table: "DailyReports",
column: "BranchId");
migrationBuilder.CreateIndex(
name: "IX_DailyReports_CafeId_BranchId_Date",
table: "DailyReports",
columns: new[] { "CafeId", "BranchId", "Date" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DailyReports");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddExpense : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Expenses",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
BranchId = table.Column<string>(type: "text", nullable: false),
ShiftId = table.Column<string>(type: "text", nullable: true),
Category = table.Column<int>(type: "integer", nullable: false),
Amount = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
Note = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
ReceiptImageUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
CreatedByUserId = table.Column<string>(type: "text", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Expenses", x => x.Id);
table.ForeignKey(
name: "FK_Expenses_Branches_BranchId",
column: x => x.BranchId,
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Expenses_RegisterShifts_ShiftId",
column: x => x.ShiftId,
principalTable: "RegisterShifts",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
});
migrationBuilder.CreateIndex(
name: "IX_Expenses_BranchId",
table: "Expenses",
column: "BranchId");
migrationBuilder.CreateIndex(
name: "IX_Expenses_CafeId_BranchId_CreatedAt",
table: "Expenses",
columns: new[] { "CafeId", "BranchId", "CreatedAt" });
migrationBuilder.CreateIndex(
name: "IX_Expenses_ShiftId",
table: "Expenses",
column: "ShiftId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Expenses");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,71 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class BranchMenuItemOverride : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BranchMenuItemOverrides",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
BranchId = table.Column<string>(type: "text", nullable: false),
MenuItemId = table.Column<string>(type: "text", nullable: false),
IsAvailable = table.Column<bool>(type: "boolean", nullable: false),
PriceOverride = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: true),
SortOrderOverride = table.Column<int>(type: "integer", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
UpdatedByUserId = table.Column<string>(type: "text", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_BranchMenuItemOverrides", x => x.Id);
table.ForeignKey(
name: "FK_BranchMenuItemOverrides_Branches_BranchId",
column: x => x.BranchId,
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_BranchMenuItemOverrides_MenuItems_MenuItemId",
column: x => x.MenuItemId,
principalTable: "MenuItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_BranchMenuItemOverrides_BranchId_MenuItemId",
table: "BranchMenuItemOverrides",
columns: new[] { "BranchId", "MenuItemId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_BranchMenuItemOverrides_CafeId",
table: "BranchMenuItemOverrides",
column: "CafeId");
migrationBuilder.CreateIndex(
name: "IX_BranchMenuItemOverrides_MenuItemId",
table: "BranchMenuItemOverrides",
column: "MenuItemId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BranchMenuItemOverrides");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,115 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddPrinterSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "AutoCutEnabled",
table: "Branches",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "KitchenPrinterIp",
table: "Branches",
type: "character varying(45)",
maxLength: 45,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "KitchenPrinterPort",
table: "Branches",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "PaperWidthMm",
table: "Branches",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "ReceiptFooter",
table: "Branches",
type: "character varying(500)",
maxLength: 500,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ReceiptHeader",
table: "Branches",
type: "character varying(500)",
maxLength: 500,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ReceiptPrinterIp",
table: "Branches",
type: "character varying(45)",
maxLength: 45,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "ReceiptPrinterPort",
table: "Branches",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "WifiPassword",
table: "Branches",
type: "character varying(100)",
maxLength: 100,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AutoCutEnabled",
table: "Branches");
migrationBuilder.DropColumn(
name: "KitchenPrinterIp",
table: "Branches");
migrationBuilder.DropColumn(
name: "KitchenPrinterPort",
table: "Branches");
migrationBuilder.DropColumn(
name: "PaperWidthMm",
table: "Branches");
migrationBuilder.DropColumn(
name: "ReceiptFooter",
table: "Branches");
migrationBuilder.DropColumn(
name: "ReceiptHeader",
table: "Branches");
migrationBuilder.DropColumn(
name: "ReceiptPrinterIp",
table: "Branches");
migrationBuilder.DropColumn(
name: "ReceiptPrinterPort",
table: "Branches");
migrationBuilder.DropColumn(
name: "WifiPassword",
table: "Branches");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,171 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class BranchTableOwnership : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "SectionId",
table: "Tables",
type: "text",
nullable: true);
migrationBuilder.AddColumn<int>(
name: "SortOrder",
table: "Tables",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.Sql(
"""
UPDATE "Tables" t
SET "BranchId" = (
SELECT b."Id"
FROM "Branches" b
WHERE b."CafeId" = t."CafeId"
ORDER BY b."CreatedAt"
LIMIT 1
)
WHERE t."BranchId" IS NULL OR t."BranchId" = '';
""");
migrationBuilder.DropForeignKey(
name: "FK_Tables_Branches_BranchId",
table: "Tables");
migrationBuilder.DropIndex(
name: "IX_Tables_BranchId",
table: "Tables");
migrationBuilder.AlterColumn<string>(
name: "BranchId",
table: "Tables",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.CreateTable(
name: "TableSections",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
BranchId = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
SortOrder = table.Column<int>(type: "integer", nullable: false),
IsActive = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TableSections", x => x.Id);
table.ForeignKey(
name: "FK_TableSections_Branches_BranchId",
column: x => x.BranchId,
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_Tables_BranchId_SectionId_SortOrder",
table: "Tables",
columns: new[] { "BranchId", "SectionId", "SortOrder" });
migrationBuilder.CreateIndex(
name: "IX_Tables_SectionId",
table: "Tables",
column: "SectionId");
migrationBuilder.CreateIndex(
name: "IX_TableSections_BranchId_Name",
table: "TableSections",
columns: new[] { "BranchId", "Name" });
migrationBuilder.CreateIndex(
name: "IX_TableSections_CafeId",
table: "TableSections",
column: "CafeId");
migrationBuilder.AddForeignKey(
name: "FK_Tables_Branches_BranchId",
table: "Tables",
column: "BranchId",
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_Tables_TableSections_SectionId",
table: "Tables",
column: "SectionId",
principalTable: "TableSections",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Tables_Branches_BranchId",
table: "Tables");
migrationBuilder.DropForeignKey(
name: "FK_Tables_TableSections_SectionId",
table: "Tables");
migrationBuilder.DropTable(
name: "TableSections");
migrationBuilder.DropIndex(
name: "IX_Tables_BranchId_SectionId_SortOrder",
table: "Tables");
migrationBuilder.DropIndex(
name: "IX_Tables_SectionId",
table: "Tables");
migrationBuilder.DropColumn(
name: "SectionId",
table: "Tables");
migrationBuilder.DropColumn(
name: "SortOrder",
table: "Tables");
migrationBuilder.AlterColumn<string>(
name: "BranchId",
table: "Tables",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.CreateIndex(
name: "IX_Tables_BranchId",
table: "Tables",
column: "BranchId");
migrationBuilder.AddForeignKey(
name: "FK_Tables_Branches_BranchId",
table: "Tables",
column: "BranchId",
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddPosDeviceSettings : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "PosDeviceIp",
table: "Branches",
type: "character varying(45)",
maxLength: 45,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "PosDevicePort",
table: "Branches",
type: "integer",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "PosDeviceIp",
table: "Branches");
migrationBuilder.DropColumn(
name: "PosDevicePort",
table: "Branches");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,33 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class FixOrderItemVoidColumns : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// VoidOrderLine (20260521134834) was recorded but its Up() was empty on some databases.
migrationBuilder.Sql(
"""
ALTER TABLE "OrderItems" ADD COLUMN IF NOT EXISTS "IsVoided" boolean NOT NULL DEFAULT false;
ALTER TABLE "OrderItems" ADD COLUMN IF NOT EXISTS "VoidedAt" timestamp with time zone;
ALTER TABLE "OrderItems" ADD COLUMN IF NOT EXISTS "VoidedByUserId" text;
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
"""
ALTER TABLE "OrderItems" DROP COLUMN IF EXISTS "VoidedByUserId";
ALTER TABLE "OrderItems" DROP COLUMN IF EXISTS "VoidedAt";
ALTER TABLE "OrderItems" DROP COLUMN IF EXISTS "IsVoided";
""");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddMenuCategoryIconAndImage : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Icon",
table: "MenuCategories",
type: "character varying(32)",
maxLength: 32,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ImageUrl",
table: "MenuCategories",
type: "character varying(500)",
maxLength: 500,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Icon",
table: "MenuCategories");
migrationBuilder.DropColumn(
name: "ImageUrl",
table: "MenuCategories");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddMenuCategoryIconPreset : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "IconPresetId",
table: "MenuCategories",
type: "character varying(48)",
maxLength: 48,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "IconStyle",
table: "MenuCategories",
type: "character varying(16)",
maxLength: 16,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "IconPresetId",
table: "MenuCategories");
migrationBuilder.DropColumn(
name: "IconStyle",
table: "MenuCategories");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddCafeThemeJson : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "ThemeJson",
table: "Cafes",
type: "character varying(8000)",
maxLength: 8000,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ThemeJson",
table: "Cafes");
}
}
}
@@ -0,0 +1,91 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class QrGuestMenuAndBranchIdentity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Source",
table: "Orders",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<bool>(
name: "AllowBranchTaxOverride",
table: "Cafes",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<decimal>(
name: "DefaultTaxRate",
table: "Cafes",
type: "numeric",
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<string>(
name: "AccentColor",
table: "Branches",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "LogoUrl",
table: "Branches",
type: "text",
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "TaxRate",
table: "Branches",
type: "numeric",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "WelcomeText",
table: "Branches",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Source",
table: "Orders");
migrationBuilder.DropColumn(
name: "AllowBranchTaxOverride",
table: "Cafes");
migrationBuilder.DropColumn(
name: "DefaultTaxRate",
table: "Cafes");
migrationBuilder.DropColumn(
name: "AccentColor",
table: "Branches");
migrationBuilder.DropColumn(
name: "LogoUrl",
table: "Branches");
migrationBuilder.DropColumn(
name: "TaxRate",
table: "Branches");
migrationBuilder.DropColumn(
name: "WelcomeText",
table: "Branches");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,200 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class DeliveryPlatformIntegration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Orders_CafeId",
table: "Orders");
migrationBuilder.AddColumn<string>(
name: "DeliveryMetaJson",
table: "Orders",
type: "character varying(4000)",
maxLength: 4000,
nullable: true);
migrationBuilder.AddColumn<int>(
name: "DeliveryPlatform",
table: "Orders",
type: "integer",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ExternalOrderId",
table: "Orders",
type: "character varying(120)",
maxLength: 120,
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "PlatformCommission",
table: "Orders",
type: "numeric(18,2)",
precision: 18,
scale: 2,
nullable: false,
defaultValue: 0m);
migrationBuilder.AlterColumn<decimal>(
name: "DefaultTaxRate",
table: "Cafes",
type: "numeric(5,2)",
precision: 5,
scale: 2,
nullable: false,
oldClrType: typeof(decimal),
oldType: "numeric");
migrationBuilder.AddColumn<string>(
name: "DigikalaVendorId",
table: "Cafes",
type: "character varying(100)",
maxLength: 100,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "Tap30VendorId",
table: "Cafes",
type: "character varying(100)",
maxLength: 100,
nullable: true);
migrationBuilder.CreateTable(
name: "DeliveryCommissionRates",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Platform = table.Column<int>(type: "integer", nullable: false),
RatePercent = table.Column<decimal>(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false),
IsActive = table.Column<bool>(type: "boolean", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_DeliveryCommissionRates", x => x.Id);
table.ForeignKey(
name: "FK_DeliveryCommissionRates_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "WebhookLogs",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
CafeId = table.Column<string>(type: "text", nullable: true),
Platform = table.Column<int>(type: "integer", nullable: false),
RawBody = table.Column<string>(type: "text", nullable: false),
SignatureHeader = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
SignatureValid = table.Column<bool>(type: "boolean", nullable: false),
Processed = table.Column<bool>(type: "boolean", nullable: false),
Success = table.Column<bool>(type: "boolean", nullable: false),
ErrorMessage = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
AttemptCount = table.Column<int>(type: "integer", nullable: false),
ExternalOrderId = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: true),
MeeziOrderId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
ProcessedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WebhookLogs", x => x.Id);
table.ForeignKey(
name: "FK_WebhookLogs_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_Orders_CafeId_DeliveryPlatform_ExternalOrderId",
table: "Orders",
columns: new[] { "CafeId", "DeliveryPlatform", "ExternalOrderId" });
migrationBuilder.CreateIndex(
name: "IX_DeliveryCommissionRates_CafeId_Platform",
table: "DeliveryCommissionRates",
columns: new[] { "CafeId", "Platform" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_WebhookLogs_CafeId",
table: "WebhookLogs",
column: "CafeId");
migrationBuilder.CreateIndex(
name: "IX_WebhookLogs_Platform_CreatedAt",
table: "WebhookLogs",
columns: new[] { "Platform", "CreatedAt" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DeliveryCommissionRates");
migrationBuilder.DropTable(
name: "WebhookLogs");
migrationBuilder.DropIndex(
name: "IX_Orders_CafeId_DeliveryPlatform_ExternalOrderId",
table: "Orders");
migrationBuilder.DropColumn(
name: "DeliveryMetaJson",
table: "Orders");
migrationBuilder.DropColumn(
name: "DeliveryPlatform",
table: "Orders");
migrationBuilder.DropColumn(
name: "ExternalOrderId",
table: "Orders");
migrationBuilder.DropColumn(
name: "PlatformCommission",
table: "Orders");
migrationBuilder.DropColumn(
name: "DigikalaVendorId",
table: "Cafes");
migrationBuilder.DropColumn(
name: "Tap30VendorId",
table: "Cafes");
migrationBuilder.AlterColumn<decimal>(
name: "DefaultTaxRate",
table: "Cafes",
type: "numeric",
nullable: false,
oldClrType: typeof(decimal),
oldType: "numeric(5,2)",
oldPrecision: 5,
oldScale: 2);
migrationBuilder.CreateIndex(
name: "IX_Orders_CafeId",
table: "Orders",
column: "CafeId");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,249 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class SystemAdminPlatform : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "IsSuspended",
table: "Cafes",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.CreateTable(
name: "CafeFeatureOverrides",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
FeatureKey = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CafeFeatureOverrides", x => x.Id);
table.ForeignKey(
name: "FK_CafeFeatureOverrides_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PlatformFeatures",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Key = table.Column<string>(type: "character varying(80)", maxLength: 80, nullable: false),
DisplayNameFa = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
DisplayNameEn = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
ModuleGroup = table.Column<string>(type: "character varying(60)", maxLength: 60, nullable: false),
IsEnabledGlobally = table.Column<bool>(type: "boolean", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PlatformFeatures", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PlatformPlanDefinitions",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Tier = table.Column<int>(type: "integer", nullable: false),
DisplayNameFa = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
DisplayNameEn = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
MonthlyPriceToman = table.Column<decimal>(type: "numeric(18,0)", precision: 18, scale: 0, nullable: false),
IsBillableOnline = table.Column<bool>(type: "boolean", nullable: false),
IsActive = table.Column<bool>(type: "boolean", nullable: false),
SortOrder = table.Column<int>(type: "integer", nullable: false),
LimitsJson = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
FeaturesJson = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PlatformPlanDefinitions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PlatformSettings",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Key = table.Column<string>(type: "character varying(120)", maxLength: 120, nullable: false),
Value = table.Column<string>(type: "character varying(8000)", maxLength: 8000, nullable: false),
Category = table.Column<string>(type: "character varying(60)", maxLength: 60, nullable: false),
DescriptionFa = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_PlatformSettings", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SupportTickets",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Subject = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
Status = table.Column<int>(type: "integer", nullable: false),
Priority = table.Column<int>(type: "integer", nullable: false),
CreatedByEmployeeId = table.Column<string>(type: "text", nullable: false),
AssignedAdminId = table.Column<string>(type: "text", nullable: true),
UpdatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
ClosedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SupportTickets", x => x.Id);
table.ForeignKey(
name: "FK_SupportTickets_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SupportTickets_Employees_CreatedByEmployeeId",
column: x => x.CreatedByEmployeeId,
principalTable: "Employees",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "SystemAdmins",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
Phone = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
IsActive = table.Column<bool>(type: "boolean", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SystemAdmins", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SupportTicketMessages",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
TicketId = table.Column<string>(type: "text", nullable: false),
SenderKind = table.Column<int>(type: "integer", nullable: false),
SenderId = table.Column<string>(type: "text", nullable: false),
Body = table.Column<string>(type: "character varying(8000)", maxLength: 8000, nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_SupportTicketMessages", x => x.Id);
table.ForeignKey(
name: "FK_SupportTicketMessages_SupportTickets_TicketId",
column: x => x.TicketId,
principalTable: "SupportTickets",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_CafeFeatureOverrides_CafeId_FeatureKey",
table: "CafeFeatureOverrides",
columns: new[] { "CafeId", "FeatureKey" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PlatformFeatures_Key",
table: "PlatformFeatures",
column: "Key",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PlatformPlanDefinitions_Tier",
table: "PlatformPlanDefinitions",
column: "Tier",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PlatformSettings_Key",
table: "PlatformSettings",
column: "Key",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_SupportTicketMessages_TicketId_CreatedAt",
table: "SupportTicketMessages",
columns: new[] { "TicketId", "CreatedAt" });
migrationBuilder.CreateIndex(
name: "IX_SupportTickets_CafeId_Status_UpdatedAt",
table: "SupportTickets",
columns: new[] { "CafeId", "Status", "UpdatedAt" });
migrationBuilder.CreateIndex(
name: "IX_SupportTickets_CreatedByEmployeeId",
table: "SupportTickets",
column: "CreatedByEmployeeId");
migrationBuilder.CreateIndex(
name: "IX_SystemAdmins_Phone",
table: "SystemAdmins",
column: "Phone",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CafeFeatureOverrides");
migrationBuilder.DropTable(
name: "PlatformFeatures");
migrationBuilder.DropTable(
name: "PlatformPlanDefinitions");
migrationBuilder.DropTable(
name: "PlatformSettings");
migrationBuilder.DropTable(
name: "SupportTicketMessages");
migrationBuilder.DropTable(
name: "SystemAdmins");
migrationBuilder.DropTable(
name: "SupportTickets");
migrationBuilder.DropColumn(
name: "IsSuspended",
table: "Cafes");
}
}
}
@@ -0,0 +1,79 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class OrderNotificationsAndGuestTracking : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "GuestTrackingToken",
table: "Orders",
type: "character varying(64)",
maxLength: 64,
nullable: true);
migrationBuilder.AddColumn<DateTime>(
name: "StatusUpdatedAt",
table: "Orders",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.CreateTable(
name: "CafeNotifications",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Type = table.Column<string>(type: "character varying(60)", maxLength: 60, nullable: false),
Title = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
Body = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: true),
ReferenceId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: true),
TableNumber = table.Column<string>(type: "character varying(40)", maxLength: 40, nullable: true),
IsRead = table.Column<bool>(type: "boolean", nullable: false),
ReadAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CafeNotifications", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_Orders_GuestTrackingToken",
table: "Orders",
column: "GuestTrackingToken");
migrationBuilder.CreateIndex(
name: "IX_CafeNotifications_CafeId_IsRead_CreatedAt",
table: "CafeNotifications",
columns: new[] { "CafeId", "IsRead", "CreatedAt" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CafeNotifications");
migrationBuilder.DropIndex(
name: "IX_Orders_GuestTrackingToken",
table: "Orders");
migrationBuilder.DropColumn(
name: "GuestTrackingToken",
table: "Orders");
migrationBuilder.DropColumn(
name: "StatusUpdatedAt",
table: "Orders");
}
}
}
@@ -0,0 +1,153 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class MenuItemRecipesAndIngredientCost : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_StockMovements_CafeId",
table: "StockMovements");
migrationBuilder.AddColumn<string>(
name: "Kind",
table: "StockMovements",
type: "character varying(30)",
maxLength: 30,
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "OrderId",
table: "StockMovements",
type: "character varying(64)",
maxLength: 64,
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "LowStockWarningPercent",
table: "Ingredients",
type: "numeric(5,2)",
precision: 5,
scale: 2,
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "ParLevel",
table: "Ingredients",
type: "numeric(18,3)",
precision: 18,
scale: 3,
nullable: false,
defaultValue: 0m);
migrationBuilder.AddColumn<decimal>(
name: "UnitCost",
table: "Ingredients",
type: "numeric(18,2)",
precision: 18,
scale: 2,
nullable: false,
defaultValue: 0m);
migrationBuilder.CreateTable(
name: "MenuItemIngredients",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
MenuItemId = table.Column<string>(type: "text", nullable: false),
IngredientId = table.Column<string>(type: "text", nullable: false),
QuantityPerUnit = table.Column<decimal>(type: "numeric(18,3)", precision: 18, scale: 3, nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_MenuItemIngredients", x => x.Id);
table.ForeignKey(
name: "FK_MenuItemIngredients_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MenuItemIngredients_Ingredients_IngredientId",
column: x => x.IngredientId,
principalTable: "Ingredients",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_MenuItemIngredients_MenuItems_MenuItemId",
column: x => x.MenuItemId,
principalTable: "MenuItems",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_StockMovements_CafeId_OrderId",
table: "StockMovements",
columns: new[] { "CafeId", "OrderId" });
migrationBuilder.CreateIndex(
name: "IX_MenuItemIngredients_CafeId_MenuItemId_IngredientId",
table: "MenuItemIngredients",
columns: new[] { "CafeId", "MenuItemId", "IngredientId" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_MenuItemIngredients_IngredientId",
table: "MenuItemIngredients",
column: "IngredientId");
migrationBuilder.CreateIndex(
name: "IX_MenuItemIngredients_MenuItemId",
table: "MenuItemIngredients",
column: "MenuItemId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MenuItemIngredients");
migrationBuilder.DropIndex(
name: "IX_StockMovements_CafeId_OrderId",
table: "StockMovements");
migrationBuilder.DropColumn(
name: "Kind",
table: "StockMovements");
migrationBuilder.DropColumn(
name: "OrderId",
table: "StockMovements");
migrationBuilder.DropColumn(
name: "LowStockWarningPercent",
table: "Ingredients");
migrationBuilder.DropColumn(
name: "ParLevel",
table: "Ingredients");
migrationBuilder.DropColumn(
name: "UnitCost",
table: "Ingredients");
migrationBuilder.CreateIndex(
name: "IX_StockMovements_CafeId",
table: "StockMovements",
column: "CafeId");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,50 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class EmployeePhoneUniquePerCafe : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MenuItemIngredients_Cafes_CafeId",
table: "MenuItemIngredients");
migrationBuilder.DropIndex(
name: "IX_Employees_CafeId_Phone",
table: "Employees");
migrationBuilder.CreateIndex(
name: "IX_Employees_CafeId_Phone",
table: "Employees",
columns: new[] { "CafeId", "Phone" },
unique: true,
filter: "\"DeletedAt\" IS NULL");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Employees_CafeId_Phone",
table: "Employees");
migrationBuilder.CreateIndex(
name: "IX_Employees_CafeId_Phone",
table: "Employees",
columns: new[] { "CafeId", "Phone" });
migrationBuilder.AddForeignKey(
name: "FK_MenuItemIngredients_Cafes_CafeId",
table: "MenuItemIngredients",
column: "CafeId",
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddMenuItemModel3d : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Model3dUrl",
table: "MenuItems",
type: "character varying(500)",
maxLength: 500,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Model3dUrl",
table: "MenuItems");
}
}
}
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddSubscriptionPaymentProvider : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "Provider",
table: "SubscriptionPayments",
type: "integer",
nullable: false,
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Provider",
table: "SubscriptionPayments");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddCafeDiscoverProfileJson : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "DiscoverProfileJson",
table: "Cafes",
type: "character varying(8000)",
maxLength: 8000,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "DiscoverProfileJson",
table: "Cafes");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class OrderDisplayNumber : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
name: "DisplayNumber",
table: "Orders",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.Sql(
"""
WITH numbered AS (
SELECT "Id",
ROW_NUMBER() OVER (PARTITION BY "CafeId" ORDER BY "CreatedAt", "Id") AS rn
FROM "Orders"
)
UPDATE "Orders" o
SET "DisplayNumber" = n.rn
FROM numbered n
WHERE o."Id" = n."Id";
""");
migrationBuilder.CreateIndex(
name: "IX_Orders_CafeId_DisplayNumber",
table: "Orders",
columns: new[] { "CafeId", "DisplayNumber" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Orders_CafeId_DisplayNumber",
table: "Orders");
migrationBuilder.DropColumn(
name: "DisplayNumber",
table: "Orders");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class StockMovementPurchaseCost : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "BranchId",
table: "StockMovements",
type: "character varying(64)",
maxLength: 64,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "ExpenseId",
table: "StockMovements",
type: "character varying(64)",
maxLength: 64,
nullable: true);
migrationBuilder.AddColumn<decimal>(
name: "TotalCostToman",
table: "StockMovements",
type: "numeric(18,2)",
precision: 18,
scale: 2,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "BranchId",
table: "StockMovements");
migrationBuilder.DropColumn(
name: "ExpenseId",
table: "StockMovements");
migrationBuilder.DropColumn(
name: "TotalCostToman",
table: "StockMovements");
}
}
}
@@ -0,0 +1,29 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class BranchScheduledPermanentDelete : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTime>(
name: "ScheduledPermanentDeleteAt",
table: "Branches",
type: "timestamp with time zone",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ScheduledPermanentDeleteAt",
table: "Branches");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,170 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class GrowthOpsFeatures : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "KitchenStationId",
table: "MenuCategories",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "DiscoverBadgesJson",
table: "Cafes",
type: "character varying(2000)",
maxLength: 2000,
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "IsHidden",
table: "CafeReviews",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.CreateTable(
name: "CafeReviewPhotos",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
ReviewId = table.Column<string>(type: "text", nullable: false),
Url = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
SortOrder = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_CafeReviewPhotos", x => x.Id);
table.ForeignKey(
name: "FK_CafeReviewPhotos_CafeReviews_ReviewId",
column: x => x.ReviewId,
principalTable: "CafeReviews",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ConsumerAccounts",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Phone = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Name = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ConsumerAccounts", x => x.Id);
});
migrationBuilder.CreateTable(
name: "KitchenStations",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
BranchId = table.Column<string>(type: "text", nullable: true),
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
PrinterIp = table.Column<string>(type: "character varying(45)", maxLength: 45, nullable: true),
PrinterPort = table.Column<int>(type: "integer", nullable: false),
SortOrder = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CafeId = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_KitchenStations", x => x.Id);
table.ForeignKey(
name: "FK_KitchenStations_Branches_BranchId",
column: x => x.BranchId,
principalTable: "Branches",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
table.ForeignKey(
name: "FK_KitchenStations_Cafes_CafeId",
column: x => x.CafeId,
principalTable: "Cafes",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_MenuCategories_KitchenStationId",
table: "MenuCategories",
column: "KitchenStationId");
migrationBuilder.CreateIndex(
name: "IX_CafeReviewPhotos_ReviewId",
table: "CafeReviewPhotos",
column: "ReviewId");
migrationBuilder.CreateIndex(
name: "IX_ConsumerAccounts_Phone",
table: "ConsumerAccounts",
column: "Phone",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_KitchenStations_BranchId",
table: "KitchenStations",
column: "BranchId");
migrationBuilder.CreateIndex(
name: "IX_KitchenStations_CafeId_SortOrder",
table: "KitchenStations",
columns: new[] { "CafeId", "SortOrder" });
migrationBuilder.AddForeignKey(
name: "FK_MenuCategories_KitchenStations_KitchenStationId",
table: "MenuCategories",
column: "KitchenStationId",
principalTable: "KitchenStations",
principalColumn: "Id",
onDelete: ReferentialAction.SetNull);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_MenuCategories_KitchenStations_KitchenStationId",
table: "MenuCategories");
migrationBuilder.DropTable(
name: "CafeReviewPhotos");
migrationBuilder.DropTable(
name: "ConsumerAccounts");
migrationBuilder.DropTable(
name: "KitchenStations");
migrationBuilder.DropIndex(
name: "IX_MenuCategories_KitchenStationId",
table: "MenuCategories");
migrationBuilder.DropColumn(
name: "KitchenStationId",
table: "MenuCategories");
migrationBuilder.DropColumn(
name: "DiscoverBadgesJson",
table: "Cafes");
migrationBuilder.DropColumn(
name: "IsHidden",
table: "CafeReviews");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,126 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddWebsiteCms : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DemoRequests",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
ContactName = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
BusinessName = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: false),
Phone = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Email = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
BranchCount = table.Column<string>(type: "character varying(20)", maxLength: 20, nullable: false),
Notes = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
Source = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false, defaultValue: "website"),
Status = table.Column<int>(type: "integer", nullable: false),
AdminNotes = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
ContactedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DemoRequests", x => x.Id);
});
migrationBuilder.CreateTable(
name: "WebsiteBlogPosts",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
Slug = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
TitleFa = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
TitleEn = table.Column<string>(type: "character varying(400)", maxLength: 400, nullable: false),
ExcerptFa = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: false),
ExcerptEn = table.Column<string>(type: "character varying(1000)", maxLength: 1000, nullable: false),
ContentFa = table.Column<string>(type: "text", nullable: false),
ContentEn = table.Column<string>(type: "text", nullable: false),
Author = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
CategoryFa = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
CategoryEn = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
TagsJson = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: false, defaultValue: "[]"),
CoverImage = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
IsPublished = table.Column<bool>(type: "boolean", nullable: false),
PublishedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
ViewCount = table.Column<int>(type: "integer", nullable: false),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WebsiteBlogPosts", x => x.Id);
table.UniqueConstraint("AK_WebsiteBlogPosts_Slug", x => x.Slug);
});
migrationBuilder.CreateTable(
name: "WebsiteComments",
columns: table => new
{
Id = table.Column<string>(type: "text", nullable: false),
PostSlug = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
AuthorName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
AuthorEmail = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
Content = table.Column<string>(type: "character varying(3000)", maxLength: 3000, nullable: false),
IsApproved = table.Column<bool>(type: "boolean", nullable: false),
IpAddress = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
CreatedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
DeletedAt = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_WebsiteComments", x => x.Id);
table.ForeignKey(
name: "FK_WebsiteComments_WebsiteBlogPosts_PostSlug",
column: x => x.PostSlug,
principalTable: "WebsiteBlogPosts",
principalColumn: "Slug",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_DemoRequests_Status_CreatedAt",
table: "DemoRequests",
columns: new[] { "Status", "CreatedAt" });
migrationBuilder.CreateIndex(
name: "IX_WebsiteBlogPosts_IsPublished_PublishedAt",
table: "WebsiteBlogPosts",
columns: new[] { "IsPublished", "PublishedAt" });
migrationBuilder.CreateIndex(
name: "IX_WebsiteBlogPosts_Slug",
table: "WebsiteBlogPosts",
column: "Slug",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_WebsiteComments_PostSlug_IsApproved_CreatedAt",
table: "WebsiteComments",
columns: new[] { "PostSlug", "IsApproved", "CreatedAt" });
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "DemoRequests");
migrationBuilder.DropTable(
name: "WebsiteComments");
migrationBuilder.DropTable(
name: "WebsiteBlogPosts");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,58 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Meezi.Infrastructure.Data.Migrations
{
/// <inheritdoc />
public partial class AddCafePublicProfile : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "GalleryJson",
table: "Cafes",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "InstagramHandle",
table: "Cafes",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "WebsiteUrl",
table: "Cafes",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "WorkingHoursJson",
table: "Cafes",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "GalleryJson",
table: "Cafes");
migrationBuilder.DropColumn(
name: "InstagramHandle",
table: "Cafes");
migrationBuilder.DropColumn(
name: "WebsiteUrl",
table: "Cafes");
migrationBuilder.DropColumn(
name: "WorkingHoursJson",
table: "Cafes");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,320 @@
using System.Text.Json;
using Meezi.Core.Constants;
using Meezi.Core.Entities;
using Meezi.Core.Enums;
using Meezi.Core.Platform;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Meezi.Infrastructure.Data;
public static class PlatformDataSeeder
{
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
public static async Task SeedAsync(IServiceProvider services)
{
var env = services.GetRequiredService<IHostEnvironment>();
if (!env.IsDevelopment())
return;
var logger = services.GetRequiredService<ILoggerFactory>().CreateLogger("PlatformDataSeeder");
await using var scope = services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await EnsureCatalogUpgradesAsync(db, logger);
if (!env.IsDevelopment())
return;
await SeedSystemAdminAsync(db, logger);
await SeedPlansAsync(db, logger);
await SeedFeaturesAsync(db, logger);
await SeedSettingsAsync(db, logger);
await EnsureIntegrationSettingsAsync(db, logger);
}
/// <summary>Idempotent plan/feature upgrades for all environments (including production).</summary>
public static async Task EnsureCatalogUpgradesAsync(IServiceProvider services)
{
await using var scope = services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("PlatformDataSeeder");
await EnsureCatalogUpgradesAsync(db, logger);
}
private static async Task EnsureCatalogUpgradesAsync(AppDbContext db, ILogger logger)
{
var featureAdds = new[]
{
("menu_3d", "منوی سه‌بعدی", "3D menu", "growth"),
("menu_3d_ai", "تولید ۳D با هوش مصنوعی", "AI 3D menu", "growth"),
("discover_profile", "پروفایل کشف", "Discover profile", "growth")
};
var existingKeys = await db.PlatformFeatures.Select(f => f.Key).ToListAsync();
var newFeatures = featureAdds
.Where(f => !existingKeys.Contains(f.Item1))
.Select(f => F(f.Item1, f.Item2, f.Item3, f.Item4))
.ToList();
if (newFeatures.Count > 0)
{
db.PlatformFeatures.AddRange(newFeatures);
await db.SaveChangesAsync();
logger.LogInformation("Platform upgrade: added {Count} features", newFeatures.Count);
}
var plans = await db.PlatformPlanDefinitions.ToListAsync();
var changed = 0;
foreach (var plan in plans)
{
if (plan.Tier is PlanTier.Free or PlanTier.Enterprise)
continue;
var keys = plan.Tier == PlanTier.Business || plan.Tier == PlanTier.Enterprise
? new[] { "menu_3d", "menu_3d_ai", "discover_profile" }
: new[] { "menu_3d", "discover_profile" };
var merged = MergeFeaturesJson(plan.FeaturesJson ?? "[]", keys);
if (merged == plan.FeaturesJson) continue;
plan.FeaturesJson = merged;
changed++;
}
if (changed > 0)
{
await db.SaveChangesAsync();
logger.LogInformation("Platform upgrade: updated features on {Count} plans", changed);
}
await EnsureIntegrationSettingsAsync(db, logger);
}
private static string MergeFeaturesJson(string json, params string[] keys)
{
var list = JsonSerializer.Deserialize<List<string>>(json, JsonOpts) ?? [];
if (list.Contains("*"))
return json;
var updated = false;
foreach (var key in keys)
{
if (!list.Contains(key))
{
list.Add(key);
updated = true;
}
}
return updated ? JsonSerializer.Serialize(list, JsonOpts) : json;
}
private static async Task EnsureIntegrationSettingsAsync(AppDbContext db, ILogger logger)
{
var defaults = new[]
{
S("payment.activeGateway", "zarinpal", "payment", "درگاه پیش‌فرض اشتراک"),
S("payment.zarinpal.enabled", "true", "payment", "فعال زرین‌پال"),
S("payment.zarinpal.sandbox", "true", "payment", "حالت تست زرین‌پال"),
S("payment.tara.enabled", "false", "payment", "فعال تارا"),
S("payment.tara.sandbox", "true", "payment", "حالت تست تارا"),
S("payment.snapppay.enabled", "false", "payment", "فعال اسنپ‌پی"),
S("payment.snapppay.sandbox", "true", "payment", "حالت تست اسنپ‌پی"),
S("payment.nextpay.enabled", "false", "payment", "فعال نکست‌پی"),
S("payment.nextpay.sandbox", "true", "payment", "حالت تست نکست‌پی"),
S("payment.vandar.enabled", "false", "payment", "فعال وندار"),
S("payment.vandar.sandbox", "true", "payment", "حالت تست وندار"),
S("integrations.kavenegar.enabled", "true", "integrations", "فعال کاوه‌نگار"),
S("integrations.kavenegar.otpTemplate", "verify", "integrations", "قالب OTP"),
S("integrations.openai.enabled", "false", "integrations", "فعال OpenAI"),
S("integrations.openai.model", "gpt-4o-mini", "integrations", "مدل OpenAI"),
S("integrations.openai.coffeeAdvisor.enabled", "true", "integrations", "مشاور قهوه"),
S("integrations.meshy.enabled", "false", "integrations", "فعال Meshy"),
S("integrations.meshy.menu3d.enabled", "true", "integrations", "ساخت ۳D منو")
};
var existing = await db.PlatformSettings.Select(s => s.Key).ToListAsync();
var missing = defaults.Where(d => !existing.Contains(d.Key)).ToList();
if (missing.Count == 0) return;
db.PlatformSettings.AddRange(missing);
await db.SaveChangesAsync();
logger.LogInformation("Platform seed: added {Count} integration settings", missing.Count);
}
private static async Task SeedSystemAdminAsync(AppDbContext db, ILogger logger)
{
const string phone = "09120000001";
if (await db.SystemAdmins.AnyAsync(a => a.Phone == phone))
return;
db.SystemAdmins.Add(new SystemAdmin
{
Id = "sysadmin_demo",
Name = "مدیر سامانه",
Phone = phone,
IsActive = true
});
await db.SaveChangesAsync();
logger.LogInformation("Platform seed: system admin phone {Phone}", phone);
}
private static async Task SeedPlansAsync(AppDbContext db, ILogger logger)
{
if (await db.PlatformPlanDefinitions.AnyAsync())
return;
var plans = new[]
{
new PlatformPlanDefinition
{
Id = "plan_free",
Tier = PlanTier.Free,
DisplayNameFa = "رایگان",
DisplayNameEn = "Free",
MonthlyPriceToman = 0,
IsBillableOnline = false,
SortOrder = 0,
LimitsJson = JsonSerializer.Serialize(PlanLimitsData.ForTier(PlanTier.Free), JsonOpts),
FeaturesJson = JsonSerializer.Serialize(new[] { "pos", "menu", "tables", "qr_menu" }, JsonOpts)
},
new PlatformPlanDefinition
{
Id = "plan_pro",
Tier = PlanTier.Pro,
DisplayNameFa = "حرفه‌ای",
DisplayNameEn = "Pro",
MonthlyPriceToman = PlanPricing.MonthlyToman(PlanTier.Pro),
IsBillableOnline = true,
SortOrder = 1,
LimitsJson = JsonSerializer.Serialize(PlanLimitsData.ForTier(PlanTier.Pro), JsonOpts),
FeaturesJson = JsonSerializer.Serialize(new[]
{
"pos", "menu", "tables", "qr_menu", "crm", "coupons", "reports", "kds", "inventory",
"menu_3d", "discover_profile"
}, JsonOpts)
},
new PlatformPlanDefinition
{
Id = "plan_business",
Tier = PlanTier.Business,
DisplayNameFa = "کسب‌وکار",
DisplayNameEn = "Business",
MonthlyPriceToman = PlanPricing.MonthlyToman(PlanTier.Business),
IsBillableOnline = true,
SortOrder = 2,
LimitsJson = JsonSerializer.Serialize(PlanLimitsData.ForTier(PlanTier.Business), JsonOpts),
FeaturesJson = JsonSerializer.Serialize(new[]
{
"pos", "menu", "tables", "qr_menu", "crm", "coupons", "reports", "kds", "inventory",
"hr", "sms", "reservations", "delivery", "expenses", "branches",
"menu_3d", "menu_3d_ai", "discover_profile"
}, JsonOpts)
},
new PlatformPlanDefinition
{
Id = "plan_enterprise",
Tier = PlanTier.Enterprise,
DisplayNameFa = "سازمانی",
DisplayNameEn = "Enterprise",
MonthlyPriceToman = 0,
IsBillableOnline = false,
SortOrder = 3,
LimitsJson = JsonSerializer.Serialize(PlanLimitsData.ForTier(PlanTier.Enterprise), JsonOpts),
FeaturesJson = JsonSerializer.Serialize(new[] { "*" }, JsonOpts)
}
};
db.PlatformPlanDefinitions.AddRange(plans);
await db.SaveChangesAsync();
logger.LogInformation("Platform seed: {Count} subscription plans", plans.Length);
}
private static async Task SeedFeaturesAsync(AppDbContext db, ILogger logger)
{
if (await db.PlatformFeatures.AnyAsync())
return;
var features = new[]
{
F("pos", "صندوق", "POS", "core"),
F("menu", "منو", "Menu", "core"),
F("tables", "میزها", "Tables", "core"),
F("qr_menu", "منوی QR", "QR menu", "core"),
F("kds", "آشپزخانه", "KDS", "operations"),
F("crm", "مشتریان", "CRM", "growth"),
F("coupons", "کوپن", "Coupons", "growth"),
F("reports", "گزارش‌ها", "Reports", "analytics"),
F("inventory", "انبار", "Inventory", "operations"),
F("hr", "منابع انسانی", "HR", "operations"),
F("sms", "پیامک", "SMS", "growth"),
F("reservations", "رزرو", "Reservations", "growth"),
F("delivery", "پذیرش آنلاین", "Delivery", "integrations"),
F("expenses", "هزینه‌ها", "Expenses", "analytics"),
F("branches", "چند شعبه", "Branches", "core"),
F("taxes", "مالیات", "Taxes", "compliance"),
F("reviews", "نظرات", "Reviews", "growth"),
F("queue", "صف", "Queue", "operations"),
F("menu_3d", "منوی سه‌بعدی", "3D menu", "growth"),
F("menu_3d_ai", "تولید ۳D با هوش مصنوعی", "AI 3D menu", "growth"),
F("discover_profile", "پروفایل کشف", "Discover profile", "growth")
};
db.PlatformFeatures.AddRange(features);
await db.SaveChangesAsync();
logger.LogInformation("Platform seed: {Count} feature flags", features.Length);
}
private static PlatformFeature F(string key, string fa, string en, string group) => new()
{
Id = $"feat_{key}",
Key = key,
DisplayNameFa = fa,
DisplayNameEn = en,
ModuleGroup = group,
IsEnabledGlobally = true
};
private static async Task SeedSettingsAsync(AppDbContext db, ILogger logger)
{
if (await db.PlatformSettings.AnyAsync())
return;
var settings = new[]
{
S("app.name", "میزی", "branding", "نام اپلیکیشن"),
S("app.tagline", "میزت منتظرته", "branding", "شعار"),
S("auth.maxOtpPerHour", "5", "auth", "حداکثر OTP در ساعت"),
S("billing.zarinpalSandbox", "true", "billing", "درگاه تست زرین‌پال"),
S("support.autoCloseDays", "14", "support", "بستن خودکار تیکت پس از روز"),
S("payment.activeGateway", "zarinpal", "payment", "درگاه فعال اشتراک"),
S("payment.zarinpal.enabled", "true", "payment", "فعال زرین‌پال"),
S("payment.zarinpal.sandbox", "true", "payment", "حالت تست زرین‌پال"),
S("payment.nextpay.enabled", "false", "payment", "فعال نکست‌پی"),
S("payment.nextpay.sandbox", "true", "payment", "حالت تست نکست‌پی"),
S("payment.vandar.enabled", "false", "payment", "فعال وندار"),
S("payment.vandar.sandbox", "true", "payment", "حالت تست وندار"),
S("integrations.kavenegar.enabled", "true", "integrations", "فعال کاوه‌نگار"),
S("integrations.kavenegar.otpTemplate", "verify", "integrations", "قالب OTP"),
S("integrations.openai.enabled", "false", "integrations", "فعال OpenAI"),
S("integrations.openai.model", "gpt-4o-mini", "integrations", "مدل OpenAI"),
S("integrations.openai.coffeeAdvisor.enabled", "true", "integrations", "مشاور قهوه"),
S("integrations.meshy.enabled", "false", "integrations", "فعال Meshy"),
S("integrations.meshy.menu3d.enabled", "true", "integrations", "ساخت ۳D منو")
};
db.PlatformSettings.AddRange(settings);
await db.SaveChangesAsync();
logger.LogInformation("Platform seed: {Count} platform settings", settings.Length);
}
private static PlatformSetting S(string key, string value, string category, string desc) => new()
{
Id = $"cfg_{key.Replace('.', '_')}",
Key = key,
Value = value,
Category = category,
DescriptionFa = desc
};
}

Some files were not shown because too many files have changed in this diff Show More