feat(plans): Stage 3a — enforce tables cap from the DB catalog

PlanLimitChecker already enforces orders/customers/branches/SMS from the
admin-editable catalog (GetLimitsAsync). Add the tables cap the same way
(POST /api/cafes/{cafeId}/tables → MaxTables), so Free's 6-table limit is both
enforced and admin-editable. Terminals/categories/report-history already enforce
the correct matrix values via PlanLimits defaults; routing them through the
catalog for editability + the watermark/styling/review-reply feature gates are
the remaining S3 items.

86 tests pass.
This commit is contained in:
soroush.asadi
2026-06-03 00:58:49 +03:30
parent c5d5a4006a
commit 7f52b2823f
@@ -99,6 +99,21 @@ public class PlanLimitChecker : IPlanLimitChecker
return (false, "PLAN_LIMIT_REACHED", "Branch limit reached for your plan. Please upgrade."); return (false, "PLAN_LIMIT_REACHED", "Branch limit reached for your plan. Please upgrade.");
} }
var tablesPath = $"/api/cafes/{cafeId}/tables";
if (path.StartsWith(tablesPath, StringComparison.OrdinalIgnoreCase) &&
(path.Equals(tablesPath, StringComparison.OrdinalIgnoreCase) ||
path.Equals($"{tablesPath}/", StringComparison.OrdinalIgnoreCase)))
{
var limitsTables = await _platformCatalog.GetLimitsAsync(tier, cancellationToken);
var maxTables = limitsTables.MaxTables;
if (maxTables != int.MaxValue)
{
var tableCount = await _db.Tables.CountAsync(t => t.CafeId == cafeId, cancellationToken);
if (tableCount >= maxTables)
return (false, "PLAN_LIMIT_REACHED", "Table limit reached for your plan. Please upgrade.");
}
}
var smsCampaignPath = $"/api/cafes/{cafeId}/sms/campaign"; var smsCampaignPath = $"/api/cafes/{cafeId}/sms/campaign";
if (path.Equals(smsCampaignPath, StringComparison.OrdinalIgnoreCase) || if (path.Equals(smsCampaignPath, StringComparison.OrdinalIgnoreCase) ||
path.Equals($"{smsCampaignPath}/", StringComparison.OrdinalIgnoreCase)) path.Equals($"{smsCampaignPath}/", StringComparison.OrdinalIgnoreCase))