dbe4170986
116 tests passing: JobPhotoService, MeasurementConversionService, PlatformSettingsService, QuoteApprovalController, QuotePhotoService, ShopCapabilityCalculator, StorageMigrationService, TenantContext, UsageQuotaController — plus expanded PricingCalculation, Registration, and Subscription tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
330 lines
13 KiB
C#
330 lines
13 KiB
C#
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<ViewResult>(result);
|
|
var row = Assert.Single(Assert.IsAssignableFrom<List<UsageRow>>(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<ViewResult>(result);
|
|
var row = Assert.Single(Assert.IsAssignableFrom<List<UsageRow>>(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<List<UsageRow>>(Assert.IsType<ViewResult>(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<List<UsageRow>>(Assert.IsType<ViewResult>(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<ViewResult>(result);
|
|
var row = Assert.Single(Assert.IsAssignableFrom<List<UsageRow>>(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<ViewResult>(result);
|
|
var row = Assert.Single(Assert.IsAssignableFrom<List<UsageRow>>(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<ViewResult>(result);
|
|
var rows = Assert.IsAssignableFrom<List<UsageRow>>(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<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
|
|
return new ApplicationDbContext(options);
|
|
}
|
|
}
|