Merge pull request #204 from Sea-Haven-Industries/fix/ab/past-due-cancel
Some checks are pending
Backend CI / Build and test (push) Waiting to run
Backend CI / architecture (push) Waiting to run
Backend CI / review (push) Waiting to run
Backend CI / ci-complete (push) Blocked by required conditions

fix(workorders): let Cancel through the Past Due status lock
This commit is contained in:
Alexandre Brandizzi 2026-10-01 23:25:42 +00:00 • committed by GitHub
commit 20315f6370
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 55 additions and 6 deletions

View file

@ -42,8 +42,14 @@ namespace SeaHaven.Services.Helpers
return resolved == WorkOrderMediaCategory.Extra || resolved == WorkOrderMediaCategory.Aveta;
}
public static bool ShouldBlockStatusChangeWhenPastDue(string field, bool isPastDue)
=> field.Equals(WorkOrderBoardFieldNames.LifecycleStatus, StringComparison.OrdinalIgnoreCase) && isPastDue;
/// <summary>
/// Past Due locks lifecycle progress until Schedule On moves (SH-262). Cancel stays
/// available: it is its own action (SH-113), not a status update.
/// </summary>
public static bool ShouldBlockStatusChangeWhenPastDue(string field, bool isPastDue, LifecycleStatus? requestedStatus)
=> field.Equals(WorkOrderBoardFieldNames.LifecycleStatus, StringComparison.OrdinalIgnoreCase)
&& isPastDue
&& requestedStatus != LifecycleStatus.Canceled;
/// <summary>
/// Concrete Schedule On: a calendar day is set and the row is not week-only.

View file

@ -92,7 +92,10 @@ namespace SeaHaven.Services.Implementation
}
var isPastDue = WorkOrderDerivedFields.IsPastDue(workOrder.ScheduledDate, workOrder.LifecycleStatus);
if (WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(canonicalField, isPastDue))
if (WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(
canonicalField,
isPastDue,
LifecycleStatusMapper.ParseLifecycleStatus(request.Value)))
throw new WorkOrderBoardValidationException("PastDueStatusBlocked", "Status cannot be changed while work order is past due. Update Schedule On first.");
_mutationData.SetExpectedWorkOrderVersion(workOrder, workOrderVersion);

View file

@ -19,11 +19,21 @@ public class WorkOrderBoardMutationRulesTests
public void ShouldBlockStatusChangeWhenPastDue_BlocksOnlyLifecycleStatus()
{
Assert.True(WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(
WorkOrderBoardFieldNames.LifecycleStatus, isPastDue: true));
WorkOrderBoardFieldNames.LifecycleStatus, isPastDue: true, LifecycleStatus.InProgress));
Assert.False(WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(
WorkOrderBoardFieldNames.ScheduledDate, isPastDue: true));
WorkOrderBoardFieldNames.ScheduledDate, isPastDue: true, null));
Assert.False(WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(
WorkOrderBoardFieldNames.DueDate, isPastDue: true));
WorkOrderBoardFieldNames.DueDate, isPastDue: true, null));
}
[Theory]
[InlineData(LifecycleStatus.Canceled, false)]
[InlineData(LifecycleStatus.Completed, true)]
[InlineData(LifecycleStatus.EnRoute, true)]
public void ShouldBlockStatusChangeWhenPastDue_LetsOnlyCancelThrough(LifecycleStatus requested, bool expected)
{
Assert.Equal(expected, WorkOrderBoardMutationRules.ShouldBlockStatusChangeWhenPastDue(
WorkOrderBoardFieldNames.LifecycleStatus, isPastDue: true, requested));
}
[Fact]

View file

@ -524,6 +524,36 @@ public class WorkOrderBoardUpdateServiceTests
Assert.DoesNotContain("due date", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task PatchField_CancelsPastDueWorkOrderWithoutTouchingSchedule()
{
var (context, service) = CreateSut();
var scheduledOn = DateTime.UtcNow.Date.AddDays(-3);
var wo = new WorkOrder
{
Id = 1,
LifecycleStatus = LifecycleStatus.Scheduled,
ScheduledDate = scheduledOn,
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.LifecycleStatus,
Value = "Canceled",
WorkOrderVersion = ToVersion(wo)
}, "actor-1");
Assert.Equal(LifecycleStatus.Canceled, result.LifecycleStatus);
var stored = await context.workOrders.SingleAsync(w => w.Id == 1);
Assert.Equal(LifecycleStatus.Canceled, stored.LifecycleStatus);
Assert.Equal(scheduledOn, stored.ScheduledDate);
Assert.Equal(1, stored.RescheduleCount);
}
[Fact]
public async Task PatchField_DueDate_DoesNotChangeScheduledDate()
{