feat(api): .NET 10 multi-tenant REST API
Full backend implementation: - Multi-tenant cafe/restaurant management (menus, orders, tables, staff) - POS order flow with ZarinPal and Snappfood payment integration - OTP authentication via Kavenegar SMS - QR digital menu with public discover/finder endpoints - Customer loyalty, coupons, CRM - PostgreSQL via EF Core, Redis for caching/sessions - Background jobs, webhook handlers - Full migration history Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
namespace Meezi.Core.Branding;
|
||||
|
||||
/// <summary>Per-café dashboard + public menu appearance (stored as JSON on <see cref="Entities.Cafe.ThemeJson"/>).</summary>
|
||||
public class CafeTheme
|
||||
{
|
||||
public string PaletteId { get; set; } = CafeThemeDefaults.PaletteMeeziGreen;
|
||||
public string PanelStyle { get; set; } = CafeThemeDefaults.PanelModern;
|
||||
public string MenuStyle { get; set; } = CafeThemeDefaults.MenuCards;
|
||||
/// <summary>Background texture for QR / guest table menu.</summary>
|
||||
public string MenuTexture { get; set; } = CafeThemeDefaults.MenuTextureNone;
|
||||
public string Density { get; set; } = CafeThemeDefaults.DensityComfortable;
|
||||
public string Radius { get; set; } = CafeThemeDefaults.RadiusMd;
|
||||
public CafeThemeCustomColors? Custom { get; set; }
|
||||
}
|
||||
|
||||
public class CafeThemeCustomColors
|
||||
{
|
||||
public string? Primary { get; set; }
|
||||
public string? Secondary { get; set; }
|
||||
public string? Accent { get; set; }
|
||||
public string? Background { get; set; }
|
||||
public string? Surface { get; set; }
|
||||
public string? Text { get; set; }
|
||||
public string? TextMuted { get; set; }
|
||||
public string? Destructive { get; set; }
|
||||
public string? Success { get; set; }
|
||||
|
||||
/// <summary>0–100 opacity for custom primary (when set).</summary>
|
||||
public int? PrimaryOpacity { get; set; }
|
||||
public int? SecondaryOpacity { get; set; }
|
||||
public int? AccentOpacity { get; set; }
|
||||
public int? BackgroundOpacity { get; set; }
|
||||
public int? SurfaceOpacity { get; set; }
|
||||
public int? TextOpacity { get; set; }
|
||||
public int? TextMutedOpacity { get; set; }
|
||||
public int? DestructiveOpacity { get; set; }
|
||||
public int? SuccessOpacity { get; set; }
|
||||
}
|
||||
|
||||
public static class CafeThemeDefaults
|
||||
{
|
||||
public const string PaletteMeeziGreen = "meezi-green";
|
||||
|
||||
public const string PanelFlat = "flat";
|
||||
public const string PanelModern = "modern";
|
||||
public const string PanelGlass = "glass";
|
||||
public const string PanelMinimal = "minimal";
|
||||
public const string PanelBold = "bold";
|
||||
public const string PanelSoft = "soft";
|
||||
public const string PanelElevated = "elevated";
|
||||
public const string PanelOutline = "outline";
|
||||
|
||||
public const string MenuCards = "cards";
|
||||
public const string MenuCompact = "compact";
|
||||
public const string MenuGrid = "grid";
|
||||
public const string MenuList = "list";
|
||||
public const string MenuMagazine = "magazine";
|
||||
public const string MenuClassic = "classic";
|
||||
|
||||
public const string MenuTextureNone = "none";
|
||||
public const string MenuTexturePaper = "paper";
|
||||
public const string MenuTextureLinen = "linen";
|
||||
public const string MenuTextureDots = "dots";
|
||||
public const string MenuTextureGrid = "grid";
|
||||
public const string MenuTextureMarble = "marble";
|
||||
public const string MenuTextureWood = "wood";
|
||||
public const string MenuTextureWarm = "warm";
|
||||
|
||||
public const string DensityCompact = "compact";
|
||||
public const string DensityComfortable = "comfortable";
|
||||
public const string DensitySpacious = "spacious";
|
||||
|
||||
public const string RadiusNone = "none";
|
||||
public const string RadiusSm = "sm";
|
||||
public const string RadiusMd = "md";
|
||||
public const string RadiusLg = "lg";
|
||||
public const string RadiusFull = "full";
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
namespace Meezi.Core.Constants;
|
||||
|
||||
/// <summary>Preset food/drink category icons (dashboard + POS). <see cref="IconStyle"/> is the visual variant.</summary>
|
||||
public static class CategoryIconPresets
|
||||
{
|
||||
public static class IconStyle
|
||||
{
|
||||
public const string Flat = "flat";
|
||||
public const string Modern = "modern";
|
||||
public const string Real = "real";
|
||||
public const string Minimal = "minimal";
|
||||
public const string Outline = "outline";
|
||||
public const string Soft = "soft";
|
||||
public const string Bold = "bold";
|
||||
public const string Gradient = "gradient";
|
||||
public const string Pastel = "pastel";
|
||||
public const string Duotone = "duotone";
|
||||
|
||||
public static readonly IReadOnlySet<string> All = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
Flat, Modern, Real, Minimal, Outline, Soft, Bold, Gradient, Pastel, Duotone
|
||||
};
|
||||
}
|
||||
|
||||
public static class PresetId
|
||||
{
|
||||
public const string DrinksHot = "drinks-hot";
|
||||
public const string DrinksCold = "drinks-cold";
|
||||
public const string DrinksTea = "drinks-tea";
|
||||
public const string DrinksJuice = "drinks-juice";
|
||||
public const string DrinksMilkshake = "drinks-milkshake";
|
||||
public const string DrinksAlcohol = "drinks-alcohol";
|
||||
public const string DrinksBeer = "drinks-beer";
|
||||
public const string Breakfast = "breakfast";
|
||||
public const string FoodMains = "food-mains";
|
||||
public const string FoodFastfood = "food-fastfood";
|
||||
public const string FoodRice = "food-rice";
|
||||
public const string PastaPizza = "pasta-pizza";
|
||||
public const string Dessert = "dessert";
|
||||
public const string IceCream = "ice-cream";
|
||||
public const string Bakery = "bakery";
|
||||
public const string Salad = "salad";
|
||||
public const string Grill = "grill";
|
||||
public const string Seafood = "seafood";
|
||||
public const string Snacks = "snacks";
|
||||
public const string SnacksSweet = "snacks-sweet";
|
||||
public const string Appetizers = "appetizers";
|
||||
public const string Vegan = "vegan";
|
||||
public const string Fruits = "fruits";
|
||||
public const string Specials = "specials";
|
||||
public const string ChefSpecial = "chef-special";
|
||||
public const string Generic = "generic";
|
||||
|
||||
public static readonly IReadOnlySet<string> All = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
DrinksHot, DrinksCold, DrinksTea, DrinksJuice, DrinksMilkshake, DrinksAlcohol, DrinksBeer,
|
||||
Breakfast, FoodMains, FoodFastfood, FoodRice, PastaPizza, Dessert, IceCream, Bakery, Salad,
|
||||
Grill, Seafood, Snacks, SnacksSweet, Appetizers, Vegan, Fruits, Specials, ChefSpecial, Generic
|
||||
};
|
||||
}
|
||||
|
||||
public static bool IsValidStyle(string? style) =>
|
||||
!string.IsNullOrWhiteSpace(style) && IconStyle.All.Contains(style.Trim());
|
||||
|
||||
public static bool IsValidPreset(string? presetId) =>
|
||||
!string.IsNullOrWhiteSpace(presetId) && PresetId.All.Contains(presetId.Trim());
|
||||
|
||||
public static string? NormalizeStyle(string? style) =>
|
||||
string.IsNullOrWhiteSpace(style) ? null : style.Trim().ToLowerInvariant();
|
||||
|
||||
public static string? NormalizePreset(string? presetId) =>
|
||||
string.IsNullOrWhiteSpace(presetId) ? null : presetId.Trim().ToLowerInvariant();
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Meezi.Core.Constants;
|
||||
|
||||
public static class MeeziClaimTypes
|
||||
{
|
||||
public const string CafeId = "cafeId";
|
||||
public const string Role = "role";
|
||||
public const string PlanTier = "planTier";
|
||||
public const string Language = "lang";
|
||||
public const string BranchId = "branchId";
|
||||
public const string Actor = "actor";
|
||||
public const string Phone = "phone";
|
||||
}
|
||||
|
||||
public static class MeeziActorKinds
|
||||
{
|
||||
public const string Merchant = "merchant";
|
||||
public const string SystemAdmin = "systemAdmin";
|
||||
public const string Consumer = "consumer";
|
||||
}
|
||||
|
||||
public static class MeeziRoles
|
||||
{
|
||||
public const string Customer = "customer";
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using Meezi.Core.Enums;
|
||||
|
||||
namespace Meezi.Core.Constants;
|
||||
|
||||
public static class PlanLimits
|
||||
{
|
||||
public static int MaxOrdersPerDay(PlanTier tier) => tier switch
|
||||
{
|
||||
PlanTier.Free => 50,
|
||||
_ => int.MaxValue
|
||||
};
|
||||
|
||||
public static int MaxTerminals(PlanTier tier) => tier switch
|
||||
{
|
||||
PlanTier.Free => 1,
|
||||
PlanTier.Pro => 3,
|
||||
_ => int.MaxValue
|
||||
};
|
||||
|
||||
public static int MaxCustomers(PlanTier tier) => tier switch
|
||||
{
|
||||
PlanTier.Free => 50,
|
||||
_ => int.MaxValue
|
||||
};
|
||||
|
||||
public static int MaxSmsPerMonth(PlanTier tier) => tier switch
|
||||
{
|
||||
PlanTier.Free => 0,
|
||||
PlanTier.Pro => 50,
|
||||
PlanTier.Business => 200,
|
||||
_ => int.MaxValue
|
||||
};
|
||||
|
||||
public static int MaxBranches(PlanTier tier) => tier switch
|
||||
{
|
||||
PlanTier.Free => 1,
|
||||
PlanTier.Pro => 3,
|
||||
PlanTier.Business => int.MaxValue,
|
||||
_ => int.MaxValue
|
||||
};
|
||||
|
||||
/// <summary>How far back daily report snapshots may be queried (inclusive of today).</summary>
|
||||
public static int MaxReportHistoryDays(PlanTier tier) => tier switch
|
||||
{
|
||||
PlanTier.Free => 8,
|
||||
PlanTier.Pro => 90,
|
||||
_ => int.MaxValue
|
||||
};
|
||||
|
||||
/// <summary>AI image-to-3D generations per calendar month (UTC).</summary>
|
||||
public static int MaxMenuAi3dPerMonth(PlanTier tier) => tier switch
|
||||
{
|
||||
PlanTier.Business => 100,
|
||||
PlanTier.Enterprise => 100,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Meezi.Core.Enums;
|
||||
|
||||
namespace Meezi.Core.Constants;
|
||||
|
||||
public static class PlanPricing
|
||||
{
|
||||
/// <summary>Monthly price in Toman (ت).</summary>
|
||||
public static decimal MonthlyToman(PlanTier tier) => tier switch
|
||||
{
|
||||
PlanTier.Pro => 1_490_000m,
|
||||
PlanTier.Business => 3_490_000m,
|
||||
_ => 0m
|
||||
};
|
||||
|
||||
public static bool IsBillableOnline(PlanTier tier) =>
|
||||
tier is PlanTier.Pro or PlanTier.Business;
|
||||
|
||||
/// <summary>ZarinPal amounts are in Rials (۱ تومان = ۱۰ ریال).</summary>
|
||||
public static long ToRials(decimal toman) => (long)(toman * 10m);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using Meezi.Core.Enums;
|
||||
|
||||
namespace Meezi.Core.Delivery;
|
||||
|
||||
public record UnifiedDeliveryCustomer(
|
||||
string Name,
|
||||
string Phone,
|
||||
string? Address = null,
|
||||
double? Lat = null,
|
||||
double? Lng = null);
|
||||
|
||||
public record UnifiedDeliveryItem(
|
||||
string Sku,
|
||||
string Name,
|
||||
int Quantity,
|
||||
decimal UnitPrice,
|
||||
string? Notes = null);
|
||||
|
||||
public record UnifiedDeliveryPayment(
|
||||
decimal Total,
|
||||
string Method,
|
||||
bool IsPaid,
|
||||
decimal? Commission = null);
|
||||
|
||||
public record UnifiedDeliveryInfo(
|
||||
string Type,
|
||||
int? EstimatedMinutes = null,
|
||||
string? DriverName = null,
|
||||
string? DriverPhone = null);
|
||||
|
||||
/// <summary>Normalized inbound order from Snappfood, Tap30, or other delivery webhooks.</summary>
|
||||
public record UnifiedDeliveryOrder(
|
||||
string ExternalId,
|
||||
DeliveryPlatform Platform,
|
||||
string VendorId,
|
||||
DateTime CreatedAt,
|
||||
UnifiedDeliveryCustomer Customer,
|
||||
IReadOnlyList<UnifiedDeliveryItem> Items,
|
||||
UnifiedDeliveryPayment Payment,
|
||||
UnifiedDeliveryInfo Delivery,
|
||||
UnifiedDeliveryStatus Status);
|
||||
@@ -0,0 +1,36 @@
|
||||
namespace Meezi.Core.Discover;
|
||||
|
||||
public record CafeBadgeDefinition(string Key, string LabelFa, string LabelEn, string LabelAr, string Icon);
|
||||
|
||||
public static class CafeBadgeCatalog
|
||||
{
|
||||
public static readonly IReadOnlyList<CafeBadgeDefinition> All =
|
||||
[
|
||||
new("verified_partner", "شریک تأییدشده", "Verified partner", "شريك موثّق", "✓"),
|
||||
new("award_winner", "برنده جایزه", "Award winner", "فائز بجمة", "🏆"),
|
||||
new("roastery", "رستری تخصصی", "Specialty roastery", "محمصة متخصصة", "☕"),
|
||||
new("eco_friendly", "دوستدار محیط زیست", "Eco-friendly", "صديق للبيئة", "🌿"),
|
||||
new("women_owned", "مدیریت زنان", "Women-owned", "إدارة نسائية", "👩"),
|
||||
new("pet_friendly", "مجاز حیوان خانگی", "Pet-friendly", "يسمح بالحيوانات", "🐾"),
|
||||
];
|
||||
|
||||
private static readonly Dictionary<string, CafeBadgeDefinition> ByKey =
|
||||
All.ToDictionary(x => x.Key, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public static bool IsValidKey(string? key) =>
|
||||
!string.IsNullOrWhiteSpace(key) && ByKey.ContainsKey(key.Trim());
|
||||
|
||||
public static CafeBadgeDefinition? Resolve(string key) =>
|
||||
ByKey.TryGetValue(key.Trim(), out var def) ? def : null;
|
||||
|
||||
public static IReadOnlyList<string> NormalizeKeys(IEnumerable<string>? keys)
|
||||
{
|
||||
if (keys is null) return [];
|
||||
return keys
|
||||
.Select(k => k.Trim())
|
||||
.Where(IsValidKey)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Take(8)
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
namespace Meezi.Core.Discover;
|
||||
|
||||
/// <summary>
|
||||
/// Café attributes for discover / AI matching (stored as JSON on <see cref="Entities.Cafe.DiscoverProfileJson"/>).
|
||||
/// </summary>
|
||||
public class CafeDiscoverProfile
|
||||
{
|
||||
/// <summary>Decor / concept themes (multi): modern, vintage, plants-heavy, …</summary>
|
||||
public List<string> Themes { get; set; } = [];
|
||||
|
||||
/// <summary>Physical scale: tiny, cozy, medium, large, spacious.</summary>
|
||||
public string? Size { get; set; }
|
||||
|
||||
/// <summary>Floor count: one, two, three, multi.</summary>
|
||||
public string? Floors { get; set; }
|
||||
|
||||
/// <summary>Atmosphere tags (multi).</summary>
|
||||
public List<string> Vibes { get; set; } = [];
|
||||
|
||||
/// <summary>Good for: date, family, friends, finding_someone, etc. (multi).</summary>
|
||||
public List<string> Occasions { get; set; } = [];
|
||||
|
||||
/// <summary>indoor, outdoor, plants, terrace, … (multi).</summary>
|
||||
public List<string> SpaceFeatures { get; set; } = [];
|
||||
|
||||
/// <summary>quiet, moderate, lively.</summary>
|
||||
public string? NoiseLevel { get; set; }
|
||||
|
||||
/// <summary>budget, mid, premium.</summary>
|
||||
public string? PriceTier { get; set; }
|
||||
}
|
||||
|
||||
public static class CafeDiscoverProfileKeys
|
||||
{
|
||||
public static readonly HashSet<string> Themes =
|
||||
[
|
||||
"modern", "minimal", "vintage", "industrial", "scandi", "persian_traditional",
|
||||
"book_cafe", "roastery", "dessert_focus", "brunch", "late_night",
|
||||
"plants_heavy", "instagrammable", "heritage", "luxury",
|
||||
// extended
|
||||
"specialty_coffee", "tea_house", "art_gallery", "sport_cafe", "gaming_cafe"
|
||||
];
|
||||
|
||||
public static readonly HashSet<string> Sizes =
|
||||
["tiny", "cozy", "medium", "large", "spacious"];
|
||||
|
||||
public static readonly HashSet<string> Floors =
|
||||
["one", "two", "three", "multi"];
|
||||
|
||||
public static readonly HashSet<string> Vibes =
|
||||
[
|
||||
"quiet", "lively", "romantic", "cozy", "trendy", "traditional",
|
||||
"artistic", "luxury", "casual", "study_friendly"
|
||||
];
|
||||
|
||||
public static readonly HashSet<string> Occasions =
|
||||
[
|
||||
"date", "family", "friends", "finding_someone", "solo",
|
||||
"business_meeting", "study_work", "celebration",
|
||||
"quick_coffee", "breakfast", "brunch",
|
||||
// extended
|
||||
"after_dinner", "group_large"
|
||||
];
|
||||
|
||||
public static readonly HashSet<string> SpaceFeatures =
|
||||
[
|
||||
"indoor", "outdoor", "terrace", "rooftop", "garden", "plants",
|
||||
"wifi", "parking", "wheelchair", "kids_friendly", "pet_friendly",
|
||||
"smoking_area", "live_music", "private_room", "counter_only",
|
||||
// extended
|
||||
"takeaway", "hookah", "board_games", "no_smoking", "prayer_room"
|
||||
];
|
||||
|
||||
public static readonly HashSet<string> NoiseLevels = ["quiet", "moderate", "lively"];
|
||||
public static readonly HashSet<string> PriceTiers = ["budget", "mid", "premium"];
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
namespace Meezi.Core.Discover;
|
||||
|
||||
/// <summary>Opening hours for a single day.</summary>
|
||||
public class DaySchedule
|
||||
{
|
||||
public bool IsOpen { get; set; }
|
||||
/// <summary>24-h HH:mm, e.g. "08:00"</summary>
|
||||
public string? Open { get; set; }
|
||||
/// <summary>24-h HH:mm, e.g. "23:30". If past midnight, still next-calendar-day open time.</summary>
|
||||
public string? Close { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full-week schedule. Iran week: Saturday = first day, Friday = last day.
|
||||
/// Stored as JSON on <see cref="Entities.Cafe.WorkingHoursJson"/>.
|
||||
/// </summary>
|
||||
public class WorkingHoursSchedule
|
||||
{
|
||||
public DaySchedule? Sat { get; set; }
|
||||
public DaySchedule? Sun { get; set; }
|
||||
public DaySchedule? Mon { get; set; }
|
||||
public DaySchedule? Tue { get; set; }
|
||||
public DaySchedule? Wed { get; set; }
|
||||
public DaySchedule? Thu { get; set; }
|
||||
public DaySchedule? Fri { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <see cref="DaySchedule"/> for the given UTC day-of-week,
|
||||
/// adjusted to Iran Standard Time (UTC+3:30).
|
||||
/// </summary>
|
||||
public DaySchedule? ForUtcNow()
|
||||
{
|
||||
// Iran Standard Time: UTC+03:30 (no DST)
|
||||
var iranOffset = TimeSpan.FromMinutes(210);
|
||||
var iranNow = DateTimeOffset.UtcNow.ToOffset(iranOffset);
|
||||
return iranNow.DayOfWeek switch
|
||||
{
|
||||
DayOfWeek.Saturday => Sat,
|
||||
DayOfWeek.Sunday => Sun,
|
||||
DayOfWeek.Monday => Mon,
|
||||
DayOfWeek.Tuesday => Tue,
|
||||
DayOfWeek.Wednesday => Wed,
|
||||
DayOfWeek.Thursday => Thu,
|
||||
DayOfWeek.Friday => Fri,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Returns true when the cafe is currently open based on Iran time.</summary>
|
||||
public bool IsOpenNow()
|
||||
{
|
||||
var day = ForUtcNow();
|
||||
if (day is null || !day.IsOpen) return false;
|
||||
if (string.IsNullOrEmpty(day.Open) || string.IsNullOrEmpty(day.Close)) return true; // open all day
|
||||
|
||||
var iranOffset = TimeSpan.FromMinutes(210);
|
||||
var iranNow = DateTimeOffset.UtcNow.ToOffset(iranOffset);
|
||||
var nowMinutes = iranNow.Hour * 60 + iranNow.Minute;
|
||||
|
||||
if (!TryParseMinutes(day.Open, out var openMin) || !TryParseMinutes(day.Close, out var closeMin))
|
||||
return true;
|
||||
|
||||
// Handle spans crossing midnight (e.g. 22:00–02:00)
|
||||
if (closeMin <= openMin)
|
||||
return nowMinutes >= openMin || nowMinutes < closeMin;
|
||||
|
||||
return nowMinutes >= openMin && nowMinutes < closeMin;
|
||||
}
|
||||
|
||||
private static bool TryParseMinutes(string? hhmm, out int minutes)
|
||||
{
|
||||
minutes = 0;
|
||||
if (string.IsNullOrEmpty(hhmm)) return false;
|
||||
var parts = hhmm.Split(':');
|
||||
if (parts.Length != 2) return false;
|
||||
if (!int.TryParse(parts[0], out var h) || !int.TryParse(parts[1], out var m)) return false;
|
||||
minutes = h * 60 + m;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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 (0–100). Takes precedence over category discount when > 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!;
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; } = [];
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum CashTransactionType
|
||||
{
|
||||
OrderPayment = 0,
|
||||
Refund = 1,
|
||||
Expense = 2,
|
||||
Withdrawal = 3,
|
||||
Deposit = 4
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum CouponType
|
||||
{
|
||||
Percentage = 0,
|
||||
FixedAmount = 1,
|
||||
FreeItem = 2
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum CustomerGroup
|
||||
{
|
||||
Regular = 0,
|
||||
Vip = 1,
|
||||
New = 2,
|
||||
Employee = 3
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum DeliveryPlatform
|
||||
{
|
||||
Direct = 0,
|
||||
Snappfood = 1,
|
||||
Tap30 = 2,
|
||||
Digikala = 3
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum DemoRequestStatus
|
||||
{
|
||||
New = 0,
|
||||
Contacted = 1,
|
||||
DemoScheduled = 2,
|
||||
Converted = 3,
|
||||
Rejected = 4
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum EmployeeRole
|
||||
{
|
||||
Owner = 0,
|
||||
Manager = 1,
|
||||
Cashier = 2,
|
||||
Waiter = 3,
|
||||
Chef = 4,
|
||||
Delivery = 5
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum ExpenseCategory
|
||||
{
|
||||
Supplies = 0,
|
||||
Utilities = 1,
|
||||
Salary = 2,
|
||||
Rent = 3,
|
||||
Maintenance = 4,
|
||||
Other = 5
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum LeaveStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Approved = 1,
|
||||
Rejected = 2
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum OrderSource
|
||||
{
|
||||
Pos = 0,
|
||||
GuestQr = 1,
|
||||
Kiosk = 2,
|
||||
SnappFood = 3,
|
||||
Tap30 = 4,
|
||||
Digikala = 5
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum OrderStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Confirmed = 1,
|
||||
Preparing = 2,
|
||||
Ready = 3,
|
||||
Delivered = 4,
|
||||
Cancelled = 5
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum OrderType
|
||||
{
|
||||
DineIn = 0,
|
||||
Takeaway = 1,
|
||||
Delivery = 2
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum PaymentMethod
|
||||
{
|
||||
Cash = 0,
|
||||
Card = 1,
|
||||
Credit = 2
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum PaymentProvider
|
||||
{
|
||||
ZarinPal = 0,
|
||||
Tara = 1,
|
||||
SnappPay = 2
|
||||
}
|
||||
|
||||
public static class PaymentProviderIds
|
||||
{
|
||||
public const string ZarinPal = "zarinpal";
|
||||
public const string Tara = "tara";
|
||||
public const string SnappPay = "snapppay";
|
||||
|
||||
public static PaymentProvider? Parse(string? id) => id?.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
ZarinPal => PaymentProvider.ZarinPal,
|
||||
Tara => PaymentProvider.Tara,
|
||||
SnappPay => PaymentProvider.SnappPay,
|
||||
_ => null
|
||||
};
|
||||
|
||||
public static string ToId(PaymentProvider provider) => provider switch
|
||||
{
|
||||
PaymentProvider.ZarinPal => ZarinPal,
|
||||
PaymentProvider.Tara => Tara,
|
||||
PaymentProvider.SnappPay => SnappPay,
|
||||
_ => ZarinPal
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum PaymentStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Completed = 1,
|
||||
Failed = 2,
|
||||
Refunded = 3
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum PlanTier
|
||||
{
|
||||
Free = 0,
|
||||
Pro = 1,
|
||||
Business = 2,
|
||||
Enterprise = 3
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum QueueTicketStatus
|
||||
{
|
||||
Waiting = 0,
|
||||
Called = 1,
|
||||
Done = 2,
|
||||
Cancelled = 3
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum ReservationStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Confirmed = 1,
|
||||
Cancelled = 2,
|
||||
/// <summary>Guest arrived; order may be open at POS.</summary>
|
||||
Seated = 3,
|
||||
/// <summary>Paid and visit finished.</summary>
|
||||
Completed = 4
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum ShiftStatus
|
||||
{
|
||||
Open = 0,
|
||||
Closed = 1
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum ShiftType
|
||||
{
|
||||
Morning = 0,
|
||||
Evening = 1,
|
||||
DayOff = 2
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum StockMovementKind
|
||||
{
|
||||
Manual = 0,
|
||||
OrderDeduction = 1,
|
||||
OrderRestore = 2,
|
||||
Purchase = 3
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum SubscriptionPaymentStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Completed = 1,
|
||||
Failed = 2
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum SupportTicketPriority
|
||||
{
|
||||
Low = 0,
|
||||
Normal = 1,
|
||||
High = 2
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum SupportTicketStatus
|
||||
{
|
||||
Open = 0,
|
||||
InProgress = 1,
|
||||
WaitingMerchant = 2,
|
||||
Resolved = 3,
|
||||
Closed = 4
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum TableBoardStatus
|
||||
{
|
||||
Free = 0,
|
||||
Busy = 1,
|
||||
Reserved = 2,
|
||||
Cleaning = 3
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
public enum TicketMessageSenderKind
|
||||
{
|
||||
Merchant = 0,
|
||||
Admin = 1
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Meezi.Core.Enums;
|
||||
|
||||
/// <summary>Platform-agnostic delivery order status before mapping to <see cref="OrderStatus"/>.</summary>
|
||||
public enum UnifiedDeliveryStatus
|
||||
{
|
||||
Pending = 0,
|
||||
Confirmed = 1,
|
||||
Preparing = 2,
|
||||
Ready = 3,
|
||||
Delivered = 4,
|
||||
Cancelled = 5
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Meezi.Core.Interfaces;
|
||||
|
||||
/// <summary>Optional branch scope from JWT (staff terminal / POS session).</summary>
|
||||
public interface IBranchContext
|
||||
{
|
||||
string? CafeId { get; }
|
||||
string? BranchId { get; }
|
||||
bool HasBranch { get; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Meezi.Core.Interfaces;
|
||||
|
||||
public interface IPlatformRuntimeConfig
|
||||
{
|
||||
Task<string?> GetAsync(string key, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyDictionary<string, string>> GetByPrefixAsync(string prefix, CancellationToken cancellationToken = default);
|
||||
void InvalidateCache();
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Meezi.Core.Interfaces;
|
||||
|
||||
public interface ISmsService
|
||||
{
|
||||
Task SendOtpAsync(string phone, string otp, CancellationToken cancellationToken = default);
|
||||
Task SendMessageAsync(string phone, string message, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Meezi.Core.Interfaces;
|
||||
|
||||
public record SnappPayInitResult(bool Success, string? PaymentToken, string? PaymentUrl, string? ErrorMessage);
|
||||
|
||||
public record SnappPayVerifyResult(bool Success, string? RefId, string? ErrorMessage);
|
||||
|
||||
public interface ISnappPayGateway
|
||||
{
|
||||
Task<bool> IsEnabledAsync(CancellationToken cancellationToken = default);
|
||||
Task<SnappPayInitResult> RequestPaymentAsync(
|
||||
long amountRials,
|
||||
string transactionId,
|
||||
string returnUrl,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<SnappPayVerifyResult> VerifyAndSettleAsync(
|
||||
string paymentToken,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace Meezi.Core.Interfaces;
|
||||
|
||||
public interface ISnappfoodClient
|
||||
{
|
||||
Task AcknowledgeOrderAsync(string snappfoodOrderId, CancellationToken cancellationToken = default);
|
||||
Task NotifyOrderDeliveredAsync(string snappfoodOrderId, CancellationToken cancellationToken = default);
|
||||
Task NotifyOrderStatusAsync(
|
||||
string snappfoodOrderId,
|
||||
string status,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Meezi.Core.Interfaces;
|
||||
|
||||
public interface ITap30Client
|
||||
{
|
||||
Task AcknowledgeOrderAsync(string tap30OrderId, CancellationToken cancellationToken = default);
|
||||
Task NotifyOrderStatusAsync(
|
||||
string tap30OrderId,
|
||||
string status,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Meezi.Core.Interfaces;
|
||||
|
||||
public record TaraInitResult(bool Success, string? TraceNumber, string? PaymentUrl, string? ErrorMessage);
|
||||
|
||||
public record TaraVerifyResult(bool Success, string? RefId, string? ErrorMessage);
|
||||
|
||||
public interface ITaraPaymentGateway
|
||||
{
|
||||
Task<bool> IsEnabledAsync(CancellationToken cancellationToken = default);
|
||||
Task<TaraInitResult> RequestPaymentAsync(
|
||||
long amountRials,
|
||||
string invoiceNumber,
|
||||
string callbackUrl,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<TaraVerifyResult> VerifyPaymentAsync(
|
||||
string traceNumber,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Meezi.Core.Interfaces;
|
||||
|
||||
public record TarazSubmitResult(bool Success, string? TrackingCode, string? Message);
|
||||
|
||||
public interface ITarazTaxService
|
||||
{
|
||||
Task<TarazSubmitResult> SubmitDailyInvoicesAsync(string cafeId, DateTime dateUtc, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using Meezi.Core.Enums;
|
||||
|
||||
namespace Meezi.Core.Interfaces;
|
||||
|
||||
public interface ITenantContext
|
||||
{
|
||||
string? UserId { get; }
|
||||
string? CafeId { get; }
|
||||
EmployeeRole? Role { get; }
|
||||
PlanTier? PlanTier { get; }
|
||||
string? Language { get; }
|
||||
/// <summary>Active branch from JWT when employee is branch-scoped.</summary>
|
||||
string? BranchId { get; }
|
||||
bool IsSystemAdmin { get; }
|
||||
bool IsAuthenticated { get; }
|
||||
bool IsCafeOwner => Role == EmployeeRole.Owner;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Meezi.Core.Interfaces;
|
||||
|
||||
public interface IWebsiteService
|
||||
{
|
||||
Task<(IReadOnlyList<object> Posts, int Total)> GetPostsAsync(
|
||||
string locale, int page, int limit, CancellationToken ct = default);
|
||||
Task<object?> GetPostAsync(string slug, string locale, CancellationToken ct = default);
|
||||
Task<IReadOnlyList<object>> GetCommentsAsync(string slug, CancellationToken ct = default);
|
||||
Task<object> AddCommentAsync(string slug, string authorName, string? email,
|
||||
string content, string? ip, CancellationToken ct = default);
|
||||
Task<object> CreateDemoRequestAsync(string contactName, string businessName,
|
||||
string phone, string? email, string branchCount, string? notes, string source,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
namespace Meezi.Core.Interfaces;
|
||||
|
||||
public record ZarinPalRequestResult(bool Success, string? Authority, string? PaymentUrl, string? ErrorMessage);
|
||||
|
||||
public record ZarinPalVerifyResult(bool Success, string? RefId, string? ErrorMessage);
|
||||
|
||||
public interface IZarinPalGateway
|
||||
{
|
||||
Task<bool> IsEnabledAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ZarinPalRequestResult> RequestPaymentAsync(
|
||||
long amountRials,
|
||||
string description,
|
||||
string callbackUrl,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ZarinPalVerifyResult> VerifyPaymentAsync(
|
||||
string authority,
|
||||
long amountRials,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Meezi.Shared\Meezi.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,43 @@
|
||||
namespace Meezi.Core.Platform;
|
||||
|
||||
public class PlanLimitsData
|
||||
{
|
||||
public int MaxOrdersPerDay { get; set; } = int.MaxValue;
|
||||
public int MaxTerminals { get; set; } = int.MaxValue;
|
||||
public int MaxCustomers { get; set; } = int.MaxValue;
|
||||
public int MaxSmsPerMonth { get; set; } = int.MaxValue;
|
||||
public int MaxBranches { get; set; } = int.MaxValue;
|
||||
public int MaxReportHistoryDays { get; set; } = int.MaxValue;
|
||||
|
||||
public static PlanLimitsData ForTier(Enums.PlanTier tier) => tier switch
|
||||
{
|
||||
Enums.PlanTier.Free => new PlanLimitsData
|
||||
{
|
||||
MaxOrdersPerDay = 50,
|
||||
MaxTerminals = 1,
|
||||
MaxCustomers = 50,
|
||||
MaxSmsPerMonth = 0,
|
||||
MaxBranches = 1,
|
||||
MaxReportHistoryDays = 8
|
||||
},
|
||||
Enums.PlanTier.Pro => new PlanLimitsData
|
||||
{
|
||||
MaxOrdersPerDay = int.MaxValue,
|
||||
MaxTerminals = 3,
|
||||
MaxCustomers = int.MaxValue,
|
||||
MaxSmsPerMonth = 50,
|
||||
MaxBranches = 3,
|
||||
MaxReportHistoryDays = 90
|
||||
},
|
||||
Enums.PlanTier.Business => new PlanLimitsData
|
||||
{
|
||||
MaxOrdersPerDay = int.MaxValue,
|
||||
MaxTerminals = int.MaxValue,
|
||||
MaxCustomers = int.MaxValue,
|
||||
MaxSmsPerMonth = 200,
|
||||
MaxBranches = int.MaxValue,
|
||||
MaxReportHistoryDays = int.MaxValue
|
||||
},
|
||||
_ => new PlanLimitsData()
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Meezi.Core.Platform;
|
||||
|
||||
/// <summary>PlatformSettings keys for third-party integrations (admin-configurable).</summary>
|
||||
public static class PlatformIntegrationKeys
|
||||
{
|
||||
public const string OpenAiApiKey = "integrations.openai.apiKey";
|
||||
public const string OpenAiEnabled = "integrations.openai.enabled";
|
||||
public const string OpenAiModel = "integrations.openai.model";
|
||||
public const string OpenAiCoffeeAdvisorEnabled = "integrations.openai.coffeeAdvisor.enabled";
|
||||
|
||||
public const string MeshyApiKey = "integrations.meshy.apiKey";
|
||||
public const string MeshyEnabled = "integrations.meshy.enabled";
|
||||
public const string MeshyMenu3dEnabled = "integrations.meshy.menu3d.enabled";
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
namespace Meezi.Core.Utilities;
|
||||
|
||||
/// <summary>Normalizes OTP input (Persian/Arabic digits, whitespace) to 6 ASCII digits.</summary>
|
||||
public static class OtpNormalizer
|
||||
{
|
||||
public static string Normalize(string? code)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(code))
|
||||
return string.Empty;
|
||||
|
||||
Span<char> buffer = stackalloc char[code.Length];
|
||||
var n = 0;
|
||||
foreach (var ch in code)
|
||||
{
|
||||
if (ch is >= '0' and <= '9')
|
||||
buffer[n++] = ch;
|
||||
else
|
||||
{
|
||||
var d = ch switch
|
||||
{
|
||||
>= '\u06F0' and <= '\u06F9' => ch - '\u06F0', // Persian
|
||||
>= '\u0660' and <= '\u0669' => ch - '\u0660', // Arabic-Indic
|
||||
_ => -1
|
||||
};
|
||||
if (d >= 0)
|
||||
buffer[n++] = (char)('0' + d);
|
||||
}
|
||||
}
|
||||
|
||||
return n == 0 ? string.Empty : new string(buffer[..n]);
|
||||
}
|
||||
|
||||
public static bool IsValidSixDigitCode(string? code)
|
||||
{
|
||||
var normalized = Normalize(code);
|
||||
return normalized.Length == 6 && normalized.All(char.IsDigit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
namespace Meezi.Core.Utilities;
|
||||
|
||||
/// <summary>Lightweight normalization for Persian discover text search.</summary>
|
||||
public static class PersianSearchNormalizer
|
||||
{
|
||||
public static string Normalize(string? input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return string.Empty;
|
||||
|
||||
var s = input.Trim().ToLowerInvariant();
|
||||
s = s.Replace('ي', 'ی')
|
||||
.Replace('ك', 'ک')
|
||||
.Replace('\u200c', ' ')
|
||||
.Replace('\u0640', ' ')
|
||||
.Replace("آ", "ا")
|
||||
.Replace("أ", "ا")
|
||||
.Replace("إ", "ا");
|
||||
|
||||
while (s.Contains(" ", StringComparison.Ordinal))
|
||||
s = s.Replace(" ", " ", StringComparison.Ordinal);
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
namespace Meezi.Core.Utilities;
|
||||
|
||||
public static class PhoneNormalizer
|
||||
{
|
||||
public static string Normalize(string phone)
|
||||
{
|
||||
var digits = new string(phone.Where(char.IsDigit).ToArray());
|
||||
|
||||
if (digits.StartsWith("98") && digits.Length == 12)
|
||||
digits = "0" + digits[2..];
|
||||
|
||||
if (digits.Length == 10 && digits.StartsWith('9'))
|
||||
digits = "0" + digits;
|
||||
|
||||
return digits;
|
||||
}
|
||||
|
||||
public static bool IsValidIranMobile(string normalized) =>
|
||||
normalized.Length == 11 && normalized.StartsWith("09");
|
||||
}
|
||||
Reference in New Issue
Block a user