using System.Text.Json; using Meezi.Core.Entities; using Meezi.Core.Interfaces; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Meezi.Infrastructure.Data; public class AppDbContext : DbContext { // Strict branch isolation. When an active branch scope is present (a // branch-scoped staff session), every branch-owned entity is filtered to that // branch at the DB layer — independent of, and backing up, controller checks. // Café-wide sessions (Owner / "all branches") and non-HTTP contexts (migrations, // background jobs, seeders) leave the scope empty so nothing is filtered. private readonly string? _branchScopeId; private readonly bool _branchScoped; public AppDbContext(DbContextOptions options, IBranchContext? branch = null) : base(options) { if (branch is { HasBranch: true }) { _branchScopeId = branch.BranchId; _branchScoped = true; } } public DbSet Cafes => Set(); public DbSet Branches => Set(); public DbSet Tables => Set
(); public DbSet TableSections => Set(); public DbSet Employees => Set(); public DbSet EmployeeBranchRoles => Set(); public DbSet MenuCategories => Set(); public DbSet MenuItems => Set(); public DbSet BranchMenuItemOverrides => Set(); public DbSet Orders => Set(); public DbSet OrderItems => Set(); public DbSet Payments => Set(); public DbSet Customers => Set(); public DbSet Coupons => Set(); public DbSet Taxes => Set(); public DbSet EmployeeSalaries => Set(); public DbSet Attendances => Set(); public DbSet EmployeeSchedules => Set(); public DbSet RegisterShifts => Set(); public DbSet CashTransactions => Set(); public DbSet LeaveRequests => Set(); public DbSet TableReservations => Set(); public DbSet CafeReviews => Set(); public DbSet CafeReviewPhotos => Set(); public DbSet ConsumerAccounts => Set(); public DbSet KitchenStations => Set(); public DbSet SubscriptionPayments => Set(); public DbSet Ingredients => Set(); public DbSet MenuItemIngredients => Set(); public DbSet StockMovements => Set(); public DbSet QueueTickets => Set(); public DbSet DailyReports => Set(); public DbSet Expenses => Set(); public DbSet WebhookLogs => Set(); public DbSet DeliveryCommissionRates => Set(); public DbSet SystemAdmins => Set(); public DbSet PlatformPlanDefinitions => Set(); public DbSet PlatformSettings => Set(); public DbSet PlatformFeatures => Set(); public DbSet CafeFeatureOverrides => Set(); public DbSet SupportTickets => Set(); public DbSet SupportTicketMessages => Set(); public DbSet CafeNotifications => Set(); // Website CMS public DbSet WebsiteBlogPosts => Set(); public DbSet WebsiteComments => Set(); public DbSet DemoRequests => Set(); // Push notifications (Pushe) public DbSet PushDevices => Set(); // Immutable audit trail of sensitive POS / management actions. public DbSet AuditLogs => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity(e => { e.HasKey(x => x.Id); e.HasIndex(x => x.Token).IsUnique(); e.HasIndex(x => x.City); e.Property(x => x.Token).HasMaxLength(256).IsRequired(); e.Property(x => x.Platform).HasMaxLength(20).IsRequired(); e.Property(x => x.City).HasMaxLength(100); e.Property(x => x.ConsumerAccountId).HasMaxLength(64); e.HasQueryFilter(x => x.DeletedAt == null); }); modelBuilder.Entity(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(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(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 && (!_branchScoped || x.BranchId == _branchScopeId)); }); modelBuilder.Entity
(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 && (!_branchScoped || x.BranchId == _branchScopeId)); }); modelBuilder.Entity(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(e => { e.HasKey(x => x.Id); e.HasIndex(x => new { x.EmployeeId, x.BranchId }) .IsUnique() .HasFilter("\"DeletedAt\" IS NULL"); e.HasIndex(x => new { x.CafeId, x.BranchId }); e.HasOne(x => x.Employee).WithMany(emp => emp.BranchRoles) .HasForeignKey(x => x.EmployeeId).OnDelete(DeleteBehavior.Cascade); e.HasOne(x => x.Branch).WithMany(b => b.StaffRoles) .HasForeignKey(x => x.BranchId).OnDelete(DeleteBehavior.Cascade); e.HasQueryFilter(x => x.DeletedAt == null); }); modelBuilder.Entity(e => { e.HasKey(x => x.Id); e.Property(x => x.Category).HasMaxLength(64).IsRequired(); e.Property(x => x.Action).HasMaxLength(96).IsRequired(); e.Property(x => x.EntityType).HasMaxLength(64); e.Property(x => x.EntityId).HasMaxLength(64); e.Property(x => x.ActorName).HasMaxLength(160); e.Property(x => x.ActorRole).HasMaxLength(32); e.Property(x => x.Summary).HasMaxLength(500).IsRequired(); e.HasIndex(x => new { x.CafeId, x.Category }); e.HasIndex(x => new { x.CafeId, x.BranchId }); e.HasIndex(x => new { x.CafeId, x.CreatedAt }); e.HasOne().WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); e.HasQueryFilter(x => x.DeletedAt == null && (!_branchScoped || x.BranchId == _branchScopeId)); }); modelBuilder.Entity(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(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(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 && (!_branchScoped || x.BranchId == _branchScopeId)); }); modelBuilder.Entity(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 && (!_branchScoped || x.BranchId == _branchScopeId)); }); modelBuilder.Entity(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(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(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(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(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(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(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(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(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 && (!_branchScoped || x.BranchId == _branchScopeId)); }); modelBuilder.Entity(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 && (!_branchScoped || x.BranchId == _branchScopeId)); }); modelBuilder.Entity(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(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(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(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(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(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 && (!_branchScoped || x.BranchId == _branchScopeId)); }); modelBuilder.Entity(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(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(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(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().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(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 && (!_branchScoped || x.BranchId == _branchScopeId)); }); modelBuilder.Entity(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 && (!_branchScoped || x.BranchId == _branchScopeId)); }); modelBuilder.Entity(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, string>( v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default), v => JsonSerializer.Deserialize>(v, JsonSerializerOptions.Default) ?? new List()); var topProductsComparer = new ValueComparer>( (a, b) => JsonSerializer.Serialize(a, JsonSerializerOptions.Default) == JsonSerializer.Serialize(b, JsonSerializerOptions.Default), v => JsonSerializer.Serialize(v, JsonSerializerOptions.Default).GetHashCode(), v => JsonSerializer.Deserialize>( 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 && (!_branchScoped || x.BranchId == _branchScopeId)); }); modelBuilder.Entity(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(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(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(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(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(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(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().WithMany().HasForeignKey(x => x.CafeId).OnDelete(DeleteBehavior.Cascade); e.HasQueryFilter(x => x.DeletedAt == null); }); modelBuilder.Entity(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(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(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(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(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(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); }); } }