feat(api): .NET 10 multi-tenant REST API

Full backend implementation:
- Multi-tenant cafe/restaurant management (menus, orders, tables, staff)
- POS order flow with ZarinPal and Snappfood payment integration
- OTP authentication via Kavenegar SMS
- QR digital menu with public discover/finder endpoints
- Customer loyalty, coupons, CRM
- PostgreSQL via EF Core, Redis for caching/sessions
- Background jobs, webhook handlers
- Full migration history

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-05-27 21:33:48 +03:30
parent 03376b3ea1
commit ef15fd6247
472 changed files with 120358 additions and 0 deletions
@@ -0,0 +1,153 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using Meezi.Core.Branding;
namespace Meezi.Infrastructure.Branding;
public static partial class CafeThemeSerializer
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true
};
private static readonly HashSet<string> ValidPalettes =
[
"meezi-green", "ocean-blue", "royal-purple", "sunset-orange", "rose-blush",
"charcoal-gold", "espresso", "forest", "midnight", "coral", "gold-luxury",
"mint-fresh", "wine-bar", "slate-modern", "cherry", "teal-wave", "sand-cafe"
];
private static readonly HashSet<string> ValidPanelStyles =
[
CafeThemeDefaults.PanelFlat, CafeThemeDefaults.PanelModern, CafeThemeDefaults.PanelGlass,
CafeThemeDefaults.PanelMinimal, CafeThemeDefaults.PanelBold, CafeThemeDefaults.PanelSoft,
CafeThemeDefaults.PanelElevated, CafeThemeDefaults.PanelOutline
];
private static readonly HashSet<string> ValidMenuStyles =
[
CafeThemeDefaults.MenuCards, CafeThemeDefaults.MenuCompact, CafeThemeDefaults.MenuGrid,
CafeThemeDefaults.MenuList, CafeThemeDefaults.MenuMagazine, CafeThemeDefaults.MenuClassic
];
private static readonly HashSet<string> ValidMenuTextures =
[
CafeThemeDefaults.MenuTextureNone, CafeThemeDefaults.MenuTexturePaper,
CafeThemeDefaults.MenuTextureLinen, CafeThemeDefaults.MenuTextureDots,
CafeThemeDefaults.MenuTextureGrid, CafeThemeDefaults.MenuTextureMarble,
CafeThemeDefaults.MenuTextureWood, CafeThemeDefaults.MenuTextureWarm
];
private static readonly HashSet<string> ValidDensities =
[
CafeThemeDefaults.DensityCompact, CafeThemeDefaults.DensityComfortable, CafeThemeDefaults.DensitySpacious
];
private static readonly HashSet<string> ValidRadius =
[
CafeThemeDefaults.RadiusNone, CafeThemeDefaults.RadiusSm, CafeThemeDefaults.RadiusMd,
CafeThemeDefaults.RadiusLg, CafeThemeDefaults.RadiusFull
];
public static CafeTheme Parse(string? json)
{
if (string.IsNullOrWhiteSpace(json))
return new CafeTheme();
try
{
var theme = JsonSerializer.Deserialize<CafeTheme>(json, JsonOptions) ?? new CafeTheme();
return Normalize(theme);
}
catch
{
return new CafeTheme();
}
}
public static string Serialize(CafeTheme theme) =>
JsonSerializer.Serialize(Normalize(theme), JsonOptions);
public static CafeTheme Normalize(CafeTheme theme)
{
theme.PaletteId = ValidPalettes.Contains(theme.PaletteId) ? theme.PaletteId : CafeThemeDefaults.PaletteMeeziGreen;
theme.PanelStyle = ValidPanelStyles.Contains(theme.PanelStyle) ? theme.PanelStyle : CafeThemeDefaults.PanelModern;
theme.MenuStyle = ValidMenuStyles.Contains(theme.MenuStyle) ? theme.MenuStyle : CafeThemeDefaults.MenuCards;
theme.MenuTexture = ValidMenuTextures.Contains(theme.MenuTexture)
? theme.MenuTexture
: CafeThemeDefaults.MenuTextureNone;
theme.Density = ValidDensities.Contains(theme.Density) ? theme.Density : CafeThemeDefaults.DensityComfortable;
theme.Radius = ValidRadius.Contains(theme.Radius) ? theme.Radius : CafeThemeDefaults.RadiusMd;
theme.Custom = NormalizeCustom(theme.Custom);
return theme;
}
private static CafeThemeCustomColors? NormalizeCustom(CafeThemeCustomColors? custom)
{
if (custom is null) return null;
var normalized = new CafeThemeCustomColors
{
Primary = NormalizeHex(custom.Primary),
Secondary = NormalizeHex(custom.Secondary),
Accent = NormalizeHex(custom.Accent),
Background = NormalizeHex(custom.Background),
Surface = NormalizeHex(custom.Surface),
Text = NormalizeHex(custom.Text),
TextMuted = NormalizeHex(custom.TextMuted),
Destructive = NormalizeHex(custom.Destructive),
Success = NormalizeHex(custom.Success),
PrimaryOpacity = NormalizeOpacity(custom.PrimaryOpacity),
SecondaryOpacity = NormalizeOpacity(custom.SecondaryOpacity),
AccentOpacity = NormalizeOpacity(custom.AccentOpacity),
BackgroundOpacity = NormalizeOpacity(custom.BackgroundOpacity),
SurfaceOpacity = NormalizeOpacity(custom.SurfaceOpacity),
TextOpacity = NormalizeOpacity(custom.TextOpacity),
TextMutedOpacity = NormalizeOpacity(custom.TextMutedOpacity),
DestructiveOpacity = NormalizeOpacity(custom.DestructiveOpacity),
SuccessOpacity = NormalizeOpacity(custom.SuccessOpacity)
};
return normalized.Primary is null
&& normalized.Secondary is null
&& normalized.Accent is null
&& normalized.Background is null
&& normalized.Surface is null
&& normalized.Text is null
&& normalized.TextMuted is null
&& normalized.Destructive is null
&& normalized.Success is null
&& normalized.PrimaryOpacity is null
&& normalized.SecondaryOpacity is null
&& normalized.AccentOpacity is null
&& normalized.BackgroundOpacity is null
&& normalized.SurfaceOpacity is null
&& normalized.TextOpacity is null
&& normalized.TextMutedOpacity is null
&& normalized.DestructiveOpacity is null
&& normalized.SuccessOpacity is null
? null
: normalized;
}
private static string? NormalizeHex(string? value)
{
if (string.IsNullOrWhiteSpace(value)) return null;
var v = value.Trim();
if (!HexColorRegex().IsMatch(v)) return null;
return v.StartsWith('#') ? v.ToUpperInvariant() : $"#{v.ToUpperInvariant()}";
}
private static int? NormalizeOpacity(int? value)
{
if (value is null) return null;
return Math.Clamp(value.Value, 0, 100);
}
[GeneratedRegex(@"^#?[0-9A-Fa-f]{6}$", RegexOptions.Compiled)]
private static partial Regex HexColorRegex();
}