fix(work-orders): audit and lock vendor when replacing an inactive primary

Creating a Pending dispatch now stages VendorId FieldChanged from the previous
assignment so field lock and concurrency checks run like a live vendor PATCH.
This commit is contained in:
Arthur Bassi 2026-08-20 15:09:34 -03:00
parent af593fe2d2
commit 14443aff18
2 changed files with 41 additions and 10 deletions

View file

@ -69,6 +69,7 @@ namespace SeaHaven.Services.Implementation
_mutationData.SetExpectedWorkOrderVersion(workOrder, workOrderVersion);
Dispatch? dispatch = null;
ResolvedDispatch? resolved = null;
if (WorkOrderBoardFieldNames.IsDispatchField(canonicalField))
{
var vendorIdHint = canonicalField.Equals(WorkOrderBoardFieldNames.VendorId, StringComparison.OrdinalIgnoreCase)
@ -77,7 +78,7 @@ namespace SeaHaven.Services.Implementation
var hadPrimaryDispatch = workOrder.PrimaryDispatchId.HasValue;
var replaceInactivePrimary = vendorIdHint.HasValue;
var resolved = await ResolveDispatchAsync(
resolved = await ResolveDispatchAsync(
workOrder,
request.PrimaryDispatchId,
vendorIdHint,
@ -108,14 +109,30 @@ namespace SeaHaven.Services.Implementation
var auditField = WorkOrderBoardFieldNames.ToAuditFieldName(canonicalField);
var changes = await ApplyFieldMutationAsync(canonicalField, workOrder, dispatch, request.Value, auditField);
changes = changes.Where(c => c.HasChanged).ToList();
if (resolved is { Created: true, Dispatch: var createdDispatch }
&& canonicalField.Equals(WorkOrderBoardFieldNames.VendorId, StringComparison.OrdinalIgnoreCase))
{
var previousVendor = resolved.Previous?.VendorId.ToString() ?? "";
changes = new List<FieldChange>
{
FieldChange.ForField(
auditField,
previousVendor,
createdDispatch.VendorId.ToString(),
createdDispatch.Id)
};
}
else
{
changes = changes.Where(c => c.HasChanged).ToList();
}
if (changes.Count == 0 && !_mutationData.HasPendingChanges())
return;
if (changes.Count == 0)
{
await _mutationData.SaveAsync(ct);
await SaveTrackedOrThrowAsync(workOrderId, ct);
return;
}
@ -136,12 +153,7 @@ namespace SeaHaven.Services.Implementation
}
}
var outcome = await _mutationData.SaveAsync(ct);
if (outcome == BoardSaveOutcome.ConcurrencyConflict)
{
var currentState = await LoadBoardRowAsync(workOrderId);
throw new WorkOrderBoardConcurrencyException(currentState);
}
await SaveTrackedOrThrowAsync(workOrderId, ct);
}, CancellationToken.None);
var row = await LoadBoardRowAsync(workOrderId);
@ -211,7 +223,7 @@ namespace SeaHaven.Services.Implementation
CreatedDate = DateTime.UtcNow
};
_mutationData.TrackNewDispatch(created);
await _mutationData.SaveAsync(cancellationToken);
await SaveTrackedOrThrowAsync(workOrder.Id, cancellationToken);
workOrder.PrimaryDispatchId = created.Id;
return new ResolvedDispatch(created, Created: true, Previous: existing);
}
@ -250,6 +262,16 @@ namespace SeaHaven.Services.Implementation
throw new WorkOrderBoardValidationException("VendorNotFound", "vendorId does not exist.");
}
private async Task SaveTrackedOrThrowAsync(int workOrderId, CancellationToken cancellationToken)
{
var outcome = await _mutationData.SaveAsync(cancellationToken);
if (outcome != BoardSaveOutcome.ConcurrencyConflict)
return;
var currentState = await LoadBoardRowAsync(workOrderId);
throw new WorkOrderBoardConcurrencyException(currentState);
}
private async Task<FieldChange> ApplyWoNumber(WorkOrder workOrder, string? value, string auditField)
{
if (!WorkOrderNumberNormalizer.TryNormalize(value, out var normalized, out var error))

View file

@ -380,6 +380,15 @@ public class WorkOrderBoardUpdateServiceTests
var leftover = await context.Dispatches.SingleAsync(d => d.Id == 10);
Assert.Equal(1, leftover.VendorId);
Assert.Equal(inactiveStatus, leftover.Status);
var vendorAudit = Assert.Single(
await context.WorkOrderAuditLogs
.Where(a => a.Action == AuditActionType.FieldChanged.ToString() && a.FieldName == "VendorId")
.ToListAsync());
Assert.Equal("1", vendorAudit.OldValue);
Assert.Equal("5", vendorAudit.NewValue);
Assert.Equal(result.PrimaryDispatchId, vendorAudit.DispatchId);
Assert.True(await context.WorkOrderFieldLocks.AnyAsync(l => l.FieldName == "VendorId"));
}
[Theory]