shoc-backend/SeaHavenIndustries.Tests/WorkOrderUpliftLiveStatusRelationalTests.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

162 lines
7.3 KiB
C#

using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Implementation;
using SeaHaven.DataServices.Interfaces;
namespace SeaHavenIndustries.Tests;
// Relational (SQLite) coverage for the live-uplift predicate. The in-memory provider
// evaluates LINQ in memory and cannot prove the "Has uplift" filter or the board uplift
// aggregate translate to SQL, so this runs the advanced search against a real provider,
// scoped to one account, with a live uplift on another account's work order.
public sealed class WorkOrderUpliftLiveStatusRelationalTests
{
[Fact]
public async Task AdvancedSearch_HasUplift_CountsOnlyLiveUpliftsWithinAccount()
{
await using var connection = new SqliteConnection("DataSource=:memory:");
await connection.OpenAsync();
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite(connection)
.Options;
await using var context = new SqliteLiveUpliftTestDbContext(options);
await context.Database.EnsureCreatedAsync();
var accountA = new Accounts { Name = "Account A" };
var accountB = new Accounts { Name = "Account B" };
var vendor = new Vendor { CompanyName = "Acme HVAC", IsActive = true };
context.AddRange(accountA, accountB, vendor);
await context.SaveChangesAsync();
var revokedOnly = NewWorkOrder("WO-REVOKED", accountA.Id);
var cancelledOnly = NewWorkOrder("WO-CANCELLED", accountA.Id);
var revokedPlusPending = NewWorkOrder("WO-MIXED", accountA.Id);
var otherAccountPending = NewWorkOrder("WO-OTHER", accountB.Id);
context.AddRange(revokedOnly, cancelledOnly, revokedPlusPending, otherAccountPending);
await context.SaveChangesAsync();
var revokedDispatch = new Dispatch { VendorId = vendor.Id, DispatchNumber = "DIS-R", Status = "Scheduled" };
var cancelledDispatch = new Dispatch { VendorId = vendor.Id, WorkOrderId = cancelledOnly.Id, DispatchNumber = "DIS-C", Status = "Scheduled" };
var mixedDispatch = new Dispatch { VendorId = vendor.Id, WorkOrderId = revokedPlusPending.Id, DispatchNumber = "DIS-M", Status = "Scheduled" };
var mixedPendingDispatch = new Dispatch { VendorId = vendor.Id, WorkOrderId = revokedPlusPending.Id, DispatchNumber = "DIS-M2", Status = "Scheduled" };
var otherDispatch = new Dispatch { VendorId = vendor.Id, WorkOrderId = otherAccountPending.Id, DispatchNumber = "DIS-O", Status = "Scheduled" };
context.Dispatches.AddRange(revokedDispatch, cancelledDispatch, mixedDispatch, mixedPendingDispatch, otherDispatch);
await context.SaveChangesAsync();
// The revoked-only work order reaches its uplift through a multi-WO dispatch link.
context.DispatchWorkOrders.Add(new DispatchWorkOrder { DispatchId = revokedDispatch.Id, WorkOrderId = revokedOnly.Id });
context.DispatchUpliftRequests.AddRange(
NewUplift(revokedDispatch.Id, "Revoked", new DateTime(2026, 6, 20, 9, 0, 0)),
NewUplift(cancelledDispatch.Id, "Withdrawn", new DateTime(2026, 6, 20, 9, 0, 0)),
NewUplift(mixedDispatch.Id, "Revoked", new DateTime(2026, 6, 20, 9, 0, 0)),
NewUplift(mixedPendingDispatch.Id, "Pending", new DateTime(2026, 6, 20, 11, 0, 0)),
NewUplift(otherDispatch.Id, "Pending", new DateTime(2026, 6, 20, 9, 0, 0)));
await context.SaveChangesAsync();
var data = new WorkOrderAdvancedSearchDataService(context);
var hasUplift = await data.SearchAsync(Query(accountA.Id, hasUplift: true, upliftStatuses: null));
var row = Assert.Single(hasUplift.Rows);
Assert.Equal(revokedPlusPending.Id, row.Id);
Assert.True(row.HasUplift);
Assert.Equal("Pending", row.PrimaryUpliftStatus);
var revoked = await data.SearchAsync(Query(accountA.Id, hasUplift: true, upliftStatuses: new[] { "revoked" }));
Assert.Equal(
new[] { revokedOnly.Id, revokedPlusPending.Id }.OrderBy(id => id),
revoked.Rows.Select(r => r.Id).OrderBy(id => id));
var all = await data.SearchAsync(Query(accountA.Id, hasUplift: false, upliftStatuses: null));
Assert.Equal(3, all.TotalCount);
Assert.DoesNotContain(all.Rows, r => r.Id == otherAccountPending.Id);
Assert.False(all.Rows.Single(r => r.Id == revokedOnly.Id).HasUplift);
Assert.Null(all.Rows.Single(r => r.Id == revokedOnly.Id).PrimaryUpliftStatus);
Assert.False(all.Rows.Single(r => r.Id == cancelledOnly.Id).HasUplift);
Assert.True(all.Rows.Single(r => r.Id == revokedPlusPending.Id).HasUplift);
}
private static WorkOrder NewWorkOrder(string number, int accountId) => new()
{
InternalWONumber = number,
WorkerOrderTitle = "Repair",
AccountId = accountId,
ScheduledDate = new DateTime(2026, 6, 23, 8, 0, 0),
LifecycleStatus = LifecycleStatus.Scheduled,
};
private static DispatchUpliftRequest NewUplift(int dispatchId, string status, DateTime createdDate) => new()
{
DispatchId = dispatchId,
Status = status,
CreatedDate = createdDate,
RequiredTier = 1,
RequestedNTE = 750m,
NotificationStatus = "Pending",
};
private static WorkOrderAdvancedSearchQuery Query(
int accountId,
bool hasUplift,
IReadOnlyList<string>? upliftStatuses) => new(
Search: null,
DateFrom: new DateOnly(2026, 6, 22),
DateTo: new DateOnly(2026, 6, 28),
UnscheduledOnly: false,
Sites: null,
Regions: null,
Types: null,
Overdue: false,
Dispatchers: null,
Statuses: null,
PmTypes: null,
VendorIds: null,
DocStatuses: null,
Severities: null,
Rescheduled: false,
CarriedOver: false,
AddOn: false,
AvetaOnly: false,
FlagColors: null,
InternalOnly: false,
HasUplift: hasUplift,
UpliftStatuses: upliftStatuses,
MyWorkOrders: false,
CurrentUserId: null,
Page: 0,
PageSize: 50,
SortBy: "scheduledDate",
SortDir: "asc",
AccountId: accountId);
private sealed class SqliteLiveUpliftTestDbContext : ApplicationDbContext
{
public SqliteLiveUpliftTestDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// SQL Server filtered index syntax is invalid on SQLite.
foreach (var index in builder.Model.GetEntityTypes().SelectMany(e => e.GetIndexes()))
{
if (index.GetFilter() != null)
index.SetFilter(null);
}
// SQLite has no rowversion type; treat as plain nullable blobs.
foreach (var entityType in new[] { typeof(WorkOrder), typeof(Dispatch) })
{
var property = builder.Entity(entityType).Property("RowVersion").Metadata;
property.ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.Never;
property.IsConcurrencyToken = false;
}
}
}
}