mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-02 08:33:22 +00:00
A reschedule moves the work order into its new week at the moment of the reschedule, so Add-On now compares that moment with the new week's cutoff, matching the prototype. Moves within the same week keep the current value. Previously an older work order moved into a week already past its cutoff stayed off because it was created before that cutoff (SH-412).
265 lines
10 KiB
C#
265 lines
10 KiB
C#
using System.Security.Claims;
|
|
using Api.SeaHavenIndustries.Controllers;
|
|
using Api.SeaHavenIndustries.DTOs;
|
|
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using SeaHaven.DataServices.Implementation;
|
|
using SeaHaven.DataServices.Models;
|
|
using SeaHaven.Services.Implementation;
|
|
using SeaHaven.Services.Validation;
|
|
|
|
namespace SeaHavenIndustries.Tests;
|
|
|
|
public sealed class WorkOrderEditAddonRescheduleTests
|
|
{
|
|
[Fact]
|
|
public async Task EditWorkorder_RescheduleIntoEarlierWeek_EnablesAndAuditsAddOn()
|
|
{
|
|
await using var context = CreateContext();
|
|
context.workOrders.Add(new WorkOrder
|
|
{
|
|
Id = 1,
|
|
CreatedDate = new DateTime(2026, 6, 21, 0, 0, 0, DateTimeKind.Utc),
|
|
ScheduledDate = new DateTime(2026, 6, 29),
|
|
IsAddOn = false
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var result = await CreateController(context).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.Equal(new DateTime(2026, 6, 25), persisted.ScheduledDate);
|
|
Assert.True(persisted.IsAddOn);
|
|
Assert.Equal(1, persisted.RescheduleCount);
|
|
var audit = await context.WorkOrderAuditLogs
|
|
.Where(x => x.WorkOrderId == 1)
|
|
.ToListAsync();
|
|
Assert.Single(audit);
|
|
Assert.Contains(audit, x => x.FieldName?.Contains("Schedule On", StringComparison.Ordinal) == true);
|
|
Assert.Contains(audit, x => x.FieldName?.Contains("IsAddOn", StringComparison.Ordinal) == true);
|
|
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()
|
|
{
|
|
await using var context = CreateContext();
|
|
context.workOrders.Add(new WorkOrder
|
|
{
|
|
Id = 1,
|
|
CreatedDate = new DateTime(2026, 6, 21, 0, 0, 0, DateTimeKind.Utc),
|
|
LifecycleStatus = LifecycleStatus.Incomplete
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var result = await CreateController(context).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.Equal(new DateOnly(2026, 7, 1), persisted.OriginalDate);
|
|
Assert.Equal(new DateOnly(2026, 6, 29), persisted.OriginalWeek);
|
|
Assert.Equal(0, persisted.RescheduleCount);
|
|
Assert.Equal(LifecycleStatus.Scheduled, persisted.LifecycleStatus);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task EditWorkorder_Reschedule_WaitsForWorkOrderMutationGateAndReadsCommittedSchedule()
|
|
{
|
|
const int workOrderId = 41201;
|
|
var databaseName = Guid.NewGuid().ToString();
|
|
await using (var seed = CreateContext(databaseName))
|
|
{
|
|
seed.workOrders.Add(new WorkOrder
|
|
{
|
|
Id = workOrderId,
|
|
CreatedDate = new DateTime(2026, 6, 21, 0, 0, 0, DateTimeKind.Utc),
|
|
ScheduledDate = new DateTime(2026, 7, 6),
|
|
OriginalDate = new DateOnly(2026, 7, 6),
|
|
IsAddOn = false
|
|
});
|
|
await seed.SaveChangesAsync();
|
|
}
|
|
|
|
// A concurrent reschedule holds the work order's mutation gate and commits its own
|
|
// date while the edit is waiting.
|
|
await using var holderContext = CreateContext(databaseName);
|
|
var held = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
var holder = new UpliftDataService(holderContext).ExecuteWorkOrderMutationAsync(
|
|
workOrderId,
|
|
async ct =>
|
|
{
|
|
held.SetResult();
|
|
await release.Task;
|
|
var workOrder = await holderContext.workOrders.SingleAsync(x => x.Id == workOrderId, ct);
|
|
workOrder.ScheduledDate = new DateTime(2026, 6, 29);
|
|
workOrder.RescheduleCount++;
|
|
await holderContext.SaveChangesAsync(ct);
|
|
return true;
|
|
},
|
|
CancellationToken.None);
|
|
await held.Task;
|
|
|
|
await using var editContext = CreateContext(databaseName);
|
|
var edit = CreateController(editContext).Editworkorder(new EditWorkorder_DTO
|
|
{
|
|
Id = workOrderId,
|
|
ScheduledDate = new DateTime(2026, 6, 25)
|
|
});
|
|
await Task.Delay(200);
|
|
|
|
Assert.False(edit.IsCompleted);
|
|
|
|
release.SetResult();
|
|
await holder;
|
|
Assert.IsType<OkObjectResult>(await edit);
|
|
|
|
await using var verify = CreateContext(databaseName);
|
|
var persisted = await verify.workOrders.SingleAsync(x => x.Id == workOrderId);
|
|
Assert.Equal(new DateTime(2026, 6, 25), persisted.ScheduledDate);
|
|
Assert.Equal(2, persisted.RescheduleCount);
|
|
Assert.True(persisted.IsAddOn);
|
|
var audit = await verify.WorkOrderAuditLogs.SingleAsync(x => x.WorkOrderId == workOrderId);
|
|
Assert.Contains("Schedule On: 2026-06-29 → 2026-06-25", audit.FieldName);
|
|
Assert.Contains("RescheduleCount: 1 → 2", audit.FieldName);
|
|
}
|
|
|
|
private static ApplicationDbContext CreateContext(string? databaseName = null)
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(databaseName ?? Guid.NewGuid().ToString())
|
|
.Options;
|
|
return new ApplicationDbContext(options);
|
|
}
|
|
|
|
// 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(
|
|
new WorkOrderDataService(context),
|
|
new CommentDataService(context),
|
|
new UserDataService(context),
|
|
new QuotesDataService(context),
|
|
fileStorage: null!,
|
|
new CreateWorkOrderValidation(),
|
|
new UpdateWorkOrderValidation(),
|
|
accountResolver,
|
|
timeProvider);
|
|
return new WorkOrderController(
|
|
workOrderService,
|
|
accountResolver,
|
|
NullLogger<WorkOrderController>.Instance)
|
|
{
|
|
ControllerContext = new ControllerContext
|
|
{
|
|
HttpContext = new DefaultHttpContext
|
|
{
|
|
User = new ClaimsPrincipal(new ClaimsIdentity(
|
|
new[] { new Claim(ClaimTypes.NameIdentifier, "dispatcher-1") },
|
|
authenticationType: "test"))
|
|
}
|
|
}
|
|
};
|
|
}
|
|
}
|