feat(plans): Stage 1 — Starter tier + admin-editable limit model

Foundation for the configurable plan system (Free·Starter·Pro·Business·Enterprise).

- PlanTier: append Starter=4 (no renumber → no data migration; existing tier ints
  keep meaning). Ordering/display via PlanDefinition.SortOrder; gating uses explicit
  tier sets, never `tier >= X`.
- PlanLimits: locked 5-tier matrix — Free orders/day 50→30, tables (new) Free 6/
  Starter 15/Pro 40/∞, categories Free 3→10, menu items now unlimited, terminals/
  branches/report-history ladders incl. Starter; CRM/analytics = explicit Pro+; AI-3D
  = Business+. SMS quotas kept (Free/Starter 0, Pro 50, Business 200) until the
  pay-as-you-go credit system ships (don't break paid SMS).
- PlanLimitsData (LimitsJson shape): + MaxTables/MaxMenuCategories/MaxMenuItems/
  MaxMenuAi3dPerMonth; ForTier now derives from PlanLimits (single source of truth).

No migration. 86 tests pass. Next: S2 seed 5 plans + feature catalog (editable),
S3 wire enforcement to DB, S4 admin editor.
This commit is contained in:
soroush.asadi
2026-06-03 00:40:37 +03:30
parent 4c98c2cce1
commit 4cb640814a
3 changed files with 71 additions and 57 deletions
+24 -30
View File
@@ -1,43 +1,37 @@
using Meezi.Core.Constants;
using Meezi.Core.Enums;
namespace Meezi.Core.Platform;
/// <summary>
/// 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 <see cref="PlanLimits"/>.
/// </summary>
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),
};
}