shoc-backend/SeaHavenIndustries.Tests/WorkOrderUpliftLiveStatusTests.cs
Alexandre Brandizzi 92cd44c481 fix(work-orders): count only live uplifts for Has uplift and the Uplift column
A work order whose uplifts were all cancelled, withdrawn, expired or revoked no
longer matches the advanced-search Has uplift filter, and the board Uplift
column no longer reports it as having an uplift or shows the dead one as its
primary status. One shared live-status list backs both queries. Explicit
cancelled/revoked sub-filter values still find those work orders.
2026-09-25 17:51:47 -03:00

266 lines
10 KiB
C#

using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Helpers;
using SeaHaven.DataServices.Implementation;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Helpers;
using SeaHaven.Services.Implementation;
namespace SeaHavenIndustries.Tests;
/// <summary>
/// "Has uplift" (advanced filter) and the board Uplift column count only live uplifts:
/// a work order whose uplifts were all cancelled, withdrawn, expired or revoked has none.
/// </summary>
public class WorkOrderUpliftLiveStatusTests
{
private static readonly DateOnly WeekStart = new(2026, 6, 22);
private const int WithdrawnOnly = 1;
private const int LegacyCancelledOnly = 2;
private const int RevokedOnlyViaLink = 3;
private const int RevokedPlusPending = 4;
private const int RejectedOnly = 5;
private const int ExpiredOnly = 6;
private const int NoUplift = 7;
private const int NewerRevokedOlderAutoApproved = 8;
private static readonly int[] LiveUpliftWorkOrders =
{
RevokedPlusPending,
RejectedOnly,
NewerRevokedOlderAutoApproved,
};
private static ApplicationDbContext CreateContext()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new ApplicationDbContext(options);
}
private static async Task<ApplicationDbContext> SeedAsync()
{
var context = CreateContext();
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Acme HVAC" });
for (var id = WithdrawnOnly; id <= NewerRevokedOlderAutoApproved; id++)
{
// The revoked-only work order reaches its uplift through a multi-WO dispatch link.
var linkedOnly = id == RevokedOnlyViaLink;
context.Dispatches.Add(new Dispatch
{
Id = 10 + id,
VendorId = 1,
WorkOrderId = linkedOnly ? null : id,
});
if (linkedOnly)
context.DispatchWorkOrders.Add(new DispatchWorkOrder { DispatchId = 10 + id, WorkOrderId = id });
context.workOrders.Add(new WorkOrder
{
Id = id,
InternalWONumber = $"1000000000{id}",
ScheduledDate = new DateTime(2026, 6, 23),
LifecycleStatus = LifecycleStatus.Scheduled,
PrimaryDispatchId = linkedOnly ? null : 10 + id,
});
}
var older = new DateTime(2026, 6, 20, 9, 0, 0, DateTimeKind.Utc);
var newer = older.AddHours(2);
context.DispatchUpliftRequests.AddRange(
Uplift(101, WithdrawnOnly, "Withdrawn", older),
Uplift(102, LegacyCancelledOnly, "Cancelled", older),
Uplift(103, RevokedOnlyViaLink, "Revoked", older),
Uplift(104, RevokedPlusPending, "Revoked", older),
Uplift(105, RevokedPlusPending, "Pending", newer),
Uplift(106, RejectedOnly, "Rejected", older),
Uplift(107, ExpiredOnly, "Expired", older),
Uplift(108, NewerRevokedOlderAutoApproved, "NoApprovalRequired", older),
Uplift(109, NewerRevokedOlderAutoApproved, "Revoked", newer));
await context.SaveChangesAsync();
return context;
}
private static DispatchUpliftRequest Uplift(int id, int workOrderId, string status, DateTime createdDate) => new()
{
Id = id,
DispatchId = 10 + workOrderId,
RequestedNTE = 1000m + id,
Status = status,
RequiredTier = 1,
NotificationStatus = "Pending",
CreatedDate = createdDate,
};
private static async Task<int[]> SearchIdsAsync(
ApplicationDbContext context,
bool hasUplift,
List<string>? upliftStatuses = null)
{
var service = new WorkOrderAdvancedSearchService(
new WorkOrderAdvancedSearchDataService(context),
WorkOrderAccountTestHelpers.Resolver(context));
var result = await service.SearchAsync(
new WorkOrderAdvancedSearchQueryDto
{
DatePreset = WorkOrderAdvancedSearchDatePreset.Custom,
DateFrom = WeekStart,
DateTo = WeekStart.AddDays(6),
HasUplift = hasUplift,
UpliftStatuses = upliftStatuses,
PageSize = 50,
},
WorkOrderAccountTestHelpers.OrgWideAdmin(),
null);
return result.Items.Select(i => i.Id).OrderBy(id => id).ToArray();
}
[Fact]
public async Task HasUplift_ExcludesWorkOrdersWhoseOnlyUpliftsAreCancelledOrRevoked()
{
await using var context = await SeedAsync();
var ids = await SearchIdsAsync(context, hasUplift: true);
Assert.Equal(LiveUpliftWorkOrders, ids);
Assert.DoesNotContain(WithdrawnOnly, ids);
Assert.DoesNotContain(LegacyCancelledOnly, ids);
Assert.DoesNotContain(RevokedOnlyViaLink, ids);
Assert.DoesNotContain(ExpiredOnly, ids);
}
[Fact]
public async Task HasUplift_IncludesWorkOrderWithRevokedAndPendingUplift()
{
await using var context = await SeedAsync();
Assert.Contains(RevokedPlusPending, await SearchIdsAsync(context, hasUplift: true));
Assert.Equal(
new[] { RevokedPlusPending },
await SearchIdsAsync(context, hasUplift: true, new List<string> { "pending" }));
}
[Fact]
public async Task HasUplift_RejectedUpliftStillCounts()
{
await using var context = await SeedAsync();
Assert.Equal(
new[] { RejectedOnly },
await SearchIdsAsync(context, hasUplift: true, new List<string> { "rejected" }));
}
[Fact]
public async Task HasUplift_ExplicitCancelledStatus_FindsCancelledWithdrawnAndExpiredUplifts()
{
await using var context = await SeedAsync();
var ids = await SearchIdsAsync(context, hasUplift: true, new List<string> { "cancelled" });
Assert.Equal(new[] { WithdrawnOnly, LegacyCancelledOnly, ExpiredOnly }, ids);
}
[Fact]
public async Task HasUplift_ExplicitRevokedStatus_FindsRevokedUpliftsOnPrimaryAndLinkedDispatches()
{
await using var context = await SeedAsync();
var ids = await SearchIdsAsync(context, hasUplift: true, new List<string> { "revoked" });
Assert.Equal(new[] { RevokedOnlyViaLink, RevokedPlusPending, NewerRevokedOlderAutoApproved }, ids);
}
[Fact]
public async Task BoardUpliftColumn_AgreesWithHasUpliftFilter()
{
await using var context = await SeedAsync();
var filtered = await SearchIdsAsync(context, hasUplift: true);
var boardService = new WorkOrderBoardService(
new WorkOrderBoardDataService(context),
WorkOrderAccountTestHelpers.Resolver(context));
var board = await boardService.GetBoardAsync(
new WorkOrderBoardQueryDto { WeekStart = WeekStart },
WorkOrderAccountTestHelpers.OrgWideAdmin(),
null);
Assert.Equal(8, board.Scheduled.Count);
foreach (var row in board.Scheduled)
{
Assert.Equal(filtered.Contains(row.Id), row.UpliftSummary!.HasUplift);
}
var searchService = new WorkOrderAdvancedSearchService(
new WorkOrderAdvancedSearchDataService(context),
WorkOrderAccountTestHelpers.Resolver(context));
var unfiltered = await searchService.SearchAsync(
new WorkOrderAdvancedSearchQueryDto
{
DatePreset = WorkOrderAdvancedSearchDatePreset.Custom,
DateFrom = WeekStart,
DateTo = WeekStart.AddDays(6),
PageSize = 50,
},
WorkOrderAccountTestHelpers.OrgWideAdmin(),
null);
Assert.Equal(8, unfiltered.Items.Count());
foreach (var item in unfiltered.Items)
{
Assert.Equal(filtered.Contains(item.Id), item.UpliftSummary!.HasUplift);
}
}
[Fact]
public async Task BoardUpliftColumn_ShowsNewestLiveUpliftAndNoStatusForNonLiveOnly()
{
await using var context = await SeedAsync();
var boardService = new WorkOrderBoardService(
new WorkOrderBoardDataService(context),
WorkOrderAccountTestHelpers.Resolver(context));
var board = await boardService.GetBoardAsync(
new WorkOrderBoardQueryDto { WeekStart = WeekStart },
WorkOrderAccountTestHelpers.OrgWideAdmin(),
null);
var byId = board.Scheduled.ToDictionary(r => r.Id, r => r.UpliftSummary!);
foreach (var id in new[] { WithdrawnOnly, LegacyCancelledOnly, RevokedOnlyViaLink, ExpiredOnly, NoUplift })
{
Assert.False(byId[id].HasUplift);
Assert.Null(byId[id].PrimaryStatus);
Assert.Null(byId[id].Amount);
}
Assert.Equal("pending", byId[RevokedPlusPending].PrimaryStatus);
Assert.Equal(1, byId[RevokedPlusPending].PendingCount);
Assert.Equal("rejected", byId[RejectedOnly].PrimaryStatus);
Assert.Equal("auto_approved", byId[NewerRevokedOlderAutoApproved].PrimaryStatus);
Assert.Equal(1108m, byId[NewerRevokedOlderAutoApproved].Amount);
}
[Theory]
[InlineData(UpliftStatus.Pending)]
[InlineData(UpliftStatus.Approved)]
[InlineData(UpliftStatus.Rejected)]
[InlineData(UpliftStatus.ChangesRequested)]
[InlineData(UpliftStatus.Withdrawn)]
[InlineData(UpliftStatus.Expired)]
[InlineData(UpliftStatus.NoApprovalRequired)]
[InlineData(UpliftStatus.Revoked)]
[InlineData(UpliftStatus.LegacyDenied)]
[InlineData(UpliftStatus.LegacyCancelled)]
public void LiveUpliftStatus_MatchesFrontendCancelledAndRevokedStatuses(string storedStatus)
{
var frontendStatus = WorkOrderUpliftContractMapper.ToFrontendStatus(storedStatus);
var expectedLive = frontendStatus is not ("cancelled" or "revoked");
Assert.Equal(expectedLive, LiveUpliftStatus.IsLive(storedStatus));
}
}