mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
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.
162 lines
7.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
}
|