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,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();
|
||||
}
|
||||
Reference in New Issue
Block a user