Add three-layer feature gating for AI Catalog Price Check

Adds platform-level, plan-level (Enterprise only), and per-company
toggles for the AI Catalog Price Check feature. Includes:
- Company.AiCatalogPriceCheckEnabled per-company flag
- SubscriptionPlanConfig.AllowAiCatalogPriceCheck plan-level flag
- PlatformSetting 'AiCatalogPriceCheckEnabled' global kill switch
- IPlatformSettingsService.GetBoolAsync helper
- ISubscriptionService.CanUseAiCatalogPriceCheckAsync
- UI controls in Companies/Edit, PlatformSubscription/Edit+Index,
  and SubscriptionManagement/Manage
- Migration AddAiCatalogPriceCheckGating applied

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 08:29:51 -04:00
parent fa9fa76231
commit cb7bbc37bd
20 changed files with 9517 additions and 6 deletions
@@ -39,6 +39,7 @@ namespace PowderCoating.Web.Controllers
private readonly ISubscriptionService _subscriptionService;
private readonly ICatalogImageService _catalogImageService;
private readonly IAiCatalogPriceCheckService _priceCheckService;
private readonly IPlatformSettingsService _platformSettings;
public CatalogItemsController(
IUnitOfWork unitOfWork,
@@ -50,7 +51,8 @@ namespace PowderCoating.Web.Controllers
IMeasurementConversionService measurementService,
ISubscriptionService subscriptionService,
ICatalogImageService catalogImageService,
IAiCatalogPriceCheckService priceCheckService)
IAiCatalogPriceCheckService priceCheckService,
IPlatformSettingsService platformSettings)
{
_unitOfWork = unitOfWork;
_mapper = mapper;
@@ -62,6 +64,7 @@ namespace PowderCoating.Web.Controllers
_subscriptionService = subscriptionService;
_catalogImageService = catalogImageService;
_priceCheckService = priceCheckService;
_platformSettings = platformSettings;
}
/// <summary>
@@ -934,6 +937,11 @@ namespace PowderCoating.Web.Controllers
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Forbid();
// Three-layer gate: platform setting → plan (Enterprise) → per-company toggle
var platformEnabled = await _platformSettings.GetBoolAsync(PlatformSettingKeys.AiCatalogPriceCheckEnabled, true);
var companyEnabled = platformEnabled && await _subscriptionService.CanUseAiCatalogPriceCheckAsync(currentUser.CompanyId);
ViewBag.AiPriceCheckEnabled = companyEnabled;
var existing = await _unitOfWork.CatalogPriceCheckReports.FindAsync(
r => r.CompanyId == currentUser.CompanyId);
var report = existing.OrderByDescending(r => r.RunAt).FirstOrDefault();
@@ -995,6 +1003,14 @@ namespace PowderCoating.Web.Controllers
var currentUser = await _userManager.GetUserAsync(User);
if (currentUser == null) return Forbid();
// Three-layer gate: platform setting → plan → per-company toggle
var platformEnabled = await _platformSettings.GetBoolAsync(PlatformSettingKeys.AiCatalogPriceCheckEnabled, true);
if (!platformEnabled || !await _subscriptionService.CanUseAiCatalogPriceCheckAsync(currentUser.CompanyId))
{
TempData["Error"] = "AI Catalog Price Check is not available on your current plan.";
return RedirectToAction(nameof(AiPriceCheck));
}
// Enforce quarterly run limit — check the most recent report for this company.
var lastReport = (await _unitOfWork.CatalogPriceCheckReports.FindAsync(
r => r.CompanyId == currentUser.CompanyId))
@@ -63,6 +63,7 @@ public class PlatformSubscriptionController : Controller
AllowAccounting = c.AllowAccounting,
AllowAiPhotoQuotes = c.AllowAiPhotoQuotes,
AllowAiInventoryAssist = c.AllowAiInventoryAssist,
AllowAiCatalogPriceCheck = c.AllowAiCatalogPriceCheck,
IsActive = c.IsActive,
SortOrder = c.SortOrder
}).ToList();
@@ -102,6 +103,7 @@ public class PlatformSubscriptionController : Controller
AllowAccounting = config.AllowAccounting,
AllowAiPhotoQuotes = config.AllowAiPhotoQuotes,
AllowAiInventoryAssist = config.AllowAiInventoryAssist,
AllowAiCatalogPriceCheck = config.AllowAiCatalogPriceCheck,
IsActive = config.IsActive
};
@@ -146,6 +148,7 @@ public class PlatformSubscriptionController : Controller
config.AllowAccounting = dto.AllowAccounting;
config.AllowAiPhotoQuotes = dto.AllowAiPhotoQuotes;
config.AllowAiInventoryAssist = dto.AllowAiInventoryAssist;
config.AllowAiCatalogPriceCheck = dto.AllowAiCatalogPriceCheck;
config.IsActive = dto.IsActive;
await _unitOfWork.SubscriptionPlanConfigs.UpdateAsync(config);
@@ -462,12 +462,14 @@ public class SubscriptionManagementController : Controller
/// <param name="id">Primary key of the company to update.</param>
/// <param name="aiPhotoQuotesEnabled">Whether AI photo quoting is enabled for this company.</param>
/// <param name="aiInventoryAssistEnabled">Whether AI inventory assistance is enabled for this company.</param>
/// <param name="aiCatalogPriceCheckEnabled">Whether AI catalog price check is enabled for this company.</param>
/// <param name="maxAiPhotoQuotesPerMonthOverride">Monthly AI photo quote limit override; 0 = plan default.</param>
[HttpPost, ValidateAntiForgeryToken]
public async Task<IActionResult> UpdateFeatureFlags(
int id,
bool aiPhotoQuotesEnabled,
bool aiInventoryAssistEnabled,
bool aiCatalogPriceCheckEnabled,
int? maxAiPhotoQuotesPerMonthOverride)
{
var company = await _db.Companies.IgnoreQueryFilters().FirstOrDefaultAsync(c => c.Id == id);
@@ -475,6 +477,7 @@ public class SubscriptionManagementController : Controller
company.AiPhotoQuotesEnabled = aiPhotoQuotesEnabled;
company.AiInventoryAssistEnabled = aiInventoryAssistEnabled;
company.AiCatalogPriceCheckEnabled = aiCatalogPriceCheckEnabled;
company.MaxAiPhotoQuotesPerMonthOverride = NullIfZero(maxAiPhotoQuotesPerMonthOverride);
company.UpdatedAt = DateTime.UtcNow;
company.UpdatedBy = User.Identity?.Name;