mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-04 19:32:10 +00:00
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:
parent
af593fe2d2
commit
14443aff18
2 changed files with 41 additions and 10 deletions
|
|
@ -69,6 +69,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
_mutationData.SetExpectedWorkOrderVersion(workOrder, workOrderVersion);
|
_mutationData.SetExpectedWorkOrderVersion(workOrder, workOrderVersion);
|
||||||
|
|
||||||
Dispatch? dispatch = null;
|
Dispatch? dispatch = null;
|
||||||
|
ResolvedDispatch? resolved = null;
|
||||||
if (WorkOrderBoardFieldNames.IsDispatchField(canonicalField))
|
if (WorkOrderBoardFieldNames.IsDispatchField(canonicalField))
|
||||||
{
|
{
|
||||||
var vendorIdHint = canonicalField.Equals(WorkOrderBoardFieldNames.VendorId, StringComparison.OrdinalIgnoreCase)
|
var vendorIdHint = canonicalField.Equals(WorkOrderBoardFieldNames.VendorId, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|
@ -77,7 +78,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
|
|
||||||
var hadPrimaryDispatch = workOrder.PrimaryDispatchId.HasValue;
|
var hadPrimaryDispatch = workOrder.PrimaryDispatchId.HasValue;
|
||||||
var replaceInactivePrimary = vendorIdHint.HasValue;
|
var replaceInactivePrimary = vendorIdHint.HasValue;
|
||||||
var resolved = await ResolveDispatchAsync(
|
resolved = await ResolveDispatchAsync(
|
||||||
workOrder,
|
workOrder,
|
||||||
request.PrimaryDispatchId,
|
request.PrimaryDispatchId,
|
||||||
vendorIdHint,
|
vendorIdHint,
|
||||||
|
|
@ -108,14 +109,30 @@ namespace SeaHaven.Services.Implementation
|
||||||
|
|
||||||
var auditField = WorkOrderBoardFieldNames.ToAuditFieldName(canonicalField);
|
var auditField = WorkOrderBoardFieldNames.ToAuditFieldName(canonicalField);
|
||||||
var changes = await ApplyFieldMutationAsync(canonicalField, workOrder, dispatch, request.Value, auditField);
|
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())
|
if (changes.Count == 0 && !_mutationData.HasPendingChanges())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (changes.Count == 0)
|
if (changes.Count == 0)
|
||||||
{
|
{
|
||||||
await _mutationData.SaveAsync(ct);
|
await SaveTrackedOrThrowAsync(workOrderId, ct);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,12 +153,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var outcome = await _mutationData.SaveAsync(ct);
|
await SaveTrackedOrThrowAsync(workOrderId, ct);
|
||||||
if (outcome == BoardSaveOutcome.ConcurrencyConflict)
|
|
||||||
{
|
|
||||||
var currentState = await LoadBoardRowAsync(workOrderId);
|
|
||||||
throw new WorkOrderBoardConcurrencyException(currentState);
|
|
||||||
}
|
|
||||||
}, CancellationToken.None);
|
}, CancellationToken.None);
|
||||||
|
|
||||||
var row = await LoadBoardRowAsync(workOrderId);
|
var row = await LoadBoardRowAsync(workOrderId);
|
||||||
|
|
@ -211,7 +223,7 @@ namespace SeaHaven.Services.Implementation
|
||||||
CreatedDate = DateTime.UtcNow
|
CreatedDate = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
_mutationData.TrackNewDispatch(created);
|
_mutationData.TrackNewDispatch(created);
|
||||||
await _mutationData.SaveAsync(cancellationToken);
|
await SaveTrackedOrThrowAsync(workOrder.Id, cancellationToken);
|
||||||
workOrder.PrimaryDispatchId = created.Id;
|
workOrder.PrimaryDispatchId = created.Id;
|
||||||
return new ResolvedDispatch(created, Created: true, Previous: existing);
|
return new ResolvedDispatch(created, Created: true, Previous: existing);
|
||||||
}
|
}
|
||||||
|
|
@ -250,6 +262,16 @@ namespace SeaHaven.Services.Implementation
|
||||||
throw new WorkOrderBoardValidationException("VendorNotFound", "vendorId does not exist.");
|
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)
|
private async Task<FieldChange> ApplyWoNumber(WorkOrder workOrder, string? value, string auditField)
|
||||||
{
|
{
|
||||||
if (!WorkOrderNumberNormalizer.TryNormalize(value, out var normalized, out var error))
|
if (!WorkOrderNumberNormalizer.TryNormalize(value, out var normalized, out var error))
|
||||||
|
|
|
||||||
|
|
@ -380,6 +380,15 @@ public class WorkOrderBoardUpdateServiceTests
|
||||||
var leftover = await context.Dispatches.SingleAsync(d => d.Id == 10);
|
var leftover = await context.Dispatches.SingleAsync(d => d.Id == 10);
|
||||||
Assert.Equal(1, leftover.VendorId);
|
Assert.Equal(1, leftover.VendorId);
|
||||||
Assert.Equal(inactiveStatus, leftover.Status);
|
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]
|
[Theory]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue