using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using SeaHaven.DataServices.Implementation; using SeaHaven.Services.Configuration; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Helpers; using SeaHaven.Services.Implementation; using SeaHaven.Services.Validation; using Xunit; namespace SeaHavenIndustries.Tests; /// /// SH-393: an uplift created from a work order must land on a dispatch the work order's /// uplift reads resolve back to it (non-deleted, owned or linked), so the request is /// listed on that work order, consumes its allowance and carries its WO number in the /// approval queue. /// public sealed class WorkOrderUpliftDispatchOwnershipTests { private const string NoPrimaryDispatchMessage = "Work order has no primary dispatch for uplift requests"; private static ApplicationDbContext CreateContext() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var context = new ApplicationDbContext(options); WorkOrderAccountTestHelpers.SeedBoardCreateScope(context); context.Vendors.Add(new Vendor { Id = 5, CompanyName = "Acme HVAC" }); context.SaveChanges(); return context; } private static WorkOrderBoardCreateService NewBoardCreateService(ApplicationDbContext context) { var boardData = new WorkOrderBoardDataService(context); var resolver = WorkOrderAccountTestHelpers.Resolver(context); var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context)); return new WorkOrderBoardCreateService( boardData, new WorkOrderBoardMutationDataService(context), new WorkOrderBoardService(boardData, resolver), new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks), new WorkOrderBoardCreateValidation(), resolver, new WorkOrderPocDataService(context)); } private static WorkOrderBoardUpdateService NewBoardUpdateService(ApplicationDbContext context) { var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context)); return new WorkOrderBoardUpdateService( new WorkOrderBoardDataService(context), new WorkOrderBoardMutationDataService(context), new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks)); } private static WorkOrderUpliftService NewUpliftService(ApplicationDbContext context) { return new WorkOrderUpliftService( new UpliftDataService(context), new DispatchDataService(context), new WorkOrderDetailDataService(context), WorkOrderAccountTestHelpers.Resolver(context), new UserDataService(context), TimeProvider.System, Options.Create(new ApprovalsOptions { UpliftTier1MaxUsd = 2500m, Tier1Roles = new[] { "Approver" }, Tier2Roles = new[] { "Manager" }, })); } private static Task BoardCreateWithVendorAsync( ApplicationDbContext context, bool? isAddOn = null) { return NewBoardCreateService(context).CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", LocationId = 1, VendorId = 5, IsAddOn = isAddOn, }, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"); } private static Task CreateUpliftAsync( ApplicationDbContext context, int workOrderId, decimal amount, int accountId = 1) { return NewUpliftService(context).CreateAsync( workOrderId, new CreateWorkOrderUpliftRequestDto { Amount = amount, Notes = "Extra coil work" }, WorkOrderAccountTestHelpers.AccountUser(accountId: accountId), CancellationToken.None); } private static async Task<(int WorkOrderId, int PrimaryDispatchId)> SeedWorkOrderAsync( ApplicationDbContext context, Dispatch primary, params Dispatch[] others) { context.Dispatches.Add(primary); context.Dispatches.AddRange(others); context.workOrders.Add(new WorkOrder { Id = 1, InternalWONumber = "SH00001", AccountId = 1, WorkOrderType = WorkOrderType.PM, PrimaryDispatchId = primary.Id, }); await context.SaveChangesAsync(); return (1, primary.Id); } [Fact] public async Task BoardCreate_WithVendor_PrimaryDispatchBelongsToWorkOrder() { await using var context = CreateContext(); var row = await BoardCreateWithVendorAsync(context); var dispatch = await context.Dispatches.SingleAsync(d => d.Id == row.PrimaryDispatchId); Assert.Equal(row.Id, dispatch.WorkOrderId); } [Fact] public async Task BoardCreatedWorkOrder_PatchApptDate_KeepsPrimaryDispatch() { await using var context = CreateContext(); var row = await BoardCreateWithVendorAsync(context); var workOrder = await context.workOrders.SingleAsync(w => w.Id == row.Id); var dispatch = await context.Dispatches.SingleAsync(d => d.Id == row.PrimaryDispatchId); workOrder.RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }; dispatch.RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 }; await context.SaveChangesAsync(); var patched = await NewBoardUpdateService(context).PatchFieldAsync(row.Id, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ApptDate, Value = "2026-10-05", WorkOrderVersion = Convert.ToBase64String(workOrder.RowVersion), DispatchVersion = Convert.ToBase64String(dispatch.RowVersion), }, "actor-1"); Assert.Equal(row.PrimaryDispatchId, patched.PrimaryDispatchId); Assert.Equal(1, await context.Dispatches.CountAsync()); } [Fact] public async Task BoardCreatedWorkOrder_AutoApprovedUplift_IsListedAndConsumesAllowance() { await using var context = CreateContext(); var row = await BoardCreateWithVendorAsync(context); var created = await CreateUpliftAsync(context, row.Id, 400m); Assert.Equal("auto_approved", created!.Status); var listed = await NewUpliftService(context).ListAsync( row.Id, WorkOrderAccountTestHelpers.AccountUser(), CancellationToken.None); var uplift = Assert.Single(listed!); Assert.Equal(created.Id, uplift.Id); Assert.Equal(400m, await new UpliftDataService(context) .SumAutoApprovedAmountForWorkOrderAsync(row.Id, CancellationToken.None)); } [Fact] public async Task BoardCreatedWorkOrder_RequestBeyondRemainingAllowance_GoesToPendingWithWorkOrderNumber() { await using var context = CreateContext(); var row = await BoardCreateWithVendorAsync(context); await CreateUpliftAsync(context, row.Id, 400m); var second = await CreateUpliftAsync(context, row.Id, 200m); Assert.Equal("pending", second!.Status); var (_, queue) = await new UpliftDataService(context) .GetPagedAsync("Pending", null, 1, 25, CancellationToken.None); var queued = Assert.Single(queue); Assert.Equal(second.Id, queued.Id); Assert.Equal(row.Id, queued.WorkOrderId); Assert.Equal(row.WoNumber, queued.WorkOrderNumber); } [Fact] public async Task BoardCreatedWorkOrder_OwnerRevokesAutoApprovedUplift() { await using var context = CreateContext(); var row = await BoardCreateWithVendorAsync(context); var created = await CreateUpliftAsync(context, row.Id, 1m); var revoked = await NewUpliftService(context).RevokeAsync( row.Id, created!.Id, new RevokeWorkOrderUpliftRequestDto(), WorkOrderAccountTestHelpers.AccountUser(), CancellationToken.None); Assert.Equal("revoked", revoked!.Status); Assert.Equal(0m, await new UpliftDataService(context) .SumAutoApprovedAmountForWorkOrderAsync(row.Id, CancellationToken.None)); } [Fact] public async Task ExAddOnWorkOrder_AfterScheduleClearsAddOn_UpliftIsListedOnWorkOrder() { await using var context = CreateContext(); var row = await BoardCreateWithVendorAsync(context, isAddOn: true); Assert.True(row.IsAddOn); var workOrder = await context.workOrders.SingleAsync(w => w.Id == row.Id); workOrder.RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }; await context.SaveChangesAsync(); var rescheduled = await NewBoardUpdateService(context).PatchFieldAsync(row.Id, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ScheduledDate, Value = DateTime.UtcNow.Date.AddDays(60).ToString("yyyy-MM-dd"), WorkOrderVersion = Convert.ToBase64String(workOrder.RowVersion), }, "actor-1"); Assert.False(rescheduled.IsAddOn); var created = await CreateUpliftAsync(context, row.Id, 1m); var listed = await NewUpliftService(context).ListAsync( row.Id, WorkOrderAccountTestHelpers.AccountUser(), CancellationToken.None); Assert.Equal(created!.Id, Assert.Single(listed!).Id); } [Fact] public async Task Create_SoftDeletedPrimaryDispatch_UsesOwnedLiveDispatch() { await using var context = CreateContext(); var (workOrderId, _) = await SeedWorkOrderAsync( context, new Dispatch { Id = 10, VendorId = 5, WorkOrderId = 1, NTEAmount = 1000m, IsDeleted = true }, new Dispatch { Id = 11, VendorId = 5, WorkOrderId = 1, NTEAmount = 800m }); var created = await CreateUpliftAsync(context, workOrderId, 100m); var request = await context.DispatchUpliftRequests.SingleAsync(u => u.Id == created!.Id); Assert.Equal(11, request.DispatchId); Assert.Equal(1000m, (await context.Dispatches.SingleAsync(d => d.Id == 10)).NTEAmount); Assert.Equal(900m, (await context.Dispatches.SingleAsync(d => d.Id == 11)).NTEAmount); } [Fact] public async Task Create_OnlySoftDeletedPrimaryDispatch_IsRejectedWithoutPersisting() { await using var context = CreateContext(); var (workOrderId, _) = await SeedWorkOrderAsync( context, new Dispatch { Id = 10, VendorId = 5, WorkOrderId = 1, NTEAmount = 1000m, IsDeleted = true }); var error = await Assert.ThrowsAsync( () => CreateUpliftAsync(context, workOrderId, 100m)); Assert.Equal(NoPrimaryDispatchMessage, error.Message); Assert.Empty(context.DispatchUpliftRequests); Assert.Empty(context.WorkOrderAuditLogs); } [Fact] public async Task Create_PrimaryDispatchOwnedByAnotherWorkOrder_IsNotUsed() { await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 2, InternalWONumber = "SH00002", AccountId = 1 }); var (workOrderId, _) = await SeedWorkOrderAsync( context, new Dispatch { Id = 10, VendorId = 5, WorkOrderId = 2, NTEAmount = 1000m }); var error = await Assert.ThrowsAsync( () => CreateUpliftAsync(context, workOrderId, 100m)); Assert.Equal(NoPrimaryDispatchMessage, error.Message); Assert.Empty(context.DispatchUpliftRequests); Assert.Equal(1000m, (await context.Dispatches.SingleAsync(d => d.Id == 10)).NTEAmount); } [Fact] public async Task Create_UnlinkedPrimaryDispatch_IsRejectedInsteadOfHidingTheUplift() { await using var context = CreateContext(); var (workOrderId, _) = await SeedWorkOrderAsync( context, new Dispatch { Id = 10, VendorId = 5, WorkOrderId = null, NTEAmount = 1000m }); var error = await Assert.ThrowsAsync( () => CreateUpliftAsync(context, workOrderId, 100m)); Assert.Equal(NoPrimaryDispatchMessage, error.Message); Assert.Empty(context.DispatchUpliftRequests); } [Fact] public async Task Create_PrimaryDispatchLinkedThroughDispatchWorkOrders_IsUsed() { await using var context = CreateContext(); context.workOrders.Add(new WorkOrder { Id = 2, InternalWONumber = "SH00002", AccountId = 1 }); var (workOrderId, _) = await SeedWorkOrderAsync( context, new Dispatch { Id = 10, VendorId = 5, WorkOrderId = 2, NTEAmount = 1000m }); context.DispatchWorkOrders.Add(new DispatchWorkOrder { DispatchId = 10, WorkOrderId = workOrderId }); await context.SaveChangesAsync(); var created = await CreateUpliftAsync(context, workOrderId, 100m); var listed = await NewUpliftService(context).ListAsync( workOrderId, WorkOrderAccountTestHelpers.AccountUser(), CancellationToken.None); Assert.Equal(created!.Id, Assert.Single(listed!).Id); } [Fact] public async Task CrossTenant_CreateAndList_AreRejected() { await using var context = CreateContext(); await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, id: 2, name: "Other Corp"); var row = await BoardCreateWithVendorAsync(context); await CreateUpliftAsync(context, row.Id, 1m); var created = await CreateUpliftAsync(context, row.Id, 1m, accountId: 2); var listed = await NewUpliftService(context).ListAsync( row.Id, WorkOrderAccountTestHelpers.AccountUser(accountId: 2), CancellationToken.None); Assert.Null(created); Assert.Null(listed); Assert.Equal(1, await context.DispatchUpliftRequests.CountAsync()); } }