mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 10:43:13 +00:00
The uplift detail modal needs the work order's assigned dispatcher, the requesting vendor's technician and the scheduled date. They now resolve from the same effective work order and vendor as the existing queue row, so the modal no longer depends on a separate work-order fetch that account-scoped staff cannot read.
195 lines
7.8 KiB
C#
195 lines
7.8 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using Microsoft.Data.Sqlite;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using SeaHaven.DataServices.Implementation;
|
|
|
|
namespace SeaHavenIndustries.Tests;
|
|
|
|
// Relational (SQLite) coverage for the uplift queue read. The in-memory provider
|
|
// evaluates LINQ in memory and cannot prove the query translates to SQL, so this
|
|
// executes GetPagedAsync against a real relational provider and asserts a dispatch
|
|
// linked only through DispatchWorkOrders still resolves its work-order context and
|
|
// closed flag through the effective primary-plus-linked work order.
|
|
public sealed class UpliftQueueReadRelationalTests
|
|
{
|
|
[Fact]
|
|
public async Task GetPagedAsync_ResolvesWorkOrder_ForDispatchLinkedOnlyThroughDispatchWorkOrders()
|
|
{
|
|
await using var connection = new SqliteConnection("DataSource=:memory:");
|
|
await connection.OpenAsync();
|
|
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseSqlite(connection)
|
|
.Options;
|
|
|
|
await using var context = new SqliteUpliftTestDbContext(options);
|
|
await context.Database.EnsureCreatedAsync();
|
|
|
|
var vendor = new Vendor { CompanyName = "Gateway", IsActive = true };
|
|
var workOrder = new WorkOrder
|
|
{
|
|
// The queue renders the internal display number; WorkerOrderNumber holds the
|
|
// CRM external id and must not surface here.
|
|
InternalWONumber = "WO-LINKED",
|
|
WorkerOrderNumber = "CRM-EXT-LINKED",
|
|
SiteCode = "SITE-Z",
|
|
Service = "Electrical",
|
|
WorkerOrderTitle = "Repair",
|
|
LifecycleStatus = LifecycleStatus.Completed
|
|
};
|
|
context.AddRange(vendor, workOrder);
|
|
await context.SaveChangesAsync();
|
|
|
|
var dispatch = new Dispatch
|
|
{
|
|
VendorId = vendor.Id,
|
|
DispatchNumber = "DIS-LINKED",
|
|
Status = "Completed"
|
|
};
|
|
context.Dispatches.Add(dispatch);
|
|
await context.SaveChangesAsync();
|
|
|
|
context.DispatchWorkOrders.Add(new DispatchWorkOrder
|
|
{
|
|
DispatchId = dispatch.Id,
|
|
WorkOrderId = workOrder.Id
|
|
});
|
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
|
{
|
|
DispatchId = dispatch.Id,
|
|
Status = "Approved",
|
|
CreatedDate = new DateTime(2026, 3, 1),
|
|
DecidedAt = new DateTime(2026, 3, 2),
|
|
RequiredTier = 1,
|
|
RequestedNTE = 90m,
|
|
NotificationStatus = "Pending"
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var data = new UpliftDataService(context);
|
|
|
|
var (total, items) = await data.GetPagedAsync("Approved", null, 1, 25, CancellationToken.None);
|
|
|
|
Assert.Equal(1, total);
|
|
var item = Assert.Single(items);
|
|
Assert.Equal(workOrder.Id, item.WorkOrderId);
|
|
Assert.Equal("WO-LINKED", item.WorkOrderNumber);
|
|
Assert.Equal("SITE-Z", item.WorkOrderSiteCode);
|
|
Assert.Equal("Electrical", item.WorkOrderService);
|
|
Assert.True(item.WorkOrderClosed);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetPagedAsync_ResolvesDetailModalContext_PerAccountWorkOrder()
|
|
{
|
|
// SH-209: dispatcher (work-order assignee navigation), technician (vendor
|
|
// contact) and scheduled date must translate to SQL through the effective
|
|
// work-order subquery, and each row resolves them from its own account's
|
|
// work order only.
|
|
await using var connection = new SqliteConnection("DataSource=:memory:");
|
|
await connection.OpenAsync();
|
|
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseSqlite(connection)
|
|
.Options;
|
|
|
|
await using var context = new SqliteUpliftTestDbContext(options);
|
|
await context.Database.EnsureCreatedAsync();
|
|
|
|
var accountA = new Accounts { Name = "Account A" };
|
|
var accountB = new Accounts { Name = "Account B" };
|
|
var dispatcherA = new ApplicationUser { Id = "dispatcher-a", FirstName = "Ann", LastName = "Alpha" };
|
|
var vendorA = new Vendor { CompanyName = "Vendor A", ContactName = "Tech A", IsActive = true };
|
|
var vendorB = new Vendor { CompanyName = "Vendor B", ContactName = "Tech B", IsActive = true };
|
|
context.AddRange(accountA, accountB, dispatcherA, vendorA, vendorB);
|
|
await context.SaveChangesAsync();
|
|
|
|
var workOrderA = new WorkOrder
|
|
{
|
|
InternalWONumber = "WO-A",
|
|
WorkerOrderTitle = "Repair",
|
|
AccountId = accountA.Id,
|
|
AssignTo = dispatcherA.Id,
|
|
ScheduledDate = new DateTime(2026, 5, 1, 8, 0, 0)
|
|
};
|
|
var workOrderB = new WorkOrder
|
|
{
|
|
InternalWONumber = "WO-B",
|
|
WorkerOrderTitle = "Repair",
|
|
AccountId = accountB.Id
|
|
};
|
|
context.AddRange(workOrderA, workOrderB);
|
|
await context.SaveChangesAsync();
|
|
|
|
var dispatchA = new Dispatch { VendorId = vendorA.Id, WorkOrderId = workOrderA.Id, DispatchNumber = "DIS-A", Status = "Scheduled" };
|
|
var dispatchB = new Dispatch { VendorId = vendorB.Id, WorkOrderId = workOrderB.Id, DispatchNumber = "DIS-B", Status = "Scheduled" };
|
|
context.Dispatches.AddRange(dispatchA, dispatchB);
|
|
await context.SaveChangesAsync();
|
|
|
|
context.DispatchUpliftRequests.AddRange(
|
|
new DispatchUpliftRequest
|
|
{
|
|
DispatchId = dispatchA.Id,
|
|
Status = "Pending",
|
|
CreatedDate = new DateTime(2026, 4, 1),
|
|
RequiredTier = 1,
|
|
RequestedNTE = 100m,
|
|
NotificationStatus = "Pending"
|
|
},
|
|
new DispatchUpliftRequest
|
|
{
|
|
DispatchId = dispatchB.Id,
|
|
Status = "Pending",
|
|
CreatedDate = new DateTime(2026, 4, 2),
|
|
RequiredTier = 1,
|
|
RequestedNTE = 200m,
|
|
NotificationStatus = "Pending"
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var data = new UpliftDataService(context);
|
|
|
|
var (total, items) = await data.GetPagedAsync("Pending", null, 1, 25, CancellationToken.None);
|
|
|
|
Assert.Equal(2, total);
|
|
var rowA = Assert.Single(items, i => i.WorkOrderId == workOrderA.Id);
|
|
Assert.Equal("Ann", rowA.WorkOrderDispatcherFirstName);
|
|
Assert.Equal("Alpha", rowA.WorkOrderDispatcherLastName);
|
|
Assert.Equal("Tech A", rowA.TechnicianName);
|
|
Assert.Equal(new DateTime(2026, 5, 1, 8, 0, 0), rowA.WorkOrderScheduledDate);
|
|
var rowB = Assert.Single(items, i => i.WorkOrderId == workOrderB.Id);
|
|
Assert.Null(rowB.WorkOrderDispatcherFirstName);
|
|
Assert.Null(rowB.WorkOrderDispatcherLastName);
|
|
Assert.Equal("Tech B", rowB.TechnicianName);
|
|
Assert.Null(rowB.WorkOrderScheduledDate);
|
|
}
|
|
|
|
private sealed class SqliteUpliftTestDbContext : ApplicationDbContext
|
|
{
|
|
public SqliteUpliftTestDbContext(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;
|
|
}
|
|
}
|
|
}
|
|
}
|