using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Implementation; using SeaHaven.Services.Constants; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Helpers; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; namespace SeaHavenIndustries.Tests; public class WorkOrderBoardUpdateServiceTests { private static (ApplicationDbContext Context, WorkOrderBoardUpdateService Service) CreateSut() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; var context = new ApplicationDbContext(options); var boardData = new WorkOrderBoardDataService(context); var mutationData = new WorkOrderBoardMutationDataService(context); var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context)); var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks); var service = new WorkOrderBoardUpdateService(boardData, mutationData, audit); return (context, service); } private static string ToVersion(WorkOrder workOrder) => Convert.ToBase64String(workOrder.RowVersion ?? new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }); private static string ToVersion(Dispatch dispatch) => Convert.ToBase64String(dispatch.RowVersion ?? new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 }); [Fact] public async Task PatchField_AutoSchedulesIncompleteToScheduled() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, AssignTo = "user-a", RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ScheduledDate, Value = "2026-06-25", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal(LifecycleStatus.Scheduled, result.LifecycleStatus); var audits = await context.WorkOrderAuditLogs.ToListAsync(); Assert.Equal(3, audits.Count); Assert.Contains(audits, a => a.FieldName == "IsAddOn"); Assert.Contains(audits, a => a.FieldName == "ScheduledDate"); Assert.Contains(audits, a => a.Action == AuditActionType.StatusChanged.ToString()); } [Fact] public async Task PatchField_IncrementsRescheduleCount() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, ScheduledDate = new DateTime(2026, 6, 20), RescheduleCount = 1, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ScheduledDate, Value = "2026-06-25", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal(2, result.RescheduleCount); } [Fact] public async Task PatchField_BlocksStatusChangeWhenPastDue() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, DueDate = DateTime.UtcNow.Date.AddDays(5), ScheduledDate = DateTime.UtcNow.Date.AddDays(-3), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.LifecycleStatus, Value = "Complete", WorkOrderVersion = ToVersion(wo) }, "actor-1")); Assert.Equal("PastDueStatusBlocked", ex.Code); Assert.Contains("Schedule On", ex.Message, StringComparison.Ordinal); Assert.DoesNotContain("due date", ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task PatchField_FutureSchedule_ClearsPastDueAndAllowsStatusChange() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, DueDate = DateTime.UtcNow.Date.AddDays(-3), ScheduledDate = DateTime.UtcNow.Date.AddDays(-1), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var futureSchedule = DateTime.UtcNow.Date.AddDays(7).ToString("yyyy-MM-dd"); var row = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ScheduledDate, Value = futureSchedule, WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.False(row.IsPastDue); Assert.Equal(DateTime.UtcNow.Date.AddDays(7), row.ScheduledDate!.Value.Date); wo = await context.workOrders.SingleAsync(w => w.Id == 1); var afterStatus = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.LifecycleStatus, Value = "Complete", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal(LifecycleStatus.Completed, afterStatus.LifecycleStatus); Assert.False(afterStatus.IsPastDue); } [Fact] public async Task PatchField_ProvisionalWoNumber_BlocksCompletionUntilReplaced() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, InternalWONumber = "SH00001", WorkerOrderNumber = "SH00001", LifecycleStatus = LifecycleStatus.Scheduled, ScheduledDate = DateTime.UtcNow.Date.AddDays(1), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var blocked = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.LifecycleStatus, Value = "Complete", WorkOrderVersion = ToVersion(wo) }, "actor-1")); Assert.Equal("ProvisionalWoNumber", blocked.Code); var renumbered = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.WoNumber, Value = "12345", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal("00000012345", renumbered.WoNumber); wo = await context.workOrders.SingleAsync(w => w.Id == 1); var completed = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.LifecycleStatus, Value = "Complete", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal(LifecycleStatus.Completed, completed.LifecycleStatus); } [Fact] public async Task PatchField_DueDateAlone_DoesNotClearPastDue() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, DueDate = DateTime.UtcNow.Date.AddDays(-3), ScheduledDate = DateTime.UtcNow.Date.AddDays(-1), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var futureDue = DateTime.UtcNow.Date.AddDays(10).ToString("yyyy-MM-dd"); var afterDue = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.DueDate, Value = futureDue, WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.True(afterDue.IsPastDue); Assert.Equal(DateTime.UtcNow.Date.AddDays(-1), afterDue.ScheduledDate!.Value.Date); wo = await context.workOrders.SingleAsync(w => w.Id == 1); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.LifecycleStatus, Value = "Complete", WorkOrderVersion = ToVersion(wo) }, "actor-1")); Assert.Equal("PastDueStatusBlocked", ex.Code); Assert.Contains("Schedule On", ex.Message, StringComparison.Ordinal); Assert.DoesNotContain("due date", ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task PatchField_DueDate_DoesNotChangeScheduledDate() { var (context, service) = CreateSut(); var scheduled = new DateTime(2026, 6, 20); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, DueDate = new DateTime(2026, 6, 18), ScheduledDate = scheduled, RescheduleCount = 2, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.DueDate, Value = "2026-07-01", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal(new DateTime(2026, 7, 1), result.DueDate!.Value.Date); Assert.Equal(scheduled, result.ScheduledDate); Assert.Equal(2, result.RescheduleCount); var persisted = await context.workOrders.SingleAsync(w => w.Id == 1); Assert.Equal(scheduled, persisted.ScheduledDate); Assert.Equal(2, persisted.RescheduleCount); } [Fact] public async Task PatchField_ScheduledDate_DoesNotChangeDueDate() { var (context, service) = CreateSut(); var due = new DateTime(2026, 6, 18); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, DueDate = due, ScheduledDate = new DateTime(2026, 6, 20), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ScheduledDate, Value = "2026-06-25", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal(new DateTime(2026, 6, 25), result.ScheduledDate!.Value.Date); Assert.Equal(due, result.DueDate); var persisted = await context.workOrders.SingleAsync(w => w.Id == 1); Assert.Equal(due, persisted.DueDate); } [Fact] public async Task PatchField_RejectsOverdueAsWorkOrderType() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, WorkOrderType = WorkOrderType.Reactive, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.WorkOrderType, Value = "Overdue", WorkOrderVersion = ToVersion(wo) }, "actor-1")); Assert.Equal("InvalidValue", ex.Code); Assert.Contains("isPastDue", ex.Message, StringComparison.Ordinal); Assert.Contains("dueDate", ex.Message, StringComparison.OrdinalIgnoreCase); } [Fact] public async Task PatchField_BlocksEditWhenCanceled() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Canceled, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.SiteCode, Value = "BK5", WorkOrderVersion = ToVersion(wo) }, "actor-1")); Assert.Equal("CanceledReadOnly", ex.Code); } [Fact] public async Task PatchField_VendorCreatesPrimaryDispatch() { var (context, service) = CreateSut(); context.Vendors.Add(new Vendor { Id = 5, CompanyName = "Vendor Co" }); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.VendorId, Value = "5", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.NotNull(result.PrimaryDispatchId); Assert.Equal(5, result.VendorId); Assert.Equal(1, await context.Dispatches.CountAsync()); } [Theory] [InlineData("Cancelled")] [InlineData("Canceled")] [InlineData("Refused")] [InlineData(" refused ")] public async Task PatchField_VendorId_CreatesNewDispatchWhenPrimaryIsInactive(string inactiveStatus) { var (context, service) = CreateSut(); context.Vendors.AddRange( new Vendor { Id = 1, CompanyName = "Old Co" }, new Vendor { Id = 5, CompanyName = "New Co" }); var dispatch = new Dispatch { Id = 10, WorkOrderId = 1, VendorId = 1, Status = inactiveStatus, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 } }; var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, PrimaryDispatchId = 10, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.Dispatches.Add(dispatch); context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.VendorId, Value = "5", WorkOrderVersion = ToVersion(wo), DispatchVersion = ToVersion(dispatch), PrimaryDispatchId = 10 }, "actor-1"); Assert.Equal(5, result.VendorId); Assert.Equal("New Co", result.VendorName); Assert.Equal("Pending", result.PrimaryDispatchStatus); Assert.NotEqual(10, result.PrimaryDispatchId); Assert.Equal(2, await context.Dispatches.CountAsync()); 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] [InlineData("Sent")] [InlineData("Verified")] [InlineData("Completed")] public async Task PatchField_VendorId_UpdatesExistingLivePrimaryDispatch(string liveStatus) { var (context, service) = CreateSut(); context.Vendors.AddRange( new Vendor { Id = 1, CompanyName = "Old Co" }, new Vendor { Id = 5, CompanyName = "New Co" }); var dispatch = new Dispatch { Id = 10, WorkOrderId = 1, VendorId = 1, Status = liveStatus, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 } }; var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, PrimaryDispatchId = 10, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.Dispatches.Add(dispatch); context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.VendorId, Value = "5", WorkOrderVersion = ToVersion(wo), DispatchVersion = ToVersion(dispatch), PrimaryDispatchId = 10 }, "actor-1"); Assert.Equal(5, result.VendorId); Assert.Equal(10, result.PrimaryDispatchId); Assert.Equal(liveStatus, result.PrimaryDispatchStatus); Assert.Equal(1, await context.Dispatches.CountAsync()); var updated = await context.Dispatches.SingleAsync(); Assert.Equal(5, updated.VendorId); Assert.Equal(liveStatus, updated.Status); } [Fact] public async Task PatchField_CreatesFieldLockAndAuditPerField() { 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"); Assert.Single(await context.WorkOrderAuditLogs.Where(a => a.Action == AuditActionType.FieldChanged.ToString()).ToListAsync()); Assert.True(await context.WorkOrderFieldLocks.AnyAsync(l => l.FieldName == "SiteCode")); } [Fact] public async Task PatchField_NormalizesWoNumber() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, InternalWONumber = "00000009999", LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.WoNumber, Value = "12345", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal("00000012345", result.WoNumber); } [Fact] public async Task PatchField_RejectsDuplicateWoNumber() { var (context, service) = CreateSut(); context.workOrders.AddRange( new WorkOrder { Id = 1, InternalWONumber = "00000011111", RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }, new WorkOrder { Id = 2, InternalWONumber = "00000022222", RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 } }); await context.SaveChangesAsync(); var wo = await context.workOrders.FindAsync(1); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.WoNumber, Value = "22222", WorkOrderVersion = ToVersion(wo!) }, "actor-1")); Assert.Equal("DuplicateWoNumber", ex.Code); } [Fact] public async Task PatchField_UpdatesPmTrade() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, Trade = "HVAC", RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.Pm, Value = "Plumbing", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal("Plumbing", result.Pm); Assert.Equal("Plumbing", (await context.workOrders.FindAsync(1))!.Trade); } [Fact] public async Task PatchField_UpdatesServiceNotesAndReturnsOnBoardRow() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, Trade = "Backflow Preventers", RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ServiceNotes, Value = "service notes", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal("service notes", result.ServiceNotes); Assert.Equal("service notes", (await context.workOrders.FindAsync(1))!.ServiceNotes); } [Fact] public async Task PatchField_UpdatesExtraServices_WhenPrimaryExists() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, Trade = "Backflow Preventers", RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ExtraServices, Value = "[\"Filter change\",\"Coil clean\"]", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal(new[] { "Filter change", "Coil clean" }, result.ExtraServices); Assert.Contains("Filter change", (await context.workOrders.FindAsync(1))!.ExtraServices); } [Fact] public async Task PatchField_ExtraServicesWithoutPrimary_Throws() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ExtraServices, Value = "[\"Filter change\"]", WorkOrderVersion = ToVersion(wo) }, "actor-1")); Assert.Equal("InvalidValue", ex.Code); } [Fact] public async Task PatchField_UpdatesAdditionalContacts() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.AdditionalContacts, Value = "[{\"name\":\"Jane Backup\",\"phone\":\"+1 555-0101\",\"notes\":\"Call first\"}]", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.NotNull(result.AdditionalContacts); Assert.Single(result.AdditionalContacts!); Assert.Equal("Jane Backup", result.AdditionalContacts![0].Name); Assert.Contains("Jane Backup", (await context.workOrders.FindAsync(1))!.AdditionalContacts); } [Fact] public async Task PatchField_InvalidAdditionalContactsJson_Throws() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.AdditionalContacts, Value = "not-json", WorkOrderVersion = ToVersion(wo) }, "actor-1")); Assert.Equal("InvalidValue", ex.Code); } [Fact] public async Task PatchField_AdditionalContacts_NameOnly_Throws() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.AdditionalContacts, Value = "[{\"name\":\"Jane Backup\",\"phone\":\"\"}]", WorkOrderVersion = ToVersion(wo) }, "actor-1")); Assert.Equal("InvalidValue", ex.Code); } [Fact] public async Task PatchField_AdditionalContacts_PhoneOnly_Throws() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.AdditionalContacts, Value = "[{\"name\":\"\",\"phone\":\"+1 555-0101\"}]", WorkOrderVersion = ToVersion(wo) }, "actor-1")); Assert.Equal("InvalidValue", ex.Code); } [Fact] public async Task PatchField_AdditionalContacts_DropsBlankRows_PreservesOrder() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.AdditionalContacts, Value = "[{\"name\":\"\",\"phone\":\"\"},{\"name\":\"Jane Backup\",\"phone\":\"+1 555-0101\"},{\"name\":\"Security Desk\",\"phone\":\"+1 555-0102\"}]", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal(2, result.AdditionalContacts!.Count); Assert.Equal("Jane Backup", result.AdditionalContacts[0].Name); Assert.Equal("Security Desk", result.AdditionalContacts[1].Name); } [Fact] public async Task PatchField_AdditionalContacts_EmptyArray_Clears() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, AdditionalContacts = "[{\"name\":\"Jane Backup\",\"phone\":\"+1 555-0101\"}]", RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.AdditionalContacts, Value = "[]", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Null(result.AdditionalContacts); Assert.Null((await context.workOrders.FindAsync(1))!.AdditionalContacts); } [Fact] public async Task PatchField_SetsFlagColor_CanonicalUppercase() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.FlagColor, Value = "#ef4444", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal("#EF4444", result.FlagColor); Assert.Equal("#EF4444", (await context.workOrders.FindAsync(1))!.FlagColor); } [Fact] public async Task PatchField_ClearsFlagColor_WithNullOrEmpty() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, FlagColor = "#EF4444", RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var cleared = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.FlagColor, Value = "", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Null(cleared.FlagColor); Assert.Null((await context.workOrders.FindAsync(1))!.FlagColor); } [Fact] public async Task PatchField_ClearsFlagColor_WithNullValue() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, FlagColor = "#3B82F6", RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var cleared = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.FlagColor, Value = null, WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Null(cleared.FlagColor); Assert.Null((await context.workOrders.FindAsync(1))!.FlagColor); } [Fact] public async Task PatchField_FlagColorOutsidePalette_Throws() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.FlagColor, Value = "#000000", WorkOrderVersion = ToVersion(wo) }, "actor-1")); Assert.Equal("InvalidValue", ex.Code); } [Fact] public async Task PatchField_FlagColorMalformed_Throws() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.FlagColor, Value = "not-a-color", WorkOrderVersion = ToVersion(wo) }, "actor-1")); Assert.Equal("InvalidValue", ex.Code); } [Fact] public async Task PatchField_FlagColor_DoesNotRequireDispatchVersion() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, PrimaryDispatchId = 10, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.Dispatches.Add(new Dispatch { Id = 10, WorkOrderId = 1, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 } }); context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.FlagColor, Value = "#8B5CF6", WorkOrderVersion = ToVersion(wo) // no DispatchVersion }, "actor-1"); Assert.Equal("#8B5CF6", result.FlagColor); } [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_WeekOnly_DoesNotAutoScheduleOnAssignTo() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, ScheduledDate = new DateTime(2026, 6, 25), ScheduleWeekOnly = true, TargetWeek = new DateOnly(2026, 6, 22), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.AssignTo, Value = "user-a", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal(LifecycleStatus.Incomplete, result.LifecycleStatus); Assert.Equal("user-a", result.DispatcherId); } [Fact] public async Task PatchField_ApptTime_UpdatesScheduledStart() { var (context, service) = CreateSut(); var dispatch = new Dispatch { Id = 10, WorkOrderId = 1, VendorId = 1, ScheduledDate = new DateTime(2026, 6, 25), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 } }; var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, PrimaryDispatchId = 10, ScheduledDate = new DateTime(2026, 6, 25), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Vendor A" }); context.Dispatches.Add(dispatch); context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ApptTime, Value = "09:00-10:00", WorkOrderVersion = ToVersion(wo), DispatchVersion = ToVersion(dispatch), PrimaryDispatchId = 10 }, "actor-1"); Assert.Contains("09:00", result.ApptTime); var reloaded = await context.workOrders.FindAsync(1); Assert.Equal(new DateTime(2026, 6, 25, 9, 0, 0), reloaded!.ScheduledStart); Assert.Equal(new DateTime(2026, 6, 25, 10, 0, 0), reloaded.ScheduledEnd); } [Fact] public async Task PatchField_ApptTime_DashPrefixedToken_TreatedAsSingleStart() { var (context, service) = CreateSut(); var dispatch = new Dispatch { Id = 10, WorkOrderId = 1, VendorId = 1, ScheduledDate = new DateTime(2026, 6, 25), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 } }; var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, PrimaryDispatchId = 10, ScheduledDate = new DateTime(2026, 6, 25), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Vendor A" }); context.Dispatches.Add(dispatch); context.workOrders.Add(wo); await context.SaveChangesAsync(); await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ApptTime, Value = "-30", WorkOrderVersion = ToVersion(wo), DispatchVersion = ToVersion(dispatch), PrimaryDispatchId = 10 }, "actor-1"); var reloaded = await context.workOrders.FindAsync(1); Assert.NotNull(reloaded!.ScheduledStart); Assert.Equal(new DateTime(2026, 6, 25), reloaded.ScheduledEnd); Assert.Equal(new DateTime(2026, 6, 25).Add(TimeSpan.FromDays(30)), reloaded.ScheduledStart); } [Fact] public async Task PatchField_ApptTime_LocksScheduledStartAndEnd_ForSync() { var (context, service) = CreateSut(); var dispatch = new Dispatch { Id = 10, WorkOrderId = 1, VendorId = 1, ScheduledDate = new DateTime(2026, 6, 25), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 } }; var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, PrimaryDispatchId = 10, ScheduledDate = new DateTime(2026, 6, 25), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Vendor A" }); context.Dispatches.Add(dispatch); context.workOrders.Add(wo); await context.SaveChangesAsync(); await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ApptTime, Value = "09:00-10:00", WorkOrderVersion = ToVersion(wo), DispatchVersion = ToVersion(dispatch), PrimaryDispatchId = 10 }, "actor-1"); var lockNames = await context.WorkOrderFieldLocks .Where(l => l.WorkOrderId == 1) .Select(l => l.FieldName) .ToListAsync(); Assert.Contains(WorkOrderFieldNames.ScheduledStart, lockNames); Assert.Contains(WorkOrderFieldNames.ScheduledEnd, lockNames); Assert.DoesNotContain("ApptTime", lockNames); var locks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context)); var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), locks); var policy = new SyncFieldMergePolicy(locks); var reloaded = await context.workOrders.SingleAsync(w => w.Id == 1); var syncContext = new WorkOrderSyncContext { WorkOrder = reloaded, FieldLocks = locks, Audit = audit }; var appliedStart = await policy.TryApplyAsync( syncContext, WorkOrderFieldNames.ScheduledStart, "2026-06-25T08:00:00"); var appliedEnd = await policy.TryApplyAsync( syncContext, WorkOrderFieldNames.ScheduledEnd, "2026-06-25T11:00:00"); Assert.False(appliedStart); Assert.False(appliedEnd); Assert.Equal(new DateTime(2026, 6, 25, 9, 0, 0), reloaded.ScheduledStart); Assert.Equal(new DateTime(2026, 6, 25, 10, 0, 0), reloaded.ScheduledEnd); } [Fact] public async Task PatchField_DispatchFieldWithoutVersion_ThrowsDispatchVersionRequired() { var (context, service) = CreateSut(); var dispatch = new Dispatch { Id = 10, WorkOrderId = 1, VendorId = 1, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 } }; var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, PrimaryDispatchId = 10, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Vendor A" }); context.Dispatches.Add(dispatch); context.workOrders.Add(wo); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ApptDate, Value = "2026-06-26", WorkOrderVersion = ToVersion(wo), PrimaryDispatchId = 10 }, "actor-1")); Assert.Equal("DispatchVersionRequired", ex.Code); } [Fact] public async Task PatchField_VendorIdNotFound_Throws() { var (context, service) = CreateSut(); var dispatch = new Dispatch { Id = 10, WorkOrderId = 1, VendorId = 1, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 } }; var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, PrimaryDispatchId = 10, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Vendor A" }); context.Dispatches.Add(dispatch); context.workOrders.Add(wo); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.VendorId, Value = "999", WorkOrderVersion = ToVersion(wo), DispatchVersion = ToVersion(dispatch), PrimaryDispatchId = 10 }, "actor-1")); Assert.Equal("VendorNotFound", ex.Code); } [Fact] public async Task PatchField_UpdatesCompletedDate() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); context.WorkOrderContacts.Add(new WorkOrderContacts { Id = 1, WorkorderId = 1, ContactId = 50 }); context.workOrderCategories.Add(new WorkOrderCategories { Id = 1, WorkorderId = 1, CategoryId = 60 }); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.CompletedDate, Value = "2026-07-20", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal(new DateTime(2026, 7, 20), result.CompletedDate); Assert.Equal(new DateTime(2026, 7, 20), context.workOrders.Single().CompletedDate); Assert.Equal(1, await context.WorkOrderContacts.CountAsync()); Assert.Equal(1, await context.workOrderCategories.CountAsync()); Assert.Equal(50, context.WorkOrderContacts.Single().ContactId); Assert.Equal(60, context.workOrderCategories.Single().CategoryId); } [Fact] public async Task PatchField_ClearsCompletedDate_WhenValueEmpty() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, CompletedDate = new DateTime(2026, 7, 1), RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.CompletedDate, Value = "", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Null(result.CompletedDate); Assert.Null(context.workOrders.Single().CompletedDate); } [Fact] public async Task PatchField_ClearsIsAddOn_WhenScheduleCleared() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, WorkOrderType = WorkOrderType.PM, ScheduledDate = new DateTime(2026, 6, 20), CreatedDate = new DateTime(2026, 6, 21, 0, 0, 0, DateTimeKind.Utc), IsAddOn = true, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ScheduledDate, Value = "", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.False(result.IsAddOn); var persisted = await context.workOrders.SingleAsync(w => w.Id == 1); Assert.False(persisted.IsAddOn); Assert.Single(await context.WorkOrderAuditLogs.Where(a => a.FieldName == "IsAddOn").ToListAsync()); } [Fact] public async Task PatchField_ScheduledDate_RecalculatesIsAddOn_AfterCrossWeekMove() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, WorkOrderType = WorkOrderType.PM, ScheduledDate = new DateTime(2026, 6, 10), CreatedDate = new DateTime(2026, 6, 21, 0, 0, 0, DateTimeKind.Utc), IsAddOn = false, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.ScheduledDate, Value = "2026-06-25", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.True(result.IsAddOn); var persisted = await context.workOrders.SingleAsync(w => w.Id == 1); Assert.True(persisted.IsAddOn); } [Fact] public async Task PatchField_WorkOrderType_DoesNotRecalculateIsAddOn() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, WorkOrderType = WorkOrderType.PM, IsAddOn = true, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.WorkOrderType, Value = "Reactive", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.Equal(WorkOrderType.Reactive, result.WorkOrderType); Assert.True(result.IsAddOn); var persisted = await context.workOrders.SingleAsync(w => w.Id == 1); Assert.Equal(WorkOrderType.Reactive, persisted.WorkOrderType); Assert.True(persisted.IsAddOn); Assert.Empty(await context.WorkOrderAuditLogs.Where(a => a.FieldName == "IsAddOn").ToListAsync()); } [Fact] public async Task PatchField_AvetaRequiredTrue_PersistsAndAudits() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.AvetaRequired, Value = "true", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.True(result.AvetaRequired); Assert.True((await context.workOrders.FindAsync(1))!.AvetaRequired); Assert.Contains( await context.WorkOrderAuditLogs.ToListAsync(), a => a.FieldName == "AvetaRequired" && a.Action == AuditActionType.FieldChanged.ToString()); } [Fact] public async Task PatchField_AvetaRequiredFalse_ClearsFlag() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, AvetaRequired = true, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.AvetaRequired, Value = "false", WorkOrderVersion = ToVersion(wo) }, "actor-1"); Assert.False(result.AvetaRequired); Assert.False((await context.workOrders.FindAsync(1))!.AvetaRequired); } [Fact] public async Task PatchField_AvetaRequiredInvalid_ThrowsInvalidValue() { var (context, service) = CreateSut(); var wo = new WorkOrder { Id = 1, LifecycleStatus = LifecycleStatus.Incomplete, RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } }; context.workOrders.Add(wo); await context.SaveChangesAsync(); var ex = await Assert.ThrowsAsync(() => service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.AvetaRequired, Value = "yes", WorkOrderVersion = ToVersion(wo) }, "actor-1")); Assert.Equal("InvalidValue", ex.Code); } }