diff --git a/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs b/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs index f11196d..f227ffd 100644 --- a/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs +++ b/Api.SeaHavenIndustries/Controllers/WorkOrderController.cs @@ -148,14 +148,6 @@ 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 diff --git a/SeaHaven.Services/Implementation/WorkOrderAuditService.cs b/SeaHaven.Services/Implementation/WorkOrderAuditService.cs index b0e7c60..0646cd0 100644 --- a/SeaHaven.Services/Implementation/WorkOrderAuditService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderAuditService.cs @@ -28,17 +28,17 @@ namespace SeaHaven.Services.Implementation => LogAsync(workOrderId, AuditActionType.SyncRejected, AuditEventType.Sync, AuditActorType.Sync, fieldName, oldValue, newValue); - 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 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 StageStatusChangedAsync(int workOrderId, string? oldStatus, string? newStatus, string? actorId) - => StageAuditAsync(workOrderId, AuditActionType.StatusChanged, "LifecycleStatus", oldStatus, newStatus, actorId); + public void StageStatusChanged(int workOrderId, string? oldStatus, string? newStatus, string? actorId) + => StageAudit(workOrderId, AuditActionType.StatusChanged, "LifecycleStatus", oldStatus, newStatus, actorId); - public Task StageAssignmentChangedAsync(int workOrderId, string? oldValue, string? newValue, string? actorId) - => StageAuditAsync(workOrderId, AuditActionType.AssignmentChanged, "AssignTo", oldValue, newValue, actorId); + public void StageAssignmentChanged(int workOrderId, string? oldValue, string? newValue, string? actorId) + => StageAudit(workOrderId, AuditActionType.AssignmentChanged, "AssignTo", oldValue, newValue, actorId); - public Task StageCreatedAsync(int workOrderId, string? actorId, string? woNumber) - => StageAuditAsync(workOrderId, AuditActionType.Create, "WorkOrder", null, woNumber, actorId); + public void StageCreated(int workOrderId, string? actorId, string? woNumber) + => StageAudit(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 async Task StageAuditAsync( + private void StageAudit( int workOrderId, AuditActionType action, string fieldName, @@ -76,13 +76,7 @@ namespace SeaHaven.Services.Implementation { AddAuditEntry(workOrderId, action, AuditEventType.Manual, AuditActorType.Dispatcher, fieldName, oldValue, newValue, actorId, dispatchId); - - if (action == AuditActionType.FieldChanged || - action == AuditActionType.StatusChanged || - action == AuditActionType.AssignmentChanged) - { - await _fieldLocks.LockFieldAsync(workOrderId, fieldName, actorId); - } + StageFieldLock(workOrderId, fieldName, actorId); } private void AddAuditEntry( @@ -113,5 +107,21 @@ 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 + }); + } } } diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardCancelService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardCancelService.cs index 78c86bd..273e26f 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardCancelService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardCancelService.cs @@ -47,7 +47,7 @@ namespace SeaHaven.Services.Implementation if (workOrder.LegacyStatus == null && workOrder.Status != null) workOrder.LegacyStatus = workOrder.Status; - await _auditService.StageStatusChangedAsync(workOrderId, oldStatus, LifecycleStatus.Canceled.ToString(), actorId); + _auditService.StageStatusChanged(workOrderId, oldStatus, LifecycleStatus.Canceled.ToString(), actorId); await _context.SaveChangesAsync(); var row = await _boardService.GetBoardRowAsync(workOrderId); diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs index 35ea71c..6d6c2a0 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs @@ -124,8 +124,8 @@ namespace SeaHaven.Services.Implementation _context.workOrders.Add(workOrder); - await _auditService.StageCreatedAsync(workOrder.Id, actorId, woNumber); - await StageChangesAsync(workOrder.Id, changes, actorId, dispatch); + _auditService.StageCreated(workOrder.Id, actorId, woNumber); + StageChanges(workOrder.Id, changes, actorId, dispatch); await _context.SaveChangesAsync(); @@ -183,7 +183,7 @@ namespace SeaHaven.Services.Implementation return normalized; } - private async Task StageChangesAsync( + private void StageChanges( int workOrderId, IEnumerable changes, string? actorId, @@ -195,13 +195,13 @@ namespace SeaHaven.Services.Implementation switch (change.Action) { case AuditActionType.StatusChanged: - await _auditService.StageStatusChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId); + _auditService.StageStatusChanged(workOrderId, change.OldValue, change.NewValue, actorId); break; case AuditActionType.AssignmentChanged: - await _auditService.StageAssignmentChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId); + _auditService.StageAssignmentChanged(workOrderId, change.OldValue, change.NewValue, actorId); break; default: - await _auditService.StageFieldChangedAsync(workOrderId, change.FieldName, change.OldValue ?? "", change.NewValue, actorId, dispatchId); + _auditService.StageFieldChanged(workOrderId, change.FieldName, change.OldValue ?? "", change.NewValue, actorId, dispatchId); break; } } diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs index 1eb1ce7..98239bb 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs @@ -106,17 +106,16 @@ 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: - await _auditService.StageStatusChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId); + _auditService.StageStatusChanged(workOrderId, change.OldValue, change.NewValue, actorId); break; case AuditActionType.AssignmentChanged: - await _auditService.StageAssignmentChangedAsync(workOrderId, change.OldValue, change.NewValue, actorId); + _auditService.StageAssignmentChanged(workOrderId, change.OldValue, change.NewValue, actorId); break; default: - await _auditService.StageFieldChangedAsync(workOrderId, change.FieldName, change.OldValue, change.NewValue, actorId, dispatchId); + _auditService.StageFieldChanged(workOrderId, change.FieldName, change.OldValue, change.NewValue, actorId, change.DispatchId); break; } } @@ -196,7 +195,8 @@ namespace SeaHaven.Services.Implementation CreatedDate = DateTime.UtcNow }; _context.Dispatches.Add(created); - workOrder.PrimaryDispatch = created; + await _context.SaveChangesAsync(); + workOrder.PrimaryDispatchId = created.Id; return created; } @@ -392,10 +392,10 @@ namespace SeaHaven.Services.Implementation var old = dispatch.VendorId.ToString(); if (dispatch.VendorId == vendorId.Value) - return FieldChange.Unchanged(auditField, ResolveDispatchIdForAudit(dispatch)); + return FieldChange.Unchanged(auditField, dispatch.Id); dispatch.VendorId = vendorId.Value; - return FieldChange.ForField(auditField, old, vendorId.Value.ToString(), ResolveDispatchIdForAudit(dispatch)); + return FieldChange.ForField(auditField, old, vendorId.Value.ToString(), dispatch.Id); } private static FieldChange ApplyApptDate(Dispatch dispatch, string? value, string auditField) @@ -411,14 +411,12 @@ namespace SeaHaven.Services.Implementation var old = FormatDate(dispatch.ScheduledDate); var newVal = FormatDate(parsed); if (old == newVal) - return FieldChange.Unchanged(auditField, ResolveDispatchIdForAudit(dispatch)); + return FieldChange.Unchanged(auditField, dispatch.Id); dispatch.ScheduledDate = parsed; - return FieldChange.ForField(auditField, old, newVal, ResolveDispatchIdForAudit(dispatch)); + return FieldChange.ForField(auditField, old, newVal, dispatch.Id); } - // 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)) @@ -437,9 +435,9 @@ namespace SeaHaven.Services.Implementation var newCombined = FormatApptRange(newStart, newEnd); if (oldCombined == newCombined) - return FieldChange.Unchanged(auditField, ResolveDispatchIdForAudit(dispatch)); + return FieldChange.Unchanged(auditField, dispatch.Id); - return FieldChange.ForField(auditField, oldCombined, newCombined, ResolveDispatchIdForAudit(dispatch)); + return FieldChange.ForField(auditField, oldCombined, newCombined, dispatch.Id); } private static FieldChange ApplyDocStatus(WorkOrder workOrder, string? value, string auditField) @@ -502,9 +500,6 @@ 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) diff --git a/SeaHaven.Services/Interfaces/IWorkOrderAuditService.cs b/SeaHaven.Services/Interfaces/IWorkOrderAuditService.cs index 3dc4b57..7029709 100644 --- a/SeaHaven.Services/Interfaces/IWorkOrderAuditService.cs +++ b/SeaHaven.Services/Interfaces/IWorkOrderAuditService.cs @@ -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); - 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); + 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); } } diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs index cb2b73a..59b180c 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs @@ -180,56 +180,6 @@ 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(() => - 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() {