mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 11:53:12 +00:00
fix(work-orders): address PR #14 board PATCH review feedback
Reuse field-lock DB checks during staged audits, defer dispatch creation to the final save, and return 404 for missing work orders.
This commit is contained in:
parent
91122d753d
commit
88caede86a
5 changed files with 94 additions and 41 deletions
|
|
@ -131,6 +131,14 @@ namespace Api.SeaHavenIndustries.Controllers
|
|||
CurrentState = ex.CurrentState
|
||||
});
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex) when (ex.Code == "NotFound")
|
||||
{
|
||||
return NotFound(new WorkOrderBoardValidationErrorDto
|
||||
{
|
||||
Code = ex.Code,
|
||||
Message = ex.Message
|
||||
});
|
||||
}
|
||||
catch (WorkOrderBoardValidationException ex)
|
||||
{
|
||||
return UnprocessableEntity(new WorkOrderBoardValidationErrorDto
|
||||
|
|
|
|||
|
|
@ -28,17 +28,17 @@ namespace SeaHaven.Services.Implementation
|
|||
=> LogAsync(workOrderId, AuditActionType.SyncRejected, AuditEventType.Sync,
|
||||
AuditActorType.Sync, fieldName, oldValue, newValue);
|
||||
|
||||
public void StageFieldChanged(int workOrderId, string fieldName, string? oldValue, string? newValue, string? actorId, int? dispatchId = null)
|
||||
=> StageAudit(workOrderId, AuditActionType.FieldChanged, fieldName, oldValue, newValue, actorId, dispatchId);
|
||||
public Task StageFieldChangedAsync(int workOrderId, string fieldName, string? oldValue, string? newValue, string? actorId, int? dispatchId = null)
|
||||
=> StageAuditAsync(workOrderId, AuditActionType.FieldChanged, fieldName, oldValue, newValue, actorId, dispatchId);
|
||||
|
||||
public void StageStatusChanged(int workOrderId, string? oldStatus, string? newStatus, string? actorId)
|
||||
=> StageAudit(workOrderId, AuditActionType.StatusChanged, "LifecycleStatus", oldStatus, newStatus, actorId);
|
||||
public Task StageStatusChangedAsync(int workOrderId, string? oldStatus, string? newStatus, string? actorId)
|
||||
=> StageAuditAsync(workOrderId, AuditActionType.StatusChanged, "LifecycleStatus", oldStatus, newStatus, actorId);
|
||||
|
||||
public void StageAssignmentChanged(int workOrderId, string? oldValue, string? newValue, string? actorId)
|
||||
=> StageAudit(workOrderId, AuditActionType.AssignmentChanged, "AssignTo", oldValue, newValue, actorId);
|
||||
public Task StageAssignmentChangedAsync(int workOrderId, string? oldValue, string? newValue, string? actorId)
|
||||
=> StageAuditAsync(workOrderId, AuditActionType.AssignmentChanged, "AssignTo", oldValue, newValue, actorId);
|
||||
|
||||
public void StageCreated(int workOrderId, string? actorId, string? woNumber)
|
||||
=> StageAudit(workOrderId, AuditActionType.Create, "WorkOrder", null, woNumber, actorId);
|
||||
public Task StageCreatedAsync(int workOrderId, string? actorId, string? woNumber)
|
||||
=> StageAuditAsync(workOrderId, AuditActionType.Create, "WorkOrder", null, woNumber, actorId);
|
||||
|
||||
public async Task LogAsync(
|
||||
int workOrderId,
|
||||
|
|
@ -65,7 +65,7 @@ namespace SeaHaven.Services.Implementation
|
|||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private void StageAudit(
|
||||
private async Task StageAuditAsync(
|
||||
int workOrderId,
|
||||
AuditActionType action,
|
||||
string fieldName,
|
||||
|
|
@ -76,7 +76,13 @@ namespace SeaHaven.Services.Implementation
|
|||
{
|
||||
AddAuditEntry(workOrderId, action, AuditEventType.Manual, AuditActorType.Dispatcher,
|
||||
fieldName, oldValue, newValue, actorId, dispatchId);
|
||||
StageFieldLock(workOrderId, fieldName, actorId);
|
||||
|
||||
if (action == AuditActionType.FieldChanged ||
|
||||
action == AuditActionType.StatusChanged ||
|
||||
action == AuditActionType.AssignmentChanged)
|
||||
{
|
||||
await _fieldLocks.LockFieldAsync(workOrderId, fieldName, actorId);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddAuditEntry(
|
||||
|
|
@ -107,21 +113,5 @@ namespace SeaHaven.Services.Implementation
|
|||
});
|
||||
}
|
||||
|
||||
private void StageFieldLock(int workOrderId, string fieldName, string? userId)
|
||||
{
|
||||
var alreadyTracked = _context.WorkOrderFieldLocks.Local
|
||||
.Any(l => l.WorkOrderId == workOrderId && l.FieldName == fieldName);
|
||||
|
||||
if (alreadyTracked)
|
||||
return;
|
||||
|
||||
_context.WorkOrderFieldLocks.Add(new WorkOrderFieldLock
|
||||
{
|
||||
WorkOrderId = workOrderId,
|
||||
FieldName = fieldName,
|
||||
LockedAt = DateTime.UtcNow,
|
||||
LockedByUserId = userId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,16 +106,17 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
foreach (var change in changes)
|
||||
{
|
||||
var dispatchId = change.DispatchId ?? (dispatch?.Id > 0 ? dispatch.Id : null);
|
||||
switch (change.Action)
|
||||
{
|
||||
case AuditActionType.StatusChanged:
|
||||
_auditService.StageStatusChanged(workOrderId, change.OldValue, change.NewValue, actorId);
|
||||
await _auditService.StageStatusChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId);
|
||||
break;
|
||||
case AuditActionType.AssignmentChanged:
|
||||
_auditService.StageAssignmentChanged(workOrderId, change.OldValue, change.NewValue, actorId);
|
||||
await _auditService.StageAssignmentChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId);
|
||||
break;
|
||||
default:
|
||||
_auditService.StageFieldChanged(workOrderId, change.FieldName, change.OldValue, change.NewValue, actorId, change.DispatchId);
|
||||
await _auditService.StageFieldChangedAsync(workOrderId, change.FieldName, change.OldValue, change.NewValue, actorId, dispatchId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -195,8 +196,7 @@ namespace SeaHaven.Services.Implementation
|
|||
CreatedDate = DateTime.UtcNow
|
||||
};
|
||||
_context.Dispatches.Add(created);
|
||||
await _context.SaveChangesAsync();
|
||||
workOrder.PrimaryDispatchId = created.Id;
|
||||
workOrder.PrimaryDispatch = created;
|
||||
return created;
|
||||
}
|
||||
|
||||
|
|
@ -392,10 +392,10 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
var old = dispatch.VendorId.ToString();
|
||||
if (dispatch.VendorId == vendorId.Value)
|
||||
return FieldChange.Unchanged(auditField, dispatch.Id);
|
||||
return FieldChange.Unchanged(auditField, ResolveDispatchIdForAudit(dispatch));
|
||||
|
||||
dispatch.VendorId = vendorId.Value;
|
||||
return FieldChange.ForField(auditField, old, vendorId.Value.ToString(), dispatch.Id);
|
||||
return FieldChange.ForField(auditField, old, vendorId.Value.ToString(), ResolveDispatchIdForAudit(dispatch));
|
||||
}
|
||||
|
||||
private static FieldChange ApplyApptDate(Dispatch dispatch, string? value, string auditField)
|
||||
|
|
@ -411,12 +411,14 @@ namespace SeaHaven.Services.Implementation
|
|||
var old = FormatDate(dispatch.ScheduledDate);
|
||||
var newVal = FormatDate(parsed);
|
||||
if (old == newVal)
|
||||
return FieldChange.Unchanged(auditField, dispatch.Id);
|
||||
return FieldChange.Unchanged(auditField, ResolveDispatchIdForAudit(dispatch));
|
||||
|
||||
dispatch.ScheduledDate = parsed;
|
||||
return FieldChange.ForField(auditField, old, newVal, dispatch.Id);
|
||||
return FieldChange.ForField(auditField, old, newVal, ResolveDispatchIdForAudit(dispatch));
|
||||
}
|
||||
|
||||
// apptTime is stored on WorkOrder (ScheduledStart/End) but requires a primary dispatch
|
||||
// for concurrency (dispatchVersion) and audit context; apptDate lives on Dispatch.ScheduledDate.
|
||||
private static FieldChange ApplyApptTime(WorkOrder workOrder, Dispatch dispatch, string? value, string auditField)
|
||||
{
|
||||
if (!WorkOrderBoardApptTimeParser.TryParse(value, out var start, out var end, out var error))
|
||||
|
|
@ -435,9 +437,9 @@ namespace SeaHaven.Services.Implementation
|
|||
var newCombined = FormatApptRange(newStart, newEnd);
|
||||
|
||||
if (oldCombined == newCombined)
|
||||
return FieldChange.Unchanged(auditField, dispatch.Id);
|
||||
return FieldChange.Unchanged(auditField, ResolveDispatchIdForAudit(dispatch));
|
||||
|
||||
return FieldChange.ForField(auditField, oldCombined, newCombined, dispatch.Id);
|
||||
return FieldChange.ForField(auditField, oldCombined, newCombined, ResolveDispatchIdForAudit(dispatch));
|
||||
}
|
||||
|
||||
private static FieldChange ApplyDocStatus(WorkOrder workOrder, string? value, string auditField)
|
||||
|
|
@ -500,6 +502,9 @@ namespace SeaHaven.Services.Implementation
|
|||
return vendorId;
|
||||
}
|
||||
|
||||
private static int? ResolveDispatchIdForAudit(Dispatch dispatch)
|
||||
=> dispatch.Id > 0 ? dispatch.Id : null;
|
||||
|
||||
private static string? FormatDate(DateTime? value) => value?.ToString("yyyy-MM-dd");
|
||||
private static string? FormatDateTime(DateTime? value) => value?.ToString("O");
|
||||
private static string? FormatApptRange(string? start, string? end)
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ namespace SeaHaven.Services.Interfaces
|
|||
Task LogAssignmentChangedAsync(int workOrderId, string? oldValue, string? newValue, string? actorId);
|
||||
Task LogSyncRejectedAsync(int workOrderId, string fieldName, string? oldValue, string? newValue);
|
||||
|
||||
void StageFieldChanged(int workOrderId, string fieldName, string? oldValue, string? newValue, string? actorId, int? dispatchId = null);
|
||||
void StageStatusChanged(int workOrderId, string? oldStatus, string? newStatus, string? actorId);
|
||||
void StageAssignmentChanged(int workOrderId, string? oldValue, string? newValue, string? actorId);
|
||||
void StageCreated(int workOrderId, string? actorId, string? woNumber);
|
||||
Task StageFieldChangedAsync(int workOrderId, string fieldName, string? oldValue, string? newValue, string? actorId, int? dispatchId = null);
|
||||
Task StageStatusChangedAsync(int workOrderId, string? oldStatus, string? newStatus, string? actorId);
|
||||
Task StageAssignmentChangedAsync(int workOrderId, string? oldValue, string? newValue, string? actorId);
|
||||
Task StageCreatedAsync(int workOrderId, string? actorId, string? woNumber);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,6 +180,56 @@ public class WorkOrderBoardUpdateServiceTests
|
|||
Assert.True(await context.WorkOrderFieldLocks.AnyAsync(l => l.FieldName == "SiteCode"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_SecondEditSameField_DoesNotDuplicateFieldLock()
|
||||
{
|
||||
var (context, service) = CreateSut();
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
SiteCode = "OLD",
|
||||
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||
};
|
||||
context.workOrders.Add(wo);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
Field = WorkOrderBoardFieldNames.SiteCode,
|
||||
Value = "BK5",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1");
|
||||
|
||||
var updated = await context.workOrders.FindAsync(1);
|
||||
var secondResult = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
Field = WorkOrderBoardFieldNames.SiteCode,
|
||||
Value = "BK6",
|
||||
WorkOrderVersion = ToVersion(updated!)
|
||||
}, "actor-1");
|
||||
|
||||
Assert.Equal("BK6", secondResult.SiteCode);
|
||||
Assert.Single(await context.WorkOrderFieldLocks.Where(l => l.FieldName == "SiteCode").ToListAsync());
|
||||
Assert.Equal(2, await context.WorkOrderAuditLogs.Where(a => a.FieldName == "SiteCode").CountAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_NotFound_ThrowsNotFoundCode()
|
||||
{
|
||||
var (_, service) = CreateSut();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
||||
service.PatchFieldAsync(999, new WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
Field = WorkOrderBoardFieldNames.SiteCode,
|
||||
Value = "BK5",
|
||||
WorkOrderVersion = Convert.ToBase64String(new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 })
|
||||
}, "actor-1"));
|
||||
|
||||
Assert.Equal("NotFound", ex.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_NormalizesWoNumber()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue