mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +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));
|
||||
}
|
||||
|
||||
[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]
|
||||
public async Task List_WithoutStatusFilter_KeepsHistoricalNewestRequestFirstOrder()
|
||||
{
|
||||
|
|
@ -558,6 +611,40 @@ public sealed class UpliftQueueReadTests
|
|||
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]
|
||||
public async Task List_PendingExposureTotal_SumsEachPendingRequestsIncreaseAcrossTheQueue()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ namespace SeaHaven.DataServices.Implementation
|
|||
public class UpliftDataService : IUpliftDataService
|
||||
{
|
||||
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;
|
||||
|
||||
public UpliftDataService(ApplicationDbContext context)
|
||||
|
|
@ -46,7 +49,9 @@ namespace SeaHaven.DataServices.Implementation
|
|||
&& (d.IsDeleted == null || d.IsDeleted == false)
|
||||
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);
|
||||
if (tier.HasValue)
|
||||
query = query.Where(x => x.u.RequiredTier == tier.Value);
|
||||
|
|
@ -54,12 +59,13 @@ namespace SeaHaven.DataServices.Implementation
|
|||
var total = await query.CountAsync(cancellationToken);
|
||||
|
||||
// Approval queue read contract: the actionable queue (Pending) surfaces the
|
||||
// oldest request first; the decision log (Approved) surfaces the most
|
||||
// recently decided first. Every other read keeps the historical
|
||||
// oldest request first; the decision logs (Approved, Rejected) surface the
|
||||
// most recently decided first. Every other read keeps the historical
|
||||
// newest-request-first order. Id is the deterministic tiebreaker.
|
||||
if (string.Equals(status, "Pending", StringComparison.Ordinal))
|
||||
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);
|
||||
else
|
||||
query = query.OrderByDescending(x => x.u.CreatedDate).ThenByDescending(x => x.u.Id);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue