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),
};
}