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:
@@ -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;
|
||||
|
||||
@@ -94,7 +94,16 @@
|
||||
<a asp-action="Index" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i> Back to Catalog
|
||||
</a>
|
||||
@if (ViewBag.NextRunAvailable != null)
|
||||
@if (!(bool)(ViewBag.AiPriceCheckEnabled ?? true))
|
||||
{
|
||||
<div class="text-end">
|
||||
<button class="btn btn-primary" disabled>
|
||||
<i class="bi bi-robot me-2"></i>Analyze Catalog with AI
|
||||
</button>
|
||||
<div class="small text-muted mt-1">Available on the Enterprise plan</div>
|
||||
</div>
|
||||
}
|
||||
else if (ViewBag.NextRunAvailable != null)
|
||||
{
|
||||
<div class="text-end">
|
||||
<button class="btn btn-primary" disabled>
|
||||
|
||||
@@ -162,6 +162,13 @@
|
||||
</div>
|
||||
<div class="form-text">Allow this company to use AI lookup on inventory items.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check form-switch">
|
||||
<input asp-for="AiCatalogPriceCheckEnabled" class="form-check-input" type="checkbox" />
|
||||
<label asp-for="AiCatalogPriceCheckEnabled" class="form-check-label fw-medium">AI Catalog Price Check</label>
|
||||
</div>
|
||||
<div class="form-text">Allow this company to run AI-powered catalog price analysis.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label asp-for="MaxAiPhotoQuotesPerMonthOverride" class="form-label">Monthly AI Quote Limit Override</label>
|
||||
<input asp-for="MaxAiPhotoQuotesPerMonthOverride" type="number" class="form-control" min="-1" placeholder="Leave blank to use plan default" />
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="mb-3">
|
||||
<div class="form-check form-switch">
|
||||
<input asp-for="AllowAiInventoryAssist" class="form-check-input" type="checkbox" role="switch" />
|
||||
<label asp-for="AllowAiInventoryAssist" class="form-check-label fw-medium">Allow AI Inventory Assist</label>
|
||||
@@ -154,6 +154,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="form-check form-switch">
|
||||
<input asp-for="AllowAiCatalogPriceCheck" class="form-check-input" type="checkbox" role="switch" />
|
||||
<label asp-for="AllowAiCatalogPriceCheck" class="form-check-label fw-medium">Allow AI Catalog Price Check</label>
|
||||
</div>
|
||||
<div class="form-text">
|
||||
When enabled, companies on this plan can run AI-powered catalog price analysis (once per quarter).
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="mb-3 pb-2 border-bottom mt-4">Stripe Integration</h5>
|
||||
|
||||
<div class="alert alert-info small mb-3" role="alert">
|
||||
|
||||
@@ -156,6 +156,19 @@
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-muted">AI Price Check</td>
|
||||
<td>
|
||||
@if (plan.AllowAiCatalogPriceCheck)
|
||||
{
|
||||
<span class="badge bg-success">Enabled</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-secondary">Disabled</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="table-light">
|
||||
<td colspan="2" class="fw-semibold small text-uppercase text-muted py-1">Stripe</td>
|
||||
</tr>
|
||||
|
||||
@@ -293,6 +293,15 @@
|
||||
</div>
|
||||
<div class="form-text">Allow AI-powered inventory lookups.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
name="aiCatalogPriceCheckEnabled" value="true" id="aiCatalogPriceCheck"
|
||||
@(Model.AiCatalogPriceCheckEnabled ? "checked" : "") />
|
||||
<label class="form-check-label" for="aiCatalogPriceCheck">AI Catalog Price Check</label>
|
||||
</div>
|
||||
<div class="form-text">Allow AI-powered catalog price analysis.</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label small fw-medium">AI Photo Quotes / Month Override</label>
|
||||
<input type="number" name="maxAiPhotoQuotesPerMonthOverride" class="form-control form-control-sm"
|
||||
|
||||
Reference in New Issue
Block a user