diff --git a/src/Meezi.API/Controllers/ReportsController.cs b/src/Meezi.API/Controllers/ReportsController.cs index 1fb884b..957f48b 100644 --- a/src/Meezi.API/Controllers/ReportsController.cs +++ b/src/Meezi.API/Controllers/ReportsController.cs @@ -4,6 +4,7 @@ using Meezi.API.Services; using Meezi.API.Utils; using Meezi.Core.Enums; using Meezi.Core.Interfaces; +using Meezi.Infrastructure.Services.Platform; using Meezi.Shared; namespace Meezi.API.Controllers; @@ -13,13 +14,21 @@ public class ReportsController : CafeApiControllerBase { private readonly IReportService _reports; private readonly IDailyReportService _dailyReports; + private readonly IPlatformCatalogService _catalog; - public ReportsController(IReportService reports, IDailyReportService dailyReports) + public ReportsController( + IReportService reports, + IDailyReportService dailyReports, + IPlatformCatalogService catalog) { _reports = reports; _dailyReports = dailyReports; + _catalog = catalog; } + private async Task MaxHistoryDaysAsync(ITenantContext tenant, CancellationToken ct) => + (await _catalog.GetLimitsAsync(tenant.PlanTier ?? PlanTier.Free, ct)).MaxReportHistoryDays; + [HttpGet("daily")] public async Task GetDailySnapshot( string cafeId, @@ -37,7 +46,7 @@ public class ReportsController : CafeApiControllerBase return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "Invalid date. Use yyyy-MM-dd.", "date"))); - if (EnsureReportDateAllowed(tenant, reportDate) is { } planError) return planError; + if (await EnsureReportDateAllowedAsync(tenant, reportDate, ct) is { } planError) return planError; var snapshot = await _dailyReports.GetReportAsync(cafeId, branchId, reportDate, ct); if (snapshot is null) @@ -62,16 +71,16 @@ public class ReportsController : CafeApiControllerBase new ApiError("VALIDATION_ERROR", "Invalid from/to. Use yyyy-MM-dd.", "from"))); var today = IranCalendar.TodayInIran; - var tier = tenant.PlanTier ?? PlanTier.Free; + var maxDays = await MaxHistoryDaysAsync(tenant, ct); - if (!ReportPlanGate.IsDateInRange(tier, startDate, today) - || !ReportPlanGate.IsDateInRange(tier, endDate, today)) + if (!ReportPlanGate.IsDateInRange(maxDays, startDate, today) + || !ReportPlanGate.IsDateInRange(maxDays, endDate, today)) { return StatusCode(403, new ApiResponse(false, null, - new ApiError("PLAN_LIMIT_REACHED", ReportPlanGate.LimitMessage(tier), "date"))); + new ApiError("PLAN_LIMIT_REACHED", ReportPlanGate.LimitMessage(maxDays), "date"))); } - var clamped = ReportPlanGate.ClampRange(tier, startDate, endDate, today); + var clamped = ReportPlanGate.ClampRange(maxDays, startDate, endDate, today); if (clamped is null) return BadRequest(new ApiResponse(false, null, new ApiError("VALIDATION_ERROR", "Invalid date range.", "from"))); @@ -91,12 +100,11 @@ public class ReportsController : CafeApiControllerBase { if (EnsureCafeAccess(cafeId, tenant) is { } denied) return denied; - var tier = tenant.PlanTier ?? PlanTier.Free; - var maxDays = Core.Constants.PlanLimits.MaxReportHistoryDays(tier); + var maxDays = await MaxHistoryDaysAsync(tenant, ct); if (days > maxDays && maxDays != int.MaxValue) { return StatusCode(403, new ApiResponse(false, null, - new ApiError("PLAN_LIMIT_REACHED", ReportPlanGate.LimitMessage(tier), "days"))); + new ApiError("PLAN_LIMIT_REACHED", ReportPlanGate.LimitMessage(maxDays), "days"))); } days = Math.Min(days, maxDays == int.MaxValue ? 365 : maxDays); @@ -180,14 +188,14 @@ public class ReportsController : CafeApiControllerBase return DateOnly.TryParse(value, out date); } - private IActionResult? EnsureReportDateAllowed(ITenantContext tenant, DateOnly date) + private async Task EnsureReportDateAllowedAsync(ITenantContext tenant, DateOnly date, CancellationToken ct) { - var tier = tenant.PlanTier ?? PlanTier.Free; + var maxDays = await MaxHistoryDaysAsync(tenant, ct); var today = IranCalendar.TodayInIran; - if (ReportPlanGate.IsDateInRange(tier, date, today)) + if (ReportPlanGate.IsDateInRange(maxDays, date, today)) return null; return StatusCode(403, new ApiResponse(false, null, - new ApiError("PLAN_LIMIT_REACHED", ReportPlanGate.LimitMessage(tier), "date"))); + new ApiError("PLAN_LIMIT_REACHED", ReportPlanGate.LimitMessage(maxDays), "date"))); } } diff --git a/src/Meezi.API/Services/MenuAi3dGenerationService.cs b/src/Meezi.API/Services/MenuAi3dGenerationService.cs index 571302d..51ed630 100644 --- a/src/Meezi.API/Services/MenuAi3dGenerationService.cs +++ b/src/Meezi.API/Services/MenuAi3dGenerationService.cs @@ -141,7 +141,7 @@ public class MenuAi3dGenerationService : IMenuAi3dGenerationService { if (!await _catalog.IsFeatureEnabledForCafeAsync(cafeId, planTier, FeatureMenu3dAi, cancellationToken)) return 0; - return PlanLimits.MaxMenuAi3dPerMonth(planTier); + return (await _catalog.GetLimitsAsync(planTier, cancellationToken)).MaxMenuAi3dPerMonth; } private static string UsageKey(string cafeId) => diff --git a/src/Meezi.API/Services/ReportPlanGate.cs b/src/Meezi.API/Services/ReportPlanGate.cs index 0982438..38d560f 100644 --- a/src/Meezi.API/Services/ReportPlanGate.cs +++ b/src/Meezi.API/Services/ReportPlanGate.cs @@ -1,13 +1,13 @@ -using Meezi.Core.Constants; -using Meezi.Core.Enums; - namespace Meezi.API.Services; +/// +/// Report-history window checks. Takes the (admin-editable) max-history days +/// directly so the limit comes from the plan catalog, not a hardcoded tier table. +/// public static class ReportPlanGate { - public static bool IsDateInRange(PlanTier tier, DateOnly date, DateOnly todayIran) + public static bool IsDateInRange(int maxDays, DateOnly date, DateOnly todayIran) { - var maxDays = PlanLimits.MaxReportHistoryDays(tier); if (maxDays == int.MaxValue) return date <= todayIran; @@ -16,16 +16,15 @@ public static class ReportPlanGate } public static (DateOnly From, DateOnly To)? ClampRange( - PlanTier tier, + int maxDays, DateOnly from, DateOnly to, DateOnly todayIran) { if (from > to) return null; - if (!IsDateInRange(tier, to, todayIran) || !IsDateInRange(tier, from, todayIran)) + if (!IsDateInRange(maxDays, to, todayIran) || !IsDateInRange(maxDays, from, todayIran)) return null; - var maxDays = PlanLimits.MaxReportHistoryDays(tier); if (maxDays == int.MaxValue) return (from, to); @@ -36,16 +35,8 @@ public static class ReportPlanGate return (clampedFrom, clampedTo); } - public static string LimitMessage(PlanTier tier) - { - var days = PlanLimits.MaxReportHistoryDays(tier); - return tier switch - { - PlanTier.Free => - "Daily reports on the Free plan are limited to today and the previous 7 days. Upgrade to Pro for 90 days of history.", - PlanTier.Pro => - "Daily reports on the Pro plan are limited to the last 90 days. Upgrade to Business for unlimited history.", - _ => "Report date is outside your plan range." - }; - } + public static string LimitMessage(int maxDays) => + maxDays == int.MaxValue + ? "Report date is outside the allowed range." + : $"Daily reports on your plan are limited to the last {maxDays} days. Upgrade for more history."; }