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.
This commit is contained in:
Alexandre Brandizzi 2026-08-19 13:48:30 -03:00
parent 7a0856ddf7
commit 6c16ce7047
2 changed files with 35 additions and 6 deletions

View file

@ -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<ILogger<VendorService>>());
[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<ILogger<VendorService>>());
var facets = await service.GetFacetsAsync(null, CancellationToken.None);

View file

@ -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<VendorService> _logger;
public VendorService(
IVendorDataService vendorDataService,
@ -33,7 +35,8 @@ namespace SeaHaven.Services.Implementation
IOptions<FrontendOptions> frontendOptions,
ICreateVendorValidation createValidator,
IUpdateVendorValidation updateValidator,
IWorkOrderVendorUpdateValidation workOrderUpdateValidator)
IWorkOrderVendorUpdateValidation workOrderUpdateValidator,
ILogger<VendorService> logger)
{
_vendorDataService = vendorDataService;
_vendorTokenService = vendorTokenService;
@ -42,6 +45,7 @@ namespace SeaHaven.Services.Implementation
_createValidator = createValidator;
_updateValidator = updateValidator;
_workOrderUpdateValidator = workOrderUpdateValidator;
_logger = logger;
}
public async Task<VendorDTO?> 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.