mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 08:23:12 +00:00
fix(uplifts): correct queue exposure projections (SH-210)
This commit is contained in:
parent
d366f319e9
commit
316d1e6a63
2 changed files with 74 additions and 4 deletions
|
|
@ -86,6 +86,7 @@ public sealed class UpliftQueueReadTests
|
|||
var vendor = new Vendor { CompanyName = vendorName, IsActive = true };
|
||||
var workOrder = new WorkOrder
|
||||
{
|
||||
InternalWONumber = number,
|
||||
WorkerOrderNumber = number,
|
||||
SiteCode = site,
|
||||
Service = service,
|
||||
|
|
@ -280,6 +281,35 @@ public sealed class UpliftQueueReadTests
|
|||
item.Status.Should().Be("Pending");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_WorkOrderNumber_RendersInternalShNumber_NotTheCrmExternalId()
|
||||
{
|
||||
using var context = NewContext();
|
||||
var vendor = new Vendor { CompanyName = "Gateway", IsActive = true };
|
||||
// Synced work orders carry the CRM external id in WorkerOrderNumber; the SH
|
||||
// display number the board renders is stamped on InternalWONumber.
|
||||
var workOrder = new WorkOrder
|
||||
{
|
||||
InternalWONumber = "10000000001",
|
||||
WorkerOrderNumber = "CRM-EXT-77",
|
||||
SiteCode = "SITE-EAST",
|
||||
Service = "HVAC",
|
||||
WorkerOrderTitle = "Repair"
|
||||
};
|
||||
context.AddRange(vendor, workOrder);
|
||||
await context.SaveChangesAsync();
|
||||
var dispatch = await SeedDispatchAsync(context, vendor, workOrder, "DIS-1");
|
||||
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);
|
||||
|
||||
var item = result.Items.Should().ContainSingle().Subject;
|
||||
item.WorkOrderNumber.Should().Be("10000000001");
|
||||
item.WorkOrderNumber.Should().NotBe("CRM-EXT-77");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_RequesterLabelFallsBackToCreatingUserName()
|
||||
{
|
||||
|
|
@ -353,7 +383,9 @@ public sealed class UpliftQueueReadTests
|
|||
var dispatchA = await SeedDispatchAsync(context, vendorA, workOrderA, "DIS-A");
|
||||
var dispatchB = await SeedDispatchAsync(context, vendorB, workOrderB, "DIS-B");
|
||||
context.DispatchUpliftRequests.AddRange(
|
||||
Request(dispatchA, "Approved", new DateTime(2026, 3, 1), decided: new DateTime(2026, 3, 2), requested: 300m),
|
||||
// Vendor-portal row (createdby null): stores the requested new NTE total,
|
||||
// so the granted amount is 400 - 100 = 300.
|
||||
Request(dispatchA, "Approved", new DateTime(2026, 3, 1), decided: new DateTime(2026, 3, 2), requested: 400m, currentNte: 100m),
|
||||
new DispatchUpliftRequest
|
||||
{
|
||||
DispatchId = dispatchA.Id,
|
||||
|
|
@ -390,7 +422,7 @@ public sealed class UpliftQueueReadTests
|
|||
RequiredTier = 1,
|
||||
RequestedNTE = 999m
|
||||
},
|
||||
Request(dispatchB, "Approved", new DateTime(2026, 3, 5), decided: new DateTime(2026, 3, 6), requested: 75m));
|
||||
Request(dispatchB, "Approved", new DateTime(2026, 3, 5), decided: new DateTime(2026, 3, 6), requested: 75m, currentNte: 0m));
|
||||
await context.SaveChangesAsync();
|
||||
var service = NewService(context);
|
||||
|
||||
|
|
@ -440,6 +472,32 @@ public sealed class UpliftQueueReadTests
|
|||
total.AutoApprovedTotal.Should().Be(0m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Exposure_AdminApprovedSumsGrantedAmountsPerCreationPath()
|
||||
{
|
||||
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.DispatchUpliftRequests.AddRange(
|
||||
// Vendor-portal rows (createdby null) store the requested new NTE total:
|
||||
// 1000 -> 1500 grants 500, then 1500 -> 1800 grants 300.
|
||||
Request(dispatch, "Approved", new DateTime(2026, 3, 1), decided: new DateTime(2026, 3, 2), requested: 1500m, currentNte: 1000m),
|
||||
Request(dispatch, "Approved", new DateTime(2026, 3, 3), decided: new DateTime(2026, 3, 4), requested: 1800m, currentNte: 1500m),
|
||||
// Work-order-path rows (createdby set) store the granted increment: 200.
|
||||
Request(dispatch, "Approved", new DateTime(2026, 3, 5), decided: new DateTime(2026, 3, 6), requested: 200m, currentNte: 1800m, createdBy: "user-7"));
|
||||
await context.SaveChangesAsync();
|
||||
var data = new UpliftDataService(context);
|
||||
|
||||
var exposure = await data.GetApprovedExposureForWorkOrdersAsync(
|
||||
new[] { workOrder.Id }, CancellationToken.None);
|
||||
|
||||
// Granted exposure is 500 + 300 + 200; summing raw RequestedNTE would
|
||||
// double-count whole NTE totals and report 3500.
|
||||
var total = exposure.Should().ContainSingle(e => e.WorkOrderId == workOrder.Id).Subject;
|
||||
total.AdminApprovedTotal.Should().Be(1000m);
|
||||
total.AutoApprovedTotal.Should().Be(0m);
|
||||
}
|
||||
|
||||
// --- Read authorization (tier roles) ---
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -61,7 +61,10 @@ namespace SeaHaven.DataServices.Implementation
|
|||
PONumber = x.d.PONumber,
|
||||
WorkOrderId = x.d.WorkOrderId,
|
||||
VendorCompanyName = x.v != null ? x.v.CompanyName : x.u.RequestedByVendorName,
|
||||
WorkOrderNumber = x.wo != null ? x.wo.WorkerOrderNumber : null,
|
||||
// The queue renders the internal display number (InternalWONumber, what the
|
||||
// board shows); WorkerOrderNumber holds the CRM external id on synced
|
||||
// work orders and must not leak into the queue display.
|
||||
WorkOrderNumber = x.wo != null ? x.wo.InternalWONumber : null,
|
||||
WorkOrderSiteCode = x.wo != null ? x.wo.SiteCode : null,
|
||||
WorkOrderService = x.wo != null ? x.wo.Service : null,
|
||||
RequestedByVendorName = x.u.RequestedByVendorName,
|
||||
|
|
@ -339,8 +342,17 @@ namespace SeaHaven.DataServices.Implementation
|
|||
DispatchId = g.Key,
|
||||
AutoApproved = g.Where(x => x.Status == "NoApprovalRequired")
|
||||
.Sum(x => (decimal?)x.RequestedNTE),
|
||||
// The two creation paths store different meanings in RequestedNTE.
|
||||
// Vendor-portal rows store the requested new NTE total, so the
|
||||
// granted amount is RequestedNTE - CurrentNTE; work-order-path rows
|
||||
// store the granted increment directly. Vendor sessions have no
|
||||
// identity user, so createdby is null only on vendor-portal rows. Summing
|
||||
// granted amounts keeps sequential approvals from double-counting whole
|
||||
// NTE totals.
|
||||
AdminApproved = g.Where(x => x.Status == "Approved")
|
||||
.Sum(x => (decimal?)x.RequestedNTE)
|
||||
.Sum(x => (decimal?)(x.createdby == null
|
||||
? x.RequestedNTE - (x.CurrentNTE ?? 0m)
|
||||
: x.RequestedNTE))
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue