diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs index c3aca36..44f965b 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs @@ -73,8 +73,8 @@ namespace SeaHaven.DataServices.Helpers if (overdue && !hasTypes) { return query.Where(w => - w.DueDate != null - && w.DueDate.Value.Date < today + w.ScheduledDate != null + && w.ScheduledDate.Value.Date < today && w.LifecycleStatus != LifecycleStatus.Completed && w.LifecycleStatus != LifecycleStatus.Canceled); } @@ -82,8 +82,8 @@ namespace SeaHaven.DataServices.Helpers if (overdue) { return query.Where(w => - (w.DueDate != null - && w.DueDate.Value.Date < today + (w.ScheduledDate != null + && w.ScheduledDate.Value.Date < today && w.LifecycleStatus != LifecycleStatus.Completed && w.LifecycleStatus != LifecycleStatus.Canceled) || (w.WorkOrderType != null && typeFilters.Contains(w.WorkOrderType.Value)) diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs index be14d8c..441802b 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs @@ -61,8 +61,8 @@ namespace SeaHaven.DataServices.Helpers || (w.IsAddOn && ("add-on".Contains(s) || "addon".Contains(s))) || (w.WorkOrderType == WorkOrderType.Other && "other".Contains(s)) || ("overdue".Contains(s) - && w.DueDate != null - && w.DueDate.Value.Date < today + && w.ScheduledDate != null + && w.ScheduledDate.Value.Date < today && w.LifecycleStatus != LifecycleStatus.Completed && w.LifecycleStatus != LifecycleStatus.Canceled) || (w.LifecycleStatus == LifecycleStatus.Incomplete && "incomplete".Contains(s)) diff --git a/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs b/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs index 9cc77b9..c7ac473 100644 --- a/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs +++ b/SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs @@ -1,4 +1,4 @@ -using Data.SeaHavenIndustries; +using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; namespace SeaHaven.Services.Helpers @@ -6,38 +6,38 @@ namespace SeaHaven.Services.Helpers public static class WorkOrderDerivedFields { /// - /// Past due overlay from DueDate (deadline) — never a WorkOrderType. - /// Independent of ScheduledDate (planned work / Schedule On). + /// Past due overlay from Schedule On — never a WorkOrderType. Matches SHOC board design (SH-185). + /// Changing Due Date alone must never trigger or clear it. /// public static bool IsPastDue(WorkOrder workOrder, DateOnly? asOf = null) { var today = asOf ?? DateOnly.FromDateTime(DateTime.UtcNow); - if (workOrder.DueDate == null) + if (workOrder.ScheduledDate == null) return false; if (LifecycleStatusMapper.IsTerminal(workOrder.LifecycleStatus)) return false; - return DateOnly.FromDateTime(workOrder.DueDate.Value.Date) < today; + return DateOnly.FromDateTime(workOrder.ScheduledDate.Value.Date) < today; } /// - /// Board read-model past-due check against DueDate. Compares UTC calendar dates; + /// Board read-model past-due check against ScheduledDate. Compares UTC calendar dates; /// business/location timezone support is planned for a later phase. /// public static bool IsPastDue( - DateTime? dueDate, + DateTime? scheduledDate, LifecycleStatus? lifecycleStatus, DateTime? utcNow = null) { - if (!dueDate.HasValue) + if (!scheduledDate.HasValue) return false; if (LifecycleStatusMapper.IsTerminal(lifecycleStatus)) return false; var today = (utcNow ?? DateTime.UtcNow).Date; - return dueDate.Value.Date < today; + return scheduledDate.Value.Date < today; } /// diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs index 0389f4c..a2bda0c 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs @@ -95,7 +95,7 @@ namespace SeaHaven.Services.Implementation CarriedOver = row.CarriedOver, IsAddOn = row.IsAddOn, WorkOrderType = row.WorkOrderType, - IsPastDue = WorkOrderDerivedFields.IsPastDue(row.DueDate, row.LifecycleStatus, utcNow), + IsPastDue = WorkOrderDerivedFields.IsPastDue(row.ScheduledDate, row.LifecycleStatus, utcNow), SiteCode = row.SiteCode, LocationName = row.LocationName, PocName = row.PocName, diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs index 1306245..fc81b2c 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs @@ -61,7 +61,7 @@ namespace SeaHaven.Services.Implementation if (WorkOrderBoardMutationRules.IsReadOnly(workOrder.LifecycleStatus)) throw new WorkOrderBoardValidationException("CanceledReadOnly", "Work order is read-only in its current status."); - var isPastDue = WorkOrderDerivedFields.IsPastDue(workOrder.DueDate, workOrder.LifecycleStatus); + 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. Update due date first."); diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardConcurrencyTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardConcurrencyTests.cs index 16f8e9e..d1014b6 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardConcurrencyTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardConcurrencyTests.cs @@ -141,8 +141,11 @@ public class WorkOrderBoardConcurrencyTests WorkOrderVersion = Convert.ToBase64String(wo!.RowVersion!) }, "actor-1"); - var auditCount = await context.WorkOrderAuditLogs.CountAsync(); - Assert.Equal(2, auditCount); + 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.FieldName == "RescheduleCount"); } [Fact] diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs index 04aa6c0..9da4328 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs @@ -379,7 +379,7 @@ public class WorkOrderAdvancedSearchServiceTests }, WorkOrderAccountTestHelpers.OrgWideAdmin(), null); Assert.Equal(1, result.TotalCount); - Assert.Equal(1, result.Items.First().Id); + Assert.Equal(3, result.Items.First().Id); Assert.True(result.Items.First().IsPastDue); } @@ -394,8 +394,8 @@ public class WorkOrderAdvancedSearchServiceTests { Id = 1, WorkOrderType = WorkOrderType.PM, - DueDate = today.AddDays(-3), - ScheduledDate = today.AddDays(5), + DueDate = today.AddDays(5), + ScheduledDate = today.AddDays(-3), LifecycleStatus = LifecycleStatus.Scheduled, InternalWONumber = "10000000001" }, @@ -498,9 +498,9 @@ public class WorkOrderAdvancedSearchServiceTests var overdue = Assert.Single(result.Items); Assert.Equal(0, result.Page); - Assert.Equal(2, overdue.Id); + Assert.Equal(1, overdue.Id); Assert.True(overdue.IsPastDue); - Assert.DoesNotContain(result.Items, i => i.Id == 1); + Assert.DoesNotContain(result.Items, i => i.Id == 2); Assert.DoesNotContain(result.Items, i => i.Id == 3); Assert.DoesNotContain(result.Items, i => i.Id == 4); } diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs index ddab1d5..69847c5 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs @@ -55,7 +55,11 @@ public class WorkOrderBoardUpdateServiceTests }, "actor-1"); Assert.Equal(LifecycleStatus.Scheduled, result.LifecycleStatus); - Assert.Equal(2, await context.WorkOrderAuditLogs.CountAsync()); + 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] @@ -91,8 +95,8 @@ public class WorkOrderBoardUpdateServiceTests { Id = 1, LifecycleStatus = LifecycleStatus.Scheduled, - DueDate = DateTime.UtcNow.Date.AddDays(-3), - ScheduledDate = DateTime.UtcNow.Date.AddDays(5), + 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); @@ -110,7 +114,7 @@ public class WorkOrderBoardUpdateServiceTests } [Fact] - public async Task PatchField_RescheduleAlone_DoesNotClearPastDueBlock() + public async Task PatchField_FutureSchedule_ClearsPastDueAndAllowsStatusChange() { var (context, service) = CreateSut(); var wo = new WorkOrder @@ -132,23 +136,23 @@ public class WorkOrderBoardUpdateServiceTests WorkOrderVersion = ToVersion(wo) }, "actor-1"); - Assert.True(row.IsPastDue); + 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 ex = await Assert.ThrowsAsync(() => - service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto - { - Field = WorkOrderBoardFieldNames.LifecycleStatus, - Value = "Complete", - WorkOrderVersion = ToVersion(wo) - }, "actor-1")); + var afterStatus = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto + { + Field = WorkOrderBoardFieldNames.LifecycleStatus, + Value = "Complete", + WorkOrderVersion = ToVersion(wo) + }, "actor-1"); - Assert.Equal("PastDueStatusBlocked", ex.Code); + Assert.Equal(LifecycleStatus.Completed, afterStatus.LifecycleStatus); + Assert.False(afterStatus.IsPastDue); } [Fact] - public async Task PatchField_FutureDueDate_ClearsPastDueAndAllowsStatusChange() + public async Task PatchField_DueDateAlone_DoesNotClearPastDue() { var (context, service) = CreateSut(); var wo = new WorkOrder @@ -170,19 +174,19 @@ public class WorkOrderBoardUpdateServiceTests WorkOrderVersion = ToVersion(wo) }, "actor-1"); - Assert.False(afterDue.IsPastDue); + 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 afterStatus = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto - { - Field = WorkOrderBoardFieldNames.LifecycleStatus, - Value = "Complete", - WorkOrderVersion = ToVersion(wo) - }, "actor-1"); + var ex = await Assert.ThrowsAsync(() => + 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); + Assert.Equal("PastDueStatusBlocked", ex.Code); } [Fact] diff --git a/SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs b/SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs index 49b5a61..2dd8c6b 100644 --- a/SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderDerivedFieldsTests.cs @@ -12,21 +12,7 @@ public class WorkOrderDerivedFieldsTests private static readonly DateTime TodayDate = DateTime.UtcNow.Date; [Fact] - public void IsPastDue_WhenDuePastAndScheduleFuture_ReturnsTrue() - { - var wo = new WorkOrder - { - DueDate = Past, - ScheduledDate = Future, - LifecycleStatus = LifecycleStatus.Scheduled - }; - - Assert.True(WorkOrderDerivedFields.IsPastDue(wo, Today)); - Assert.True(WorkOrderDerivedFields.IsPastDue(wo.DueDate, wo.LifecycleStatus)); - } - - [Fact] - public void IsPastDue_WhenDueFutureAndSchedulePast_ReturnsFalse() + public void IsPastDue_WhenSchedulePastAndDueFuture_ReturnsTrue() { var wo = new WorkOrder { @@ -35,16 +21,31 @@ public class WorkOrderDerivedFieldsTests LifecycleStatus = LifecycleStatus.Scheduled }; - Assert.False(WorkOrderDerivedFields.IsPastDue(wo, Today)); + Assert.True(WorkOrderDerivedFields.IsPastDue(wo, Today)); + Assert.True(WorkOrderDerivedFields.IsPastDue(wo.ScheduledDate, wo.LifecycleStatus)); } [Fact] - public void IsPastDue_WhenDueDateNull_ReturnsFalse() + public void IsPastDue_WhenScheduleFutureAndDuePast_ReturnsFalse() { var wo = new WorkOrder { - DueDate = null, - ScheduledDate = Past, + DueDate = Past, + ScheduledDate = Future, + LifecycleStatus = LifecycleStatus.Scheduled + }; + + Assert.False(WorkOrderDerivedFields.IsPastDue(wo, Today)); + Assert.False(WorkOrderDerivedFields.IsPastDue(wo.ScheduledDate, wo.LifecycleStatus)); + } + + [Fact] + public void IsPastDue_WhenScheduledDateNull_ReturnsFalse() + { + var wo = new WorkOrder + { + DueDate = Past, + ScheduledDate = null, LifecycleStatus = LifecycleStatus.Scheduled }; @@ -53,12 +54,12 @@ public class WorkOrderDerivedFieldsTests } [Fact] - public void IsPastDue_WhenDueEqualsToday_ReturnsFalse() + public void IsPastDue_WhenScheduleEqualsToday_ReturnsFalse() { var wo = new WorkOrder { - DueDate = TodayDate, - ScheduledDate = Past, + DueDate = Past, + ScheduledDate = TodayDate, LifecycleStatus = LifecycleStatus.Scheduled }; diff --git a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs index 044567b..1d7440e 100644 --- a/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderPhase6Tests.cs @@ -111,8 +111,8 @@ public class WorkOrderDetailServiceTests Id = 1, InternalWONumber = "00000000001", LifecycleStatus = LifecycleStatus.Scheduled, - DueDate = DateTime.UtcNow.AddDays(-2), - ScheduledDate = DateTime.UtcNow.AddDays(3), + DueDate = DateTime.UtcNow.AddDays(3), + ScheduledDate = DateTime.UtcNow.AddDays(-2), Trade = "HVAC PM", WorkOrderType = WorkOrderType.PM, DocStatus = DocStatus.No, diff --git a/SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs b/SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs index 539d4ab..c299e21 100644 --- a/SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderWeekRolledTests.cs @@ -310,8 +310,8 @@ public class WorkOrderWeekRolledTests context.workOrders.Add(new WorkOrder { Id = 1, - DueDate = pastDate, - ScheduledDate = DateTime.UtcNow.Date.AddDays(5), + DueDate = DateTime.UtcNow.Date.AddDays(5), + ScheduledDate = pastDate, LifecycleStatus = LifecycleStatus.Scheduled, OperationalFlags = OperationalFlags.None }); @@ -335,8 +335,8 @@ public class WorkOrderWeekRolledTests context.workOrders.Add(new WorkOrder { Id = 1, - DueDate = futureDate, - ScheduledDate = DateTime.UtcNow.Date.AddDays(-3), + DueDate = DateTime.UtcNow.Date.AddDays(-3), + ScheduledDate = futureDate, LifecycleStatus = LifecycleStatus.Scheduled, OperationalFlags = OperationalFlags.PastDue });