using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; using PowderCoating.Infrastructure.Data; using PowderCoating.Web.Controllers; namespace PowderCoating.UnitTests; public class UsageQuotaControllerTests { [Fact] public async Task Index_UsesOverridesAndCountsOnlyActiveResources() { await using var context = CreateContext(); SeedPlan(context, plan: 1, maxUsers: 10, maxJobs: 5, maxCustomers: 10, maxQuotes: 10, maxCatalogItems: 10); SeedCompany(context, companyId: 1, plan: 1, maxUsersOverride: 5); SeedLookupRows(context, companyId: 1); context.Users.AddRange( CreateUser("u1", 1), CreateUser("u2", 1), CreateUser("u3", 1), CreateUser("u4", 1)); context.Customers.AddRange( new Customer { Id = 1, CompanyId = 1, CompanyName = "Cust 1" }, new Customer { Id = 2, CompanyId = 1, CompanyName = "Cust 2" }); context.Jobs.AddRange( new Job { Id = 1, CompanyId = 1, CustomerId = 1, Description = "Active", JobNumber = "JOB-1", JobStatusId = 10, JobPriorityId = 1 }, new Job { Id = 2, CompanyId = 1, CustomerId = 1, Description = "Done", JobNumber = "JOB-2", JobStatusId = 11, JobPriorityId = 1 }); context.Quotes.AddRange( new Quote { Id = 1, CompanyId = 1, QuoteNumber = "Q-1", QuoteStatusId = 10 }, new Quote { Id = 2, CompanyId = 1, QuoteNumber = "Q-2", QuoteStatusId = 11 }, new Quote { Id = 3, CompanyId = 1, QuoteNumber = "Q-3", QuoteStatusId = 12 }); context.CatalogItems.AddRange( new CatalogItem { Id = 1, CompanyId = 1, Name = "Wheel", CategoryId = 1 }, new CatalogItem { Id = 2, CompanyId = 1, Name = "Frame", CategoryId = 1 }); await context.SaveChangesAsync(); var controller = new UsageQuotaController(context); var result = await controller.Index(null, null, null, null); var view = Assert.IsType(result); var row = Assert.Single(Assert.IsAssignableFrom>(view.Model)); Assert.Equal(4, row.Users); Assert.Equal(5, row.MaxUsers); Assert.Equal(1, row.ActiveJobs); Assert.Equal(1, row.ActiveQuotes); Assert.Equal(2, row.Customers); Assert.Equal(2, row.CatalogItems); Assert.True(row.IsNearLimit); Assert.False(row.IsAtLimit); Assert.Equal(0, controller.ViewBag.AtLimitCount); Assert.Equal(1, controller.ViewBag.NearLimitCount); } [Fact] public async Task Index_ForCompedCompany_ReturnsUnlimitedLimitsWithoutFlags() { await using var context = CreateContext(); SeedPlan(context, plan: 2, maxUsers: 1, maxJobs: 1, maxCustomers: 1, maxQuotes: 1, maxCatalogItems: 1); SeedCompany(context, companyId: 2, plan: 2, isComped: true); SeedLookupRows(context, companyId: 2); context.Users.AddRange(CreateUser("u1", 2), CreateUser("u2", 2)); context.Customers.Add(new Customer { Id = 10, CompanyId = 2, CompanyName = "Comped Customer" }); context.Jobs.Add(new Job { Id = 10, CompanyId = 2, CustomerId = 10, Description = "Active", JobNumber = "JOB-C", JobStatusId = 20, JobPriorityId = 2 }); context.Quotes.Add(new Quote { Id = 10, CompanyId = 2, QuoteNumber = "Q-C", QuoteStatusId = 20 }); context.CatalogItems.Add(new CatalogItem { Id = 10, CompanyId = 2, Name = "Rack", CategoryId = 1 }); await context.SaveChangesAsync(); var controller = new UsageQuotaController(context); var result = await controller.Index(null, null, null, null); var view = Assert.IsType(result); var row = Assert.Single(Assert.IsAssignableFrom>(view.Model)); Assert.True(row.IsComped); Assert.Equal(-1, row.MaxUsers); Assert.Equal(-1, row.MaxActiveJobs); Assert.Equal(-1, row.MaxCustomers); Assert.Equal(-1, row.MaxActiveQuotes); Assert.Equal(-1, row.MaxCatalogItems); Assert.False(row.IsNearLimit); Assert.False(row.IsAtLimit); } [Fact] public async Task Index_ConcernFilters_SeparateNearAndAtLimitRows() { await using var context = CreateContext(); SeedPlan(context, plan: 3, maxUsers: 5, maxJobs: 5, maxCustomers: 5, maxQuotes: 5, maxCatalogItems: 5); SeedCompany(context, companyId: 3, plan: 3, companyName: "Near Co"); SeedCompany(context, companyId: 4, plan: 3, companyName: "At Co"); SeedCompany(context, companyId: 5, plan: 3, companyName: "Safe Co"); SeedLookupRows(context, 3); SeedLookupRows(context, 4); SeedLookupRows(context, 5); context.Users.AddRange( CreateUser("n1", 3), CreateUser("n2", 3), CreateUser("n3", 3), CreateUser("n4", 3), CreateUser("a1", 4), CreateUser("a2", 4), CreateUser("a3", 4), CreateUser("a4", 4), CreateUser("a5", 4), CreateUser("s1", 5)); await context.SaveChangesAsync(); var controller = new UsageQuotaController(context); var limitResult = await controller.Index(null, null, null, "limit"); var limitRows = Assert.IsAssignableFrom>(Assert.IsType(limitResult).Model); Assert.Equal(2, limitRows.Count); Assert.Contains(limitRows, r => r.CompanyName == "Near Co" && r.IsNearLimit); Assert.Contains(limitRows, r => r.CompanyName == "At Co" && r.IsAtLimit); var atLimitResult = await controller.Index(null, null, null, "atlimit"); var atLimitRows = Assert.IsAssignableFrom>(Assert.IsType(atLimitResult).Model); var atLimitRow = Assert.Single(atLimitRows); Assert.Equal("At Co", atLimitRow.CompanyName); Assert.True(atLimitRow.IsAtLimit); } [Fact] public async Task Index_AppliesSearchStatusAndPlanFilters() { await using var context = CreateContext(); SeedPlan(context, plan: 6, maxUsers: 10, maxJobs: 10, maxCustomers: 10, maxQuotes: 10, maxCatalogItems: 10, displayName: "Plan Six"); SeedPlan(context, plan: 7, maxUsers: 10, maxJobs: 10, maxCustomers: 10, maxQuotes: 10, maxCatalogItems: 10, displayName: "Plan Seven"); SeedCompany(context, companyId: 6, plan: 6, companyName: "Acme Powder", status: SubscriptionStatus.Active); SeedCompany(context, companyId: 7, plan: 6, companyName: "Beta Powder", status: SubscriptionStatus.Expired); SeedCompany(context, companyId: 8, plan: 7, companyName: "Acme East", status: SubscriptionStatus.Active); await context.SaveChangesAsync(); var controller = new UsageQuotaController(context); var result = await controller.Index("Acme", nameof(SubscriptionStatus.Active), "6", null); var view = Assert.IsType(result); var row = Assert.Single(Assert.IsAssignableFrom>(view.Model)); Assert.Equal("Acme Powder", row.CompanyName); Assert.Equal(nameof(SubscriptionStatus.Active), controller.ViewBag.StatusFilter); Assert.Equal("6", controller.ViewBag.PlanFilter); } [Fact] public async Task Index_WhenUsageIsExactlyEightyPercent_MarksRowNearLimit() { await using var context = CreateContext(); SeedPlan(context, plan: 8, maxUsers: 5, maxJobs: 10, maxCustomers: 10, maxQuotes: 10, maxCatalogItems: 10); SeedCompany(context, companyId: 9, plan: 8, companyName: "Threshold Co"); await context.SaveChangesAsync(); context.Users.AddRange( CreateUser("t1", 9), CreateUser("t2", 9), CreateUser("t3", 9), CreateUser("t4", 9)); await context.SaveChangesAsync(); var controller = new UsageQuotaController(context); var result = await controller.Index(null, null, null, null); var view = Assert.IsType(result); var row = Assert.Single(Assert.IsAssignableFrom>(view.Model)); Assert.Equal(4, row.Users); Assert.Equal(5, row.MaxUsers); Assert.True(row.IsNearLimit); Assert.False(row.IsAtLimit); } [Fact] public async Task Index_WhenFiltersAreInvalid_IgnoresThem() { await using var context = CreateContext(); SeedPlan(context, plan: 9, maxUsers: 10, maxJobs: 10, maxCustomers: 10, maxQuotes: 10, maxCatalogItems: 10); SeedPlan(context, plan: 10, maxUsers: 10, maxJobs: 10, maxCustomers: 10, maxQuotes: 10, maxCatalogItems: 10); SeedCompany(context, companyId: 10, plan: 9, companyName: "Alpha Co", status: SubscriptionStatus.Active); SeedCompany(context, companyId: 11, plan: 10, companyName: "Beta Co", status: SubscriptionStatus.Expired); await context.SaveChangesAsync(); var controller = new UsageQuotaController(context); var result = await controller.Index(null, "NotARealStatus", "not-a-plan", null); var view = Assert.IsType(result); var rows = Assert.IsAssignableFrom>(view.Model); Assert.Equal(2, rows.Count); Assert.Equal(2, controller.ViewBag.TotalCount); Assert.Equal("NotARealStatus", controller.ViewBag.StatusFilter); Assert.Equal("not-a-plan", controller.ViewBag.PlanFilter); } private static ApplicationUser CreateUser(string id, int companyId) { return new ApplicationUser { Id = id, CompanyId = companyId, UserName = $"{id}@example.com", Email = $"{id}@example.com", FirstName = "Test", LastName = "User" }; } private static void SeedPlan( ApplicationDbContext context, int plan, int maxUsers, int maxJobs, int maxCustomers, int maxQuotes, int maxCatalogItems, string? displayName = null) { context.SubscriptionPlanConfigs.Add(new SubscriptionPlanConfig { Id = plan, CompanyId = 0, Plan = plan, DisplayName = displayName ?? $"Plan {plan}", SortOrder = plan, IsActive = true, MaxUsers = maxUsers, MaxActiveJobs = maxJobs, MaxCustomers = maxCustomers, MaxQuotes = maxQuotes, MaxCatalogItems = maxCatalogItems }); } private static void SeedCompany( ApplicationDbContext context, int companyId, int plan, string? companyName = null, SubscriptionStatus status = SubscriptionStatus.Active, bool isComped = false, int? maxUsersOverride = null) { context.Companies.Add(new Company { Id = companyId, CompanyId = companyId, CompanyName = companyName ?? $"Company {companyId}", PrimaryContactName = "Owner", PrimaryContactEmail = $"owner{companyId}@example.com", SubscriptionPlan = plan, SubscriptionStatus = status, IsComped = isComped, MaxUsersOverride = maxUsersOverride, IsActive = true }); } private static void SeedLookupRows(ApplicationDbContext context, int companyId) { context.JobPriorityLookups.Add(new JobPriorityLookup { Id = companyId, CompanyId = companyId, PriorityCode = "NORMAL", DisplayName = "Normal", DisplayOrder = 1 }); context.JobStatusLookups.AddRange( new JobStatusLookup { Id = companyId * 10, CompanyId = companyId, StatusCode = "ACTIVE", DisplayName = "Active", DisplayOrder = 1, IsTerminalStatus = false }, new JobStatusLookup { Id = companyId * 10 + 1, CompanyId = companyId, StatusCode = "DONE", DisplayName = "Done", DisplayOrder = 2, IsTerminalStatus = true }); context.QuoteStatusLookups.AddRange( new QuoteStatusLookup { Id = companyId * 10, CompanyId = companyId, StatusCode = "PENDING", DisplayName = "Pending", DisplayOrder = 1 }, new QuoteStatusLookup { Id = companyId * 10 + 1, CompanyId = companyId, StatusCode = "REJECTED", DisplayName = "Rejected", DisplayOrder = 2, IsRejectedStatus = true }, new QuoteStatusLookup { Id = companyId * 10 + 2, CompanyId = companyId, StatusCode = "CONVERTED", DisplayName = "Converted", DisplayOrder = 3, IsConvertedStatus = true }); } private static ApplicationDbContext CreateContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new ApplicationDbContext(options); } }