From 6c16ce70477f11676a65e1af95ed869082edba25 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Wed, 19 Aug 2026 13:48:30 -0300 Subject: [PATCH] feat(vendors): record deactivations confirmed past open work orders SH-44's user story is about not silently orphaning active work. The confirmation dialog tells the operator, but nothing told the system, so a deactivation that left work orders open was indistinguishable from one that had none. VendorService now takes an ILogger and writes a warning naming the vendor, the user and the number of work orders left open whenever the guard is cleared by confirmation. Both the update and delete paths are covered; nothing is logged when there was nothing to leave open. --- .../VendorServiceTests.cs | 7 ++-- .../Implementation/VendorService.cs | 34 ++++++++++++++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/Api.SeaHavenIndustries.Tests/VendorServiceTests.cs b/Api.SeaHavenIndustries.Tests/VendorServiceTests.cs index 24c4ea2..ba42cb5 100644 --- a/Api.SeaHavenIndustries.Tests/VendorServiceTests.cs +++ b/Api.SeaHavenIndustries.Tests/VendorServiceTests.cs @@ -1,3 +1,4 @@ +using Microsoft.Extensions.Logging; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using FluentAssertions; @@ -27,7 +28,8 @@ public class VendorServiceTests Microsoft.Extensions.Options.Options.Create(new FrontendOptions()), new CreateVendorValidation(), new UpdateVendorValidation(), - new WorkOrderVendorUpdateValidation()); + new WorkOrderVendorUpdateValidation(), + Mock.Of>()); [Fact] public async Task CreateVendor_PersistsCompleteProfile() @@ -758,7 +760,8 @@ public class VendorServiceTests Microsoft.Extensions.Options.Options.Create(new FrontendOptions()), new CreateVendorValidation(), new UpdateVendorValidation(), - new WorkOrderVendorUpdateValidation()); + new WorkOrderVendorUpdateValidation(), + Mock.Of>()); var facets = await service.GetFacetsAsync(null, CancellationToken.None); diff --git a/SeaHaven.Services/Implementation/VendorService.cs b/SeaHaven.Services/Implementation/VendorService.cs index cadddfe..8765fb3 100644 --- a/SeaHaven.Services/Implementation/VendorService.cs +++ b/SeaHaven.Services/Implementation/VendorService.cs @@ -2,6 +2,7 @@ using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using FluentValidation; using FluentValidation.Results; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SeaHaven.DataServices.Interfaces; using SeaHaven.DataServices.Models; @@ -25,6 +26,7 @@ namespace SeaHaven.Services.Implementation private readonly ICreateVendorValidation _createValidator; private readonly IUpdateVendorValidation _updateValidator; private readonly IWorkOrderVendorUpdateValidation _workOrderUpdateValidator; + private readonly ILogger _logger; public VendorService( IVendorDataService vendorDataService, @@ -33,7 +35,8 @@ namespace SeaHaven.Services.Implementation IOptions frontendOptions, ICreateVendorValidation createValidator, IUpdateVendorValidation updateValidator, - IWorkOrderVendorUpdateValidation workOrderUpdateValidator) + IWorkOrderVendorUpdateValidation workOrderUpdateValidator, + ILogger logger) { _vendorDataService = vendorDataService; _vendorTokenService = vendorTokenService; @@ -42,6 +45,7 @@ namespace SeaHaven.Services.Implementation _createValidator = createValidator; _updateValidator = updateValidator; _workOrderUpdateValidator = workOrderUpdateValidator; + _logger = logger; } public async Task GetVendorByIdAsync(int id) @@ -193,8 +197,13 @@ namespace SeaHaven.Services.Implementation if (vendor == null) throw new InvalidOperationException($"Vendor with ID {id} not found"); - if (dto.IsActive.HasValue && !dto.IsActive.Value && !dto.ConfirmOpenWorkOrders) - await AssertNoOpenLinkedWorkOrdersAsync(id); + if (dto.IsActive.HasValue && !dto.IsActive.Value) + { + if (dto.ConfirmOpenWorkOrders) + await LogConfirmedDeactivationAsync(id, userId); + else + await AssertNoOpenLinkedWorkOrdersAsync(id); + } if (dto.Name != null) vendor.CompanyName = dto.Name; if (dto.ContactName != null) vendor.ContactName = dto.ContactName; @@ -230,7 +239,9 @@ namespace SeaHaven.Services.Implementation if (vendor == null) throw new InvalidOperationException($"Vendor with ID {id} not found"); - if (!confirmOpenWorkOrders) + if (confirmOpenWorkOrders) + await LogConfirmedDeactivationAsync(id, userId); + else await AssertNoOpenLinkedWorkOrdersAsync(id); vendor.IsActive = false; @@ -503,6 +514,21 @@ namespace SeaHaven.Services.Implementation dto.CompanyPhone = VendorPhoneNormalizer.NormalizeToCanonical(dto.CompanyPhone); } + // SH-44's story is about not *silently* orphaning active work. When the caller + // confirms past the guard, the work orders it chose to leave open are recorded. + private async Task LogConfirmedDeactivationAsync(int vendorId, string userId) + { + var linked = await _vendorDataService.GetLinkedWorkOrdersAsync(vendorId); + var openCount = linked.Count(wo => !IsTerminalWorkOrderStatus(wo.LifecycleStatus, wo.Status)); + if (openCount == 0) return; + + _logger.LogWarning( + "Vendor {VendorId} deactivated by user {UserId} with {OpenWorkOrderCount} open work orders, confirmed by the caller.", + vendorId, + userId, + openCount); + } + // Guard for the unconfirmed path only. SH-44 and SH-82 left "blocks or requires // explicit confirmation" to be settled with the team; SH-254 settles it as // explicit confirmation, so a caller that has not confirmed is still blocked.