Merge remote-tracking branch 'origin/feat/ab/sh-207-uplift-queue' into feat/ab/sh-210-uplift-decisions

This commit is contained in:
Alexandre Brandizzi 2026-09-16 22:48:17 -03:00
commit e0ec8dbc73
2 changed files with 145 additions and 0 deletions

View file

@ -1,4 +1,5 @@
using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
@ -372,6 +373,149 @@ public sealed class UpliftQueueReadTests
item.EvidenceSizeBytes.Should().Be(512);
}
// --- Queue contract fields (SH-208/SH-213) ---
[Fact]
public async Task List_MarksWorkOrderClosed_OnlyForTerminalLifecycle()
{
using var context = NewContext();
var (vendorA, workOrderA) = await SeedWorkOrderAsync(context, "WO-A", "SITE-A", "HVAC");
var (vendorB, workOrderB) = await SeedWorkOrderAsync(context, "WO-B", "SITE-B", "Plumbing");
workOrderA.LifecycleStatus = LifecycleStatus.Completed;
workOrderB.LifecycleStatus = LifecycleStatus.Canceled;
await context.SaveChangesAsync();
var dispatchA = await SeedDispatchAsync(context, vendorA, workOrderA, "DIS-A");
var dispatchB = await SeedDispatchAsync(context, vendorB, workOrderB, "DIS-B");
var (vendorC, workOrderC) = await SeedWorkOrderAsync(context, "WO-C", "SITE-C", "Electrical");
var dispatchC = await SeedDispatchAsync(context, vendorC, workOrderC, "DIS-C");
context.DispatchUpliftRequests.AddRange(
Request(dispatchA, "Approved", new DateTime(2026, 3, 1), decided: new DateTime(2026, 3, 2)),
Request(dispatchB, "Approved", new DateTime(2026, 3, 3), decided: new DateTime(2026, 3, 4)),
Request(dispatchC, "Approved", new DateTime(2026, 3, 5), decided: new DateTime(2026, 3, 6)));
await context.SaveChangesAsync();
var service = NewService(context);
var result = await service.ListAsync(UserWithRoles("Admin"), "Approved", null, 1, 25, CancellationToken.None);
result.Items.Single(i => i.WorkOrderNumber == "WO-A").WorkOrderClosed.Should().BeTrue();
result.Items.Single(i => i.WorkOrderNumber == "WO-B").WorkOrderClosed.Should().BeTrue();
result.Items.Single(i => i.WorkOrderNumber == "WO-C").WorkOrderClosed.Should().BeFalse();
}
[Fact]
public async Task List_ApprovedRow_CarriesDecidedByNameFromDecidingUser()
{
using var context = NewContext();
var (vendor, workOrder) = await SeedWorkOrderAsync(context, "WO-A", "SITE-A", "HVAC");
var dispatch = await SeedDispatchAsync(context, vendor, workOrder, "DIS-A");
context.Users.Add(new ApplicationUser { Id = "user-9", FirstName = "Grace", LastName = "Hopper" });
await context.SaveChangesAsync();
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
{
DispatchId = dispatch.Id,
Status = "Approved",
CreatedDate = new DateTime(2026, 3, 1),
DecidedAt = new DateTime(2026, 3, 2),
DecidedByUserId = "user-9",
RequiredTier = 1,
RequestedNTE = 300m,
NotificationStatus = "Pending"
});
await context.SaveChangesAsync();
var service = NewService(context);
var result = await service.ListAsync(UserWithRoles("Approver"), "Approved", null, 1, 25, CancellationToken.None);
result.Items.Single().DecidedByName.Should().Be("Grace Hopper");
}
[Fact]
public async Task List_AttachmentCount_CountsActiveDocumentsOnTheDispatch()
{
using var context = NewContext();
var (vendor, workOrder) = await SeedWorkOrderAsync(context, "WO-A", "SITE-A", "HVAC");
var dispatch = await SeedDispatchAsync(context, vendor, workOrder, "DIS-A");
context.VendorCompletionDocuments.AddRange(
new VendorCompletionDocument
{
VendorId = vendor.Id,
DispatchId = dispatch.Id,
WorkOrderId = workOrder.Id,
OriginalFileName = "quote.pdf",
StoredFileName = "a.bin",
ContentType = "application/pdf",
SizeBytes = 512,
ScanStatus = "Passed",
ReviewStatus = "Approved",
Purpose = "UpliftEvidence",
Version = 1
},
new VendorCompletionDocument
{
VendorId = vendor.Id,
DispatchId = dispatch.Id,
WorkOrderId = workOrder.Id,
OriginalFileName = "photo.jpg",
StoredFileName = "b.bin",
ContentType = "image/jpeg",
SizeBytes = 2048,
ScanStatus = "Passed",
ReviewStatus = "Approved",
Purpose = "Completion",
Version = 1
},
new VendorCompletionDocument
{
VendorId = vendor.Id,
DispatchId = dispatch.Id,
WorkOrderId = workOrder.Id,
OriginalFileName = "old.pdf",
StoredFileName = "c.bin",
ContentType = "application/pdf",
SizeBytes = 128,
ScanStatus = "Passed",
ReviewStatus = "Approved",
Purpose = "Completion",
Version = 1,
IsDeleted = true
});
await context.SaveChangesAsync();
context.DispatchUpliftRequests.Add(Request(dispatch, "Pending", new DateTime(2026, 3, 2)));
await context.SaveChangesAsync();
var service = NewService(context);
var result = await service.ListAsync(UserWithRoles("Approver"), "Pending", null, 1, 25, CancellationToken.None);
result.Items.Single().AttachmentCount.Should().Be(2);
}
[Fact]
public async Task List_PendingExposureTotal_SumsGrantedAmountsAcrossAllPendingRequests()
{
using var context = NewContext();
var (vendor, workOrder) = await SeedWorkOrderAsync(context, "WO-A", "SITE-A", "HVAC");
var dispatch = await SeedDispatchAsync(context, vendor, workOrder, "DIS-A");
context.Users.Add(new ApplicationUser { Id = "user-7", FirstName = "Ada", LastName = "Smith" });
await context.SaveChangesAsync();
context.DispatchUpliftRequests.AddRange(
// Vendor-portal rows (createdby null) store the requested new NTE total,
// so the pending exposure is the delta: 400 - 100 = 300 and 600 - 250 = 350.
Request(dispatch, "Pending", new DateTime(2026, 3, 1), requested: 400m, currentNte: 100m),
Request(dispatch, "Pending", new DateTime(2026, 3, 2), requested: 600m, currentNte: 250m),
// Work-order-path rows (createdby set) store the requested increment: 90.
Request(dispatch, "Pending", new DateTime(2026, 3, 3), requested: 90m, currentNte: 600m, createdBy: "user-7"),
// Non-pending rows never add pending exposure.
Request(dispatch, "Approved", new DateTime(2026, 2, 1), decided: new DateTime(2026, 2, 2), requested: 500m, currentNte: 100m));
await context.SaveChangesAsync();
var service = NewService(context);
// Page 1 with a single row still reports the total across ALL pending rows.
var result = await service.ListAsync(UserWithRoles("Approver"), "Pending", null, 1, 1, CancellationToken.None);
result.Items.Should().HaveCount(1);
result.PendingExposureTotal.Should().Be(740m);
}
// --- Approved-on-WO exposure totals ---
[Fact]

View file

@ -431,6 +431,7 @@ public class WorkOrderBoardCancelServiceTests
public Task<(int TotalCount, IReadOnlyList<UpliftListItemData> Items)> GetPagedAsync(
string? status, int? tier, int page, int pageSize, CancellationToken cancellationToken) =>
throw new NotSupportedException();
public Task<decimal> GetPendingExposureAsync(CancellationToken cancellationToken) => throw new NotSupportedException();
public Task<IReadOnlyList<UpliftForDispatchData>> GetForDispatchAsync(int dispatchId, CancellationToken cancellationToken) => throw new NotSupportedException();
public Task<IReadOnlyList<UpliftForWorkOrderData>> GetForWorkOrderAsync(int workOrderId, CancellationToken cancellationToken) => throw new NotSupportedException();
public Task<DispatchUpliftRequest?> GetByIdAndWorkOrderAsync(int requestId, int workOrderId, CancellationToken cancellationToken) => throw new NotSupportedException();