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:
@@ -158,6 +158,7 @@ public class UpdateCompanyDto
|
|||||||
// AI feature flags
|
// AI feature flags
|
||||||
public bool AiPhotoQuotesEnabled { get; set; }
|
public bool AiPhotoQuotesEnabled { get; set; }
|
||||||
public bool AiInventoryAssistEnabled { get; set; }
|
public bool AiInventoryAssistEnabled { get; set; }
|
||||||
|
public bool AiCatalogPriceCheckEnabled { get; set; }
|
||||||
public int? MaxAiPhotoQuotesPerMonthOverride { get; set; }
|
public int? MaxAiPhotoQuotesPerMonthOverride { get; set; }
|
||||||
|
|
||||||
// Per-company feature overrides (null = use plan default)
|
// Per-company feature overrides (null = use plan default)
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public class SubscriptionPlanConfigDto
|
|||||||
public bool AllowAccounting { get; set; }
|
public bool AllowAccounting { get; set; }
|
||||||
public bool AllowAiPhotoQuotes { get; set; }
|
public bool AllowAiPhotoQuotes { get; set; }
|
||||||
public bool AllowAiInventoryAssist { get; set; }
|
public bool AllowAiInventoryAssist { get; set; }
|
||||||
|
public bool AllowAiCatalogPriceCheck { get; set; }
|
||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
public int SortOrder { get; set; }
|
public int SortOrder { get; set; }
|
||||||
}
|
}
|
||||||
@@ -70,6 +71,7 @@ public class UpdateSubscriptionPlanConfigDto
|
|||||||
public bool AllowAccounting { get; set; }
|
public bool AllowAccounting { get; set; }
|
||||||
public bool AllowAiPhotoQuotes { get; set; }
|
public bool AllowAiPhotoQuotes { get; set; }
|
||||||
public bool AllowAiInventoryAssist { get; set; }
|
public bool AllowAiInventoryAssist { get; set; }
|
||||||
|
public bool AllowAiCatalogPriceCheck { get; set; }
|
||||||
|
|
||||||
public bool IsActive { get; set; }
|
public bool IsActive { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace PowderCoating.Application.Interfaces;
|
|||||||
public interface IPlatformSettingsService
|
public interface IPlatformSettingsService
|
||||||
{
|
{
|
||||||
Task<string?> GetAsync(string key);
|
Task<string?> GetAsync(string key);
|
||||||
|
Task<bool> GetBoolAsync(string key, bool defaultValue = false);
|
||||||
Task SetAsync(string key, string? value, string? updatedBy = null);
|
Task SetAsync(string key, string? value, string? updatedBy = null);
|
||||||
Task<IReadOnlyList<PlatformSetting>> GetAllAsync();
|
Task<IReadOnlyList<PlatformSetting>> GetAllAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ public class Company : BaseEntity
|
|||||||
public bool AiPhotoQuotesEnabled { get; set; } = true;
|
public bool AiPhotoQuotesEnabled { get; set; } = true;
|
||||||
/// <summary>Enables/disables the AI Inventory Assist lookup for this company.</summary>
|
/// <summary>Enables/disables the AI Inventory Assist lookup for this company.</summary>
|
||||||
public bool AiInventoryAssistEnabled { get; set; } = true;
|
public bool AiInventoryAssistEnabled { get; set; } = true;
|
||||||
|
/// <summary>Enables/disables the AI Catalog Price Check for this company.</summary>
|
||||||
|
public bool AiCatalogPriceCheckEnabled { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stores the billing period the customer selected at registration (or last changed on the Billing page).
|
/// Stores the billing period the customer selected at registration (or last changed on the Billing page).
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ public static class PlatformSettingKeys
|
|||||||
public const string StripeWebhookRetentionDays = "StripeWebhookRetentionDays";
|
public const string StripeWebhookRetentionDays = "StripeWebhookRetentionDays";
|
||||||
public const string MaxTenants = "MaxTenants";
|
public const string MaxTenants = "MaxTenants";
|
||||||
public const string SmsEnabled = "SmsEnabled";
|
public const string SmsEnabled = "SmsEnabled";
|
||||||
|
public const string AiCatalogPriceCheckEnabled = "AiCatalogPriceCheckEnabled";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ public class SubscriptionPlanConfig : BaseEntity
|
|||||||
/// <summary>When true, companies on this plan can use the AI Inventory Assist lookup feature.</summary>
|
/// <summary>When true, companies on this plan can use the AI Inventory Assist lookup feature.</summary>
|
||||||
public bool AllowAiInventoryAssist { get; set; } = false;
|
public bool AllowAiInventoryAssist { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>When true, companies on this plan can run the AI Catalog Price Check (Enterprise only).</summary>
|
||||||
|
public bool AllowAiCatalogPriceCheck { get; set; } = false;
|
||||||
|
|
||||||
public bool IsActive { get; set; } = true;
|
public bool IsActive { get; set; } = true;
|
||||||
public int SortOrder { get; set; }
|
public int SortOrder { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ public interface ISubscriptionService
|
|||||||
Task<bool> CanUseAiPhotoQuoteAsync(int companyId);
|
Task<bool> CanUseAiPhotoQuoteAsync(int companyId);
|
||||||
/// <summary>Returns (used this month, monthly max). Max = -1 means unlimited.</summary>
|
/// <summary>Returns (used this month, monthly max). Max = -1 means unlimited.</summary>
|
||||||
Task<(int Used, int Max)> GetAiPhotoQuoteUsageAsync(int companyId);
|
Task<(int Used, int Max)> GetAiPhotoQuoteUsageAsync(int companyId);
|
||||||
|
/// <summary>Returns true if the AI Catalog Price Check is enabled for this company (plan gate + per-company flag).</summary>
|
||||||
|
Task<bool> CanUseAiCatalogPriceCheckAsync(int companyId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns days until expiry (negative = days past expiry). Returns null if no end date set.
|
/// Returns days until expiry (negative = days past expiry). Returns null if no end date set.
|
||||||
|
|||||||
Generated
+9301
File diff suppressed because it is too large
Load Diff
+98
@@ -0,0 +1,98 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace PowderCoating.Infrastructure.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddAiCatalogPriceCheckGating : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "AllowAiCatalogPriceCheck",
|
||||||
|
table: "SubscriptionPlanConfigs",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "AiCatalogPriceCheckEnabled",
|
||||||
|
table: "Companies",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.InsertData(
|
||||||
|
table: "PlatformSettings",
|
||||||
|
columns: ["Id", "Key", "Value", "Label", "Description", "GroupName"],
|
||||||
|
values: new object[,]
|
||||||
|
{
|
||||||
|
{ 9, "AiCatalogPriceCheckEnabled", "true", "AI Catalog Price Check Enabled",
|
||||||
|
"When true (default), the AI Catalog Price Check feature is available to companies on qualifying plans. Set to false to disable it platform-wide.",
|
||||||
|
"AI Features" }
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 4, 26, 12, 26, 21, 727, DateTimeKind.Utc).AddTicks(5012));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 4, 26, 12, 26, 21, 727, DateTimeKind.Utc).AddTicks(5018));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 4, 26, 12, 26, 21, 727, DateTimeKind.Utc).AddTicks(5020));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DeleteData(
|
||||||
|
table: "PlatformSettings",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 9);
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AllowAiCatalogPriceCheck",
|
||||||
|
table: "SubscriptionPlanConfigs");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AiCatalogPriceCheckEnabled",
|
||||||
|
table: "Companies");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 4, 25, 22, 34, 50, 1, DateTimeKind.Utc).AddTicks(6987));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 4, 25, 22, 34, 50, 1, DateTimeKind.Utc).AddTicks(6993));
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "PricingTiers",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "CreatedAt",
|
||||||
|
value: new DateTime(2026, 4, 25, 22, 34, 50, 1, DateTimeKind.Utc).AddTicks(6994));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1530,6 +1530,9 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
b.Property<string>("Address")
|
b.Property<string>("Address")
|
||||||
.HasColumnType("nvarchar(max)");
|
.HasColumnType("nvarchar(max)");
|
||||||
|
|
||||||
|
b.Property<bool>("AiCatalogPriceCheckEnabled")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<bool>("AiInventoryAssistEnabled")
|
b.Property<bool>("AiInventoryAssistEnabled")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
@@ -5833,7 +5836,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 4, 25, 22, 34, 50, 1, DateTimeKind.Utc).AddTicks(6987),
|
CreatedAt = new DateTime(2026, 4, 26, 12, 26, 21, 727, DateTimeKind.Utc).AddTicks(5012),
|
||||||
Description = "Standard pricing for regular customers",
|
Description = "Standard pricing for regular customers",
|
||||||
DiscountPercent = 0m,
|
DiscountPercent = 0m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -5844,7 +5847,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 4, 25, 22, 34, 50, 1, DateTimeKind.Utc).AddTicks(6993),
|
CreatedAt = new DateTime(2026, 4, 26, 12, 26, 21, 727, DateTimeKind.Utc).AddTicks(5018),
|
||||||
Description = "5% discount for preferred customers",
|
Description = "5% discount for preferred customers",
|
||||||
DiscountPercent = 5m,
|
DiscountPercent = 5m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -5855,7 +5858,7 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 3,
|
Id = 3,
|
||||||
CompanyId = 0,
|
CompanyId = 0,
|
||||||
CreatedAt = new DateTime(2026, 4, 25, 22, 34, 50, 1, DateTimeKind.Utc).AddTicks(6994),
|
CreatedAt = new DateTime(2026, 4, 26, 12, 26, 21, 727, DateTimeKind.Utc).AddTicks(5020),
|
||||||
Description = "10% discount for premium customers",
|
Description = "10% discount for premium customers",
|
||||||
DiscountPercent = 10m,
|
DiscountPercent = 10m,
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
@@ -7183,6 +7186,9 @@ namespace PowderCoating.Infrastructure.Migrations
|
|||||||
b.Property<bool>("AllowAccounting")
|
b.Property<bool>("AllowAccounting")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<bool>("AllowAiCatalogPriceCheck")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
b.Property<bool>("AllowAiInventoryAssist")
|
b.Property<bool>("AllowAiInventoryAssist")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,17 @@ public class PlatformSettingsService : IPlatformSettingsService
|
|||||||
return setting?.Value;
|
return setting?.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads a platform setting as a boolean. Returns <paramref name="defaultValue"/> when
|
||||||
|
/// the key is missing or the stored value cannot be parsed as a boolean.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> GetBoolAsync(string key, bool defaultValue = false)
|
||||||
|
{
|
||||||
|
var value = await GetAsync(key);
|
||||||
|
if (value == null) return defaultValue;
|
||||||
|
return bool.TryParse(value, out var result) ? result : defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates or updates the platform setting identified by <paramref name="key"/>.
|
/// Creates or updates the platform setting identified by <paramref name="key"/>.
|
||||||
/// Records <paramref name="updatedBy"/> (typically the SuperAdmin's username) and
|
/// Records <paramref name="updatedBy"/> (typically the SuperAdmin's username) and
|
||||||
|
|||||||
@@ -323,6 +323,19 @@ public class SubscriptionService : ISubscriptionService
|
|||||||
/// (3) monthly usage quota from <see cref="GetAiPhotoQuoteUsageAsync"/>.
|
/// (3) monthly usage quota from <see cref="GetAiPhotoQuoteUsageAsync"/>.
|
||||||
/// Comped companies bypass all three gates.
|
/// Comped companies bypass all three gates.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
public async Task<bool> CanUseAiCatalogPriceCheckAsync(int companyId)
|
||||||
|
{
|
||||||
|
var (company, config) = await GetCompanyAndConfigAsync(companyId);
|
||||||
|
if (company == null) return false;
|
||||||
|
if (company.IsComped) return true;
|
||||||
|
|
||||||
|
// Plan-level gate: Enterprise plan must have this feature enabled
|
||||||
|
if (config != null && !config.AllowAiCatalogPriceCheck) return false;
|
||||||
|
|
||||||
|
// Company-level toggle: SuperAdmin can disable per company
|
||||||
|
return company.AiCatalogPriceCheckEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> CanUseAiPhotoQuoteAsync(int companyId)
|
public async Task<bool> CanUseAiPhotoQuoteAsync(int companyId)
|
||||||
{
|
{
|
||||||
var (company, config) = await GetCompanyAndConfigAsync(companyId);
|
var (company, config) = await GetCompanyAndConfigAsync(companyId);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ namespace PowderCoating.Web.Controllers
|
|||||||
private readonly ISubscriptionService _subscriptionService;
|
private readonly ISubscriptionService _subscriptionService;
|
||||||
private readonly ICatalogImageService _catalogImageService;
|
private readonly ICatalogImageService _catalogImageService;
|
||||||
private readonly IAiCatalogPriceCheckService _priceCheckService;
|
private readonly IAiCatalogPriceCheckService _priceCheckService;
|
||||||
|
private readonly IPlatformSettingsService _platformSettings;
|
||||||
|
|
||||||
public CatalogItemsController(
|
public CatalogItemsController(
|
||||||
IUnitOfWork unitOfWork,
|
IUnitOfWork unitOfWork,
|
||||||
@@ -50,7 +51,8 @@ namespace PowderCoating.Web.Controllers
|
|||||||
IMeasurementConversionService measurementService,
|
IMeasurementConversionService measurementService,
|
||||||
ISubscriptionService subscriptionService,
|
ISubscriptionService subscriptionService,
|
||||||
ICatalogImageService catalogImageService,
|
ICatalogImageService catalogImageService,
|
||||||
IAiCatalogPriceCheckService priceCheckService)
|
IAiCatalogPriceCheckService priceCheckService,
|
||||||
|
IPlatformSettingsService platformSettings)
|
||||||
{
|
{
|
||||||
_unitOfWork = unitOfWork;
|
_unitOfWork = unitOfWork;
|
||||||
_mapper = mapper;
|
_mapper = mapper;
|
||||||
@@ -62,6 +64,7 @@ namespace PowderCoating.Web.Controllers
|
|||||||
_subscriptionService = subscriptionService;
|
_subscriptionService = subscriptionService;
|
||||||
_catalogImageService = catalogImageService;
|
_catalogImageService = catalogImageService;
|
||||||
_priceCheckService = priceCheckService;
|
_priceCheckService = priceCheckService;
|
||||||
|
_platformSettings = platformSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -934,6 +937,11 @@ namespace PowderCoating.Web.Controllers
|
|||||||
var currentUser = await _userManager.GetUserAsync(User);
|
var currentUser = await _userManager.GetUserAsync(User);
|
||||||
if (currentUser == null) return Forbid();
|
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(
|
var existing = await _unitOfWork.CatalogPriceCheckReports.FindAsync(
|
||||||
r => r.CompanyId == currentUser.CompanyId);
|
r => r.CompanyId == currentUser.CompanyId);
|
||||||
var report = existing.OrderByDescending(r => r.RunAt).FirstOrDefault();
|
var report = existing.OrderByDescending(r => r.RunAt).FirstOrDefault();
|
||||||
@@ -995,6 +1003,14 @@ namespace PowderCoating.Web.Controllers
|
|||||||
var currentUser = await _userManager.GetUserAsync(User);
|
var currentUser = await _userManager.GetUserAsync(User);
|
||||||
if (currentUser == null) return Forbid();
|
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.
|
// Enforce quarterly run limit — check the most recent report for this company.
|
||||||
var lastReport = (await _unitOfWork.CatalogPriceCheckReports.FindAsync(
|
var lastReport = (await _unitOfWork.CatalogPriceCheckReports.FindAsync(
|
||||||
r => r.CompanyId == currentUser.CompanyId))
|
r => r.CompanyId == currentUser.CompanyId))
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ public class PlatformSubscriptionController : Controller
|
|||||||
AllowAccounting = c.AllowAccounting,
|
AllowAccounting = c.AllowAccounting,
|
||||||
AllowAiPhotoQuotes = c.AllowAiPhotoQuotes,
|
AllowAiPhotoQuotes = c.AllowAiPhotoQuotes,
|
||||||
AllowAiInventoryAssist = c.AllowAiInventoryAssist,
|
AllowAiInventoryAssist = c.AllowAiInventoryAssist,
|
||||||
|
AllowAiCatalogPriceCheck = c.AllowAiCatalogPriceCheck,
|
||||||
IsActive = c.IsActive,
|
IsActive = c.IsActive,
|
||||||
SortOrder = c.SortOrder
|
SortOrder = c.SortOrder
|
||||||
}).ToList();
|
}).ToList();
|
||||||
@@ -102,6 +103,7 @@ public class PlatformSubscriptionController : Controller
|
|||||||
AllowAccounting = config.AllowAccounting,
|
AllowAccounting = config.AllowAccounting,
|
||||||
AllowAiPhotoQuotes = config.AllowAiPhotoQuotes,
|
AllowAiPhotoQuotes = config.AllowAiPhotoQuotes,
|
||||||
AllowAiInventoryAssist = config.AllowAiInventoryAssist,
|
AllowAiInventoryAssist = config.AllowAiInventoryAssist,
|
||||||
|
AllowAiCatalogPriceCheck = config.AllowAiCatalogPriceCheck,
|
||||||
IsActive = config.IsActive
|
IsActive = config.IsActive
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -146,6 +148,7 @@ public class PlatformSubscriptionController : Controller
|
|||||||
config.AllowAccounting = dto.AllowAccounting;
|
config.AllowAccounting = dto.AllowAccounting;
|
||||||
config.AllowAiPhotoQuotes = dto.AllowAiPhotoQuotes;
|
config.AllowAiPhotoQuotes = dto.AllowAiPhotoQuotes;
|
||||||
config.AllowAiInventoryAssist = dto.AllowAiInventoryAssist;
|
config.AllowAiInventoryAssist = dto.AllowAiInventoryAssist;
|
||||||
|
config.AllowAiCatalogPriceCheck = dto.AllowAiCatalogPriceCheck;
|
||||||
config.IsActive = dto.IsActive;
|
config.IsActive = dto.IsActive;
|
||||||
|
|
||||||
await _unitOfWork.SubscriptionPlanConfigs.UpdateAsync(config);
|
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="id">Primary key of the company to update.</param>
|
||||||
/// <param name="aiPhotoQuotesEnabled">Whether AI photo quoting is enabled for this company.</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="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>
|
/// <param name="maxAiPhotoQuotesPerMonthOverride">Monthly AI photo quote limit override; 0 = plan default.</param>
|
||||||
[HttpPost, ValidateAntiForgeryToken]
|
[HttpPost, ValidateAntiForgeryToken]
|
||||||
public async Task<IActionResult> UpdateFeatureFlags(
|
public async Task<IActionResult> UpdateFeatureFlags(
|
||||||
int id,
|
int id,
|
||||||
bool aiPhotoQuotesEnabled,
|
bool aiPhotoQuotesEnabled,
|
||||||
bool aiInventoryAssistEnabled,
|
bool aiInventoryAssistEnabled,
|
||||||
|
bool aiCatalogPriceCheckEnabled,
|
||||||
int? maxAiPhotoQuotesPerMonthOverride)
|
int? maxAiPhotoQuotesPerMonthOverride)
|
||||||
{
|
{
|
||||||
var company = await _db.Companies.IgnoreQueryFilters().FirstOrDefaultAsync(c => c.Id == id);
|
var company = await _db.Companies.IgnoreQueryFilters().FirstOrDefaultAsync(c => c.Id == id);
|
||||||
@@ -475,6 +477,7 @@ public class SubscriptionManagementController : Controller
|
|||||||
|
|
||||||
company.AiPhotoQuotesEnabled = aiPhotoQuotesEnabled;
|
company.AiPhotoQuotesEnabled = aiPhotoQuotesEnabled;
|
||||||
company.AiInventoryAssistEnabled = aiInventoryAssistEnabled;
|
company.AiInventoryAssistEnabled = aiInventoryAssistEnabled;
|
||||||
|
company.AiCatalogPriceCheckEnabled = aiCatalogPriceCheckEnabled;
|
||||||
company.MaxAiPhotoQuotesPerMonthOverride = NullIfZero(maxAiPhotoQuotesPerMonthOverride);
|
company.MaxAiPhotoQuotesPerMonthOverride = NullIfZero(maxAiPhotoQuotesPerMonthOverride);
|
||||||
company.UpdatedAt = DateTime.UtcNow;
|
company.UpdatedAt = DateTime.UtcNow;
|
||||||
company.UpdatedBy = User.Identity?.Name;
|
company.UpdatedBy = User.Identity?.Name;
|
||||||
|
|||||||
@@ -94,7 +94,16 @@
|
|||||||
<a asp-action="Index" class="btn btn-outline-secondary btn-sm">
|
<a asp-action="Index" class="btn btn-outline-secondary btn-sm">
|
||||||
<i class="bi bi-arrow-left me-1"></i> Back to Catalog
|
<i class="bi bi-arrow-left me-1"></i> Back to Catalog
|
||||||
</a>
|
</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">
|
<div class="text-end">
|
||||||
<button class="btn btn-primary" disabled>
|
<button class="btn btn-primary" disabled>
|
||||||
|
|||||||
@@ -162,6 +162,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-text">Allow this company to use AI lookup on inventory items.</div>
|
<div class="form-text">Allow this company to use AI lookup on inventory items.</div>
|
||||||
</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">
|
<div class="col-md-4">
|
||||||
<label asp-for="MaxAiPhotoQuotesPerMonthOverride" class="form-label">Monthly AI Quote Limit Override</label>
|
<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" />
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-3">
|
||||||
<div class="form-check form-switch">
|
<div class="form-check form-switch">
|
||||||
<input asp-for="AllowAiInventoryAssist" class="form-check-input" type="checkbox" role="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>
|
<label asp-for="AllowAiInventoryAssist" class="form-check-label fw-medium">Allow AI Inventory Assist</label>
|
||||||
@@ -154,6 +154,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<h5 class="mb-3 pb-2 border-bottom mt-4">Stripe Integration</h5>
|
||||||
|
|
||||||
<div class="alert alert-info small mb-3" role="alert">
|
<div class="alert alert-info small mb-3" role="alert">
|
||||||
|
|||||||
@@ -156,6 +156,19 @@
|
|||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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">
|
<tr class="table-light">
|
||||||
<td colspan="2" class="fw-semibold small text-uppercase text-muted py-1">Stripe</td>
|
<td colspan="2" class="fw-semibold small text-uppercase text-muted py-1">Stripe</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -293,6 +293,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-text">Allow AI-powered inventory lookups.</div>
|
<div class="form-text">Allow AI-powered inventory lookups.</div>
|
||||||
</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">
|
<div class="col-md-4">
|
||||||
<label class="form-label small fw-medium">AI Photo Quotes / Month Override</label>
|
<label class="form-label small fw-medium">AI Photo Quotes / Month Override</label>
|
||||||
<input type="number" name="maxAiPhotoQuotesPerMonthOverride" class="form-control form-control-sm"
|
<input type="number" name="maxAiPhotoQuotesPerMonthOverride" class="form-control form-control-sm"
|
||||||
|
|||||||
Reference in New Issue
Block a user