mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
fix(work-orders): derive Past Due from DueDate instead of ScheduledDate
Past Due must track the deadline (Due Date), not Schedule On. Keep dueDate and scheduledDate PATCH mutations independent so rescheduling alone does not clear Past Due.
This commit is contained in:
parent
5f614fe893
commit
05dd262e80
14 changed files with 250 additions and 38 deletions
|
|
@ -71,8 +71,8 @@ namespace SeaHaven.DataServices.Helpers
|
|||
if (overdue && !hasTypes)
|
||||
{
|
||||
return query.Where(w =>
|
||||
w.ScheduledDate != null
|
||||
&& w.ScheduledDate.Value.Date < today
|
||||
w.DueDate != null
|
||||
&& w.DueDate.Value.Date < today
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled);
|
||||
}
|
||||
|
|
@ -80,8 +80,8 @@ namespace SeaHaven.DataServices.Helpers
|
|||
if (overdue)
|
||||
{
|
||||
return query.Where(w =>
|
||||
(w.ScheduledDate != null
|
||||
&& w.ScheduledDate.Value.Date < today
|
||||
(w.DueDate != null
|
||||
&& w.DueDate.Value.Date < today
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled)
|
||||
|| (w.WorkOrderType != null && typeFilters.Contains(w.WorkOrderType.Value)));
|
||||
|
|
|
|||
|
|
@ -60,8 +60,8 @@ namespace SeaHaven.DataServices.Helpers
|
|||
|| (w.WorkOrderType == WorkOrderType.AddOn && ("add-on".Contains(s) || "addon".Contains(s)))
|
||||
|| (w.WorkOrderType == WorkOrderType.Other && "other".Contains(s))
|
||||
|| ("overdue".Contains(s)
|
||||
&& w.ScheduledDate != null
|
||||
&& w.ScheduledDate.Value.Date < today
|
||||
&& w.DueDate != null
|
||||
&& w.DueDate.Value.Date < today
|
||||
&& w.LifecycleStatus != LifecycleStatus.Completed
|
||||
&& w.LifecycleStatus != LifecycleStatus.Canceled)
|
||||
|| (w.LifecycleStatus == LifecycleStatus.Incomplete && "incomplete".Contains(s))
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ namespace SeaHaven.DataServices.Implementation
|
|||
var batch = await _context.workOrders
|
||||
.Where(w => w.IsDeleted != true)
|
||||
.Where(w => w.istemplate != true)
|
||||
.Where(w => w.ScheduledDate != null)
|
||||
.Where(w => w.DueDate != null)
|
||||
.OrderBy(w => w.Id)
|
||||
.Skip(skip)
|
||||
.Take(batchSize)
|
||||
|
|
@ -132,7 +132,7 @@ namespace SeaHaven.DataServices.Implementation
|
|||
foreach (var workOrder in batch)
|
||||
{
|
||||
examinedCount++;
|
||||
var shouldBePastDue = workOrder.ScheduledDate!.Value.Date < today
|
||||
var shouldBePastDue = workOrder.DueDate!.Value.Date < today
|
||||
&& !TerminalStatuses.Contains(workOrder.LifecycleStatus);
|
||||
var hasFlag = workOrder.OperationalFlags.HasFlag(OperationalFlags.PastDue);
|
||||
|
||||
|
|
|
|||
|
|
@ -6,37 +6,38 @@ namespace SeaHaven.Services.Helpers
|
|||
public static class WorkOrderDerivedFields
|
||||
{
|
||||
/// <summary>
|
||||
/// Past due overlay — never a WorkOrderType. Matches SHOC board design.
|
||||
/// Past due overlay from DueDate (deadline) — never a WorkOrderType.
|
||||
/// Independent of ScheduledDate (planned work / Schedule On).
|
||||
/// </summary>
|
||||
public static bool IsPastDue(WorkOrder workOrder, DateOnly? asOf = null)
|
||||
{
|
||||
var today = asOf ?? DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
if (workOrder.ScheduledDate == null)
|
||||
if (workOrder.DueDate == null)
|
||||
return false;
|
||||
|
||||
if (LifecycleStatusMapper.IsTerminal(workOrder.LifecycleStatus))
|
||||
return false;
|
||||
|
||||
return DateOnly.FromDateTime(workOrder.ScheduledDate.Value.Date) < today;
|
||||
return DateOnly.FromDateTime(workOrder.DueDate.Value.Date) < today;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Board read-model past-due check. Phase 1 compares UTC calendar dates;
|
||||
/// Board read-model past-due check against DueDate. Compares UTC calendar dates;
|
||||
/// business/location timezone support is planned for a later phase.
|
||||
/// </summary>
|
||||
public static bool IsPastDue(
|
||||
DateTime? scheduledDate,
|
||||
DateTime? dueDate,
|
||||
LifecycleStatus? lifecycleStatus,
|
||||
DateTime? utcNow = null)
|
||||
{
|
||||
if (!scheduledDate.HasValue)
|
||||
if (!dueDate.HasValue)
|
||||
return false;
|
||||
|
||||
if (LifecycleStatusMapper.IsTerminal(lifecycleStatus))
|
||||
return false;
|
||||
|
||||
var today = (utcNow ?? DateTime.UtcNow).Date;
|
||||
return scheduledDate.Value.Date < today;
|
||||
return dueDate.Value.Date < today;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ namespace SeaHaven.Services.Implementation
|
|||
RescheduleCount = row.RescheduleCount,
|
||||
CarriedOver = row.CarriedOver,
|
||||
WorkOrderType = row.WorkOrderType,
|
||||
IsPastDue = WorkOrderDerivedFields.IsPastDue(row.ScheduledDate, row.LifecycleStatus, utcNow),
|
||||
IsPastDue = WorkOrderDerivedFields.IsPastDue(row.DueDate, row.LifecycleStatus, utcNow),
|
||||
SiteCode = row.SiteCode,
|
||||
LocationName = row.LocationName,
|
||||
PocName = row.PocName,
|
||||
|
|
|
|||
|
|
@ -61,9 +61,9 @@ 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.ScheduledDate, workOrder.LifecycleStatus);
|
||||
var isPastDue = WorkOrderDerivedFields.IsPastDue(workOrder.DueDate, workOrder.LifecycleStatus);
|
||||
if (WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(canonicalField, isPastDue))
|
||||
throw new WorkOrderBoardValidationException("PastDueStatusBlocked", "Status cannot be changed while work order is past due. Reschedule first.");
|
||||
throw new WorkOrderBoardValidationException("PastDueStatusBlocked", "Status cannot be changed while work order is past due. Update due date first.");
|
||||
|
||||
_mutationData.SetExpectedWorkOrderVersion(workOrder, workOrderVersion);
|
||||
|
||||
|
|
@ -232,7 +232,7 @@ namespace SeaHaven.Services.Implementation
|
|||
if (string.Equals(value.Trim(), "Overdue", StringComparison.OrdinalIgnoreCase))
|
||||
throw new WorkOrderBoardValidationException(
|
||||
"InvalidValue",
|
||||
"Overdue is not a work order type; it is derived from scheduledDate (isPastDue).");
|
||||
"Overdue is not a work order type; it is derived from dueDate (isPastDue).");
|
||||
|
||||
if (!WorkOrderTypeMapper.TryParseFeLabel(value, out var parsed))
|
||||
throw new WorkOrderBoardValidationException("InvalidValue", $"Invalid workOrderType: {value}");
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ public class WorkOrderBoardMutationRulesTests
|
|||
WorkOrderBoardFieldNames.LifecycleStatus, isPastDue: true));
|
||||
Assert.False(WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(
|
||||
WorkOrderBoardFieldNames.ScheduledDate, isPastDue: true));
|
||||
Assert.False(WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(
|
||||
WorkOrderBoardFieldNames.DueDate, isPastDue: true));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -342,7 +342,8 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
{
|
||||
Id = 1,
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
ScheduledDate = today.AddDays(-3),
|
||||
DueDate = today.AddDays(-3),
|
||||
ScheduledDate = today.AddDays(5),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000001"
|
||||
},
|
||||
|
|
@ -358,7 +359,8 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
{
|
||||
Id = 3,
|
||||
WorkOrderType = WorkOrderType.Reactive,
|
||||
ScheduledDate = today.AddDays(1),
|
||||
DueDate = today.AddDays(1),
|
||||
ScheduledDate = today.AddDays(-5),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000003"
|
||||
});
|
||||
|
|
@ -392,7 +394,8 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
{
|
||||
Id = 1,
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
ScheduledDate = today.AddDays(-3),
|
||||
DueDate = today.AddDays(-3),
|
||||
ScheduledDate = today.AddDays(5),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000001"
|
||||
},
|
||||
|
|
@ -408,6 +411,7 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
{
|
||||
Id = 3,
|
||||
WorkOrderType = WorkOrderType.Reactive,
|
||||
DueDate = today.AddDays(1),
|
||||
ScheduledDate = today.AddDays(1),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000003"
|
||||
|
|
@ -444,7 +448,8 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
{
|
||||
Id = 1,
|
||||
WorkOrderType = WorkOrderType.Other,
|
||||
ScheduledDate = today.AddDays(5),
|
||||
DueDate = today.AddDays(5),
|
||||
ScheduledDate = today.AddDays(-5),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000001"
|
||||
},
|
||||
|
|
@ -452,7 +457,8 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
{
|
||||
Id = 2,
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
ScheduledDate = today.AddDays(-3),
|
||||
DueDate = today.AddDays(-3),
|
||||
ScheduledDate = today.AddDays(5),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
InternalWONumber = "10000000002"
|
||||
},
|
||||
|
|
@ -460,6 +466,7 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
{
|
||||
Id = 3,
|
||||
WorkOrderType = WorkOrderType.Reactive,
|
||||
DueDate = today.AddDays(-2),
|
||||
ScheduledDate = today.AddDays(-2),
|
||||
LifecycleStatus = LifecycleStatus.Completed,
|
||||
InternalWONumber = "10000000003"
|
||||
|
|
@ -468,6 +475,7 @@ public class WorkOrderAdvancedSearchServiceTests
|
|||
{
|
||||
Id = 4,
|
||||
WorkOrderType = WorkOrderType.Reactive,
|
||||
DueDate = today.AddDays(-1),
|
||||
ScheduledDate = today.AddDays(-1),
|
||||
LifecycleStatus = LifecycleStatus.Canceled,
|
||||
InternalWONumber = "10000000004"
|
||||
|
|
|
|||
|
|
@ -391,6 +391,7 @@ public class WorkOrderBoardServiceTests
|
|||
{
|
||||
Id = 1,
|
||||
InternalWONumber = "10000000001",
|
||||
DueDate = new DateTime(2026, 6, 20),
|
||||
ScheduledDate = new DateTime(2026, 6, 24, 12, 0, 0, DateTimeKind.Utc),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled
|
||||
});
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@ public class WorkOrderBoardUpdateServiceTests
|
|||
{
|
||||
Id = 1,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(-3),
|
||||
DueDate = DateTime.UtcNow.Date.AddDays(-3),
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(5),
|
||||
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||
};
|
||||
context.workOrders.Add(wo);
|
||||
|
|
@ -108,6 +109,145 @@ public class WorkOrderBoardUpdateServiceTests
|
|||
Assert.Equal("PastDueStatusBlocked", ex.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_RescheduleAlone_DoesNotClearPastDueBlock()
|
||||
{
|
||||
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.True(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<WorkOrderBoardValidationException>(() =>
|
||||
service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
Field = WorkOrderBoardFieldNames.LifecycleStatus,
|
||||
Value = "Complete",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1"));
|
||||
|
||||
Assert.Equal("PastDueStatusBlocked", ex.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_FutureDueDate_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 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.False(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");
|
||||
|
||||
Assert.Equal(LifecycleStatus.Completed, afterStatus.LifecycleStatus);
|
||||
Assert.False(afterStatus.IsPastDue);
|
||||
}
|
||||
|
||||
[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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -6,28 +6,79 @@ namespace SeaHavenIndustries.Tests;
|
|||
|
||||
public class WorkOrderDerivedFieldsTests
|
||||
{
|
||||
private static readonly DateOnly Today = DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
private static readonly DateTime Past = DateTime.UtcNow.Date.AddDays(-2);
|
||||
private static readonly DateTime Future = DateTime.UtcNow.Date.AddDays(3);
|
||||
private static readonly DateTime TodayDate = DateTime.UtcNow.Date;
|
||||
|
||||
[Fact]
|
||||
public void IsPastDue_WhenScheduledBeforeTodayAndNotTerminal_ReturnsTrue()
|
||||
public void IsPastDue_WhenDuePastAndScheduleFuture_ReturnsTrue()
|
||||
{
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
ScheduledDate = DateTime.UtcNow.AddDays(-2),
|
||||
DueDate = Past,
|
||||
ScheduledDate = Future,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled
|
||||
};
|
||||
|
||||
Assert.True(WorkOrderDerivedFields.IsPastDue(wo, DateOnly.FromDateTime(DateTime.UtcNow)));
|
||||
Assert.True(WorkOrderDerivedFields.IsPastDue(wo, Today));
|
||||
Assert.True(WorkOrderDerivedFields.IsPastDue(wo.DueDate, wo.LifecycleStatus));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPastDue_WhenCompleted_ReturnsFalse()
|
||||
public void IsPastDue_WhenDueFutureAndSchedulePast_ReturnsFalse()
|
||||
{
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
ScheduledDate = DateTime.UtcNow.AddDays(-2),
|
||||
LifecycleStatus = LifecycleStatus.Completed
|
||||
DueDate = Future,
|
||||
ScheduledDate = Past,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled
|
||||
};
|
||||
|
||||
Assert.False(WorkOrderDerivedFields.IsPastDue(wo, DateOnly.FromDateTime(DateTime.UtcNow)));
|
||||
Assert.False(WorkOrderDerivedFields.IsPastDue(wo, Today));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPastDue_WhenDueDateNull_ReturnsFalse()
|
||||
{
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
DueDate = null,
|
||||
ScheduledDate = Past,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled
|
||||
};
|
||||
|
||||
Assert.False(WorkOrderDerivedFields.IsPastDue(wo, Today));
|
||||
Assert.False(WorkOrderDerivedFields.IsPastDue(null, LifecycleStatus.Scheduled));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPastDue_WhenDueEqualsToday_ReturnsFalse()
|
||||
{
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
DueDate = TodayDate,
|
||||
ScheduledDate = Past,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled
|
||||
};
|
||||
|
||||
Assert.False(WorkOrderDerivedFields.IsPastDue(wo, Today));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(LifecycleStatus.Completed)]
|
||||
[InlineData(LifecycleStatus.Canceled)]
|
||||
public void IsPastDue_WhenTerminal_ReturnsFalse(LifecycleStatus status)
|
||||
{
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
DueDate = Past,
|
||||
ScheduledDate = Past,
|
||||
LifecycleStatus = status
|
||||
};
|
||||
|
||||
Assert.False(WorkOrderDerivedFields.IsPastDue(wo, Today));
|
||||
Assert.False(WorkOrderDerivedFields.IsPastDue(Past, status));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ public class WorkOrderDetailServiceTests
|
|||
Id = 1,
|
||||
InternalWONumber = "00000000001",
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
ScheduledDate = DateTime.UtcNow.AddDays(-2),
|
||||
DueDate = DateTime.UtcNow.AddDays(-2),
|
||||
ScheduledDate = DateTime.UtcNow.AddDays(3),
|
||||
Trade = "HVAC PM",
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
DocStatus = DocStatus.No,
|
||||
|
|
|
|||
|
|
@ -240,12 +240,14 @@ public class WorkOrderWeekRolledTests
|
|||
new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
DueDate = pastDate,
|
||||
ScheduledDate = pastDate,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled
|
||||
},
|
||||
new WorkOrder
|
||||
{
|
||||
Id = 2,
|
||||
DueDate = pastDate,
|
||||
ScheduledDate = pastDate,
|
||||
LifecycleStatus = LifecycleStatus.Completed,
|
||||
OperationalFlags = OperationalFlags.PastDue
|
||||
|
|
@ -253,6 +255,7 @@ public class WorkOrderWeekRolledTests
|
|||
new WorkOrder
|
||||
{
|
||||
Id = 3,
|
||||
DueDate = futureDate,
|
||||
ScheduledDate = futureDate,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
OperationalFlags = OperationalFlags.PastDue
|
||||
|
|
@ -307,7 +310,8 @@ public class WorkOrderWeekRolledTests
|
|||
context.workOrders.Add(new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
ScheduledDate = pastDate,
|
||||
DueDate = pastDate,
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(5),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
OperationalFlags = OperationalFlags.None
|
||||
});
|
||||
|
|
@ -319,7 +323,7 @@ public class WorkOrderWeekRolledTests
|
|||
|
||||
Assert.NotNull(row);
|
||||
Assert.True(row!.IsPastDue);
|
||||
Assert.False(WorkOrderDerivedFields.IsPastDue(pastDate, LifecycleStatus.Scheduled) == false);
|
||||
Assert.True(WorkOrderDerivedFields.IsPastDue(pastDate, LifecycleStatus.Scheduled));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -331,7 +335,8 @@ public class WorkOrderWeekRolledTests
|
|||
context.workOrders.Add(new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
ScheduledDate = futureDate,
|
||||
DueDate = futureDate,
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(-3),
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
OperationalFlags = OperationalFlags.PastDue
|
||||
});
|
||||
|
|
|
|||
|
|
@ -88,10 +88,13 @@ Lista opções para o filtro/dropdown de assignee do board.
|
|||
### isPastDue
|
||||
|
||||
```
|
||||
ScheduledDate < UTC hoje
|
||||
AND LifecycleStatus NOT IN (Complete, Canceled, Closed)
|
||||
DueDate != null
|
||||
AND DueDate < UTC hoje
|
||||
AND LifecycleStatus NOT IN (Completed, Canceled)
|
||||
```
|
||||
|
||||
`DueDate` null → `isPastDue = false`. Independent of `ScheduledDate` (Schedule On).
|
||||
|
||||
Implementação: `SeaHaven.Services/Helpers/WorkOrderDerivedFields.cs`
|
||||
|
||||
### Vendor / Appt
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue