mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 14:13:12 +00:00
feat(work-orders): re-derive lifecycle when board status is patched
Scheduled still requires a concrete date, and Incomplete/Pending with a date must promote even when only lifecycleStatus is sent.
This commit is contained in:
parent
12354f9bf7
commit
8b4aec300f
4 changed files with 202 additions and 10 deletions
|
|
@ -33,7 +33,7 @@ namespace SeaHaven.Services.Helpers
|
|||
}
|
||||
|
||||
if (requested == LifecycleStatus.Scheduled
|
||||
&& (!scheduledDate.HasValue || scheduleWeekOnly == true))
|
||||
&& !WorkOrderBoardMutationRules.HasConcreteSchedule(scheduledDate, scheduleWeekOnly))
|
||||
{
|
||||
throw new WorkOrderBoardValidationException(
|
||||
"ScheduledRequiresDate",
|
||||
|
|
|
|||
|
|
@ -183,7 +183,7 @@ namespace SeaHaven.Services.Implementation
|
|||
WorkOrderBoardFieldNames.WorkOrderType => ApplyWorkOrderType(workOrder, value, auditField),
|
||||
WorkOrderBoardFieldNames.Severity => new List<FieldChange> { ApplySeverity(workOrder, value, auditField) },
|
||||
WorkOrderBoardFieldNames.SiteCode => new List<FieldChange> { ApplyStringField(value, auditField, v => workOrder.SiteCode = v, () => workOrder.SiteCode) },
|
||||
WorkOrderBoardFieldNames.LifecycleStatus => new List<FieldChange> { ApplyLifecycleStatus(workOrder, value, auditField) },
|
||||
WorkOrderBoardFieldNames.LifecycleStatus => ApplyLifecycleStatus(workOrder, value, auditField),
|
||||
WorkOrderBoardFieldNames.AssignTo => ApplyAssignTo(workOrder, value, auditField),
|
||||
WorkOrderBoardFieldNames.DueDate => new List<FieldChange> { ApplyDateField(value, auditField, v => workOrder.DueDate = v, () => workOrder.DueDate) },
|
||||
WorkOrderBoardFieldNames.ScheduledDate => ApplyScheduledDate(workOrder, value, auditField),
|
||||
|
|
@ -503,7 +503,7 @@ namespace SeaHaven.Services.Implementation
|
|||
return normalized;
|
||||
}
|
||||
|
||||
private static FieldChange ApplyLifecycleStatus(WorkOrder workOrder, string? value, string auditField)
|
||||
private static List<FieldChange> ApplyLifecycleStatus(WorkOrder workOrder, string? value, string auditField)
|
||||
{
|
||||
var parsed = LifecycleStatusMapper.ParseLifecycleStatus(value);
|
||||
if (parsed == null)
|
||||
|
|
@ -516,16 +516,31 @@ namespace SeaHaven.Services.Implementation
|
|||
"Replace the provisional SH work order number before marking the work order Completed.");
|
||||
}
|
||||
|
||||
if (parsed == LifecycleStatus.Scheduled
|
||||
&& !WorkOrderBoardMutationRules.HasConcreteSchedule(
|
||||
workOrder.ScheduledDate,
|
||||
workOrder.ScheduleWeekOnly))
|
||||
{
|
||||
throw new WorkOrderBoardValidationException(
|
||||
"ScheduledRequiresDate",
|
||||
"Scheduled requires a concrete scheduledDate when scheduleWeekOnly is not true.");
|
||||
}
|
||||
|
||||
var changes = new List<FieldChange>();
|
||||
var old = workOrder.LifecycleStatus?.ToString() ?? workOrder.Status;
|
||||
if (workOrder.LifecycleStatus == parsed)
|
||||
return FieldChange.Unchanged(auditField);
|
||||
if (workOrder.LifecycleStatus != parsed)
|
||||
{
|
||||
workOrder.LifecycleStatus = parsed;
|
||||
workOrder.Status = WorkOrderDerivedFields.GetLifecycleStatusLabel(parsed);
|
||||
if (workOrder.LegacyStatus == null && workOrder.Status != null)
|
||||
workOrder.LegacyStatus = workOrder.Status;
|
||||
|
||||
workOrder.LifecycleStatus = parsed;
|
||||
workOrder.Status = WorkOrderDerivedFields.GetLifecycleStatusLabel(parsed);
|
||||
if (workOrder.LegacyStatus == null && workOrder.Status != null)
|
||||
workOrder.LegacyStatus = workOrder.Status;
|
||||
changes.Add(FieldChange.StatusChanged(auditField, old, parsed.ToString()));
|
||||
}
|
||||
|
||||
return FieldChange.StatusChanged(auditField, old, parsed.ToString());
|
||||
// why: Incomplete/Pending patched while a concrete date already exists must still promote.
|
||||
changes.AddRange(ApplyAutoScheduleSideEffects(workOrder));
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static List<FieldChange> ApplyAssignTo(WorkOrder workOrder, string? value, string auditField)
|
||||
|
|
|
|||
|
|
@ -742,6 +742,26 @@ public class WorkOrderBoardCreateServiceTests
|
|||
Assert.Equal("ScheduledRequiresDate", ex.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_WithScheduledLifecycleWeekOnly_Throws()
|
||||
{
|
||||
var (_, service) = CreateSut();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
||||
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||
{
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
SiteCode = "BK5",
|
||||
LocationId = 1,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
ScheduledDate = new DateTime(2026, 7, 15),
|
||||
ScheduleWeekOnly = true,
|
||||
TargetWeek = new DateOnly(2026, 7, 13),
|
||||
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
||||
|
||||
Assert.Equal("ScheduledRequiresDate", ex.Code);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_EmergencyWithSeverity_Persists()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -128,6 +128,9 @@ public class WorkOrderBoardUpdateServiceTests
|
|||
|
||||
Assert.Equal(LifecycleStatus.Pending, result.LifecycleStatus);
|
||||
Assert.Null(result.ScheduledDate);
|
||||
var audits = await context.WorkOrderAuditLogs.ToListAsync();
|
||||
Assert.Contains(audits, a => a.Action == AuditActionType.StatusChanged.ToString());
|
||||
Assert.Contains(audits, a => a.FieldName == "LifecycleStatus" && a.NewValue == LifecycleStatus.Pending.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -154,6 +157,136 @@ public class WorkOrderBoardUpdateServiceTests
|
|||
|
||||
Assert.Equal(LifecycleStatus.Pending, result.LifecycleStatus);
|
||||
Assert.True(result.ScheduleWeekOnly);
|
||||
var audits = await context.WorkOrderAuditLogs.ToListAsync();
|
||||
Assert.Contains(audits, a => a.Action == AuditActionType.StatusChanged.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_ScheduleWeekOnlyFalse_PromotesPendingToScheduled()
|
||||
{
|
||||
var (context, service) = CreateSut();
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
LifecycleStatus = LifecycleStatus.Pending,
|
||||
ScheduledDate = new DateTime(2026, 12, 15),
|
||||
ScheduleWeekOnly = 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.ScheduleWeekOnly,
|
||||
Value = "false",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1");
|
||||
|
||||
Assert.Equal(LifecycleStatus.Scheduled, result.LifecycleStatus);
|
||||
Assert.False(result.ScheduleWeekOnly);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_LifecycleStatusScheduledWithoutDate_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<WorkOrderBoardValidationException>(() =>
|
||||
service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
Field = WorkOrderBoardFieldNames.LifecycleStatus,
|
||||
Value = "Scheduled",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1"));
|
||||
|
||||
Assert.Equal("ScheduledRequiresDate", ex.Code);
|
||||
Assert.Equal(LifecycleStatus.Incomplete, (await context.workOrders.SingleAsync()).LifecycleStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_LifecycleStatusScheduledWithWeekOnly_Throws()
|
||||
{
|
||||
var (context, service) = CreateSut();
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
LifecycleStatus = LifecycleStatus.Pending,
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(14),
|
||||
ScheduleWeekOnly = true,
|
||||
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||
};
|
||||
context.workOrders.Add(wo);
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
||||
service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||
{
|
||||
Field = WorkOrderBoardFieldNames.LifecycleStatus,
|
||||
Value = "Scheduled",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1"));
|
||||
|
||||
Assert.Equal("ScheduledRequiresDate", ex.Code);
|
||||
Assert.Equal(LifecycleStatus.Pending, (await context.workOrders.SingleAsync()).LifecycleStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_LifecycleStatusScheduledWithConcreteDate_Persists()
|
||||
{
|
||||
var (context, service) = CreateSut();
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
LifecycleStatus = LifecycleStatus.Incomplete,
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(14),
|
||||
ScheduleWeekOnly = 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.LifecycleStatus,
|
||||
Value = "Scheduled",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1");
|
||||
|
||||
Assert.Equal(LifecycleStatus.Scheduled, result.LifecycleStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_LifecycleStatusPendingWithConcreteDate_PromotesToScheduled()
|
||||
{
|
||||
var (context, service) = CreateSut();
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
LifecycleStatus = LifecycleStatus.Incomplete,
|
||||
ScheduledDate = DateTime.UtcNow.Date.AddDays(14),
|
||||
ScheduleWeekOnly = 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.LifecycleStatus,
|
||||
Value = "Pending",
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1");
|
||||
|
||||
Assert.Equal(LifecycleStatus.Scheduled, result.LifecycleStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -180,6 +313,30 @@ public class WorkOrderBoardUpdateServiceTests
|
|||
Assert.Equal(LifecycleStatus.InProgress, result.LifecycleStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_EnRoute_ClearDate_DoesNotChangeStatus()
|
||||
{
|
||||
var (context, service) = CreateSut();
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
LifecycleStatus = LifecycleStatus.EnRoute,
|
||||
ScheduledDate = new DateTime(2026, 6, 25),
|
||||
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 = null,
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1");
|
||||
|
||||
Assert.Equal(LifecycleStatus.EnRoute, result.LifecycleStatus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_IncrementsRescheduleCount()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue