using AutoMapper; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using PowderCoating.Application.DTOs.Subscription; using PowderCoating.Core.Interfaces; using PowderCoating.Shared.Constants; namespace PowderCoating.Web.Controllers; /// /// SuperAdmin-only interface for managing subscription plan configurations /// (limits, pricing, feature flags, and Stripe price IDs). Changes here /// affect every new tenant assignment of the plan and any quota checks that /// read plan limits at runtime. Plan DisplayName and Plan enum /// value are intentionally not editable here to prevent breaking existing company /// subscription records that reference them by integer value. /// [Authorize(Policy = AppConstants.Policies.SuperAdminOnly)] public class PlatformSubscriptionController : Controller { private readonly IUnitOfWork _unitOfWork; private readonly ILogger _logger; public PlatformSubscriptionController(IUnitOfWork unitOfWork, ILogger logger) { _unitOfWork = unitOfWork; _logger = logger; } /// /// Lists all subscription plan configurations ordered by display sort order. /// Uses ignoreQueryFilters: true because plan configs carry /// CompanyId = 0 and are otherwise hidden by the multi-tenancy filter. /// Projects to to avoid exposing the /// full entity (including internal Stripe secrets) to the view layer. /// [HttpGet] public async Task Index() { var configs = (await _unitOfWork.SubscriptionPlanConfigs.GetAllAsync(ignoreQueryFilters: true)) .OrderBy(c => c.SortOrder) .ToList(); var dtos = configs.Select(c => new SubscriptionPlanConfigDto { Id = c.Id, Plan = c.Plan, DisplayName = c.DisplayName, Description = c.Description, MaxUsers = c.MaxUsers, MaxActiveJobs = c.MaxActiveJobs, MaxCustomers = c.MaxCustomers, MaxQuotes = c.MaxQuotes, MaxCatalogItems = c.MaxCatalogItems, MaxJobPhotos = c.MaxJobPhotos, MaxQuotePhotos = c.MaxQuotePhotos, MaxAiPhotoQuotesPerMonth = c.MaxAiPhotoQuotesPerMonth, MonthlyPrice = c.MonthlyPrice, AnnualPrice = c.AnnualPrice, StripePriceIdMonthly = c.StripePriceIdMonthly, StripePriceIdAnnual = c.StripePriceIdAnnual, AllowOnlinePayments = c.AllowOnlinePayments, AllowAccounting = c.AllowAccounting, AllowAiPhotoQuotes = c.AllowAiPhotoQuotes, AllowAiInventoryAssist = c.AllowAiInventoryAssist, AllowAiCatalogPriceCheck = c.AllowAiCatalogPriceCheck, AllowSms = c.AllowSms, IsActive = c.IsActive, SortOrder = c.SortOrder }).ToList(); return View(dtos); } /// /// Returns the Edit form for a plan config, loaded into an /// to prevent over-posting of /// immutable fields such as Plan, DisplayName, and SortOrder. /// The plan display name is placed in ViewBag for the page heading. /// [HttpGet] public async Task Edit(int id) { var config = await _unitOfWork.SubscriptionPlanConfigs.GetByIdAsync(id, ignoreQueryFilters: true); if (config == null) return NotFound(); var dto = new UpdateSubscriptionPlanConfigDto { Id = config.Id, Description = config.Description, MaxUsers = config.MaxUsers, MaxActiveJobs = config.MaxActiveJobs, MaxCustomers = config.MaxCustomers, MaxQuotes = config.MaxQuotes, MaxCatalogItems = config.MaxCatalogItems, MaxJobPhotos = config.MaxJobPhotos, MaxQuotePhotos = config.MaxQuotePhotos, MaxAiPhotoQuotesPerMonth = config.MaxAiPhotoQuotesPerMonth, MonthlyPrice = config.MonthlyPrice, AnnualPrice = config.AnnualPrice, StripePriceIdMonthly = config.StripePriceIdMonthly, StripePriceIdAnnual = config.StripePriceIdAnnual, AllowOnlinePayments = config.AllowOnlinePayments, AllowAccounting = config.AllowAccounting, AllowAiPhotoQuotes = config.AllowAiPhotoQuotes, AllowAiInventoryAssist = config.AllowAiInventoryAssist, AllowAiCatalogPriceCheck = config.AllowAiCatalogPriceCheck, AllowSms = config.AllowSms, IsActive = config.IsActive }; ViewBag.PlanName = config.DisplayName; return View(dto); } /// /// Applies the updated plan configuration values and saves. Uses explicit field /// mapping from to the tracked entity so that immutable /// identity fields (Plan, DisplayName, SortOrder) are never /// overwritten. Logs the change at Information level for the audit trail. /// [HttpPost] [ValidateAntiForgeryToken] public async Task Edit(int id, UpdateSubscriptionPlanConfigDto dto) { if (!ModelState.IsValid) { var configForView = await _unitOfWork.SubscriptionPlanConfigs.GetByIdAsync(id, ignoreQueryFilters: true); ViewBag.PlanName = configForView?.DisplayName ?? "Unknown"; return View(dto); } var config = await _unitOfWork.SubscriptionPlanConfigs.GetByIdAsync(id, ignoreQueryFilters: true); if (config == null) return NotFound(); config.Description = dto.Description; config.MaxUsers = dto.MaxUsers; config.MaxActiveJobs = dto.MaxActiveJobs; config.MaxCustomers = dto.MaxCustomers; config.MaxQuotes = dto.MaxQuotes; config.MaxCatalogItems = dto.MaxCatalogItems; config.MaxJobPhotos = dto.MaxJobPhotos; config.MaxQuotePhotos = dto.MaxQuotePhotos; config.MaxAiPhotoQuotesPerMonth = dto.MaxAiPhotoQuotesPerMonth; config.MonthlyPrice = dto.MonthlyPrice; config.AnnualPrice = dto.AnnualPrice; config.StripePriceIdMonthly = dto.StripePriceIdMonthly; config.StripePriceIdAnnual = dto.StripePriceIdAnnual; config.AllowOnlinePayments = dto.AllowOnlinePayments; config.AllowAccounting = dto.AllowAccounting; config.AllowAiPhotoQuotes = dto.AllowAiPhotoQuotes; config.AllowAiInventoryAssist = dto.AllowAiInventoryAssist; config.AllowAiCatalogPriceCheck = dto.AllowAiCatalogPriceCheck; config.AllowSms = dto.AllowSms; config.IsActive = dto.IsActive; await _unitOfWork.SubscriptionPlanConfigs.UpdateAsync(config); await _unitOfWork.CompleteAsync(); _logger.LogInformation("SuperAdmin updated subscription plan config: {Plan}", config.DisplayName); TempData["Success"] = $"{config.DisplayName} plan configuration updated successfully."; return RedirectToAction(nameof(Index)); } }