using Data.SeaHavenIndustries; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using SeaHaven.DataServices.Implementation; using SeaHaven.Services.Configuration; using SeaHaven.Services.DTOs; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; using System.Security.Claims; using Xunit; namespace SeaHavenIndustries.Tests; public sealed class WorkOrderUpliftServiceTests { private static ApplicationDbContext CreateContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; return new ApplicationDbContext(options); } private static ApprovalsOptions NewOptions() => new() { UpliftTier1MaxUsd = 2500m, Tier1Roles = new[] { "Approver" }, Tier2Roles = new[] { "Manager" }, }; private static WorkOrderUpliftService NewService(ApplicationDbContext context) { return new WorkOrderUpliftService( new UpliftDataService(context), new DispatchDataService(context), new WorkOrderDetailDataService(context), WorkOrderAccountTestHelpers.Resolver(context), new UserDataService(context), TimeProvider.System, Options.Create(NewOptions())); } private static ClaimsPrincipal Dispatcher(string userId = "dispatcher-1") => WorkOrderAccountTestHelpers.OrgWideAdmin(userId); private static async Task<(WorkOrder WorkOrder, Dispatch Dispatch)> SeedWorkOrderAsync(ApplicationDbContext context) { await WorkOrderAccountTestHelpers.EnsureAccountAsync(context); context.Users.Add(new ApplicationUser { Id = "dispatcher-1", UserName = "dispatcher-1", FirstName = "Alex", LastName = "Dispatcher", }); context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" }); context.Dispatches.Add(new Dispatch { Id = 10, VendorId = 1, WorkOrderId = 1, NTEAmount = 1000m, DispatchNumber = "DIS-10", Status = "Completed", }); var workOrder = new WorkOrder { Id = 1, InternalWONumber = "10000000001", PrimaryDispatchId = 10, AccountId = 1, }; context.workOrders.Add(workOrder); await context.SaveChangesAsync(); return (workOrder, context.Dispatches.Single()); } [Fact] public async Task ListAsync_ReturnsWorkOrderScopedUplifts() { await using var context = CreateContext(); var (workOrder, dispatch) = await SeedWorkOrderAsync(context); context.DispatchUpliftRequests.Add(new DispatchUpliftRequest { Id = 100, DispatchId = dispatch.Id, RequestedNTE = 1500m, Status = "Pending", RequiredTier = 1, NotificationStatus = "Pending", VendorReason = "Extra coil work", }); await context.SaveChangesAsync(); var service = NewService(context); var items = await service.ListAsync(workOrder.Id, Dispatcher(), CancellationToken.None); Assert.NotNull(items); var uplift = Assert.Single(items!); Assert.Equal("pending", uplift.Status); Assert.Equal(1500m, uplift.Amount); } [Fact] public async Task CreateAsync_AboveCurrentNte_CreatesPendingRequest() { await using var context = CreateContext(); var (workOrder, _) = await SeedWorkOrderAsync(context); var service = NewService(context); var created = await service.CreateAsync( workOrder.Id, new CreateWorkOrderUpliftRequestDto { Amount = 1800m, Notes = "Parts" }, Dispatcher(), CancellationToken.None); Assert.NotNull(created); Assert.Equal("pending", created!.Status); Assert.Equal("dispatcher-1", created.RequestedById); Assert.Equal("Pending", Assert.Single(context.DispatchUpliftRequests).Status); } [Fact] public async Task CreateAsync_AtOrBelowCurrentNte_CreatesAutoApprovedRequest() { await using var context = CreateContext(); var (workOrder, _) = await SeedWorkOrderAsync(context); var service = NewService(context); var created = await service.CreateAsync( workOrder.Id, new CreateWorkOrderUpliftRequestDto { Amount = 900m, Notes = "Within limit" }, Dispatcher(), CancellationToken.None); Assert.NotNull(created); Assert.Equal("auto_approved", created!.Status); } [Fact] public async Task CancelAsync_PendingRequest_Withdraws() { await using var context = CreateContext(); var (workOrder, dispatch) = await SeedWorkOrderAsync(context); context.DispatchUpliftRequests.Add(new DispatchUpliftRequest { Id = 100, DispatchId = dispatch.Id, RequestedNTE = 1500m, Status = "Pending", RequiredTier = 1, NotificationStatus = "Pending", createdby = "dispatcher-1", }); await context.SaveChangesAsync(); var service = NewService(context); var cancelled = await service.CancelAsync(workOrder.Id, 100, Dispatcher(), CancellationToken.None); Assert.NotNull(cancelled); Assert.Equal("cancelled", cancelled!.Status); } [Fact] public async Task RevokeAsync_ApprovedRequiresAdminReason() { await using var context = CreateContext(); var (workOrder, dispatch) = await SeedWorkOrderAsync(context); context.DispatchUpliftRequests.Add(new DispatchUpliftRequest { Id = 100, DispatchId = dispatch.Id, RequestedNTE = 1500m, CurrentNTE = 1000m, Status = "Approved", RequiredTier = 1, NotificationStatus = "Sent", }); await context.SaveChangesAsync(); var service = NewService(context); await Assert.ThrowsAsync(() => service.RevokeAsync( workOrder.Id, 100, new RevokeWorkOrderUpliftRequestDto(), Dispatcher("dispatcher-1"), CancellationToken.None)); } [Fact] public async Task RevokeAsync_AdminRevokesApproved_ReturnsRevokedStatus() { await using var context = CreateContext(); var (workOrder, dispatch) = await SeedWorkOrderAsync(context); dispatch.NTEAmount = 1500m; context.DispatchUpliftRequests.Add(new DispatchUpliftRequest { Id = 100, DispatchId = dispatch.Id, RequestedNTE = 1500m, CurrentNTE = 1000m, Status = "Approved", RequiredTier = 1, NotificationStatus = "Sent", }); await context.SaveChangesAsync(); var service = NewService(context); var revoked = await service.RevokeAsync( workOrder.Id, 100, new RevokeWorkOrderUpliftRequestDto { Reason = "Policy change" }, WorkOrderAccountTestHelpers.OrgWideAdmin("admin-1"), CancellationToken.None); Assert.NotNull(revoked); Assert.Equal("revoked", revoked!.Status); Assert.Equal(1000m, dispatch.NTEAmount); } }