Merge remote-tracking branch 'origin/codex/backend-layering-refactor' into codex/backend-governance-quality-system

This commit is contained in:
Alexandre Brandizzi 2026-07-24 16:42:56 -03:00
commit 332b7e713e

View file

@ -33,98 +33,98 @@ namespace SeaHaven.Services.Implementation
{ {
await _mutationData.ExecuteTransactionalAsync(async ct => await _mutationData.ExecuteTransactionalAsync(async ct =>
{ {
if (string.IsNullOrWhiteSpace(request.Field)) if (string.IsNullOrWhiteSpace(request.Field))
throw new WorkOrderBoardValidationException("InvalidField", "Field is required."); throw new WorkOrderBoardValidationException("InvalidField", "Field is required.");
var field = request.Field.Trim(); var field = request.Field.Trim();
var canonicalField = WorkOrderBoardFieldNames.Canonicalize(field); var canonicalField = WorkOrderBoardFieldNames.Canonicalize(field);
if (canonicalField == null) if (canonicalField == null)
throw new WorkOrderBoardValidationException("InvalidField", $"Field '{field}' is not editable."); throw new WorkOrderBoardValidationException("InvalidField", $"Field '{field}' is not editable.");
var workOrderVersion = ParseRowVersion(request.WorkOrderVersion, "workOrderVersion"); var workOrderVersion = ParseRowVersion(request.WorkOrderVersion, "workOrderVersion");
if (workOrderVersion == null) if (workOrderVersion == null)
throw new WorkOrderBoardValidationException("WorkOrderVersionRequired", "workOrderVersion is required."); throw new WorkOrderBoardValidationException("WorkOrderVersionRequired", "workOrderVersion is required.");
var dispatchVersion = ParseRowVersion(request.DispatchVersion, "dispatchVersion"); var dispatchVersion = ParseRowVersion(request.DispatchVersion, "dispatchVersion");
var workOrder = await _mutationData.GetTrackedWorkOrderAsync(workOrderId, ct); var workOrder = await _mutationData.GetTrackedWorkOrderAsync(workOrderId, ct);
if (workOrder == null) if (workOrder == null)
throw new WorkOrderBoardValidationException("NotFound", "Work order not found."); throw new WorkOrderBoardValidationException("NotFound", "Work order not found.");
if (!RowVersionsMatch(workOrder.RowVersion, workOrderVersion)) if (!RowVersionsMatch(workOrder.RowVersion, workOrderVersion))
{
var staleState = await LoadBoardRowAsync(workOrderId);
throw new WorkOrderBoardConcurrencyException(staleState);
}
if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus))
throw new WorkOrderBoardValidationException("CanceledReadOnly", "Work order is read-only in its current status.");
var isPastDue = WorkOrderDerivedFields.IsPastDue(workOrder.ScheduledDate, workOrder.LifecycleStatus);
if (WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(canonicalField, isPastDue))
throw new WorkOrderBoardValidationException("PastDueStatusBlocked", "Status cannot be changed while work order is past due. Reschedule first.");
_mutationData.SetExpectedWorkOrderVersion(workOrder, workOrderVersion);
Dispatch? dispatch = null;
if (WorkOrderBoardFieldNames.IsDispatchField(canonicalField))
{
var vendorIdHint = canonicalField.Equals(WorkOrderBoardFieldNames.VendorId, StringComparison.OrdinalIgnoreCase)
? ParseVendorIdHint(request.Value)
: null;
var hadPrimaryDispatch = workOrder.PrimaryDispatchId.HasValue;
dispatch = await ResolveDispatchAsync(workOrder, request.PrimaryDispatchId, vendorIdHint, ct);
if (dispatchVersion != null && !RowVersionsMatch(dispatch.RowVersion, dispatchVersion))
{ {
var staleState = await LoadBoardRowAsync(workOrderId); var staleState = await LoadBoardRowAsync(workOrderId);
throw new WorkOrderBoardConcurrencyException(staleState); throw new WorkOrderBoardConcurrencyException(staleState);
} }
if (dispatchVersion != null) if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus))
_mutationData.SetExpectedDispatchVersion(dispatch, dispatchVersion); throw new WorkOrderBoardValidationException("CanceledReadOnly", "Work order is read-only in its current status.");
else if (hadPrimaryDispatch && dispatch.RowVersion != null)
throw new WorkOrderBoardValidationException("DispatchVersionRequired", "dispatchVersion is required for dispatch fields.");
}
var auditField = WorkOrderBoardFieldNames.ToAuditFieldName(canonicalField); var isPastDue = WorkOrderDerivedFields.IsPastDue(workOrder.ScheduledDate, workOrder.LifecycleStatus);
var changes = await ApplyFieldMutationAsync(canonicalField, workOrder, dispatch, request.Value, auditField); if (WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(canonicalField, isPastDue))
changes = changes.Where(c => c.HasChanged).ToList(); throw new WorkOrderBoardValidationException("PastDueStatusBlocked", "Status cannot be changed while work order is past due. Reschedule first.");
if (changes.Count == 0 && !_mutationData.HasPendingChanges()) _mutationData.SetExpectedWorkOrderVersion(workOrder, workOrderVersion);
return;
if (changes.Count == 0) Dispatch? dispatch = null;
{ if (WorkOrderBoardFieldNames.IsDispatchField(canonicalField))
await _mutationData.SaveAsync(ct);
return;
}
foreach (var change in changes)
{
var dispatchId = change.DispatchId ?? (dispatch?.Id > 0 ? dispatch.Id : null);
switch (change.Action)
{ {
case AuditActionType.StatusChanged: var vendorIdHint = canonicalField.Equals(WorkOrderBoardFieldNames.VendorId, StringComparison.OrdinalIgnoreCase)
await _auditService.StageStatusChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId); ? ParseVendorIdHint(request.Value)
break; : null;
case AuditActionType.AssignmentChanged:
await _auditService.StageAssignmentChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId);
break;
default:
await _auditService.StageFieldChangedAsync(workOrderId, change.FieldName, change.OldValue, change.NewValue, actorId, dispatchId);
break;
}
}
var outcome = await _mutationData.SaveAsync(ct); var hadPrimaryDispatch = workOrder.PrimaryDispatchId.HasValue;
if (outcome == BoardSaveOutcome.ConcurrencyConflict) dispatch = await ResolveDispatchAsync(workOrder, request.PrimaryDispatchId, vendorIdHint, ct);
{
var currentState = await LoadBoardRowAsync(workOrderId); if (dispatchVersion != null && !RowVersionsMatch(dispatch.RowVersion, dispatchVersion))
throw new WorkOrderBoardConcurrencyException(currentState); {
} var staleState = await LoadBoardRowAsync(workOrderId);
throw new WorkOrderBoardConcurrencyException(staleState);
}
if (dispatchVersion != null)
_mutationData.SetExpectedDispatchVersion(dispatch, dispatchVersion);
else if (hadPrimaryDispatch && dispatch.RowVersion != null)
throw new WorkOrderBoardValidationException("DispatchVersionRequired", "dispatchVersion is required for dispatch fields.");
}
var auditField = WorkOrderBoardFieldNames.ToAuditFieldName(canonicalField);
var changes = await ApplyFieldMutationAsync(canonicalField, workOrder, dispatch, request.Value, auditField);
changes = changes.Where(c => c.HasChanged).ToList();
if (changes.Count == 0 && !_mutationData.HasPendingChanges())
return;
if (changes.Count == 0)
{
await _mutationData.SaveAsync(ct);
return;
}
foreach (var change in changes)
{
var dispatchId = change.DispatchId ?? (dispatch?.Id > 0 ? dispatch.Id : null);
switch (change.Action)
{
case AuditActionType.StatusChanged:
await _auditService.StageStatusChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId);
break;
case AuditActionType.AssignmentChanged:
await _auditService.StageAssignmentChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId);
break;
default:
await _auditService.StageFieldChangedAsync(workOrderId, change.FieldName, change.OldValue, change.NewValue, actorId, dispatchId);
break;
}
}
var outcome = await _mutationData.SaveAsync(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);