mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
feat(uplifts): order the rejected queue by decision time and include legacy denied rows
This commit is contained in:
parent
e9a1d8f53f
commit
5353899418
2 changed files with 97 additions and 4 deletions
|
|
@ -164,6 +164,59 @@ public sealed class UpliftQueueReadTests
|
||||||
new DateTime(2026, 3, 10));
|
new DateTime(2026, 3, 10));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task List_RejectedStatus_OrdersMostRecentlyRejectedFirst()
|
||||||
|
{
|
||||||
|
using var context = NewContext();
|
||||||
|
var vendor = new Vendor { CompanyName = "Gateway", IsActive = true };
|
||||||
|
var workOrder = new WorkOrder { WorkerOrderTitle = "Repair" };
|
||||||
|
context.AddRange(vendor, workOrder);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
var dispatch = await SeedDispatchAsync(context, vendor, workOrder, "DIS-1");
|
||||||
|
// Request order (3/1, 3/2, 3/3) deliberately disagrees with decision order.
|
||||||
|
context.DispatchUpliftRequests.AddRange(
|
||||||
|
Request(dispatch, "Rejected", new DateTime(2026, 3, 1), decided: new DateTime(2026, 3, 10)),
|
||||||
|
Request(dispatch, "Rejected", new DateTime(2026, 3, 2), decided: new DateTime(2026, 3, 20)),
|
||||||
|
Request(dispatch, "Rejected", new DateTime(2026, 3, 3), decided: new DateTime(2026, 3, 15)));
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
var service = NewService(context);
|
||||||
|
|
||||||
|
var result = await service.ListAsync(UserWithRoles("Approver"), "Rejected", null, 1, 25, CancellationToken.None);
|
||||||
|
|
||||||
|
result.Items.Select(i => i.DecidedAt).Should().Equal(
|
||||||
|
new DateTime(2026, 3, 20),
|
||||||
|
new DateTime(2026, 3, 15),
|
||||||
|
new DateTime(2026, 3, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task List_RejectedStatus_ReturnsOnlyRejectedRequests_IncludingLegacyDenied()
|
||||||
|
{
|
||||||
|
using var context = NewContext();
|
||||||
|
var vendor = new Vendor { CompanyName = "Gateway", IsActive = true };
|
||||||
|
var workOrder = new WorkOrder { WorkerOrderTitle = "Repair" };
|
||||||
|
context.AddRange(vendor, workOrder);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
var dispatch = await SeedDispatchAsync(context, vendor, workOrder, "DIS-1");
|
||||||
|
context.DispatchUpliftRequests.AddRange(
|
||||||
|
Request(dispatch, "Pending", new DateTime(2026, 3, 1)),
|
||||||
|
Request(dispatch, "Approved", new DateTime(2026, 3, 2), decided: new DateTime(2026, 3, 3)),
|
||||||
|
Request(dispatch, "Revoked", new DateTime(2026, 3, 4), decided: new DateTime(2026, 3, 5)),
|
||||||
|
Request(dispatch, "Withdrawn", new DateTime(2026, 3, 6)),
|
||||||
|
Request(dispatch, "Rejected", new DateTime(2026, 3, 7), decided: new DateTime(2026, 3, 8)),
|
||||||
|
Request(dispatch, "Denied", new DateTime(2026, 2, 1), decided: new DateTime(2026, 2, 2)));
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
var service = NewService(context);
|
||||||
|
|
||||||
|
var result = await service.ListAsync(UserWithRoles("Approver"), "Rejected", null, 1, 25, CancellationToken.None);
|
||||||
|
|
||||||
|
result.Total.Should().Be(2);
|
||||||
|
result.Items.Select(i => i.Status).Should().Equal("Rejected", "Rejected");
|
||||||
|
result.Items.Select(i => i.DecidedAt).Should().Equal(
|
||||||
|
new DateTime(2026, 3, 8),
|
||||||
|
new DateTime(2026, 2, 2));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task List_WithoutStatusFilter_KeepsHistoricalNewestRequestFirstOrder()
|
public async Task List_WithoutStatusFilter_KeepsHistoricalNewestRequestFirstOrder()
|
||||||
{
|
{
|
||||||
|
|
@ -558,6 +611,40 @@ public sealed class UpliftQueueReadTests
|
||||||
result.Items.Single().DecidedByName.Should().Be("Grace Hopper");
|
result.Items.Single().DecidedByName.Should().Be("Grace Hopper");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task List_RejectedRow_CarriesRejecterRequesterDecisionTimeAndReason()
|
||||||
|
{
|
||||||
|
using var context = NewContext();
|
||||||
|
var (vendor, workOrder) = await SeedWorkOrderAsync(context, "WO-R", "SITE-R", "Plumbing");
|
||||||
|
var dispatch = await SeedDispatchAsync(context, vendor, workOrder, "DIS-R");
|
||||||
|
context.Users.Add(new ApplicationUser { Id = "user-7", FirstName = "Ada", LastName = "Lovelace" });
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
||||||
|
{
|
||||||
|
DispatchId = dispatch.Id,
|
||||||
|
Status = "Rejected",
|
||||||
|
CreatedDate = new DateTime(2026, 3, 1),
|
||||||
|
DecidedAt = new DateTime(2026, 3, 2, 16, 40, 0),
|
||||||
|
DecidedByUserId = "user-7",
|
||||||
|
DecisionNote = "Outside this work order's scope",
|
||||||
|
RequestedByVendorName = "Gateway",
|
||||||
|
RequiredTier = 1,
|
||||||
|
RequestedNTE = 250m,
|
||||||
|
NotificationStatus = "Pending"
|
||||||
|
});
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
var service = NewService(context);
|
||||||
|
|
||||||
|
var result = await service.ListAsync(UserWithRoles("Approver"), "Rejected", null, 1, 25, CancellationToken.None);
|
||||||
|
|
||||||
|
var row = result.Items.Single();
|
||||||
|
row.DecidedByName.Should().Be("Ada Lovelace");
|
||||||
|
row.RequestedByName.Should().Be("Gateway");
|
||||||
|
row.DecidedAt.Should().Be(new DateTime(2026, 3, 2, 16, 40, 0));
|
||||||
|
row.DecisionNote.Should().Be("Outside this work order's scope");
|
||||||
|
row.WorkOrderNumber.Should().Be("WO-R");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task List_PendingExposureTotal_SumsEachPendingRequestsIncreaseAcrossTheQueue()
|
public async Task List_PendingExposureTotal_SumsEachPendingRequestsIncreaseAcrossTheQueue()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
public class UpliftDataService : IUpliftDataService
|
public class UpliftDataService : IUpliftDataService
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<int, SemaphoreSlim> WorkOrderGates = new();
|
private static readonly ConcurrentDictionary<int, SemaphoreSlim> WorkOrderGates = new();
|
||||||
|
|
||||||
|
// The Rejected queue also surfaces the legacy "Denied" spelling, which reads as Rejected.
|
||||||
|
private static readonly string[] RejectedStatuses = { "Rejected", "Denied" };
|
||||||
private readonly ApplicationDbContext _context;
|
private readonly ApplicationDbContext _context;
|
||||||
|
|
||||||
public UpliftDataService(ApplicationDbContext context)
|
public UpliftDataService(ApplicationDbContext context)
|
||||||
|
|
@ -46,7 +49,9 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
&& (d.IsDeleted == null || d.IsDeleted == false)
|
&& (d.IsDeleted == null || d.IsDeleted == false)
|
||||||
select new { u, d, v, ev, effectiveWorkOrderId, workOrder, reqUser, decUser };
|
select new { u, d, v, ev, effectiveWorkOrderId, workOrder, reqUser, decUser };
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(status))
|
if (string.Equals(status, "Rejected", StringComparison.Ordinal))
|
||||||
|
query = query.Where(x => RejectedStatuses.Contains(x.u.Status));
|
||||||
|
else if (!string.IsNullOrWhiteSpace(status))
|
||||||
query = query.Where(x => x.u.Status == status);
|
query = query.Where(x => x.u.Status == status);
|
||||||
if (tier.HasValue)
|
if (tier.HasValue)
|
||||||
query = query.Where(x => x.u.RequiredTier == tier.Value);
|
query = query.Where(x => x.u.RequiredTier == tier.Value);
|
||||||
|
|
@ -54,12 +59,13 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
var total = await query.CountAsync(cancellationToken);
|
var total = await query.CountAsync(cancellationToken);
|
||||||
|
|
||||||
// Approval queue read contract: the actionable queue (Pending) surfaces the
|
// Approval queue read contract: the actionable queue (Pending) surfaces the
|
||||||
// oldest request first; the decision log (Approved) surfaces the most
|
// oldest request first; the decision logs (Approved, Rejected) surface the
|
||||||
// recently decided first. Every other read keeps the historical
|
// most recently decided first. Every other read keeps the historical
|
||||||
// newest-request-first order. Id is the deterministic tiebreaker.
|
// newest-request-first order. Id is the deterministic tiebreaker.
|
||||||
if (string.Equals(status, "Pending", StringComparison.Ordinal))
|
if (string.Equals(status, "Pending", StringComparison.Ordinal))
|
||||||
query = query.OrderBy(x => x.u.CreatedDate).ThenBy(x => x.u.Id);
|
query = query.OrderBy(x => x.u.CreatedDate).ThenBy(x => x.u.Id);
|
||||||
else if (string.Equals(status, "Approved", StringComparison.Ordinal))
|
else if (string.Equals(status, "Approved", StringComparison.Ordinal)
|
||||||
|
|| string.Equals(status, "Rejected", StringComparison.Ordinal))
|
||||||
query = query.OrderByDescending(x => x.u.DecidedAt).ThenByDescending(x => x.u.Id);
|
query = query.OrderByDescending(x => x.u.DecidedAt).ThenByDescending(x => x.u.Id);
|
||||||
else
|
else
|
||||||
query = query.OrderByDescending(x => x.u.CreatedDate).ThenByDescending(x => x.u.Id);
|
query = query.OrderByDescending(x => x.u.CreatedDate).ThenByDescending(x => x.u.Id);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue