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
+12
View File
@@ -0,0 +1,12 @@
namespace Meezi.Core.Entities;
public class Attendance : BaseEntity
{
public string EmployeeId { get; set; } = string.Empty;
public DateOnly Date { get; set; }
public DateTime? ClockIn { get; set; }
public DateTime? ClockOut { get; set; }
public string? Notes { get; set; }
public Employee Employee { get; set; } = null!;
}
+13
View File
@@ -0,0 +1,13 @@
namespace Meezi.Core.Entities;
public abstract class BaseEntity
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? DeletedAt { get; set; }
}
public abstract class TenantEntity : BaseEntity
{
public string CafeId { get; set; } = string.Empty;
}
+42
View File
@@ -0,0 +1,42 @@
namespace Meezi.Core.Entities;
/// <summary>Physical branch (شعبه) under a café tenant.</summary>
public class Branch : TenantEntity
{
public string Name { get; set; } = string.Empty;
public string? Address { get; set; }
public string? City { get; set; }
public string? Phone { get; set; }
public bool IsActive { get; set; } = true;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
/// <summary>When set with <see cref="BaseEntity.DeletedAt"/>, branch can be restored until this UTC time.</summary>
public DateTime? ScheduledPermanentDeleteAt { get; set; }
// Thermal printer (TCP ESC/POS)
public string? ReceiptPrinterIp { get; set; }
public int? ReceiptPrinterPort { get; set; }
public string? KitchenPrinterIp { get; set; }
public int? KitchenPrinterPort { get; set; }
public int PaperWidthMm { get; set; } = 80;
public bool AutoCutEnabled { get; set; } = true;
public string? ReceiptHeader { get; set; }
public string? ReceiptFooter { get; set; }
public string? WifiPassword { get; set; }
/// <summary>Branch-specific logo on QR guest menu (falls back to café logo).</summary>
public string? LogoUrl { get; set; }
public string? WelcomeText { get; set; }
public string? AccentColor { get; set; }
/// <summary>Branch tax % when café <see cref="Cafe.AllowBranchTaxOverride"/> is true.</summary>
public decimal? TaxRate { get; set; }
// Card POS terminal (HTTP bridge on local network)
public string? PosDeviceIp { get; set; }
public int? PosDevicePort { get; set; }
public Cafe Cafe { get; set; } = null!;
public ICollection<TableSection> Sections { get; set; } = [];
public ICollection<Table> Tables { get; set; } = [];
public ICollection<Order> Orders { get; set; } = [];
public ICollection<Employee> Staff { get; set; } = [];
}
@@ -0,0 +1,18 @@
namespace Meezi.Core.Entities;
/// <summary>Per-branch availability and price override for a catalog menu item.</summary>
public class BranchMenuItemOverride : TenantEntity
{
public string BranchId { get; set; } = string.Empty;
public string MenuItemId { get; set; } = string.Empty;
/// <summary>false = hidden at this branch.</summary>
public bool IsAvailable { get; set; } = true;
/// <summary>null = use MenuItem.Price.</summary>
public decimal? PriceOverride { get; set; }
public int? SortOrderOverride { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public string? UpdatedByUserId { get; set; }
public Branch Branch { get; set; } = null!;
public MenuItem MenuItem { get; set; } = null!;
}
+56
View File
@@ -0,0 +1,56 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class Cafe : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string? NameAr { get; set; }
public string? NameEn { get; set; }
public string Slug { get; set; } = string.Empty;
public string? Phone { get; set; }
public string? Address { get; set; }
public string? City { get; set; }
public string? LogoUrl { get; set; }
public string? CoverImageUrl { get; set; }
public string? Description { get; set; }
public PlanTier PlanTier { get; set; } = PlanTier.Free;
public DateTime? PlanExpiresAt { get; set; }
public bool IsVerified { get; set; }
/// <summary>When true, merchant API access is blocked until reactivated by platform admin.</summary>
public bool IsSuspended { get; set; }
public string? SnappfoodVendorId { get; set; }
public string? Tap30VendorId { get; set; }
public string? DigikalaVendorId { get; set; }
public string PreferredLanguage { get; set; } = "fa";
/// <summary>JSON <see cref="Branding.CafeTheme"/> for dashboard + guest menu colors/styles.</summary>
public string? ThemeJson { get; set; }
/// <summary>JSON <see cref="Discover.CafeDiscoverProfile"/> for discover / AI matching.</summary>
public string? DiscoverProfileJson { get; set; }
/// <summary>JSON array of <see cref="Discover.CafeBadgeCatalog"/> keys (Enterprise, admin-assigned).</summary>
public string? DiscoverBadgesJson { get; set; }
/// <summary>JSON array of up to 8 gallery photo URLs (owner-managed).</summary>
public string? GalleryJson { get; set; }
/// <summary>JSON <see cref="Discover.WorkingHoursSchedule"/> per-day schedule.</summary>
public string? WorkingHoursJson { get; set; }
/// <summary>Instagram handle without @, max 80 chars.</summary>
public string? InstagramHandle { get; set; }
/// <summary>Cafe website URL, max 300 chars.</summary>
public string? WebsiteUrl { get; set; }
/// <summary>Default VAT/sales tax % for all branches unless branch override is allowed.</summary>
public decimal DefaultTaxRate { get; set; } = 9m;
public bool AllowBranchTaxOverride { get; set; }
public ICollection<Branch> Branches { get; set; } = [];
public ICollection<Table> Tables { get; set; } = [];
public ICollection<Employee> Employees { get; set; } = [];
public ICollection<MenuCategory> MenuCategories { get; set; } = [];
public ICollection<MenuItem> MenuItems { get; set; } = [];
public ICollection<Order> Orders { get; set; } = [];
public ICollection<Customer> Customers { get; set; } = [];
public ICollection<Coupon> Coupons { get; set; } = [];
public ICollection<Tax> Taxes { get; set; } = [];
public ICollection<CafeReview> Reviews { get; set; } = [];
public ICollection<SubscriptionPayment> SubscriptionPayments { get; set; } = [];
public ICollection<Ingredient> Ingredients { get; set; } = [];
}
@@ -0,0 +1,7 @@
namespace Meezi.Core.Entities;
public class CafeFeatureOverride : TenantEntity
{
public string FeatureKey { get; set; } = string.Empty;
public bool IsEnabled { get; set; }
}
@@ -0,0 +1,12 @@
namespace Meezi.Core.Entities;
public class CafeNotification : TenantEntity
{
public string Type { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? Body { get; set; }
public string? ReferenceId { get; set; }
public string? TableNumber { get; set; }
public bool IsRead { get; set; }
public DateTime? ReadAt { get; set; }
}
+16
View File
@@ -0,0 +1,16 @@
namespace Meezi.Core.Entities;
public class CafeReview : BaseEntity
{
public string CafeId { get; set; } = string.Empty;
public string AuthorName { get; set; } = string.Empty;
public string? AuthorPhone { get; set; }
public int Rating { get; set; }
public string? Comment { get; set; }
public string? OwnerReply { get; set; }
public DateTime? OwnerRepliedAt { get; set; }
public bool IsHidden { get; set; }
public Cafe Cafe { get; set; } = null!;
public ICollection<CafeReviewPhoto> Photos { get; set; } = [];
}
@@ -0,0 +1,10 @@
namespace Meezi.Core.Entities;
public class CafeReviewPhoto : BaseEntity
{
public string ReviewId { get; set; } = string.Empty;
public string Url { get; set; } = string.Empty;
public int SortOrder { get; set; }
public CafeReview Review { get; set; } = null!;
}
@@ -0,0 +1,18 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class CashTransaction : TenantEntity
{
public string ShiftId { get; set; } = string.Empty;
public string? BranchId { get; set; }
public CashTransactionType Type { get; set; }
public PaymentMethod Method { get; set; }
public decimal Amount { get; set; }
public string? ReferenceId { get; set; }
public string? Note { get; set; }
public string CreatedByUserId { get; set; } = string.Empty;
public Shift Shift { get; set; } = null!;
public Branch? Branch { get; set; }
}
@@ -0,0 +1,8 @@
namespace Meezi.Core.Entities;
/// <summary>Platform guest account (OTP login) for order history across cafés.</summary>
public class ConsumerAccount : BaseEntity
{
public string Phone { get; set; } = string.Empty;
public string? Name { get; set; }
}
+21
View File
@@ -0,0 +1,21 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class Coupon : TenantEntity
{
public string Code { get; set; } = string.Empty;
public CouponType Type { get; set; }
public decimal Value { get; set; }
public decimal? MinOrderAmount { get; set; }
public decimal? MaxDiscount { get; set; }
public int? UsageLimit { get; set; }
public int UsedCount { get; set; }
public CustomerGroup? TargetGroup { get; set; }
public DateTime? StartsAt { get; set; }
public DateTime? ExpiresAt { get; set; }
public bool IsActive { get; set; } = true;
public Cafe Cafe { get; set; } = null!;
public ICollection<Order> Orders { get; set; } = [];
}
+17
View File
@@ -0,0 +1,17 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class Customer : TenantEntity
{
public string Name { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string? NationalId { get; set; }
public string? BirthDateJalali { get; set; }
public CustomerGroup Group { get; set; } = CustomerGroup.Regular;
public int LoyaltyPoints { get; set; }
public string? ReferredBy { get; set; }
public Cafe Cafe { get; set; } = null!;
public ICollection<Order> Orders { get; set; } = [];
}
+22
View File
@@ -0,0 +1,22 @@
namespace Meezi.Core.Entities;
/// <summary>Pre-aggregated daily metrics per branch for fast dashboard reads.</summary>
public class DailyReport : TenantEntity
{
public string BranchId { get; set; } = string.Empty;
public DateOnly Date { get; set; }
public decimal TotalRevenue { get; set; }
public decimal CashRevenue { get; set; }
public decimal CardRevenue { get; set; }
public decimal CreditRevenue { get; set; }
public int TotalOrders { get; set; }
public decimal AvgOrderValue { get; set; }
public int TotalVoids { get; set; }
public decimal VoidAmount { get; set; }
public decimal TotalExpenses { get; set; }
public decimal NetIncome { get; set; }
public List<TopProductEntry> TopProducts { get; set; } = [];
public DateTime GeneratedAt { get; set; } = DateTime.UtcNow;
public Branch Branch { get; set; } = null!;
}
@@ -0,0 +1,14 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
/// <summary>Configurable platform commission % per café (falls back to app defaults).</summary>
public class DeliveryCommissionRate : TenantEntity
{
public DeliveryPlatform Platform { get; set; }
public decimal RatePercent { get; set; }
public bool IsActive { get; set; } = true;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public Cafe Cafe { get; set; } = null!;
}
+17
View File
@@ -0,0 +1,17 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class DemoRequest : BaseEntity
{
public string ContactName { get; set; } = string.Empty;
public string BusinessName { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string? Email { get; set; }
public string BranchCount { get; set; } = "1";
public string? Notes { get; set; }
public string Source { get; set; } = "website";
public DemoRequestStatus Status { get; set; } = DemoRequestStatus.New;
public string? AdminNotes { get; set; }
public DateTime? ContactedAt { get; set; }
}
+22
View File
@@ -0,0 +1,22 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class Employee : TenantEntity
{
public string? BranchId { get; set; }
public string Name { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public string? NationalId { get; set; }
public EmployeeRole Role { get; set; }
public decimal BaseSalary { get; set; }
public string? PinCode { get; set; }
public Cafe Cafe { get; set; } = null!;
public Branch? Branch { get; set; }
public ICollection<Order> Orders { get; set; } = [];
public ICollection<EmployeeSalary> Salaries { get; set; } = [];
public ICollection<Attendance> Attendances { get; set; } = [];
public ICollection<EmployeeSchedule> Schedules { get; set; } = [];
public ICollection<LeaveRequest> LeaveRequests { get; set; } = [];
}
+14
View File
@@ -0,0 +1,14 @@
namespace Meezi.Core.Entities;
public class EmployeeSalary : BaseEntity
{
public string EmployeeId { get; set; } = string.Empty;
public string MonthYear { get; set; } = string.Empty;
public decimal BaseSalary { get; set; }
public decimal OvertimePay { get; set; }
public decimal Deductions { get; set; }
public decimal NetSalary { get; set; }
public bool IsPaid { get; set; }
public Employee Employee { get; set; } = null!;
}
@@ -0,0 +1,13 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
/// <summary>Weekly HR work schedule for an employee (not cash register).</summary>
public class EmployeeSchedule : BaseEntity
{
public string EmployeeId { get; set; } = string.Empty;
public int DayOfWeek { get; set; }
public ShiftType ShiftType { get; set; }
public Employee Employee { get; set; } = null!;
}
+17
View File
@@ -0,0 +1,17 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class Expense : TenantEntity
{
public string BranchId { get; set; } = string.Empty;
public string? ShiftId { get; set; }
public ExpenseCategory Category { get; set; }
public decimal Amount { get; set; }
public string? Note { get; set; }
public string? ReceiptImageUrl { get; set; }
public string CreatedByUserId { get; set; } = string.Empty;
public Branch Branch { get; set; } = null!;
public Shift? Shift { get; set; }
}
+19
View File
@@ -0,0 +1,19 @@
namespace Meezi.Core.Entities;
public class Ingredient : TenantEntity
{
public string Name { get; set; } = string.Empty;
public string Unit { get; set; } = "عدد";
public decimal QuantityOnHand { get; set; }
public decimal ReorderLevel { get; set; }
/// <summary>Cost per unit (Toman) for COGS reporting.</summary>
public decimal UnitCost { get; set; }
/// <summary>Target / full stock level used with warning percent (e.g. 500 g).</summary>
public decimal ParLevel { get; set; }
/// <summary>Warn when on-hand falls below ParLevel × (this ÷ 100). Default 20 = 20%.</summary>
public decimal LowStockWarningPercent { get; set; } = 20m;
public Cafe Cafe { get; set; } = null!;
public ICollection<StockMovement> Movements { get; set; } = [];
public ICollection<MenuItemIngredient> MenuItemRecipes { get; set; } = [];
}
+15
View File
@@ -0,0 +1,15 @@
namespace Meezi.Core.Entities;
/// <summary>Kitchen/bar print station — routes category items to a dedicated printer.</summary>
public class KitchenStation : TenantEntity
{
public string? BranchId { get; set; }
public string Name { get; set; } = string.Empty;
public string? PrinterIp { get; set; }
public int PrinterPort { get; set; } = 9100;
public int SortOrder { get; set; }
public Cafe Cafe { get; set; } = null!;
public Branch? Branch { get; set; }
public ICollection<MenuCategory> Categories { get; set; } = [];
}
+15
View File
@@ -0,0 +1,15 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class LeaveRequest : BaseEntity
{
public string EmployeeId { get; set; } = string.Empty;
public DateOnly StartDate { get; set; }
public DateOnly EndDate { get; set; }
public string? Reason { get; set; }
public LeaveStatus Status { get; set; } = LeaveStatus.Pending;
public string? ReviewedBy { get; set; }
public Employee Employee { get; set; } = null!;
}
+25
View File
@@ -0,0 +1,25 @@
namespace Meezi.Core.Entities;
public class MenuCategory : TenantEntity
{
public string Name { get; set; } = string.Empty;
public string? NameAr { get; set; }
public string? NameEn { get; set; }
public int SortOrder { get; set; }
public string? TaxId { get; set; }
public decimal DiscountPercent { get; set; }
/// <summary>Emoji or short icon label shown when no preset/image is set.</summary>
public string? Icon { get; set; }
/// <summary>Preset icon id (see CategoryIconPresets.PresetId).</summary>
public string? IconPresetId { get; set; }
/// <summary>Preset visual style: flat, modern, real, minimal, outline.</summary>
public string? IconStyle { get; set; }
public string? ImageUrl { get; set; }
public bool IsActive { get; set; } = true;
public string? KitchenStationId { get; set; }
public Cafe Cafe { get; set; } = null!;
public KitchenStation? KitchenStation { get; set; }
public Tax? Tax { get; set; }
public ICollection<MenuItem> MenuItems { get; set; } = [];
}
+24
View File
@@ -0,0 +1,24 @@
namespace Meezi.Core.Entities;
public class MenuItem : TenantEntity
{
public string CategoryId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? NameAr { get; set; }
public string? NameEn { get; set; }
public string? Description { get; set; }
public decimal Price { get; set; }
/// <summary>Item-level promo (0100). Takes precedence over category discount when &gt; 0.</summary>
public decimal DiscountPercent { get; set; }
public string? ImageUrl { get; set; }
public string? VideoUrl { get; set; }
/// <summary>GLB/GLTF 3D model for QR menu interactive view (poster = ImageUrl).</summary>
public string? Model3dUrl { get; set; }
public bool IsAvailable { get; set; } = true;
public Cafe Cafe { get; set; } = null!;
public MenuCategory Category { get; set; } = null!;
public ICollection<OrderItem> OrderItems { get; set; } = [];
public ICollection<BranchMenuItemOverride> BranchOverrides { get; set; } = [];
public ICollection<MenuItemIngredient> RecipeIngredients { get; set; } = [];
}
@@ -0,0 +1,13 @@
namespace Meezi.Core.Entities;
/// <summary>Bill of materials: how much of an ingredient each menu item consumes per unit sold.</summary>
public class MenuItemIngredient : TenantEntity
{
public string MenuItemId { get; set; } = string.Empty;
public string IngredientId { get; set; } = string.Empty;
/// <summary>Amount of ingredient used per 1 sold unit (e.g. 10 g coffee per espresso).</summary>
public decimal QuantityPerUnit { get; set; }
public MenuItem MenuItem { get; set; } = null!;
public Ingredient Ingredient { get; set; } = null!;
}
+46
View File
@@ -0,0 +1,46 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class Order : TenantEntity
{
public string? BranchId { get; set; }
public string? TableId { get; set; }
public string? ReservationId { get; set; }
/// <summary>Walk-in label at POS when no CRM customer is linked.</summary>
public string? GuestName { get; set; }
public string? GuestPhone { get; set; }
public string? CustomerId { get; set; }
public string? EmployeeId { get; set; }
public OrderType OrderType { get; set; }
public OrderSource Source { get; set; } = OrderSource.Pos;
public OrderStatus Status { get; set; } = OrderStatus.Pending;
/// <summary>Human-facing order number (digits only), unique per cafe.</summary>
public int DisplayNumber { get; set; }
public DateTime StatusUpdatedAt { get; set; } = DateTime.UtcNow;
/// <summary>Secret token for guest order tracking (QR) without login.</summary>
public string? GuestTrackingToken { get; set; }
public string? CouponId { get; set; }
public decimal DiscountAmount { get; set; }
public decimal Subtotal { get; set; }
public decimal TaxTotal { get; set; }
public decimal Total { get; set; }
public string? SnappfoodOrderId { get; set; }
/// <summary>External order id from delivery platform (Snappfood, Tap30, etc.).</summary>
public string? ExternalOrderId { get; set; }
public DeliveryPlatform? DeliveryPlatform { get; set; }
/// <summary>Platform commission amount deducted from gross (stored for finance reports).</summary>
public decimal PlatformCommission { get; set; }
/// <summary>JSON snapshot: driver, address, delivery ETA, etc.</summary>
public string? DeliveryMetaJson { get; set; }
public Cafe Cafe { get; set; } = null!;
public Branch? Branch { get; set; }
public Table? Table { get; set; }
public TableReservation? Reservation { get; set; }
public Customer? Customer { get; set; }
public Employee? Employee { get; set; }
public Coupon? Coupon { get; set; }
public ICollection<OrderItem> Items { get; set; } = [];
public ICollection<Payment> Payments { get; set; } = [];
}
+16
View File
@@ -0,0 +1,16 @@
namespace Meezi.Core.Entities;
public class OrderItem : BaseEntity
{
public string OrderId { get; set; } = string.Empty;
public string MenuItemId { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public string? Notes { get; set; }
public bool IsVoided { get; set; }
public DateTime? VoidedAt { get; set; }
public string? VoidedByUserId { get; set; }
public Order Order { get; set; } = null!;
public MenuItem MenuItem { get; set; } = null!;
}
+14
View File
@@ -0,0 +1,14 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class Payment : BaseEntity
{
public string OrderId { get; set; } = string.Empty;
public PaymentMethod Method { get; set; }
public decimal Amount { get; set; }
public PaymentStatus Status { get; set; } = PaymentStatus.Pending;
public string? Reference { get; set; }
public Order Order { get; set; } = null!;
}
@@ -0,0 +1,11 @@
namespace Meezi.Core.Entities;
public class PlatformFeature : BaseEntity
{
public string Key { get; set; } = string.Empty;
public string DisplayNameFa { get; set; } = string.Empty;
public string? DisplayNameEn { get; set; }
public string ModuleGroup { get; set; } = "general";
public bool IsEnabledGlobally { get; set; } = true;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
@@ -0,0 +1,18 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class PlatformPlanDefinition : BaseEntity
{
public PlanTier Tier { get; set; }
public string DisplayNameFa { get; set; } = string.Empty;
public string? DisplayNameEn { get; set; }
public decimal MonthlyPriceToman { get; set; }
public bool IsBillableOnline { get; set; }
public bool IsActive { get; set; } = true;
public int SortOrder { get; set; }
/// <summary>JSON <see cref="Platform.PlanLimitsData"/>.</summary>
public string LimitsJson { get; set; } = "{}";
/// <summary>JSON array of feature keys enabled on this plan.</summary>
public string? FeaturesJson { get; set; }
}
@@ -0,0 +1,10 @@
namespace Meezi.Core.Entities;
public class PlatformSetting : BaseEntity
{
public string Key { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public string Category { get; set; } = "general";
public string? DescriptionFa { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
}
+20
View File
@@ -0,0 +1,20 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
/// <summary>Daily take-a-number ticket; sequence resets each calendar day (branch-local).</summary>
public class QueueTicket : TenantEntity
{
public string? BranchId { get; set; }
public DateOnly ServiceDate { get; set; }
public int Number { get; set; }
public string? CustomerLabel { get; set; }
public string? IssuedByUserId { get; set; }
public QueueTicketStatus Status { get; set; } = QueueTicketStatus.Waiting;
public string? OrderId { get; set; }
public DateTime IssuedAt { get; set; }
public Cafe Cafe { get; set; } = null!;
public Branch? Branch { get; set; }
public Order? Order { get; set; }
}
+24
View File
@@ -0,0 +1,24 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
/// <summary>Cash register session (باز / بسته کردن صندوق) for a branch.</summary>
public class Shift : TenantEntity
{
public string BranchId { get; set; } = string.Empty;
public string OpenedByUserId { get; set; } = string.Empty;
public string? ClosedByUserId { get; set; }
public DateTime OpenedAt { get; set; }
public DateTime? ClosedAt { get; set; }
public decimal OpeningCash { get; set; }
public decimal? ClosingCash { get; set; }
public decimal ExpectedCash { get; set; }
public decimal? Discrepancy { get; set; }
public ShiftStatus Status { get; set; } = ShiftStatus.Open;
public Cafe Cafe { get; set; } = null!;
public Branch Branch { get; set; } = null!;
public Employee OpenedBy { get; set; } = null!;
public Employee? ClosedBy { get; set; }
public ICollection<CashTransaction> Transactions { get; set; } = [];
}
+20
View File
@@ -0,0 +1,20 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class StockMovement : TenantEntity
{
public string IngredientId { get; set; } = string.Empty;
public string? BranchId { get; set; }
public decimal Delta { get; set; }
public StockMovementKind Kind { get; set; } = StockMovementKind.Manual;
public string? OrderId { get; set; }
/// <summary>Total amount paid for this movement (Toman), when stock is received.</summary>
public decimal? TotalCostToman { get; set; }
/// <summary>Linked expense row for budget / reports (purchase only).</summary>
public string? ExpenseId { get; set; }
public string? Note { get; set; }
public Cafe Cafe { get; set; } = null!;
public Ingredient Ingredient { get; set; } = null!;
}
@@ -0,0 +1,17 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class SubscriptionPayment : TenantEntity
{
public PlanTier PlanTier { get; set; }
public int Months { get; set; }
public decimal AmountToman { get; set; }
public long AmountRials { get; set; }
public PaymentProvider Provider { get; set; } = PaymentProvider.ZarinPal;
public string? Authority { get; set; }
public string? RefId { get; set; }
public SubscriptionPaymentStatus Status { get; set; } = SubscriptionPaymentStatus.Pending;
public Cafe Cafe { get; set; } = null!;
}
+18
View File
@@ -0,0 +1,18 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class SupportTicket : TenantEntity
{
public string Subject { get; set; } = string.Empty;
public SupportTicketStatus Status { get; set; } = SupportTicketStatus.Open;
public SupportTicketPriority Priority { get; set; } = SupportTicketPriority.Normal;
public string CreatedByEmployeeId { get; set; } = string.Empty;
public string? AssignedAdminId { get; set; }
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
public DateTime? ClosedAt { get; set; }
public Cafe? Cafe { get; set; }
public Employee? CreatedByEmployee { get; set; }
public ICollection<SupportTicketMessage> Messages { get; set; } = [];
}
@@ -0,0 +1,13 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class SupportTicketMessage : BaseEntity
{
public string TicketId { get; set; } = string.Empty;
public TicketMessageSenderKind SenderKind { get; set; }
public string SenderId { get; set; } = string.Empty;
public string Body { get; set; } = string.Empty;
public SupportTicket? Ticket { get; set; }
}
+8
View File
@@ -0,0 +1,8 @@
namespace Meezi.Core.Entities;
public class SystemAdmin : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
}
+21
View File
@@ -0,0 +1,21 @@
namespace Meezi.Core.Entities;
public class Table : TenantEntity
{
public string BranchId { get; set; } = string.Empty;
public string? SectionId { get; set; }
public string Number { get; set; } = string.Empty;
public int Capacity { get; set; } = 4;
public string? Floor { get; set; }
public int SortOrder { get; set; }
public string QrCode { get; set; } = string.Empty;
public string? ImageUrl { get; set; }
public string? VideoUrl { get; set; }
public bool IsActive { get; set; } = true;
public bool IsCleaning { get; set; }
public Cafe Cafe { get; set; } = null!;
public Branch Branch { get; set; } = null!;
public TableSection? Section { get; set; }
public ICollection<Order> Orders { get; set; } = [];
}
@@ -0,0 +1,21 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
public class TableReservation : BaseEntity
{
public string CafeId { get; set; } = string.Empty;
public string? TableId { get; set; }
public string? CustomerId { get; set; }
public string GuestName { get; set; } = string.Empty;
public string GuestPhone { get; set; } = string.Empty;
public DateOnly Date { get; set; }
public TimeOnly Time { get; set; }
public int PartySize { get; set; }
public ReservationStatus Status { get; set; } = ReservationStatus.Pending;
public string? Notes { get; set; }
public Cafe Cafe { get; set; } = null!;
public Table? Table { get; set; }
public Customer? Customer { get; set; }
}
+13
View File
@@ -0,0 +1,13 @@
namespace Meezi.Core.Entities;
/// <summary>Floor area within a branch (سالن، تراس، VIP).</summary>
public class TableSection : TenantEntity
{
public string BranchId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public int SortOrder { get; set; }
public bool IsActive { get; set; } = true;
public Branch Branch { get; set; } = null!;
public ICollection<Table> Tables { get; set; } = [];
}
+13
View File
@@ -0,0 +1,13 @@
namespace Meezi.Core.Entities;
public class Tax : TenantEntity
{
public string Name { get; set; } = string.Empty;
public decimal Rate { get; set; }
public bool IsDefault { get; set; }
public bool IsRequired { get; set; }
public bool IsCompound { get; set; }
public Cafe Cafe { get; set; } = null!;
public ICollection<MenuCategory> MenuCategories { get; set; } = [];
}
@@ -0,0 +1,9 @@
namespace Meezi.Core.Entities;
public class TopProductEntry
{
public string ProductId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public int Quantity { get; set; }
public decimal Revenue { get; set; }
}
+22
View File
@@ -0,0 +1,22 @@
using Meezi.Core.Enums;
namespace Meezi.Core.Entities;
/// <summary>Audit log for inbound delivery platform webhooks (debugging & dead-letter).</summary>
public class WebhookLog : BaseEntity
{
public string? CafeId { get; set; }
public DeliveryPlatform Platform { get; set; }
public string RawBody { get; set; } = string.Empty;
public string? SignatureHeader { get; set; }
public bool SignatureValid { get; set; }
public bool Processed { get; set; }
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public int AttemptCount { get; set; }
public string? ExternalOrderId { get; set; }
public string? MeeziOrderId { get; set; }
public DateTime? ProcessedAt { get; set; }
public Cafe? Cafe { get; set; }
}
@@ -0,0 +1,23 @@
namespace Meezi.Core.Entities;
public class WebsiteBlogPost : BaseEntity
{
public string Slug { get; set; } = string.Empty;
public string TitleFa { get; set; } = string.Empty;
public string TitleEn { get; set; } = string.Empty;
public string ExcerptFa { get; set; } = string.Empty;
public string ExcerptEn { get; set; } = string.Empty;
public string ContentFa { get; set; } = string.Empty; // Markdown
public string ContentEn { get; set; } = string.Empty;
public string Author { get; set; } = "تیم میزی";
public string CategoryFa { get; set; } = string.Empty;
public string CategoryEn { get; set; } = string.Empty;
public string TagsJson { get; set; } = "[]"; // JSON array of strings
public string? CoverImage { get; set; }
public bool IsPublished { get; set; } = false;
public DateTime? PublishedAt { get; set; }
public int ViewCount { get; set; } = 0;
// Navigation
public ICollection<WebsiteComment> Comments { get; set; } = [];
}
+14
View File
@@ -0,0 +1,14 @@
namespace Meezi.Core.Entities;
public class WebsiteComment : BaseEntity
{
public string PostSlug { get; set; } = string.Empty;
public string AuthorName { get; set; } = string.Empty;
public string? AuthorEmail { get; set; }
public string Content { get; set; } = string.Empty;
public bool IsApproved { get; set; } = false;
public string? IpAddress { get; set; }
// Navigation
public WebsiteBlogPost? Post { get; set; }
}