mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-02 08:33:22 +00:00
Merge pull request #202 from Sea-Haven-Industries/fix/ab/sh-412-addon-reschedule-time
SH-412: recalculate Add-On at reschedule time
This commit is contained in:
commit
2c918b0f6a
7 changed files with 279 additions and 43 deletions
|
|
@ -3,6 +3,7 @@ namespace SeaHaven.Services.Helpers
|
|||
/// <summary>
|
||||
/// Server-side Add-On cutoff: Saturday 23:59:59 UTC of the week prior to the schedule week.
|
||||
/// SH-184: recalculated whenever scheduling is set, changed, or cleared; audited on change.
|
||||
/// At creation the WO arrives when it is created; on a reschedule it arrives when it is moved.
|
||||
/// Create may accept a client hint when unscheduled; schedule present ignores override.
|
||||
/// </summary>
|
||||
public static class WorkOrderAddonCutoff
|
||||
|
|
@ -36,14 +37,40 @@ namespace SeaHaven.Services.Helpers
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// SH-184 authoritative recalc after schedule set/change/clear. No manual override on patch.
|
||||
/// Cleared schedule yields false.
|
||||
/// SH-412: a reschedule into another week makes the WO arrive in that week at the moment
|
||||
/// of the reschedule, so Add-On compares <paramref name="rescheduledAtUtc"/> (not the
|
||||
/// creation time) with the new week's cutoff. A move within the same week keeps the
|
||||
/// current value; a cleared schedule yields false. Matches the prototype's reschedule rule.
|
||||
/// </summary>
|
||||
public static bool RecalculateIsAddOnFromSchedule(
|
||||
DateTime createdAtUtc,
|
||||
public static bool RecalculateIsAddOnOnReschedule(
|
||||
bool currentIsAddOn,
|
||||
DateTime? previousScheduledDate,
|
||||
DateOnly? previousTargetWeek,
|
||||
DateTime? scheduledDate,
|
||||
DateOnly? targetWeek)
|
||||
=> ComputeFromScheduleAnchor(createdAtUtc, scheduledDate, targetWeek, manualOverride: false);
|
||||
DateOnly? targetWeek,
|
||||
DateTime rescheduledAtUtc)
|
||||
{
|
||||
var anchor = ScheduleAnchor(scheduledDate, targetWeek);
|
||||
if (!anchor.HasValue)
|
||||
return false;
|
||||
|
||||
var previousAnchor = ScheduleAnchor(previousScheduledDate, previousTargetWeek);
|
||||
if (previousAnchor.HasValue
|
||||
&& WorkOrderBoardMutationRules.GetWeekStartMonday(previousAnchor.Value)
|
||||
== WorkOrderBoardMutationRules.GetWeekStartMonday(anchor.Value))
|
||||
return currentIsAddOn;
|
||||
|
||||
return NormalizeUtc(rescheduledAtUtc) > GetAddonCutoffUtc(anchor.Value);
|
||||
}
|
||||
|
||||
private static DateTime? ScheduleAnchor(DateTime? scheduledDate, DateOnly? targetWeek)
|
||||
{
|
||||
if (scheduledDate.HasValue)
|
||||
return scheduledDate.Value;
|
||||
if (targetWeek.HasValue)
|
||||
return targetWeek.Value.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool ComputeFromScheduleAnchor(
|
||||
DateTime createdAtUtc,
|
||||
|
|
@ -51,12 +78,7 @@ namespace SeaHaven.Services.Helpers
|
|||
DateOnly? targetWeek,
|
||||
bool? manualOverride)
|
||||
{
|
||||
DateTime? anchor = null;
|
||||
if (scheduledDate.HasValue)
|
||||
anchor = scheduledDate.Value;
|
||||
else if (targetWeek.HasValue)
|
||||
anchor = targetWeek.Value.ToDateTime(TimeOnly.MinValue, DateTimeKind.Utc);
|
||||
|
||||
var anchor = ScheduleAnchor(scheduledDate, targetWeek);
|
||||
if (!anchor.HasValue)
|
||||
return manualOverride ?? false;
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ namespace SeaHaven.Services.Implementation
|
|||
private readonly IWorkOrderUpliftService _upliftService;
|
||||
private readonly IUpliftDataService _upliftData;
|
||||
private readonly IServicesRegistryService? _servicesRegistryService;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public WorkOrderBoardUpdateService(
|
||||
IWorkOrderBoardDataService boardDataService,
|
||||
|
|
@ -26,7 +27,8 @@ namespace SeaHaven.Services.Implementation
|
|||
IWorkOrderAuditService auditService,
|
||||
IWorkOrderUpliftService upliftService,
|
||||
IUpliftDataService upliftData,
|
||||
IServicesRegistryService? servicesRegistryService = null)
|
||||
IServicesRegistryService? servicesRegistryService = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_boardDataService = boardDataService;
|
||||
_mutationData = mutationData;
|
||||
|
|
@ -34,6 +36,7 @@ namespace SeaHaven.Services.Implementation
|
|||
_upliftService = upliftService;
|
||||
_upliftData = upliftData;
|
||||
_servicesRegistryService = servicesRegistryService;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<WorkOrderBoardRowDto> PatchFieldAsync(
|
||||
|
|
@ -753,9 +756,10 @@ namespace SeaHaven.Services.Implementation
|
|||
return FieldChange.ForField(auditField, old, newVal);
|
||||
}
|
||||
|
||||
private static List<FieldChange> ApplyScheduledDate(WorkOrder workOrder, string? value, string auditField)
|
||||
private List<FieldChange> ApplyScheduledDate(WorkOrder workOrder, string? value, string auditField)
|
||||
{
|
||||
var previousDate = workOrder.ScheduledDate;
|
||||
var previousTargetWeek = workOrder.TargetWeek;
|
||||
DateTime? parsed = null;
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
|
|
@ -790,12 +794,14 @@ namespace SeaHaven.Services.Implementation
|
|||
}
|
||||
|
||||
changes.AddRange(ApplyAutoScheduleSideEffects(workOrder));
|
||||
AppendIsAddOnRecalculation(workOrder, changes);
|
||||
AppendIsAddOnRecalculation(workOrder, previousDate, previousTargetWeek, changes);
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static List<FieldChange> ApplyTargetWeek(WorkOrder workOrder, string? value, string auditField)
|
||||
private List<FieldChange> ApplyTargetWeek(WorkOrder workOrder, string? value, string auditField)
|
||||
{
|
||||
var previousDate = workOrder.ScheduledDate;
|
||||
var previousTargetWeek = workOrder.TargetWeek;
|
||||
DateOnly? parsed = null;
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
|
|
@ -813,28 +819,33 @@ namespace SeaHaven.Services.Implementation
|
|||
changes.Add(FieldChange.ForField(auditField, old, newVal));
|
||||
}
|
||||
|
||||
AppendIsAddOnRecalculation(workOrder, changes);
|
||||
AppendIsAddOnRecalculation(workOrder, previousDate, previousTargetWeek, changes);
|
||||
return changes;
|
||||
}
|
||||
|
||||
// Toggling week-only mode keeps the scheduled week, so Add-On is unchanged (SH-412).
|
||||
private static List<FieldChange> ApplyScheduleWeekOnly(WorkOrder workOrder, string? value, string auditField)
|
||||
{
|
||||
var change = ApplyBoolField(value, auditField, v => workOrder.ScheduleWeekOnly = v, () => workOrder.ScheduleWeekOnly);
|
||||
var changes = new List<FieldChange> { change };
|
||||
if (change.HasChanged)
|
||||
AppendIsAddOnRecalculation(workOrder, changes);
|
||||
changes.AddRange(ApplyAutoScheduleSideEffects(workOrder));
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static void AppendIsAddOnRecalculation(WorkOrder workOrder, List<FieldChange> changes)
|
||||
private void AppendIsAddOnRecalculation(
|
||||
WorkOrder workOrder,
|
||||
DateTime? previousScheduledDate,
|
||||
DateOnly? previousTargetWeek,
|
||||
List<FieldChange> changes)
|
||||
{
|
||||
var createdAt = workOrder.CreatedDate ?? DateTime.UtcNow;
|
||||
var oldValue = workOrder.IsAddOn.ToString();
|
||||
var newIsAddOn = WorkOrderAddonCutoff.RecalculateIsAddOnFromSchedule(
|
||||
createdAt,
|
||||
var newIsAddOn = WorkOrderAddonCutoff.RecalculateIsAddOnOnReschedule(
|
||||
workOrder.IsAddOn,
|
||||
previousScheduledDate,
|
||||
previousTargetWeek,
|
||||
workOrder.ScheduledDate,
|
||||
workOrder.TargetWeek);
|
||||
workOrder.TargetWeek,
|
||||
_timeProvider.GetUtcNow().UtcDateTime);
|
||||
|
||||
if (workOrder.IsAddOn == newIsAddOn)
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ namespace SeaHaven.Services.Implementation
|
|||
private readonly ICreateWorkOrderValidation _createValidator;
|
||||
private readonly IUpdateWorkOrderValidation _updateValidator;
|
||||
private readonly IWorkOrderAccountResolver _accountResolver;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
public WorkOrderService(
|
||||
IWorkOrderDataService workOrderDataService,
|
||||
|
|
@ -31,7 +32,8 @@ namespace SeaHaven.Services.Implementation
|
|||
IFileStoragePort fileStorage,
|
||||
ICreateWorkOrderValidation createValidator,
|
||||
IUpdateWorkOrderValidation updateValidator,
|
||||
IWorkOrderAccountResolver accountResolver)
|
||||
IWorkOrderAccountResolver accountResolver,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
_workOrderDataService = workOrderDataService;
|
||||
_commentDataService = commentDataService;
|
||||
|
|
@ -41,6 +43,7 @@ namespace SeaHaven.Services.Implementation
|
|||
_createValidator = createValidator;
|
||||
_updateValidator = updateValidator;
|
||||
_accountResolver = accountResolver;
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
public async Task<WorkOrderDTO?> GetWorkOrderByIdAsync(int id)
|
||||
|
|
@ -116,6 +119,8 @@ namespace SeaHaven.Services.Implementation
|
|||
var scheduledDateChanged = dto.ScheduledDate.HasValue
|
||||
&& dto.ScheduledDate.Value.Date != existingWorkOrder.ScheduledDate?.Date;
|
||||
var previousIsAddOn = existingWorkOrder.IsAddOn;
|
||||
var previousScheduledDate = existingWorkOrder.ScheduledDate;
|
||||
var previousTargetWeek = existingWorkOrder.TargetWeek;
|
||||
UpdateWorkOrderDTO.UpdateEntity(dto, existingWorkOrder);
|
||||
if (scheduledDateChanged)
|
||||
{
|
||||
|
|
@ -126,10 +131,13 @@ namespace SeaHaven.Services.Implementation
|
|||
changes.AddRange(scheduleChanges
|
||||
.Where(c => c.HasChanged && c.FieldName != WorkOrderBoardFieldNames.ScheduledDate)
|
||||
.Select(c => $"{c.FieldName}: {c.OldValue} → {c.NewValue}"));
|
||||
existingWorkOrder.IsAddOn = WorkOrderAddonCutoff.RecalculateIsAddOnFromSchedule(
|
||||
existingWorkOrder.CreatedDate ?? DateTime.UtcNow,
|
||||
existingWorkOrder.IsAddOn = WorkOrderAddonCutoff.RecalculateIsAddOnOnReschedule(
|
||||
previousIsAddOn,
|
||||
previousScheduledDate,
|
||||
previousTargetWeek,
|
||||
existingWorkOrder.ScheduledDate,
|
||||
existingWorkOrder.TargetWeek);
|
||||
existingWorkOrder.TargetWeek,
|
||||
_timeProvider.GetUtcNow().UtcDateTime);
|
||||
if (existingWorkOrder.IsAddOn != previousIsAddOn)
|
||||
changes.Add($"IsAddOn: {previousIsAddOn} → {existingWorkOrder.IsAddOn}");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,14 +19,16 @@ internal static class WorkOrderAccountTestHelpers
|
|||
public static WorkOrderBoardUpdateService BoardUpdateService(
|
||||
ApplicationDbContext context,
|
||||
IWorkOrderAuditService audit,
|
||||
IServicesRegistryService? servicesRegistry = null)
|
||||
IServicesRegistryService? servicesRegistry = null,
|
||||
TimeProvider? timeProvider = null)
|
||||
=> new(
|
||||
new WorkOrderBoardDataService(context),
|
||||
new WorkOrderBoardMutationDataService(context),
|
||||
audit,
|
||||
UpliftService(context),
|
||||
new UpliftDataService(context),
|
||||
servicesRegistry);
|
||||
servicesRegistry,
|
||||
timeProvider);
|
||||
|
||||
public static WorkOrderUpliftService UpliftService(ApplicationDbContext context)
|
||||
=> new(
|
||||
|
|
|
|||
|
|
@ -114,21 +114,78 @@ public class WorkOrderAddonCutoffTests
|
|||
Assert.True(result);
|
||||
}
|
||||
|
||||
// Rescheduled on Wed 2026-06-24: the week of 06-22 is past its cutoff (Sat 06-20 23:59:59),
|
||||
// the week of 06-29 is not (cutoff Sat 06-27 23:59:59).
|
||||
private static readonly DateTime RescheduledAt = new(2026, 6, 24, 10, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
[Fact]
|
||||
public void RecalculateIsAddOnFromSchedule_NoSchedule_ReturnsFalse()
|
||||
public void RecalculateOnReschedule_NoSchedule_ReturnsFalse()
|
||||
{
|
||||
var created = new DateTime(2026, 6, 21, 0, 0, 0, DateTimeKind.Utc);
|
||||
Assert.False(WorkOrderAddonCutoff.RecalculateIsAddOnFromSchedule(created, null, null));
|
||||
Assert.False(WorkOrderAddonCutoff.RecalculateIsAddOnOnReschedule(
|
||||
currentIsAddOn: true,
|
||||
previousScheduledDate: new DateTime(2026, 6, 24),
|
||||
previousTargetWeek: null,
|
||||
scheduledDate: null,
|
||||
targetWeek: null,
|
||||
RescheduledAt));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecalculateIsAddOnFromSchedule_AfterCutoff_ReturnsTrue()
|
||||
public void RecalculateOnReschedule_IntoWeekPastCutoff_TurnsOnForWorkOrderCreatedLongBefore()
|
||||
{
|
||||
var created = new DateTime(2026, 6, 21, 0, 0, 0, DateTimeKind.Utc);
|
||||
var result = WorkOrderAddonCutoff.RecalculateIsAddOnFromSchedule(
|
||||
created,
|
||||
scheduledDate: new DateTime(2026, 6, 24),
|
||||
targetWeek: null);
|
||||
// SH-412: the WO was created weeks earlier; it arrives in the current week at the
|
||||
// moment of the reschedule, which is already past that week's cutoff.
|
||||
var result = WorkOrderAddonCutoff.RecalculateIsAddOnOnReschedule(
|
||||
currentIsAddOn: false,
|
||||
previousScheduledDate: new DateTime(2026, 7, 8),
|
||||
previousTargetWeek: null,
|
||||
scheduledDate: new DateTime(2026, 6, 25),
|
||||
targetWeek: null,
|
||||
RescheduledAt);
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecalculateOnReschedule_IntoWeekBeforeCutoff_TurnsOff()
|
||||
{
|
||||
var result = WorkOrderAddonCutoff.RecalculateIsAddOnOnReschedule(
|
||||
currentIsAddOn: true,
|
||||
previousScheduledDate: new DateTime(2026, 6, 24),
|
||||
previousTargetWeek: null,
|
||||
scheduledDate: new DateTime(2026, 7, 1),
|
||||
targetWeek: null,
|
||||
RescheduledAt);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true)]
|
||||
[InlineData(false)]
|
||||
public void RecalculateOnReschedule_WithinSameWeek_KeepsCurrentValue(bool currentIsAddOn)
|
||||
{
|
||||
var result = WorkOrderAddonCutoff.RecalculateIsAddOnOnReschedule(
|
||||
currentIsAddOn,
|
||||
previousScheduledDate: new DateTime(2026, 6, 22),
|
||||
previousTargetWeek: null,
|
||||
scheduledDate: new DateTime(2026, 6, 26),
|
||||
targetWeek: null,
|
||||
RescheduledAt);
|
||||
|
||||
Assert.Equal(currentIsAddOn, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RecalculateOnReschedule_FirstScheduleIntoWeekPastCutoff_TurnsOn()
|
||||
{
|
||||
var result = WorkOrderAddonCutoff.RecalculateIsAddOnOnReschedule(
|
||||
currentIsAddOn: false,
|
||||
previousScheduledDate: null,
|
||||
previousTargetWeek: null,
|
||||
scheduledDate: new DateTime(2026, 6, 26),
|
||||
targetWeek: null,
|
||||
RescheduledAt);
|
||||
|
||||
Assert.True(result);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ namespace SeaHavenIndustries.Tests;
|
|||
|
||||
public class WorkOrderBoardUpdateServiceTests
|
||||
{
|
||||
private static (ApplicationDbContext Context, WorkOrderBoardUpdateService Service) CreateSut()
|
||||
private static (ApplicationDbContext Context, WorkOrderBoardUpdateService Service) CreateSut(
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||
|
|
@ -23,10 +24,16 @@ public class WorkOrderBoardUpdateServiceTests
|
|||
var mutationData = new WorkOrderBoardMutationDataService(context);
|
||||
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
||||
var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks);
|
||||
var service = WorkOrderAccountTestHelpers.BoardUpdateService(context, audit);
|
||||
var service = WorkOrderAccountTestHelpers.BoardUpdateService(context, audit, timeProvider: timeProvider);
|
||||
return (context, service);
|
||||
}
|
||||
|
||||
// Wed 2026-06-24: the week of 06-22 is past its cutoff, the week of 06-29 is not.
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
public override DateTimeOffset GetUtcNow() => new(2026, 6, 24, 10, 0, 0, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
private static string ToVersion(WorkOrder workOrder)
|
||||
=> Convert.ToBase64String(workOrder.RowVersion ?? new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 });
|
||||
|
||||
|
|
@ -2260,6 +2267,46 @@ public class WorkOrderBoardUpdateServiceTests
|
|||
Assert.True(persisted.IsAddOn);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("2026-07-08", false, "2026-06-25", true)] // into the current week, past its cutoff
|
||||
[InlineData("2026-06-24", true, "2026-07-01", false)] // into next week, before its cutoff
|
||||
[InlineData("2026-07-06", false, "2026-07-09", false)] // same week: unchanged
|
||||
[InlineData("2026-06-22", true, "2026-06-26", true)] // same week: unchanged
|
||||
public async Task PatchField_ScheduledDate_RecalculatesIsAddOnAtRescheduleTime(
|
||||
string from,
|
||||
bool isAddOn,
|
||||
string to,
|
||||
bool expectedIsAddOn)
|
||||
{
|
||||
var (context, service) = CreateSut(new FixedTimeProvider());
|
||||
var wo = new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
ScheduledDate = DateTime.Parse(from),
|
||||
// Created weeks before any of these cutoffs: creation time must not decide (SH-412).
|
||||
CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
IsAddOn = isAddOn,
|
||||
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 = to,
|
||||
WorkOrderVersion = ToVersion(wo)
|
||||
}, "actor-1");
|
||||
|
||||
Assert.Equal(expectedIsAddOn, result.IsAddOn);
|
||||
var persisted = await context.workOrders.SingleAsync(w => w.Id == 1);
|
||||
Assert.Equal(expectedIsAddOn, persisted.IsAddOn);
|
||||
var addOnAudits = await context.WorkOrderAuditLogs.Where(a => a.FieldName == "IsAddOn").ToListAsync();
|
||||
Assert.Equal(isAddOn == expectedIsAddOn ? 0 : 1, addOnAudits.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PatchField_WorkOrderType_DoesNotRecalculateIsAddOn()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -49,6 +49,86 @@ public sealed class WorkOrderEditAddonRescheduleTests
|
|||
Assert.Contains(audit, x => x.FieldName?.Contains("RescheduleCount: 0 → 1", StringComparison.Ordinal) == true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditWorkorder_RescheduleOlderWorkOrderIntoWeekPastCutoff_EnablesAndAuditsAddOn()
|
||||
{
|
||||
// SH-412 retest: the WO was created weeks before the target week's cutoff, so the
|
||||
// creation-time rule kept Add-On off. It arrives in the week when it is rescheduled.
|
||||
await using var context = CreateContext();
|
||||
context.workOrders.Add(new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
ScheduledDate = new DateTime(2026, 7, 8),
|
||||
IsAddOn = false
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var result = await CreateController(context, new FixedTimeProvider()).Editworkorder(new EditWorkorder_DTO
|
||||
{
|
||||
Id = 1,
|
||||
ScheduledDate = new DateTime(2026, 6, 25)
|
||||
});
|
||||
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
var persisted = await context.workOrders.SingleAsync(x => x.Id == 1);
|
||||
Assert.True(persisted.IsAddOn);
|
||||
var audit = await context.WorkOrderAuditLogs.SingleAsync(x => x.WorkOrderId == 1);
|
||||
Assert.Contains("IsAddOn: False → True", audit.FieldName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditWorkorder_RescheduleIntoWeekBeforeCutoff_ClearsAndAuditsAddOn()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
context.workOrders.Add(new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
CreatedDate = new DateTime(2026, 6, 23, 0, 0, 0, DateTimeKind.Utc),
|
||||
ScheduledDate = new DateTime(2026, 6, 24),
|
||||
IsAddOn = true
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var result = await CreateController(context, new FixedTimeProvider()).Editworkorder(new EditWorkorder_DTO
|
||||
{
|
||||
Id = 1,
|
||||
ScheduledDate = new DateTime(2026, 7, 1)
|
||||
});
|
||||
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
var persisted = await context.workOrders.SingleAsync(x => x.Id == 1);
|
||||
Assert.False(persisted.IsAddOn);
|
||||
var audit = await context.WorkOrderAuditLogs.SingleAsync(x => x.WorkOrderId == 1);
|
||||
Assert.Contains("IsAddOn: True → False", audit.FieldName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditWorkorder_RescheduleWithinSameWeek_KeepsAddOn()
|
||||
{
|
||||
await using var context = CreateContext();
|
||||
context.workOrders.Add(new WorkOrder
|
||||
{
|
||||
Id = 1,
|
||||
CreatedDate = new DateTime(2026, 6, 1, 0, 0, 0, DateTimeKind.Utc),
|
||||
ScheduledDate = new DateTime(2026, 7, 6),
|
||||
IsAddOn = false
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
|
||||
var result = await CreateController(context, new FixedTimeProvider()).Editworkorder(new EditWorkorder_DTO
|
||||
{
|
||||
Id = 1,
|
||||
ScheduledDate = new DateTime(2026, 7, 9)
|
||||
});
|
||||
|
||||
Assert.IsType<OkObjectResult>(result);
|
||||
var persisted = await context.workOrders.SingleAsync(x => x.Id == 1);
|
||||
Assert.False(persisted.IsAddOn);
|
||||
var audit = await context.WorkOrderAuditLogs.SingleAsync(x => x.WorkOrderId == 1);
|
||||
Assert.DoesNotContain("IsAddOn", audit.FieldName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditWorkorder_FirstSchedule_SetsOriginalWeekAndPromotesIncomplete()
|
||||
{
|
||||
|
|
@ -145,7 +225,15 @@ public sealed class WorkOrderEditAddonRescheduleTests
|
|||
return new ApplicationDbContext(options);
|
||||
}
|
||||
|
||||
private static WorkOrderController CreateController(ApplicationDbContext context)
|
||||
// Wed 2026-06-24: the week of 06-22 is past its cutoff, the week of 06-29 is not.
|
||||
private sealed class FixedTimeProvider : TimeProvider
|
||||
{
|
||||
public override DateTimeOffset GetUtcNow() => new(2026, 6, 24, 10, 0, 0, TimeSpan.Zero);
|
||||
}
|
||||
|
||||
private static WorkOrderController CreateController(
|
||||
ApplicationDbContext context,
|
||||
TimeProvider? timeProvider = null)
|
||||
{
|
||||
var accountResolver = WorkOrderAccountTestHelpers.Resolver(context);
|
||||
var workOrderService = new WorkOrderService(
|
||||
|
|
@ -156,7 +244,8 @@ public sealed class WorkOrderEditAddonRescheduleTests
|
|||
fileStorage: null!,
|
||||
new CreateWorkOrderValidation(),
|
||||
new UpdateWorkOrderValidation(),
|
||||
accountResolver);
|
||||
accountResolver,
|
||||
timeProvider);
|
||||
return new WorkOrderController(
|
||||
workOrderService,
|
||||
accountResolver,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue