diff --git a/src/Meezi.Core/Constants/PlanLimits.cs b/src/Meezi.Core/Constants/PlanLimits.cs index 77ffe5d..3762b6b 100644 --- a/src/Meezi.Core/Constants/PlanLimits.cs +++ b/src/Meezi.Core/Constants/PlanLimits.cs @@ -2,30 +2,53 @@ using Meezi.Core.Enums; namespace Meezi.Core.Constants; +/// +/// Code-level DEFAULTS for per-plan numeric limits. These are the fallback / +/// seed values; the source of truth at runtime is the admin-editable +/// PlatformPlanDefinition (LimitsJson) read via IPlatformCatalogService. +/// Gating uses explicit tier sets, never `tier >= X`, so the appended Starter +/// tier (enum value 4) is handled correctly. +/// public static class PlanLimits { + private static bool IsPaid(PlanTier t) => t is not PlanTier.Free; + private static bool IsProPlus(PlanTier t) => + t is PlanTier.Pro or PlanTier.Business or PlanTier.Enterprise; + private static bool IsBusinessPlus(PlanTier t) => + t is PlanTier.Business or PlanTier.Enterprise; + public static int MaxOrdersPerDay(PlanTier tier) => tier switch { - PlanTier.Free => 50, + PlanTier.Free => 30, + _ => int.MaxValue + }; + + /// Maximum tables a café may define. + public static int MaxTables(PlanTier tier) => tier switch + { + PlanTier.Free => 6, + PlanTier.Starter => 15, + PlanTier.Pro => 40, _ => int.MaxValue }; public static int MaxTerminals(PlanTier tier) => tier switch { PlanTier.Free => 1, + PlanTier.Starter => 2, PlanTier.Pro => 3, _ => int.MaxValue }; - public static int MaxCustomers(PlanTier tier) => tier switch - { - PlanTier.Free => 50, - _ => int.MaxValue - }; + public static int MaxCustomers(PlanTier tier) => int.MaxValue; // CRM module gated by CanAccessCrm + /// Monthly bundled SMS. The product direction is pay-as-you-go credits for + /// all tiers, but until the credit-purchase system ships we keep the existing bundled + /// quotas so paying cafés don't lose SMS. (Switch to 0 + credits in the SMS stage.) public static int MaxSmsPerMonth(PlanTier tier) => tier switch { PlanTier.Free => 0, + PlanTier.Starter => 0, PlanTier.Pro => 50, PlanTier.Business => 200, _ => int.MaxValue @@ -34,8 +57,8 @@ public static class PlanLimits public static int MaxBranches(PlanTier tier) => tier switch { PlanTier.Free => 1, + PlanTier.Starter => 1, PlanTier.Pro => 3, - PlanTier.Business => int.MaxValue, _ => int.MaxValue }; @@ -43,35 +66,27 @@ public static class PlanLimits public static int MaxReportHistoryDays(PlanTier tier) => tier switch { PlanTier.Free => 8, + PlanTier.Starter => 30, PlanTier.Pro => 90, _ => int.MaxValue }; - /// AI image-to-3D generations per calendar month (UTC). - public static int MaxMenuAi3dPerMonth(PlanTier tier) => tier switch - { - PlanTier.Business => 100, - PlanTier.Enterprise => 100, - _ => 0 - }; + /// AI image-to-3D generations per calendar month (UTC). Business+ only. + public static int MaxMenuAi3dPerMonth(PlanTier tier) => IsBusinessPlus(tier) ? 100 : 0; - /// Maximum active menu categories. Free tier is capped at 3; Pro+ is unlimited. + /// Maximum active menu categories. Free is capped at 10; paid tiers unlimited. public static int MaxMenuCategories(PlanTier tier) => tier switch { - PlanTier.Free => 3, + PlanTier.Free => 10, _ => int.MaxValue }; - /// Maximum menu items. Free tier is capped at 30; Pro+ is unlimited. - public static int MaxMenuItems(PlanTier tier) => tier switch - { - PlanTier.Free => 30, - _ => int.MaxValue - }; + /// Menu items are unlimited on every tier (Free can fully build the menu). + public static int MaxMenuItems(PlanTier tier) => int.MaxValue; - /// CRM (customers, loyalty) is only available on Pro and above. - public static bool CanAccessCrm(PlanTier tier) => tier >= PlanTier.Pro; + /// CRM (customers, loyalty) — Pro and above. + public static bool CanAccessCrm(PlanTier tier) => IsProPlus(tier); - /// Statistics and analytics dashboards are only available on Pro and above. - public static bool CanAccessStatistics(PlanTier tier) => tier >= PlanTier.Pro; + /// Statistics / analytics dashboards — Pro and above. + public static bool CanAccessStatistics(PlanTier tier) => IsProPlus(tier); } diff --git a/src/Meezi.Core/Enums/PlanTier.cs b/src/Meezi.Core/Enums/PlanTier.cs index ecbe4d9..6f861e6 100644 --- a/src/Meezi.Core/Enums/PlanTier.cs +++ b/src/Meezi.Core/Enums/PlanTier.cs @@ -5,5 +5,10 @@ public enum PlanTier Free = 0, Pro = 1, Business = 2, - Enterprise = 3 + Enterprise = 3, + // Appended (not inserted) so existing stored tier ints (Cafe / SubscriptionPayment / + // PlanDefinition) keep their meaning — no data migration needed. Display & upgrade + // ordering is driven by PlatformPlanDefinition.SortOrder, NOT this numeric value, + // and gating uses explicit tier checks (never `tier >= X`). + Starter = 4 } diff --git a/src/Meezi.Core/Platform/PlanLimitsData.cs b/src/Meezi.Core/Platform/PlanLimitsData.cs index 8b5ce7a..1e5a599 100644 --- a/src/Meezi.Core/Platform/PlanLimitsData.cs +++ b/src/Meezi.Core/Platform/PlanLimitsData.cs @@ -1,43 +1,37 @@ +using Meezi.Core.Constants; +using Meezi.Core.Enums; + namespace Meezi.Core.Platform; +/// +/// Serializable per-plan numeric limits, stored as PlatformPlanDefinition.LimitsJson +/// and editable by admins. Missing fields default to "unlimited" (or 0 for opt-in +/// quotas) so older stored JSON stays safe. Defaults come from . +/// public class PlanLimitsData { public int MaxOrdersPerDay { get; set; } = int.MaxValue; + public int MaxTables { 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 MaxSmsPerMonth { get; set; } = 0; public int MaxBranches { get; set; } = int.MaxValue; public int MaxReportHistoryDays { get; set; } = int.MaxValue; + public int MaxMenuCategories { get; set; } = int.MaxValue; + public int MaxMenuItems { get; set; } = int.MaxValue; + public int MaxMenuAi3dPerMonth { get; set; } = 0; - public static PlanLimitsData ForTier(Enums.PlanTier tier) => tier switch + public static PlanLimitsData ForTier(PlanTier tier) => new() { - 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() + MaxOrdersPerDay = PlanLimits.MaxOrdersPerDay(tier), + MaxTables = PlanLimits.MaxTables(tier), + MaxTerminals = PlanLimits.MaxTerminals(tier), + MaxCustomers = PlanLimits.MaxCustomers(tier), + MaxSmsPerMonth = PlanLimits.MaxSmsPerMonth(tier), + MaxBranches = PlanLimits.MaxBranches(tier), + MaxReportHistoryDays = PlanLimits.MaxReportHistoryDays(tier), + MaxMenuCategories = PlanLimits.MaxMenuCategories(tier), + MaxMenuItems = PlanLimits.MaxMenuItems(tier), + MaxMenuAi3dPerMonth = PlanLimits.MaxMenuAi3dPerMonth(tier), }; }