From 36d0a638a2991e6833556a2cbe4076ea2bf22f86 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Tue, 28 Jul 2026 14:19:15 -0300 Subject: [PATCH] fix(vendors): return completion documents in portal detail --- .../VendorPortalDocumentTests.cs | 137 +++++++++++++++++- .../VendorDocumentDataService.cs | 10 ++ .../Interfaces/IVendorDocumentDataService.cs | 1 + .../DTOs/VendorPortalServiceDTOs.cs | 18 +++ .../Implementation/VendorPortalService.cs | 21 ++- 5 files changed, 183 insertions(+), 4 deletions(-) diff --git a/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs b/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs index 33ef432..3e20aa3 100644 --- a/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs +++ b/Api.SeaHavenIndustries.Tests/VendorPortalDocumentTests.cs @@ -35,6 +35,11 @@ public sealed class VendorPortalDocumentTests : IDisposable } private VendorPortalController NewController(ApplicationDbContext context) + { + return NewController(NewService(context)); + } + + private VendorPortalService NewService(ApplicationDbContext context) { var vendorData = new VendorDataService(context); var tokenService = new VendorPortalTokenService( @@ -45,11 +50,18 @@ public sealed class VendorPortalDocumentTests : IDisposable storageEnvironment.SetupGet(item => item.ContentRootPath).Returns(_contentRoot); var storage = new VendorDocumentStorageAdapter(storageEnvironment.Object); - var service = new VendorPortalService( + var upliftData = new Mock(); + upliftData.Setup(u => u.GetForVendorDispatchAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + var commentData = new Mock(); + commentData.Setup(c => c.GetVendorViewableForDispatchAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new List()); + + return new VendorPortalService( tokenService, new DispatchDataService(context), - Mock.Of(), - Mock.Of(), + upliftData.Object, + commentData.Object, Mock.Of(), Mock.Of(), documentData, @@ -57,7 +69,10 @@ public sealed class VendorPortalDocumentTests : IDisposable Microsoft.Extensions.Options.Options.Create(new FrontendOptions()), Microsoft.Extensions.Options.Options.Create(new ApprovalsOptions()), Microsoft.Extensions.Options.Options.Create(new VendorDocumentsOptions())); + } + private static VendorPortalController NewController(VendorPortalService service) + { var controller = new VendorPortalController(service, Mock.Of>()); controller.ControllerContext = new ControllerContext { @@ -292,6 +307,122 @@ public sealed class VendorPortalDocumentTests : IDisposable context.VendorCompletionDocuments.Should().HaveCount(1); } + [Fact] + public async Task GetDispatchDetail_ReturnsUploadedDocumentWithQuarantineAndReplacementMetadata() + { + using var context = NewContext(); + var (_, dispatch) = await SeedDispatch(context); + var service = NewService(context); + var pdf = "%PDF-1.4\nvendor completion"u8.ToArray(); + + await NewController(service).UploadCompletionDocument( + dispatch.Id, + FormFile(pdf, "completion-v1.pdf", "application/pdf")); + + var session = await service.ResolveSessionAsync(Token, CancellationToken.None); + var firstDetail = await service.GetDispatchDetailAsync(session!, dispatch.Id, CancellationToken.None); + + firstDetail.Should().NotBeNull(); + var firstDoc = firstDetail!.Documents.Should().ContainSingle().Subject; + firstDoc.OriginalFileName.Should().Be("completion-v1.pdf"); + firstDoc.ContentType.Should().Be("application/pdf"); + firstDoc.SizeBytes.Should().Be(pdf.Length); + firstDoc.ScanStatus.Should().Be("Pending"); + firstDoc.ReviewStatus.Should().Be("Processing"); + firstDoc.Version.Should().Be(1); + firstDoc.ReplacesDocumentId.Should().BeNull(); + firstDoc.CanDownload.Should().BeFalse(); + firstDoc.CreatedDate.Should().NotBeNull(); + + var first = await context.VendorCompletionDocuments.SingleAsync(); + await NewController(service).UploadCompletionDocument( + dispatch.Id, + FormFile(pdf, "completion-v2.pdf", "application/pdf"), + first.Id); + + var secondDetail = await service.GetDispatchDetailAsync(session!, dispatch.Id, CancellationToken.None); + var ordered = secondDetail!.Documents.OrderByDescending(d => d.Version).ToList(); + ordered.Should().HaveCount(2); + ordered[0].Version.Should().Be(2); + ordered[0].ReplacesDocumentId.Should().Be(first.Id); + ordered[0].OriginalFileName.Should().Be("completion-v2.pdf"); + ordered[1].Version.Should().Be(1); + ordered[1].ReplacesDocumentId.Should().BeNull(); + } + + [Fact] + public async Task GetDispatchDetail_EnablesDownloadOnlyWhenScanPasses() + { + using var context = NewContext(); + var (_, dispatch) = await SeedDispatch(context); + var service = NewService(context); + var pdf = "%PDF-1.4\nvendor completion"u8.ToArray(); + + await NewController(service).UploadCompletionDocument( + dispatch.Id, + FormFile(pdf, "completion.pdf", "application/pdf")); + + var document = await context.VendorCompletionDocuments.SingleAsync(); + document.ScanStatus = "Passed"; + document.ScannedAt = DateTime.UtcNow; + await context.SaveChangesAsync(); + + var session = await service.ResolveSessionAsync(Token, CancellationToken.None); + var detail = await service.GetDispatchDetailAsync(session!, dispatch.Id, CancellationToken.None); + + var doc = detail!.Documents.Should().ContainSingle().Subject; + doc.ScanStatus.Should().Be("Passed"); + doc.CanDownload.Should().BeTrue(); + doc.ScannedAt.Should().NotBeNull(); + } + + [Theory] + [InlineData("Verified")] + [InlineData("Cancelled")] + public async Task GetDispatchDetail_DisablesDownloadForLockedDispatches(string status) + { + using var context = NewContext(); + var (_, dispatch) = await SeedDispatch(context); + var service = NewService(context); + var pdf = "%PDF-1.4\nvendor completion"u8.ToArray(); + + await NewController(service).UploadCompletionDocument( + dispatch.Id, + FormFile(pdf, "completion.pdf", "application/pdf")); + + var document = await context.VendorCompletionDocuments.SingleAsync(); + document.ScanStatus = "Passed"; + dispatch.Status = status; + await context.SaveChangesAsync(); + + var session = await service.ResolveSessionAsync(Token, CancellationToken.None); + var detail = await service.GetDispatchDetailAsync(session!, dispatch.Id, CancellationToken.None); + + detail!.Documents.Should().ContainSingle().Which.CanDownload.Should().BeFalse(); + } + + [Fact] + public async Task GetDispatchDetail_ExcludesSoftDeletedDocuments() + { + using var context = NewContext(); + var (_, dispatch) = await SeedDispatch(context); + var service = NewService(context); + var pdf = "%PDF-1.4\nvendor completion"u8.ToArray(); + + await NewController(service).UploadCompletionDocument( + dispatch.Id, + FormFile(pdf, "completion.pdf", "application/pdf")); + + var document = await context.VendorCompletionDocuments.SingleAsync(); + document.IsDeleted = true; + await context.SaveChangesAsync(); + + var session = await service.ResolveSessionAsync(Token, CancellationToken.None); + var detail = await service.GetDispatchDetailAsync(session!, dispatch.Id, CancellationToken.None); + + detail!.Documents.Should().BeEmpty(); + } + public void Dispose() { if (Directory.Exists(_contentRoot)) diff --git a/SeaHaven.DataServices/Implementation/VendorDocumentDataService.cs b/SeaHaven.DataServices/Implementation/VendorDocumentDataService.cs index 2ee5192..05aa4e9 100644 --- a/SeaHaven.DataServices/Implementation/VendorDocumentDataService.cs +++ b/SeaHaven.DataServices/Implementation/VendorDocumentDataService.cs @@ -30,6 +30,16 @@ namespace SeaHaven.DataServices.Implementation && (document.IsDeleted == null || document.IsDeleted == false), cancellationToken); } + public Task> ListForVendorDispatchAsync(int dispatchId, int vendorId, CancellationToken cancellationToken) + { + return _context.VendorCompletionDocuments + .Where(document => document.DispatchId == dispatchId + && document.VendorId == vendorId + && (document.IsDeleted == null || document.IsDeleted == false)) + .OrderByDescending(document => document.Version) + .ToListAsync(cancellationToken); + } + public async Task AddAsync(VendorCompletionDocument document, CancellationToken cancellationToken) { await _context.VendorCompletionDocuments.AddAsync(document, cancellationToken); diff --git a/SeaHaven.DataServices/Interfaces/IVendorDocumentDataService.cs b/SeaHaven.DataServices/Interfaces/IVendorDocumentDataService.cs index 5849b51..d6c76a6 100644 --- a/SeaHaven.DataServices/Interfaces/IVendorDocumentDataService.cs +++ b/SeaHaven.DataServices/Interfaces/IVendorDocumentDataService.cs @@ -6,6 +6,7 @@ namespace SeaHaven.DataServices.Interfaces { Task GetLatestForDispatchAsync(int dispatchId, CancellationToken cancellationToken); Task GetForVendorDispatchAsync(int documentId, int dispatchId, int vendorId, CancellationToken cancellationToken); + Task> ListForVendorDispatchAsync(int dispatchId, int vendorId, CancellationToken cancellationToken); Task AddAsync(VendorCompletionDocument document, CancellationToken cancellationToken); Task SaveChangesAsync(CancellationToken cancellationToken); } diff --git a/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs b/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs index 27cc8e7..0fe7040 100644 --- a/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs +++ b/SeaHaven.Services/DTOs/VendorPortalServiceDTOs.cs @@ -109,6 +109,24 @@ namespace SeaHaven.Services.DTOs public IEnumerable Signoffs { get; set; } = Enumerable.Empty(); public IEnumerable Comments { get; set; } = Enumerable.Empty(); public IEnumerable UpliftRequests { get; set; } = Enumerable.Empty(); + public IEnumerable Documents { get; set; } = Enumerable.Empty(); + } + + public class VendorPortalDocumentDTO + { + public int Id { get; set; } + public string? OriginalFileName { get; set; } + public string? ContentType { get; set; } + public long SizeBytes { get; set; } + public string? ScanStatus { get; set; } + public string? ReviewStatus { get; set; } + public string? RejectionReason { get; set; } + public int Version { get; set; } + public int? ReplacesDocumentId { get; set; } + public DateTime? CreatedDate { get; set; } + public DateTime? ScannedAt { get; set; } + public DateTime? ReviewedAt { get; set; } + public bool CanDownload { get; set; } } public class AcceptDispatchResultDTO diff --git a/SeaHaven.Services/Implementation/VendorPortalService.cs b/SeaHaven.Services/Implementation/VendorPortalService.cs index b3b2362..3606639 100644 --- a/SeaHaven.Services/Implementation/VendorPortalService.cs +++ b/SeaHaven.Services/Implementation/VendorPortalService.cs @@ -90,6 +90,7 @@ namespace SeaHaven.Services.Implementation var signoffs = await _dispatchData.GetPortalSignoffsAsync(id, cancellationToken); var rawUplifts = await _upliftData.GetForVendorDispatchAsync(id, cancellationToken); var rawComments = await _commentData.GetVendorViewableForDispatchAsync(id, cancellationToken); + var rawDocuments = await _documentData.ListForVendorDispatchAsync(id, session.Id, cancellationToken); var upliftRequests = rawUplifts.Select(u => new PortalUpliftDTO { @@ -192,7 +193,25 @@ namespace SeaHaven.Services.Implementation SignedAt = s.SignedAt }), Comments = comments, - UpliftRequests = upliftRequests + UpliftRequests = upliftRequests, + Documents = rawDocuments.Select(document => new VendorPortalDocumentDTO + { + Id = document.Id, + OriginalFileName = document.OriginalFileName, + ContentType = document.ContentType, + SizeBytes = document.SizeBytes, + ScanStatus = document.ScanStatus, + ReviewStatus = document.ReviewStatus, + RejectionReason = document.RejectionReason, + Version = document.Version, + ReplacesDocumentId = document.ReplacesDocumentId, + CreatedDate = document.CreatedDate, + ScannedAt = document.ScannedAt, + ReviewedAt = document.ReviewedAt, + CanDownload = string.Equals(document.ScanStatus, "Passed", StringComparison.OrdinalIgnoreCase) + && dispatch.Status != "Verified" + && dispatch.Status != "Cancelled" + }) }; }