From b11b66538ae59ff94e6d4ac3f65a7ce0551f62ae Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 1 Oct 2026 14:55:07 +0000 Subject: [PATCH] fix(uplifts): resolve evidence without a primary dispatch Upload and status use the same dispatch create already selects, so a work order with no primary can still attach evidence. Status no longer rejects a terminal work order or dispatch, and create, cancel, and revoke return the stored evidence file name and attachments. Co-authored-by: Arthur Bassi --- .../Helpers/WorkOrderUpliftContractMapper.cs | 27 +++- .../WorkOrderUpliftEvidenceService.cs | 75 ++++++--- .../Implementation/WorkOrderUpliftService.cs | 58 ++++++- .../WorkOrderUpliftEvidenceServiceTests.cs | 148 ++++++++++++++++++ .../WorkOrderUpliftServiceTests.cs | 79 ++++++++++ 5 files changed, 352 insertions(+), 35 deletions(-) diff --git a/SeaHaven.Services/Helpers/WorkOrderUpliftContractMapper.cs b/SeaHaven.Services/Helpers/WorkOrderUpliftContractMapper.cs index 8ce25c8..407448d 100644 --- a/SeaHaven.Services/Helpers/WorkOrderUpliftContractMapper.cs +++ b/SeaHaven.Services/Helpers/WorkOrderUpliftContractMapper.cs @@ -6,6 +6,10 @@ using SeaHaven.Services.Implementation; namespace SeaHaven.Services.Helpers { + public sealed record StoredUpliftEvidence( + string? FileName, + IReadOnlyList Attachments); + public static class WorkOrderUpliftContractMapper { public static WorkOrderUpliftSummaryDto MapSummary( @@ -53,13 +57,15 @@ namespace SeaHaven.Services.Helpers DecisionNote = row.DecisionNote ?? "", EvidenceDocumentId = row.EvidenceDocumentId, EvidenceFileName = row.EvidenceFileName ?? "", - Attachments = row.Attachments - .Select(file => new WorkOrderUpliftAttachmentDto { Id = file.Id, Name = file.Name }) - .ToList(), + Attachments = MapAttachments(row.Attachments), }; } - public static WorkOrderUpliftDto MapItem(DispatchUpliftRequest request, string? requestedByName, string? decidedByName) + public static WorkOrderUpliftDto MapItem( + DispatchUpliftRequest request, + string? requestedByName, + string? decidedByName, + StoredUpliftEvidence evidence) => new() { Id = request.Id, @@ -73,18 +79,27 @@ namespace SeaHaven.Services.Helpers DecidedByName = decidedByName ?? "", DecisionNote = request.DecisionNote ?? "", EvidenceDocumentId = request.EvidenceDocumentId, + EvidenceFileName = evidence.FileName ?? "", + Attachments = MapAttachments(evidence.Attachments), }; public static WorkOrderUpliftDto MapRevokedItem( DispatchUpliftRequest request, string? requestedByName, - string decidedByName) + string decidedByName, + StoredUpliftEvidence evidence) { - var dto = MapItem(request, requestedByName, decidedByName); + var dto = MapItem(request, requestedByName, decidedByName, evidence); dto.Status = "revoked"; return dto; } + private static List MapAttachments( + IEnumerable files) + => files + .Select(file => new WorkOrderUpliftAttachmentDto { Id = file.Id, Name = file.Name }) + .ToList(); + public static string ToFrontendStatus(string? backendStatus) { return UpliftStatus.ToCanonical(backendStatus) switch diff --git a/SeaHaven.Services/Implementation/WorkOrderUpliftEvidenceService.cs b/SeaHaven.Services/Implementation/WorkOrderUpliftEvidenceService.cs index 775b4fe..a1ac913 100644 --- a/SeaHaven.Services/Implementation/WorkOrderUpliftEvidenceService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderUpliftEvidenceService.cs @@ -18,6 +18,7 @@ namespace SeaHaven.Services.Implementation }; private readonly IWorkOrderDetailDataService _detailData; + private readonly IUpliftDataService _upliftData; private readonly IDispatchDataService _dispatchData; private readonly IVendorDocumentDataService _documentData; private readonly IVendorDocumentStoragePort _documentStorage; @@ -26,6 +27,7 @@ namespace SeaHaven.Services.Implementation public WorkOrderUpliftEvidenceService( IWorkOrderDetailDataService detailData, + IUpliftDataService upliftData, IDispatchDataService dispatchData, IVendorDocumentDataService documentData, IVendorDocumentStoragePort documentStorage, @@ -33,6 +35,7 @@ namespace SeaHaven.Services.Implementation IOptions documentOptions) { _detailData = detailData; + _upliftData = upliftData; _dispatchData = dispatchData; _documentData = documentData; _documentStorage = documentStorage; @@ -46,20 +49,29 @@ namespace SeaHaven.Services.Implementation ClaimsPrincipal user, CancellationToken cancellationToken) { - var target = await ResolveOpenDispatchAsync(workOrderId, user, cancellationToken); - if (target == null) + var workOrder = await FindAccessibleWorkOrderAsync(workOrderId, user, cancellationToken); + if (workOrder == null) return null; + var dispatch = await ResolveUpliftDispatchAsync( + workOrderId, + workOrder.PrimaryDispatchId, + cancellationToken); + if (dispatch == null) + throw new InvalidOperationException("Work order has no primary dispatch for uplift requests"); + + RejectTerminalTarget(workOrder, dispatch); + var (contentType, bytes) = await ReadAcceptedFileAsync(file, cancellationToken); - var latest = await _documentData.GetLatestForDispatchAsync(target.Dispatch.Id, cancellationToken); + var latest = await _documentData.GetLatestForDispatchAsync(dispatch.Id, cancellationToken); var version = (latest?.Version ?? 0) + 1; - var storedFileName = $"{target.Dispatch.Id}_{version}_{Guid.NewGuid():N}{SafeExtension(file.FileName)}"; + var storedFileName = $"{dispatch.Id}_{version}_{Guid.NewGuid():N}{SafeExtension(file.FileName)}"; var now = DateTime.UtcNow; var passWithoutScanner = _documentOptions.PassWhenScannerUnavailable; var document = new VendorCompletionDocument { - VendorId = target.Dispatch.VendorId, - DispatchId = target.Dispatch.Id, + VendorId = dispatch.VendorId, + DispatchId = dispatch.Id, WorkOrderId = workOrderId, OriginalFileName = Path.GetFileName(file.FileName), StoredFileName = storedFileName, @@ -77,9 +89,9 @@ namespace SeaHaven.Services.Implementation await _dispatchData.StageAuditLogAsync(new WorkOrderAuditLog { WorkOrderId = workOrderId, - DispatchId = target.Dispatch.Id, + DispatchId = dispatch.Id, UserId = user.FindFirstValue(ClaimTypes.NameIdentifier), - FieldName = $"Dispatch {target.Dispatch.DispatchNumber} Uplift Evidence", + FieldName = $"Dispatch {dispatch.DispatchNumber} Uplift Evidence", NewValue = document.OriginalFileName, Action = "uplift_evidence_uploaded", ActorType = "internal", @@ -89,8 +101,8 @@ namespace SeaHaven.Services.Implementation using var stored = new MemoryStream(bytes); await _documentStorage.SaveAsync( - target.Dispatch.VendorId, - target.Dispatch.Id, + dispatch.VendorId, + dispatch.Id, storedFileName, stored, cancellationToken); @@ -111,14 +123,23 @@ namespace SeaHaven.Services.Implementation ClaimsPrincipal user, CancellationToken cancellationToken) { - var target = await ResolveOpenDispatchAsync(workOrderId, user, cancellationToken); - if (target == null) + // A poll reads the document on the dispatch create would pick. It does not + // require that work order or dispatch to still accept a new upload. + var workOrder = await FindAccessibleWorkOrderAsync(workOrderId, user, cancellationToken); + if (workOrder == null) + return null; + + var dispatch = await ResolveUpliftDispatchAsync( + workOrderId, + workOrder.PrimaryDispatchId, + cancellationToken); + if (dispatch == null) return null; var document = await _documentData.GetMetadataForVendorDispatchAsync( documentId, - target.Dispatch.Id, - target.Dispatch.VendorId, + dispatch.Id, + dispatch.VendorId, cancellationToken); if (document == null || document.Purpose != VendorDocumentPurpose.UpliftEvidence) return null; @@ -133,7 +154,7 @@ namespace SeaHaven.Services.Implementation }; } - private async Task ResolveOpenDispatchAsync( + private async Task FindAccessibleWorkOrderAsync( int workOrderId, ClaimsPrincipal user, CancellationToken cancellationToken) @@ -142,27 +163,31 @@ namespace SeaHaven.Services.Implementation if (!await _detailData.ExistsAsync(workOrderId, cancellationToken, accountFilter)) return null; - var workOrder = await _detailData.GetWorkOrderForMediaAsync( + return await _detailData.GetWorkOrderForMediaAsync( workOrderId, cancellationToken, accountFilter); - if (workOrder?.PrimaryDispatchId is not int dispatchId) - throw new InvalidOperationException("Work order has no primary dispatch for uplift requests"); + } + private Task ResolveUpliftDispatchAsync( + int workOrderId, + int? primaryDispatchId, + CancellationToken cancellationToken) + => _upliftData.GetUpliftDispatchForWorkOrderAsync( + workOrderId, + primaryDispatchId, + cancellationToken); + + private static void RejectTerminalTarget(WorkOrder workOrder, Dispatch dispatch) + { if (workOrder.LifecycleStatus is LifecycleStatus.Completed or LifecycleStatus.Canceled) { throw new InvalidOperationException( $"Cannot attach uplift evidence on a '{workOrder.LifecycleStatus}' work order"); } - var dispatch = await _dispatchData.GetByIdAsync(dispatchId); - if (dispatch == null) - throw new KeyNotFoundException("Dispatch not found"); - if (dispatch.Status is "Verified" or "Cancelled" or "Canceled" or "Refused") throw new InvalidOperationException($"Cannot attach uplift evidence on a '{dispatch.Status}' dispatch"); - - return new OpenDispatch(dispatch); } private async Task<(string ContentType, byte[] Bytes)> ReadAcceptedFileAsync( @@ -220,7 +245,5 @@ namespace SeaHaven.Services.Implementation var extension = Path.GetExtension(fileName); return string.IsNullOrWhiteSpace(extension) ? string.Empty : extension; } - - private sealed record OpenDispatch(Dispatch Dispatch); } } diff --git a/SeaHaven.Services/Implementation/WorkOrderUpliftService.cs b/SeaHaven.Services/Implementation/WorkOrderUpliftService.cs index e241200..9421182 100644 --- a/SeaHaven.Services/Implementation/WorkOrderUpliftService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderUpliftService.cs @@ -284,7 +284,12 @@ namespace SeaHaven.Services.Implementation await _upliftData.SaveChangesAsync(cancellationToken); var requesterName = req.RequestedByVendorName ?? await ResolveUserDisplayNameAsync(req.createdby, cancellationToken); - return WorkOrderUpliftContractMapper.MapItem(req, requesterName, await ResolveUserDisplayNameAsync(userId, cancellationToken)); + return await MapStoredRequestAsync( + req, + dispatch, + requesterName, + await ResolveUserDisplayNameAsync(userId, cancellationToken), + cancellationToken); } public async Task RevokeAsync( @@ -375,7 +380,7 @@ namespace SeaHaven.Services.Implementation var decidedByName = await ResolveUserDisplayNameAsync(userId, cancellationToken); var requesterName = req.RequestedByVendorName ?? await ResolveUserDisplayNameAsync(req.createdby, cancellationToken); - return WorkOrderUpliftContractMapper.MapRevokedItem(req, requesterName, decidedByName); + return await MapStoredRequestAsync(req, dispatch, requesterName, decidedByName, cancellationToken); } public async Task WithdrawPendingForWorkOrderAsync( @@ -432,7 +437,54 @@ namespace SeaHaven.Services.Implementation _upliftData.StageEvidenceDocuments(created, evidenceDocumentIds); await StageAuditAsync(dispatch, workOrderId, userId, currentNte, amount, auditAction, now, cancellationToken); await _upliftData.SaveChangesAsync(cancellationToken); - return WorkOrderUpliftContractMapper.MapItem(created, requesterName, null); + return await MapStoredRequestAsync(created, dispatch, requesterName, null, cancellationToken); + } + + private async Task MapStoredRequestAsync( + DispatchUpliftRequest request, + Dispatch dispatch, + string? requestedByName, + string? decidedByName, + CancellationToken cancellationToken) + { + var evidence = await LoadStoredEvidenceAsync(request, dispatch, cancellationToken); + if (request.Status == UpliftStatus.Revoked) + { + return WorkOrderUpliftContractMapper.MapRevokedItem( + request, + requestedByName, + decidedByName ?? "", + evidence); + } + + return WorkOrderUpliftContractMapper.MapItem( + request, + requestedByName, + decidedByName, + evidence); + } + + private async Task LoadStoredEvidenceAsync( + DispatchUpliftRequest request, + Dispatch dispatch, + CancellationToken cancellationToken) + { + var files = await _upliftData.GetEvidenceFilesAsync(new[] { request.Id }, cancellationToken); + if (!files.TryGetValue(request.Id, out var attachments)) + attachments = Array.Empty(); + + string? fileName = null; + if (request.EvidenceDocumentId is int documentId) + { + var document = await _documentData.GetMetadataForVendorDispatchAsync( + documentId, + dispatch.Id, + dispatch.VendorId, + cancellationToken); + fileName = document?.OriginalFileName; + } + + return new StoredUpliftEvidence(fileName, attachments); } /// diff --git a/SeaHavenIndustries.Tests/WorkOrderUpliftEvidenceServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderUpliftEvidenceServiceTests.cs index e0b90ab..1be029d 100644 --- a/SeaHavenIndustries.Tests/WorkOrderUpliftEvidenceServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderUpliftEvidenceServiceTests.cs @@ -2,10 +2,12 @@ using System.Security.Claims; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using SeaHaven.DataServices.Implementation; using SeaHaven.Services.Configuration; +using SeaHaven.Services.DTOs; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; using Xunit; @@ -62,6 +64,7 @@ public sealed class WorkOrderUpliftEvidenceServiceTests var storage = new MemoryStorage(); var service = new WorkOrderUpliftEvidenceService( new WorkOrderDetailDataService(context), + new UpliftDataService(context), new DispatchDataService(context), new VendorDocumentDataService(context), storage, @@ -186,11 +189,156 @@ public sealed class WorkOrderUpliftEvidenceServiceTests ScanStatus = "Passed", Version = 1, }); + context.Dispatches.Add(new Dispatch + { + Id = 11, + VendorId = 1, + WorkOrderId = workOrder.Id, + DispatchNumber = "DIS-11", + Status = "Scheduled", + }); + context.VendorCompletionDocuments.Add(new VendorCompletionDocument + { + Id = 91, + VendorId = 1, + DispatchId = 11, + WorkOrderId = workOrder.Id, + OriginalFileName = "other.pdf", + StoredFileName = "other.pdf", + ContentType = "application/pdf", + Purpose = VendorDocumentPurpose.UpliftEvidence, + ScanStatus = "Passed", + Version = 1, + }); await context.SaveChangesAsync(); var passed = await service.GetStatusAsync(workOrder.Id, uploaded.Id, Dispatcher(), CancellationToken.None); Assert.Equal("Passed", passed!.ScanStatus); Assert.Null(await service.GetStatusAsync(workOrder.Id, 90, Dispatcher(), CancellationToken.None)); + Assert.Null(await service.GetStatusAsync(workOrder.Id, 91, Dispatcher(), CancellationToken.None)); + } + + [Theory] + [InlineData(LifecycleStatus.Completed, "Scheduled")] + [InlineData(LifecycleStatus.Canceled, "Scheduled")] + [InlineData(LifecycleStatus.Scheduled, "Verified")] + [InlineData(LifecycleStatus.Scheduled, "Cancelled")] + [InlineData(LifecycleStatus.Scheduled, "Canceled")] + [InlineData(LifecycleStatus.Scheduled, "Refused")] + public async Task GetStatusAsync_ReturnsUpliftEvidenceWhenTargetIsTerminal( + LifecycleStatus lifecycle, + string dispatchStatus) + { + await using var context = CreateContext(); + var workOrder = await SeedWorkOrderAsync(context); + var (service, _) = NewService(context); + var uploaded = await service.UploadAsync( + workOrder.Id, + FormFile("%PDF-1.4"u8.ToArray(), "quote.pdf", "application/pdf"), + Dispatcher(), + CancellationToken.None); + + workOrder.LifecycleStatus = lifecycle; + context.Dispatches.Single().Status = dispatchStatus; + await context.SaveChangesAsync(); + + var status = await service.GetStatusAsync(workOrder.Id, uploaded!.Id, Dispatcher(), CancellationToken.None); + + Assert.Equal("Pending", status!.ScanStatus); + Assert.Equal("quote.pdf", status.OriginalFileName); + } + + [Theory] + [InlineData(LifecycleStatus.Completed, "Scheduled", "Completed")] + [InlineData(LifecycleStatus.Scheduled, "Verified", "Verified")] + public async Task UploadAsync_TerminalWorkOrderOrDispatch_Rejects( + LifecycleStatus lifecycle, + string dispatchStatus, + string expected) + { + await using var context = CreateContext(); + var workOrder = await SeedWorkOrderAsync(context); + workOrder.LifecycleStatus = lifecycle; + context.Dispatches.Single().Status = dispatchStatus; + await context.SaveChangesAsync(); + var (service, storage) = NewService(context); + + var ex = await Assert.ThrowsAsync(() => + service.UploadAsync( + workOrder.Id, + FormFile("%PDF-1.4"u8.ToArray(), "quote.pdf", "application/pdf"), + Dispatcher(), + CancellationToken.None)); + + Assert.Contains(expected, ex.Message, StringComparison.Ordinal); + Assert.Empty(context.VendorCompletionDocuments); + Assert.Empty(storage.Saved); + } + + [Fact] + public async Task UploadThenCreate_WithoutPrimaryDispatch_PersistsUpliftOnTheSameDispatch() + { + await using var context = CreateContext(); + var workOrder = await SeedWorkOrderAsync(context); + workOrder.PrimaryDispatchId = null; + var role = new IdentityRole("Admin"); + context.Roles.Add(role); + context.UserRoles.Add(new IdentityUserRole + { + UserId = "dispatcher-1", + RoleId = role.Id, + }); + await context.SaveChangesAsync(); + + var (evidence, _) = NewService(context); + var uploaded = await evidence.UploadAsync( + workOrder.Id, + FormFile("%PDF-1.4"u8.ToArray(), "quote.pdf", "application/pdf"), + Dispatcher(), + CancellationToken.None); + + var pending = await evidence.GetStatusAsync( + workOrder.Id, + uploaded!.Id, + Dispatcher(), + CancellationToken.None); + Assert.Equal("Pending", pending!.ScanStatus); + + var document = Assert.Single(context.VendorCompletionDocuments); + document.ScanStatus = "Passed"; + await context.SaveChangesAsync(); + + var created = await NewUpliftService(context).CreateAsync( + workOrder.Id, + new CreateWorkOrderUpliftRequestDto + { + Amount = 400m, + Notes = "Quote", + EvidenceDocumentId = uploaded.Id, + }, + Dispatcher(), + CancellationToken.None); + + var request = Assert.Single(context.DispatchUpliftRequests); + Assert.NotNull(created); + Assert.Equal(uploaded.Id, request.EvidenceDocumentId); + Assert.Equal(document.DispatchId, request.DispatchId); + Assert.Equal(10, request.DispatchId); + } + + private static WorkOrderUpliftService NewUpliftService(ApplicationDbContext context) + { + return new WorkOrderUpliftService( + new UpliftDataService(context), + new DispatchDataService(context), + new WorkOrderDetailDataService(context), + WorkOrderAccountTestHelpers.Resolver(context), + new UserDataService(context), + new TeamPermissionOverrideDataService(context), + new TeamPermissionPolicy(), + TimeProvider.System, + Options.Create(new ApprovalsOptions { UpliftTier1MaxUsd = 2500m }), + new VendorDocumentDataService(context)); } private sealed class MemoryStorage : IVendorDocumentStoragePort diff --git a/SeaHavenIndustries.Tests/WorkOrderUpliftServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderUpliftServiceTests.cs index e6ae9ee..70bd6e6 100644 --- a/SeaHavenIndustries.Tests/WorkOrderUpliftServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderUpliftServiceTests.cs @@ -515,6 +515,43 @@ public sealed class WorkOrderUpliftServiceTests Assert.Equal("cancelled", cancelled!.Status); } + [Fact] + public async Task CancelAsync_ReturnsStoredEvidenceFileNameAndAttachments() + { + await using var context = CreateContext(); + var (workOrder, dispatch) = await SeedWorkOrderAsync(context); + var document = SeedEvidence(context, dispatch.Id, dispatch.VendorId, workOrder.Id); + context.DispatchUpliftRequests.Add(new DispatchUpliftRequest + { + Id = 100, + DispatchId = dispatch.Id, + RequestedNTE = 600m, + Status = "Pending", + RequiredTier = 1, + NotificationStatus = "Pending", + createdby = "dispatcher-1", + EvidenceDocumentId = document.Id, + }); + context.DispatchUpliftRequestDocuments.Add(new DispatchUpliftRequestDocument + { + UpliftRequestId = 100, + DocumentId = document.Id, + }); + await context.SaveChangesAsync(); + + var cancelled = await NewService(context).CancelAsync( + workOrder.Id, + 100, + Dispatcher(), + CancellationToken.None); + + Assert.Equal("cancelled", cancelled!.Status); + Assert.Equal("quote.pdf", cancelled.EvidenceFileName); + var attachment = Assert.Single(cancelled.Attachments); + Assert.Equal(document.Id, attachment.Id); + Assert.Equal("quote.pdf", attachment.Name); + } + [Fact] public async Task RevokeAsync_ApprovedRequiresAdminReason() { @@ -761,6 +798,40 @@ public sealed class WorkOrderUpliftServiceTests .SumAutoApprovedAmountForWorkOrderAsync(workOrder.Id, CancellationToken.None)); } + [Fact] + public async Task RevokeAsync_ReturnsStoredEvidenceFileNameAndAttachments() + { + await using var context = CreateContext(); + var (workOrder, dispatch) = await SeedWorkOrderAsync(context); + var document = SeedEvidence(context, dispatch.Id, dispatch.VendorId, workOrder.Id); + await context.SaveChangesAsync(); + var service = NewService(context); + + var created = await service.CreateAsync( + workOrder.Id, + new CreateWorkOrderUpliftRequestDto + { + Amount = 400m, + Notes = "Quote", + EvidenceDocumentId = document.Id, + }, + Dispatcher(), + CancellationToken.None); + + var revoked = await service.RevokeAsync( + workOrder.Id, + created!.Id, + new RevokeWorkOrderUpliftRequestDto(), + DispatcherRoleOnly(), + CancellationToken.None); + + Assert.Equal("revoked", revoked!.Status); + Assert.Equal("quote.pdf", revoked.EvidenceFileName); + var attachment = Assert.Single(revoked.Attachments); + Assert.Equal(document.Id, attachment.Id); + Assert.Equal("quote.pdf", attachment.Name); + } + [Fact] public async Task RevokeAsync_AdminOwnerRevokingAutoApproved_IsForbiddenAndKeepsAllowanceConsumed() { @@ -868,6 +939,10 @@ public sealed class WorkOrderUpliftServiceTests CancellationToken.None); Assert.Equal(document.Id, created!.EvidenceDocumentId); + Assert.Equal("quote.pdf", created.EvidenceFileName); + var attachment = Assert.Single(created.Attachments); + Assert.Equal(document.Id, attachment.Id); + Assert.Equal("quote.pdf", attachment.Name); Assert.Equal(document.Id, Assert.Single(context.DispatchUpliftRequests).EvidenceDocumentId); } @@ -942,6 +1017,10 @@ public sealed class WorkOrderUpliftServiceTests CancellationToken.None); Assert.Equal(quote.Id, created!.EvidenceDocumentId); + Assert.Equal("quote.pdf", created.EvidenceFileName); + Assert.Equal( + new[] { "quote.pdf", "photo.jpg" }, + created.Attachments.Select(file => file.Name).ToArray()); Assert.Equal(2, context.DispatchUpliftRequestDocuments.Count()); var listed = await service.ListAsync(workOrder.Id, Dispatcher(), CancellationToken.None); var names = Assert.Single(listed!).Attachments.Select(file => file.Name).OrderBy(name => name).ToArray();