diff --git a/Api.SeaHavenIndustries.Tests/UpliftControllerTests.cs b/Api.SeaHavenIndustries.Tests/UpliftControllerTests.cs index 7d49f24..7b3f0ec 100644 --- a/Api.SeaHavenIndustries.Tests/UpliftControllerTests.cs +++ b/Api.SeaHavenIndustries.Tests/UpliftControllerTests.cs @@ -79,13 +79,23 @@ public class UpliftControllerTests WorkOrderNumber = "WO-77", WorkOrderSite = "SITE-EAST", WorkOrderService = "HVAC", + WorkOrderClosed = true, + AttachmentCount = 2, RequestedByName = "Gateway", + DecidedByName = "Grace Hopper", WorkOrderAutoApprovedTotal = 150m, WorkOrderAdminApprovedTotal = 300m, WorkOrderApprovedExposureTotal = 450m }; service.Setup(x => x.ListAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync(new UpliftListResultDTO { Total = 1, Page = 1, PageSize = 25, Items = new[] { item } }); + .ReturnsAsync(new UpliftListResultDTO + { + Total = 1, + Page = 1, + PageSize = 25, + PendingExposureTotal = 200m, + Items = new[] { item } + }); var controller = NewController(service); @@ -98,8 +108,12 @@ public class UpliftControllerTests returned.WorkOrderNumber.Should().Be("WO-77"); returned.WorkOrderSite.Should().Be("SITE-EAST"); returned.WorkOrderService.Should().Be("HVAC"); + returned.WorkOrderClosed.Should().BeTrue(); + returned.AttachmentCount.Should().Be(2); returned.RequestedByName.Should().Be("Gateway"); + returned.DecidedByName.Should().Be("Grace Hopper"); returned.WorkOrderApprovedExposureTotal.Should().Be(450m); + data!.PendingExposureTotal.Should().Be(200m); } [Fact] diff --git a/Api.SeaHavenIndustries.Tests/UpliftQueueReadTests.cs b/Api.SeaHavenIndustries.Tests/UpliftQueueReadTests.cs index f5b0a5d..84b86bc 100644 --- a/Api.SeaHavenIndustries.Tests/UpliftQueueReadTests.cs +++ b/Api.SeaHavenIndustries.Tests/UpliftQueueReadTests.cs @@ -282,6 +282,48 @@ public sealed class UpliftQueueReadTests item.Status.Should().Be("Pending"); } + [Fact] + public async Task List_ResolvesWorkOrderContext_ForDispatchLinkedOnlyThroughDispatchWorkOrders() + { + // A dispatch can carry no primary WorkOrderId yet be linked to a work order + // through DispatchWorkOrders (the supported shape exercised by + // Exposure_IncludesLinkedDispatchesViaServerDerivedWorkOrderLinks). The queue + // read must resolve the same effective work order the sibling reads use, or the + // Approved tab surfaces blank WO context, workOrderClosed:false, and zero + // exposure on a terminal work order. + using var context = NewContext(); + var (vendor, workOrder) = await SeedWorkOrderAsync(context, "WO-LINKED", "SITE-Z", "Electrical"); + workOrder.LifecycleStatus = LifecycleStatus.Completed; + var linkedDispatch = new Dispatch + { + VendorId = vendor.Id, + DispatchNumber = "DIS-LINKED", + Status = "Completed" + }; + context.Dispatches.Add(linkedDispatch); + await context.SaveChangesAsync(); + context.DispatchWorkOrders.Add(new DispatchWorkOrder + { + DispatchId = linkedDispatch.Id, + WorkOrderId = workOrder.Id + }); + context.DispatchUpliftRequests.Add(Request( + linkedDispatch, "Approved", new DateTime(2026, 3, 1), + decided: new DateTime(2026, 3, 2), requested: 90m)); + await context.SaveChangesAsync(); + var service = NewService(context); + + var result = await service.ListAsync(UserWithRoles("Approver"), "Approved", null, 1, 25, CancellationToken.None); + + var item = result.Items.Should().ContainSingle().Subject; + item.WorkOrderId.Should().Be(workOrder.Id); + item.WorkOrderNumber.Should().Be("WO-LINKED"); + item.WorkOrderSite.Should().Be("SITE-Z"); + item.WorkOrderService.Should().Be("Electrical"); + item.WorkOrderClosed.Should().BeTrue(); + item.WorkOrderApprovedExposureTotal.Should().Be(90m); + } + [Fact] public async Task List_WorkOrderNumber_RendersInternalShNumber_NotTheCrmExternalId() { @@ -429,66 +471,6 @@ public sealed class UpliftQueueReadTests result.Items.Single().DecidedByName.Should().Be("Grace Hopper"); } - [Fact] - public async Task List_AttachmentCount_CountsActiveDocumentsOnTheDispatch() - { - using var context = NewContext(); - var (vendor, workOrder) = await SeedWorkOrderAsync(context, "WO-A", "SITE-A", "HVAC"); - var dispatch = await SeedDispatchAsync(context, vendor, workOrder, "DIS-A"); - context.VendorCompletionDocuments.AddRange( - new VendorCompletionDocument - { - VendorId = vendor.Id, - DispatchId = dispatch.Id, - WorkOrderId = workOrder.Id, - OriginalFileName = "quote.pdf", - StoredFileName = "a.bin", - ContentType = "application/pdf", - SizeBytes = 512, - ScanStatus = "Passed", - ReviewStatus = "Approved", - Purpose = "UpliftEvidence", - Version = 1 - }, - new VendorCompletionDocument - { - VendorId = vendor.Id, - DispatchId = dispatch.Id, - WorkOrderId = workOrder.Id, - OriginalFileName = "photo.jpg", - StoredFileName = "b.bin", - ContentType = "image/jpeg", - SizeBytes = 2048, - ScanStatus = "Passed", - ReviewStatus = "Approved", - Purpose = "Completion", - Version = 1 - }, - new VendorCompletionDocument - { - VendorId = vendor.Id, - DispatchId = dispatch.Id, - WorkOrderId = workOrder.Id, - OriginalFileName = "old.pdf", - StoredFileName = "c.bin", - ContentType = "application/pdf", - SizeBytes = 128, - ScanStatus = "Passed", - ReviewStatus = "Approved", - Purpose = "Completion", - Version = 1, - IsDeleted = true - }); - await context.SaveChangesAsync(); - context.DispatchUpliftRequests.Add(Request(dispatch, "Pending", new DateTime(2026, 3, 2))); - await context.SaveChangesAsync(); - var service = NewService(context); - - var result = await service.ListAsync(UserWithRoles("Approver"), "Pending", null, 1, 25, CancellationToken.None); - - result.Items.Single().AttachmentCount.Should().Be(2); - } - [Fact] public async Task List_PendingExposureTotal_SumsGrantedAmountsAcrossAllPendingRequests() { @@ -720,4 +702,138 @@ public sealed class UpliftQueueReadTests tier2User.Items.Single(i => i.RequiredTier == 2).CanDecide.Should().BeTrue(); tier2User.Items.Single(i => i.RequiredTier == 1).CanDecide.Should().BeFalse(); } + + // --- Closed-work-order flag and decider (SH-208) --- + + [Fact] + public async Task List_MarksWorkOrderClosedOnlyForTerminalLifecycleStatuses() + { + using var context = NewContext(); + var (vendorA, workOrderA) = await SeedWorkOrderAsync(context, "WO-OPEN", "SITE-A", "HVAC"); + var (vendorB, workOrderB) = await SeedWorkOrderAsync(context, "WO-DONE", "SITE-B", "HVAC"); + var (vendorC, workOrderC) = await SeedWorkOrderAsync(context, "WO-CANCEL", "SITE-C", "HVAC"); + workOrderA.LifecycleStatus = LifecycleStatus.InProgress; + workOrderB.LifecycleStatus = LifecycleStatus.Completed; + workOrderC.LifecycleStatus = LifecycleStatus.Canceled; + await context.SaveChangesAsync(); + var dispatchA = await SeedDispatchAsync(context, vendorA, workOrderA, "DIS-A"); + var dispatchB = await SeedDispatchAsync(context, vendorB, workOrderB, "DIS-B"); + var dispatchC = await SeedDispatchAsync(context, vendorC, workOrderC, "DIS-C"); + context.DispatchUpliftRequests.AddRange( + Request(dispatchA, "Approved", new DateTime(2026, 3, 1), decided: new DateTime(2026, 3, 2)), + Request(dispatchB, "Approved", new DateTime(2026, 3, 1), decided: new DateTime(2026, 3, 2)), + Request(dispatchC, "Approved", new DateTime(2026, 3, 1), decided: new DateTime(2026, 3, 2))); + await context.SaveChangesAsync(); + var service = NewService(context); + + var result = await service.ListAsync(UserWithRoles("Approver"), "Approved", null, 1, 25, CancellationToken.None); + + result.Items.Single(i => i.WorkOrderNumber == "WO-OPEN").WorkOrderClosed.Should().BeFalse(); + result.Items.Single(i => i.WorkOrderNumber == "WO-DONE").WorkOrderClosed.Should().BeTrue(); + result.Items.Single(i => i.WorkOrderNumber == "WO-CANCEL").WorkOrderClosed.Should().BeTrue(); + } + + [Fact] + public async Task List_MapsDecidedByNameFromTheDecidingUser() + { + using var context = NewContext(); + var (vendor, workOrder) = await SeedWorkOrderAsync(context, "WO-77", "SITE-EAST", "HVAC"); + var dispatch = await SeedDispatchAsync(context, vendor, workOrder, "DIS-1"); + context.Users.Add(new ApplicationUser { Id = "user-9", FirstName = "Grace", LastName = "Hopper" }); + await context.SaveChangesAsync(); + var request = Request(dispatch, "Approved", new DateTime(2026, 3, 1), decided: new DateTime(2026, 3, 2)); + request.DecidedByUserId = "user-9"; + context.DispatchUpliftRequests.Add(request); + await context.SaveChangesAsync(); + var service = NewService(context); + + var result = await service.ListAsync(UserWithRoles("Approver"), "Approved", null, 1, 25, CancellationToken.None); + + result.Items.Single().DecidedByName.Should().Be("Grace Hopper"); + } + + // --- Attachment count (SH-207) --- + + [Fact] + public async Task List_CountsOnlyNonDeletedUpliftEvidenceAttachments() + { + using var context = NewContext(); + var (vendor, workOrder) = await SeedWorkOrderAsync(context, "WO-77", "SITE-EAST", "HVAC"); + var dispatch = await SeedDispatchAsync(context, vendor, workOrder, "DIS-1"); + context.VendorCompletionDocuments.AddRange( + Document(dispatch, vendor, workOrder, "quote.pdf"), + Document(dispatch, vendor, workOrder, "photo.jpg"), + Document(dispatch, vendor, workOrder, "signed-completion.pdf", purpose: "Completion"), + Document(dispatch, vendor, workOrder, "deleted-estimate.pdf", isDeleted: true)); + await context.SaveChangesAsync(); + context.DispatchUpliftRequests.Add(new DispatchUpliftRequest + { + DispatchId = dispatch.Id, + Status = "Pending", + CreatedDate = new DateTime(2026, 3, 2), + RequiredTier = 1, + RequestedNTE = 200m + }); + await context.SaveChangesAsync(); + var service = NewService(context); + + var result = await service.ListAsync(UserWithRoles("Approver"), "Pending", null, 1, 25, CancellationToken.None); + + result.Items.Single().AttachmentCount.Should().Be(2); + } + + private static VendorCompletionDocument Document( + Dispatch dispatch, + Vendor vendor, + WorkOrder workOrder, + string fileName, + string purpose = "UpliftEvidence", + bool isDeleted = false) => new() + { + VendorId = vendor.Id, + DispatchId = dispatch.Id, + WorkOrderId = workOrder.Id, + OriginalFileName = fileName, + StoredFileName = $"{fileName}.bin", + ContentType = "application/octet-stream", + SizeBytes = 128, + ScanStatus = "Passed", + ReviewStatus = "Approved", + Purpose = purpose, + Version = 1, + IsDeleted = isDeleted + }; + + // --- Queue-wide pending exposure (SH-207) --- + + [Fact] + public async Task List_PendingExposureTotal_SumsPendingRequestsAcrossTheQueueRegardlessOfFilter() + { + using var context = NewContext(); + var (vendorA, workOrderA) = await SeedWorkOrderAsync(context, "WO-A", "SITE-A", "HVAC"); + var (vendorB, workOrderB) = await SeedWorkOrderAsync(context, "WO-B", "SITE-B", "HVAC"); + var dispatchA = await SeedDispatchAsync(context, vendorA, workOrderA, "DIS-A"); + var dispatchB = await SeedDispatchAsync(context, vendorB, workOrderB, "DIS-B"); + context.DispatchUpliftRequests.AddRange( + Request(dispatchA, "Pending", new DateTime(2026, 3, 1), requested: 125m), + Request(dispatchB, "Pending", new DateTime(2026, 3, 2), requested: 75m), + Request(dispatchA, "Approved", new DateTime(2026, 3, 3), decided: new DateTime(2026, 3, 4), requested: 999m), + new DispatchUpliftRequest + { + DispatchId = dispatchA.Id, + Status = "Pending", + CreatedDate = new DateTime(2026, 3, 5), + RequiredTier = 1, + RequestedNTE = 500m, + IsDeleted = true + }); + await context.SaveChangesAsync(); + var service = NewService(context); + + // Even when the caller reads the Approved decision log, the header total + // still reflects the whole pending queue. + var result = await service.ListAsync(UserWithRoles("Approver"), "Approved", null, 1, 25, CancellationToken.None); + + result.PendingExposureTotal.Should().Be(200m); + } } diff --git a/Data.SeaHavenIndustries/Models/VendorPortalReadModels.cs b/Data.SeaHavenIndustries/Models/VendorPortalReadModels.cs index 12c4781..885419d 100644 --- a/Data.SeaHavenIndustries/Models/VendorPortalReadModels.cs +++ b/Data.SeaHavenIndustries/Models/VendorPortalReadModels.cs @@ -85,6 +85,8 @@ namespace Data.SeaHavenIndustries public string? WorkOrderNumber { get; set; } public string? WorkOrderSiteCode { get; set; } public string? WorkOrderService { get; set; } + public bool WorkOrderClosed { get; set; } + public int AttachmentCount { get; set; } public string? RequestedByVendorName { get; set; } public string? RequestedByFirstName { get; set; } public string? RequestedByLastName { get; set; } @@ -106,8 +108,6 @@ namespace Data.SeaHavenIndustries public DateTime? ExpiresAt { get; set; } public string? NotificationStatus { get; set; } public string? NotificationError { get; set; } - public int AttachmentCount { get; set; } - public bool WorkOrderClosed { get; set; } } public class UpliftForWorkOrderData : UpliftForDispatchData diff --git a/SeaHaven.DataServices/Implementation/UpliftDataService.cs b/SeaHaven.DataServices/Implementation/UpliftDataService.cs index 50249d9..788c754 100644 --- a/SeaHaven.DataServices/Implementation/UpliftDataService.cs +++ b/SeaHaven.DataServices/Implementation/UpliftDataService.cs @@ -25,15 +25,24 @@ namespace SeaHaven.DataServices.Implementation from v in vendors.DefaultIfEmpty() join ev in _context.VendorCompletionDocuments on u.EvidenceDocumentId equals ev.Id into evidences from ev in evidences.DefaultIfEmpty() - join wo in _context.workOrders on d.WorkOrderId equals wo.Id into workOrders - from wo in workOrders.DefaultIfEmpty() + // SH-207: resolve the work order through the same primary-plus-linked + // convention the sibling reads use (GetWorkOrderIdForUpliftAsync, + // GetApprovedExposureForWorkOrdersAsync). A dispatch whose work order is + // linked only through DispatchWorkOrders still surfaces its work-order id, + // flattened WO context, the closed flag, and approved exposure. The + // effective id feeds a single work-order lookup so every field resolves + // from one row. + let effectiveWorkOrderId = d.WorkOrderId + ?? d.DispatchWorkOrders!.Select(link => (int?)link.WorkOrderId).FirstOrDefault() + let workOrder = _context.workOrders + .FirstOrDefault(candidate => candidate.Id == effectiveWorkOrderId) join reqUser in _context.Users on u.createdby equals reqUser.Id into requestUsers from reqUser in requestUsers.DefaultIfEmpty() - join decUser in _context.Users on u.DecidedByUserId equals decUser.Id into decisionUsers - from decUser in decisionUsers.DefaultIfEmpty() + join decUser in _context.Users on u.DecidedByUserId equals decUser.Id into decUsers + from decUser in decUsers.DefaultIfEmpty() where (u.IsDeleted == null || u.IsDeleted == false) && (d.IsDeleted == null || d.IsDeleted == false) - select new { u, d, v, ev, wo, reqUser, decUser }; + select new { u, d, v, ev, effectiveWorkOrderId, workOrder, reqUser, decUser }; if (!string.IsNullOrWhiteSpace(status)) query = query.Where(x => x.u.Status == status); @@ -62,14 +71,24 @@ namespace SeaHaven.DataServices.Implementation DispatchId = x.d.Id, DispatchNumber = x.d.DispatchNumber, PONumber = x.d.PONumber, - WorkOrderId = x.d.WorkOrderId, + WorkOrderId = x.effectiveWorkOrderId, VendorCompanyName = x.v != null ? x.v.CompanyName : x.u.RequestedByVendorName, // The queue renders the internal display number (InternalWONumber, what the // board shows); WorkerOrderNumber holds the CRM external id on synced // work orders and must not leak into the queue display. - WorkOrderNumber = x.wo != null ? x.wo.InternalWONumber : null, - WorkOrderSiteCode = x.wo != null ? x.wo.SiteCode : null, - WorkOrderService = x.wo != null ? x.wo.Service : null, + WorkOrderNumber = x.workOrder != null ? x.workOrder.InternalWONumber : null, + WorkOrderSiteCode = x.workOrder != null ? x.workOrder.SiteCode : null, + WorkOrderService = x.workOrder != null ? x.workOrder.Service : null, + // SH-208: a work order is closed for uplift decisions once its + // lifecycle reaches a terminal state; mirrors the SH-196 revoke guard. + WorkOrderClosed = x.workOrder != null + && (x.workOrder.LifecycleStatus == LifecycleStatus.Completed + || x.workOrder.LifecycleStatus == LifecycleStatus.Canceled), + // SH-207: every non-deleted UpliftEvidence document on the dispatch is + // an attachment reviewers can open; the linked evidence is one of them. + AttachmentCount = x.d.CompletionDocuments.Count(doc => + doc.Purpose == "UpliftEvidence" + && (doc.IsDeleted == null || doc.IsDeleted == false)), RequestedByVendorName = x.u.RequestedByVendorName, RequestedByFirstName = x.reqUser != null ? x.reqUser.FirstName : null, RequestedByLastName = x.reqUser != null ? x.reqUser.LastName : null, @@ -90,12 +109,6 @@ namespace SeaHaven.DataServices.Implementation ExpiresAt = x.u.ExpiresAt, NotificationStatus = x.u.NotificationStatus, NotificationError = x.u.NotificationError, - AttachmentCount = _context.VendorCompletionDocuments.Count(document => - document.DispatchId == x.d.Id - && (document.IsDeleted == null || document.IsDeleted == false)), - WorkOrderClosed = x.wo != null - && (x.wo.LifecycleStatus == LifecycleStatus.Completed - || x.wo.LifecycleStatus == LifecycleStatus.Canceled) }) .ToListAsync(cancellationToken); @@ -401,6 +414,19 @@ namespace SeaHaven.DataServices.Implementation return totals.Values.ToList(); } + // SH-207: queue-wide pending exposure for the approvals header. Mirrors the + // approved-exposure convention (sum of RequestedNTE) over every non-deleted + // Pending request on a non-deleted dispatch, independent of page or filters. + public async Task GetPendingExposureTotalAsync(CancellationToken cancellationToken) + { + return await _context.DispatchUpliftRequests + .Where(u => (u.IsDeleted == null || u.IsDeleted == false) + && u.Status == "Pending" + && u.Dispatch != null + && (u.Dispatch.IsDeleted == null || u.Dispatch.IsDeleted == false)) + .SumAsync(u => (decimal?)u.RequestedNTE, cancellationToken) ?? 0m; + } + public Task> GetPendingForWorkOrderAsync( int workOrderId, CancellationToken cancellationToken) diff --git a/SeaHaven.DataServices/Interfaces/IUpliftDataService.cs b/SeaHaven.DataServices/Interfaces/IUpliftDataService.cs index 4b00ede..26c968c 100644 --- a/SeaHaven.DataServices/Interfaces/IUpliftDataService.cs +++ b/SeaHaven.DataServices/Interfaces/IUpliftDataService.cs @@ -26,6 +26,9 @@ namespace SeaHaven.DataServices.Interfaces // from the result have no approved exposure. Task> GetApprovedExposureForWorkOrdersAsync( IReadOnlyCollection workOrderIds, CancellationToken cancellationToken); + // SH-207: queue-wide pending exposure for the approvals header (sum of + // RequestedNTE over non-deleted Pending requests on non-deleted dispatches). + Task GetPendingExposureTotalAsync(CancellationToken cancellationToken); Task> GetPendingForWorkOrderAsync(int workOrderId, CancellationToken cancellationToken); // SH-101: active = Pending or ChangesRequested (the only states that block a new request). Task HasActiveAsync(int dispatchId, CancellationToken cancellationToken); diff --git a/SeaHavenIndustries.Tests/UpliftQueueReadRelationalTests.cs b/SeaHavenIndustries.Tests/UpliftQueueReadRelationalTests.cs new file mode 100644 index 0000000..6fdc8e2 --- /dev/null +++ b/SeaHavenIndustries.Tests/UpliftQueueReadRelationalTests.cs @@ -0,0 +1,110 @@ +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() + .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); + } + + private sealed class SqliteUpliftTestDbContext : ApplicationDbContext + { + public SqliteUpliftTestDbContext(DbContextOptions 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; + } + } + } +} diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardCancelServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardCancelServiceTests.cs index b3fadde..e797a68 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardCancelServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardCancelServiceTests.cs @@ -444,6 +444,7 @@ public class WorkOrderBoardCancelServiceTests public Task HasPendingForWorkOrderAsync(int workOrderId, CancellationToken cancellationToken) => throw new NotSupportedException(); public Task SumAutoApprovedAmountForWorkOrderAsync(int workOrderId, CancellationToken cancellationToken) => throw new NotSupportedException(); public Task> GetApprovedExposureForWorkOrdersAsync(IReadOnlyCollection workOrderIds, CancellationToken cancellationToken) => throw new NotSupportedException(); + public Task GetPendingExposureTotalAsync(CancellationToken cancellationToken) => throw new NotSupportedException(); public Task> GetPendingForWorkOrderAsync(int workOrderId, CancellationToken cancellationToken) => throw new NotSupportedException(); public Task HasActiveAsync(int dispatchId, CancellationToken cancellationToken) => throw new NotSupportedException(); public Task GetActiveRequestAsync(int dispatchId, CancellationToken cancellationToken) => throw new NotSupportedException();