mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 11:53:12 +00:00
fix(uplifts): pending exposure header sums the row deltas
The approvals header summed the whole RequestedNTE for work-order-path requests, while each Pending row shows RequestedNTE - CurrentNTE. When a work order already had an NTE the header overstated exposure by that NTE. The header now sums the same Delta the rows display, over the same rows the Pending tab lists (non-deleted request on a non-deleted dispatch). The unused duplicate aggregate is removed so one definition remains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
50e5553e57
commit
f40ad7c1ef
5 changed files with 40 additions and 22 deletions
|
|
@ -558,7 +558,7 @@ public sealed class UpliftQueueReadTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_PendingExposureTotal_SumsGrantedAmountsAcrossAllPendingRequests()
|
||||
public async Task List_PendingExposureTotal_SumsRowDeltasAcrossAllPendingRequests()
|
||||
{
|
||||
using var context = NewContext();
|
||||
var (vendor, workOrder) = await SeedWorkOrderAsync(context, "WO-A", "SITE-A", "HVAC");
|
||||
|
|
@ -566,11 +566,10 @@ public sealed class UpliftQueueReadTests
|
|||
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.
|
||||
// Each pending row contributes its displayed Delta (RequestedNTE - CurrentNTE),
|
||||
// whichever path created it: 400 - 100 = 300, 600 - 250 = 350, 90 - 600 = -510.
|
||||
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));
|
||||
|
|
@ -581,7 +580,37 @@ public sealed class UpliftQueueReadTests
|
|||
var result = await service.ListAsync(UserWithRoles("Approver"), "Pending", null, 1, 1, CancellationToken.None);
|
||||
|
||||
result.Items.Should().HaveCount(1);
|
||||
result.PendingExposureTotal.Should().Be(740m);
|
||||
result.PendingExposureTotal.Should().Be(140m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_PendingExposureTotal_EqualsSumOfPendingRowDeltas_WhenWorkOrderAlreadyHasNte()
|
||||
{
|
||||
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", "HVAC");
|
||||
var (vendorC, workOrderC) = await SeedWorkOrderAsync(context, "WO-C", "SITE-C", "HVAC");
|
||||
var dispatchA = await SeedDispatchAsync(context, vendorA, workOrderA, "DIS-A");
|
||||
var dispatchB = await SeedDispatchAsync(context, vendorB, workOrderB, "DIS-B");
|
||||
var deletedDispatch = await SeedDispatchAsync(context, vendorC, workOrderC, "DIS-C");
|
||||
deletedDispatch.IsDeleted = true;
|
||||
context.Users.Add(new ApplicationUser { Id = "user-7", FirstName = "Ada", LastName = "Smith" });
|
||||
await context.SaveChangesAsync();
|
||||
context.DispatchUpliftRequests.AddRange(
|
||||
// Raised from the work order, which already carries a 3,000 NTE.
|
||||
Request(dispatchA, "Pending", new DateTime(2026, 3, 1), requested: 5_000_000_000m, currentNte: 3_000m, createdBy: "user-7"),
|
||||
// Vendor-portal request on a dispatch with no NTE recorded yet.
|
||||
Request(dispatchB, "Pending", new DateTime(2026, 3, 2), requested: 333m),
|
||||
// A pending request on a deleted dispatch is not a queue row, so it adds nothing.
|
||||
Request(deletedDispatch, "Pending", new DateTime(2026, 3, 3), requested: 700m, currentNte: 100m));
|
||||
await context.SaveChangesAsync();
|
||||
var service = NewService(context);
|
||||
|
||||
var result = await service.ListAsync(UserWithRoles("Approver"), "Pending", null, 1, 25, CancellationToken.None);
|
||||
|
||||
result.Items.Should().HaveCount(2);
|
||||
result.PendingExposureTotal.Should().Be(4_999_997_333m);
|
||||
result.PendingExposureTotal.Should().Be(result.Items.Sum(i => i.Delta));
|
||||
}
|
||||
|
||||
// --- Approved-on-WO exposure totals ---
|
||||
|
|
|
|||
|
|
@ -128,16 +128,6 @@ namespace SeaHaven.DataServices.Implementation
|
|||
return (total, items);
|
||||
}
|
||||
|
||||
public Task<decimal> GetPendingExposureAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return _context.DispatchUpliftRequests
|
||||
.Where(u => (u.IsDeleted == null || u.IsDeleted == false)
|
||||
&& u.Status == "Pending")
|
||||
.SumAsync(u => u.createdby == null
|
||||
? u.RequestedNTE - (u.CurrentNTE ?? 0m)
|
||||
: u.RequestedNTE, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<UpliftForDispatchData>> GetForDispatchAsync(int dispatchId, CancellationToken cancellationToken)
|
||||
{
|
||||
return await (from u in _context.DispatchUpliftRequests
|
||||
|
|
@ -446,9 +436,10 @@ namespace SeaHaven.DataServices.Implementation
|
|||
return totals.Values.ToList();
|
||||
}
|
||||
|
||||
// SH-207: queue-wide pending exposure for the approvals header. Mirrors the
|
||||
// approved-exposure convention (sum of RequestedNTE) over every non-deleted
|
||||
// Pending request on a non-deleted dispatch, independent of page or filters.
|
||||
// Queue-wide pending exposure for the approvals header: the sum of the Delta each
|
||||
// Pending queue row displays (RequestedNTE - CurrentNTE), over the same rows the
|
||||
// Pending tab lists (non-deleted request on a non-deleted dispatch), independent
|
||||
// of page or filters.
|
||||
public async Task<decimal> GetPendingExposureTotalAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return await _context.DispatchUpliftRequests
|
||||
|
|
@ -456,7 +447,7 @@ namespace SeaHaven.DataServices.Implementation
|
|||
&& u.Status == "Pending"
|
||||
&& u.Dispatch != null
|
||||
&& (u.Dispatch.IsDeleted == null || u.Dispatch.IsDeleted == false))
|
||||
.SumAsync(u => (decimal?)u.RequestedNTE, cancellationToken) ?? 0m;
|
||||
.SumAsync(u => (decimal?)(u.RequestedNTE - (u.CurrentNTE ?? 0m)), cancellationToken) ?? 0m;
|
||||
}
|
||||
|
||||
public Task<List<DispatchUpliftRequest>> GetPendingForWorkOrderAsync(
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
{
|
||||
Task<(int TotalCount, IReadOnlyList<UpliftListItemData> Items)> GetPagedAsync(
|
||||
string? status, int? tier, int page, int pageSize, CancellationToken cancellationToken);
|
||||
Task<decimal> GetPendingExposureAsync(CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<UpliftForDispatchData>> GetForDispatchAsync(int dispatchId, CancellationToken cancellationToken);
|
||||
Task<IReadOnlyList<UpliftForWorkOrderData>> GetForWorkOrderAsync(int workOrderId, CancellationToken cancellationToken);
|
||||
Task<DispatchUpliftRequest?> GetByIdAndWorkOrderAsync(int requestId, int workOrderId, CancellationToken cancellationToken);
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ namespace SeaHaven.Services.Implementation
|
|||
public async Task<UpliftListResultDTO> ListAsync(ClaimsPrincipal user, string? status, int? tier, int page, int pageSize, CancellationToken cancellationToken)
|
||||
{
|
||||
var (total, items) = await _upliftData.GetPagedAsync(status, tier, page, pageSize, cancellationToken);
|
||||
var pendingExposureTotal = await _upliftData.GetPendingExposureAsync(cancellationToken);
|
||||
var pendingExposureTotal = await _upliftData.GetPendingExposureTotalAsync(cancellationToken);
|
||||
|
||||
var mapped = items.Select(r => new UpliftListItemDTO
|
||||
{
|
||||
|
|
|
|||
|
|
@ -447,7 +447,6 @@ 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();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue