using Microsoft.EntityFrameworkCore; using PowderCoating.Core.Entities; using PowderCoating.Core.Enums; using PowderCoating.Infrastructure.Data; using PowderCoating.Infrastructure.Repositories; using PowderCoating.Infrastructure.Services; using Xunit; namespace PowderCoating.UnitTests; public class SubscriptionServiceTests { [Fact] public async Task GetUserCountAsync_PrefersCompanyOverrideOverPlanDefault() { await using var context = CreateContext(); SeedCompanyAndPlan(context, companyId: 7, plan: 1, maxUsers: 3); var company = context.Companies.Local.Single(c => c.Id == 7); company.MaxUsersOverride = 7; context.Users.AddRange( new ApplicationUser { Id = "u1", CompanyId = 7, UserName = "u1", Email = "u1@example.com", FirstName = "A", LastName = "One", IsActive = true }, new ApplicationUser { Id = "u2", CompanyId = 7, UserName = "u2", Email = "u2@example.com", FirstName = "B", LastName = "Two", IsActive = true }); await context.SaveChangesAsync(); var service = new SubscriptionService(new UnitOfWork(context), context); var (used, max) = await service.GetUserCountAsync(7); Assert.Equal(2, used); Assert.Equal(7, max); } [Fact] public async Task GetJobCountAsync_ExcludesTerminalStatuses() { await using var context = CreateContext(); SeedCompanyAndPlan(context, companyId: 8, plan: 2, maxActiveJobs: 50); SeedJobStatuses(context, 8); context.Jobs.AddRange( new Job { Id = 1, CompanyId = 8, JobNumber = "JOB-1", CustomerId = 1, Description = "Active", JobStatusId = 1, JobPriorityId = 1 }, new Job { Id = 2, CompanyId = 8, JobNumber = "JOB-2", CustomerId = 1, Description = "Done", JobStatusId = 2, JobPriorityId = 1 }, new Job { Id = 3, CompanyId = 8, JobNumber = "JOB-3", CustomerId = 1, Description = "Delivered", JobStatusId = 3, JobPriorityId = 1 }); await context.SaveChangesAsync(); var service = new SubscriptionService(new UnitOfWork(context), context); var (used, max) = await service.GetJobCountAsync(8); Assert.Equal(1, used); Assert.Equal(50, max); } [Fact] public async Task GetQuoteCountAsync_CountsOnlyCurrentMonth() { await using var context = CreateContext(); SeedCompanyAndPlan(context, companyId: 9, plan: 3, maxQuotes: 5); var currentQuote = new Quote { Id = 1, CompanyId = 9, QuoteNumber = "Q-001", QuoteStatusId = 1 }; var oldQuote = new Quote { Id = 2, CompanyId = 9, QuoteNumber = "Q-OLD", QuoteStatusId = 1 }; context.Quotes.AddRange(currentQuote, oldQuote); await context.SaveChangesAsync(); oldQuote.CreatedAt = DateTime.UtcNow.AddMonths(-1); await context.SaveChangesAsync(); var service = new SubscriptionService(new UnitOfWork(context), context); var (used, max) = await service.GetQuoteCountAsync(9); Assert.Equal(1, used); Assert.Equal(5, max); } [Fact] public async Task CanAddCustomerAsync_CompedCompany_BypassesPlanLimits() { await using var context = CreateContext(); SeedCompanyAndPlan(context, companyId: 10, plan: 4, maxCustomers: 0); var company = await context.Companies.FindAsync(10); company!.IsComped = true; context.Customers.Add(new Customer { Id = 1, CompanyId = 10, CompanyName = "Customer A" }); await context.SaveChangesAsync(); var service = new SubscriptionService(new UnitOfWork(context), context); var allowed = await service.CanAddCustomerAsync(10); Assert.True(allowed); } [Fact] public async Task CanUseAiPhotoQuoteAsync_RequiresFeatureEnabledAndQuotaAvailable() { await using var context = CreateContext(); SeedCompanyAndPlan(context, companyId: 11, plan: 5, maxAiPhotoQuotesPerMonth: 2, allowAiPhotoQuotes: true); context.AiItemPredictions.Add(new AiItemPrediction { Id = 1, CompanyId = 11, CreatedAt = DateTime.UtcNow.AddDays(-1) }); await context.SaveChangesAsync(); var service = new SubscriptionService(new UnitOfWork(context), context); var allowed = await service.CanUseAiPhotoQuoteAsync(11); Assert.True(allowed); } [Fact] public async Task CanUseAiPhotoQuoteAsync_ReturnsFalse_WhenPlanDisablesFeature() { await using var context = CreateContext(); SeedCompanyAndPlan(context, companyId: 12, plan: 6, maxAiPhotoQuotesPerMonth: 10, allowAiPhotoQuotes: false); await context.SaveChangesAsync(); var service = new SubscriptionService(new UnitOfWork(context), context); var allowed = await service.CanUseAiPhotoQuoteAsync(12); Assert.False(allowed); } private static ApplicationDbContext CreateContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new ApplicationDbContext(options); } private static void SeedCompanyAndPlan( ApplicationDbContext context, int companyId, int plan, int maxUsers = -1, int maxActiveJobs = -1, int maxCustomers = -1, int maxQuotes = -1, int maxAiPhotoQuotesPerMonth = -1, bool allowAiPhotoQuotes = true) { context.Companies.Add(new Company { Id = companyId, CompanyId = companyId, CompanyName = $"Company {companyId}", PrimaryContactName = "Owner", PrimaryContactEmail = $"owner{companyId}@example.com", SubscriptionPlan = plan, SubscriptionStatus = SubscriptionStatus.Active, IsActive = true }); context.SubscriptionPlanConfigs.Add(new SubscriptionPlanConfig { Id = companyId, CompanyId = 0, Plan = plan, DisplayName = $"Plan {plan}", IsActive = true, MaxUsers = maxUsers, MaxActiveJobs = maxActiveJobs, MaxCustomers = maxCustomers, MaxQuotes = maxQuotes, MaxAiPhotoQuotesPerMonth = maxAiPhotoQuotesPerMonth, AllowAiPhotoQuotes = allowAiPhotoQuotes }); context.JobPriorityLookups.Add(new JobPriorityLookup { Id = companyId, CompanyId = companyId, PriorityCode = "NORMAL", DisplayName = "Normal", DisplayOrder = 1 }); } private static void SeedJobStatuses(ApplicationDbContext context, int companyId) { context.JobStatusLookups.AddRange( new JobStatusLookup { Id = 1, CompanyId = companyId, StatusCode = "Pending", DisplayName = "Pending", DisplayOrder = 1, IsTerminalStatus = false }, new JobStatusLookup { Id = 2, CompanyId = companyId, StatusCode = "Completed", DisplayName = "Completed", DisplayOrder = 2, IsTerminalStatus = true }, new JobStatusLookup { Id = 3, CompanyId = companyId, StatusCode = "Delivered", DisplayName = "Delivered", DisplayOrder = 3, IsTerminalStatus = true }); } }