shoc-backend/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs

394 lines
15 KiB
C#
Raw Normal View History

using Data.SeaHavenIndustries;
using Data.SeaHavenIndustries.Enums;
using Microsoft.EntityFrameworkCore;
using SeaHaven.DataServices.Implementation;
using SeaHaven.DataServices.Interfaces;
using SeaHaven.Services.DTOs;
using SeaHaven.Services.Exceptions;
using SeaHaven.Services.Helpers;
using SeaHaven.Services.Implementation;
using SeaHaven.Services.Validation;
namespace SeaHavenIndustries.Tests;
// Overdue is a dispatcher-assigned work-order type: no severity, PM catalog
// (services and completion-doc templates), and distinct from the derived
// past-due overlay.
public class WorkOrderOverdueTypeTests
{
private const int PmServiceId = 10;
private const int ReactiveServiceId = 11;
private static ApplicationDbContext NewContext()
{
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
return new ApplicationDbContext(options);
}
private static void SeedServices(ApplicationDbContext context)
{
context.Services.AddRange(
NewService(PmServiceId, "Backflow Preventers", WorkOrderType.PM),
NewService(ReactiveServiceId, "Leak Repair", WorkOrderType.Reactive));
}
private static Service NewService(int id, string name, WorkOrderType type) => new()
{
Id = id,
Name = name,
NormalizedName = name.ToUpperInvariant(),
Trade = "Plumbing & Water Systems",
IconKey = "plumbing-water-systems",
IsActive = true,
SupportedWorkOrderTypes = new List<ServiceWorkOrderType>
{
new() { WorkOrderType = type }
}
};
private static WorkOrderBoardCreateService NewCreateService(ApplicationDbContext context)
{
WorkOrderAccountTestHelpers.SeedBoardCreateScope(context);
var boardData = new WorkOrderBoardDataService(context);
var resolver = WorkOrderAccountTestHelpers.Resolver(context);
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
return new WorkOrderBoardCreateService(
boardData,
new WorkOrderBoardMutationDataService(context),
new WorkOrderBoardService(boardData, resolver),
new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks),
new WorkOrderBoardCreateValidation(),
resolver,
new WorkOrderPocDataService(context),
new ServicesRegistryService(new ServicesRegistryDataService(context)));
}
private static WorkOrderBoardUpdateService NewUpdateService(ApplicationDbContext context)
{
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
return new WorkOrderBoardUpdateService(
new WorkOrderBoardDataService(context),
new WorkOrderBoardMutationDataService(context),
new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks),
new ServicesRegistryService(new ServicesRegistryDataService(context)));
}
private static string ToVersion(WorkOrder workOrder)
=> Convert.ToBase64String(workOrder.RowVersion ?? new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 });
[Fact]
public async Task Create_Overdue_WithoutSeverity_PersistsOverdueType()
{
await using var context = NewContext();
var service = NewCreateService(context);
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
{
WorkOrderType = WorkOrderType.Overdue,
SiteCode = "BK5",
LocationId = 1
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
Assert.Equal(WorkOrderType.Overdue, result.WorkOrderType);
Assert.Null(result.Severity);
var persisted = await context.workOrders.SingleAsync();
Assert.Equal(WorkOrderType.Overdue, persisted.WorkOrderType);
Assert.Equal(8, (int)persisted.WorkOrderType!.Value);
}
[Fact]
public async Task Create_Overdue_DropsSuppliedSeverity()
{
await using var context = NewContext();
var service = NewCreateService(context);
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
{
WorkOrderType = WorkOrderType.Overdue,
SiteCode = "BK5",
LocationId = 1,
Severity = 2
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
Assert.Null(result.Severity);
Assert.Null((await context.workOrders.SingleAsync()).Severity);
}
[Fact]
public async Task Create_Overdue_AcceptsPmCatalogService()
{
await using var context = NewContext();
SeedServices(context);
var service = NewCreateService(context);
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
{
WorkOrderType = WorkOrderType.Overdue,
SiteCode = "BK5",
LocationId = 1,
ServiceId = PmServiceId
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
Assert.Equal(PmServiceId, result.ServiceId);
Assert.Equal("Backflow Preventers", (await context.workOrders.SingleAsync()).ServiceNameSnapshot);
}
[Fact]
public async Task Create_Overdue_RejectsReactiveOnlyService()
{
await using var context = NewContext();
SeedServices(context);
var service = NewCreateService(context);
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
service.CreateAsync(new WorkOrderBoardCreateRequestDto
{
WorkOrderType = WorkOrderType.Overdue,
SiteCode = "BK5",
LocationId = 1,
ServiceId = ReactiveServiceId
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
Assert.Equal("ServiceInvalid", ex.Code);
}
[Fact]
public async Task PatchType_ToOverdue_ClearsSeverity()
{
await using var context = NewContext();
var wo = new WorkOrder
{
Id = 1,
LifecycleStatus = LifecycleStatus.Scheduled,
WorkOrderType = WorkOrderType.Reactive,
Severity = "3",
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
};
context.workOrders.Add(wo);
await context.SaveChangesAsync();
var result = await NewUpdateService(context).PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
{
Field = WorkOrderBoardFieldNames.WorkOrderType,
Value = "Overdue",
WorkOrderVersion = ToVersion(wo)
}, "actor-1");
Assert.Equal(WorkOrderType.Overdue, result.WorkOrderType);
Assert.Null(result.Severity);
var persisted = await context.workOrders.SingleAsync(w => w.Id == 1);
Assert.Equal(WorkOrderType.Overdue, persisted.WorkOrderType);
Assert.Null(persisted.Severity);
}
[Fact]
public async Task PatchType_PmWithPmService_ToOverdue_KeepsService()
{
await using var context = NewContext();
SeedServices(context);
var wo = new WorkOrder
{
Id = 1,
LifecycleStatus = LifecycleStatus.Scheduled,
WorkOrderType = WorkOrderType.PM,
ServiceId = PmServiceId,
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
};
context.workOrders.Add(wo);
await context.SaveChangesAsync();
var result = await NewUpdateService(context).PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
{
Field = WorkOrderBoardFieldNames.WorkOrderType,
Value = "Overdue",
WorkOrderVersion = ToVersion(wo)
}, "actor-1");
Assert.Equal(WorkOrderType.Overdue, result.WorkOrderType);
Assert.Equal(PmServiceId, (await context.workOrders.SingleAsync(w => w.Id == 1)).ServiceId);
}
[Fact]
public async Task PatchServiceId_OnOverdue_AcceptsPmCatalogService()
{
await using var context = NewContext();
SeedServices(context);
var wo = new WorkOrder
{
Id = 1,
LifecycleStatus = LifecycleStatus.Scheduled,
WorkOrderType = WorkOrderType.Overdue,
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
};
context.workOrders.Add(wo);
await context.SaveChangesAsync();
await NewUpdateService(context).PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
{
Field = WorkOrderBoardFieldNames.ServiceId,
Value = PmServiceId.ToString(),
WorkOrderVersion = ToVersion(wo)
}, "actor-1");
var persisted = await context.workOrders.SingleAsync(w => w.Id == 1);
Assert.Equal(PmServiceId, persisted.ServiceId);
Assert.Equal("Backflow Preventers", persisted.Trade);
}
[Fact]
public async Task GetDetail_Overdue_ResolvesPmCompletionTemplate()
{
await using var context = NewContext();
context.workOrders.Add(new WorkOrder
{
Id = 1,
InternalWONumber = "10000000001",
LifecycleStatus = LifecycleStatus.Scheduled,
WorkOrderType = WorkOrderType.Overdue,
ScheduledDate = DateTime.UtcNow.AddDays(1),
DocStatus = DocStatus.No,
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
});
context.CompletionDocTemplates.Add(new CompletionDocTemplate
{
Name = "PM Completion",
ServiceKey = "pm-generic",
WorkOrderType = WorkOrderType.PM,
TemplateUrl = "https://example.com/pm.pdf",
IsActive = true
});
await context.SaveChangesAsync();
var boardData = new WorkOrderBoardDataService(context);
var resolver = WorkOrderAccountTestHelpers.Resolver(context);
var service = new WorkOrderDetailService(
new WorkOrderBoardService(boardData, resolver),
new WorkOrderDetailDataService(context),
new CompletionDocTemplateDataService(context),
new UserDataService(context),
resolver);
var detail = await service.GetDetailAsync(1, WorkOrderAccountTestHelpers.OrgWideAdmin());
Assert.NotNull(detail);
Assert.Equal(DocStatus.No, detail!.Completion.DocStatus);
Assert.NotNull(detail.Completion.Template);
Assert.Equal("PM Completion", detail.Completion.Template!.Name);
}
[Fact]
public async Task Search_TypeOverdue_ReturnsOverdueTypeOnly_NotPastDueRows()
{
await using var context = NewContext();
var today = DateTime.UtcNow.Date;
context.workOrders.AddRange(
new WorkOrder
{
Id = 1,
WorkOrderType = WorkOrderType.Overdue,
ScheduledDate = today.AddDays(2),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000001"
},
new WorkOrder
{
Id = 2,
WorkOrderType = WorkOrderType.PM,
ScheduledDate = today.AddDays(-3),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000002"
},
new WorkOrder
{
Id = 3,
WorkOrderType = WorkOrderType.Overdue,
ScheduledDate = today.AddDays(-2),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000003"
});
await context.SaveChangesAsync();
var result = await NewSearchService(context).SearchAsync(new WorkOrderAdvancedSearchQueryDto
{
DatePreset = WorkOrderAdvancedSearchDatePreset.Custom,
DateFrom = DateOnly.FromDateTime(today.AddMonths(-1)),
DateTo = DateOnly.FromDateTime(today.AddMonths(1)),
Types = new List<WorkOrderType> { WorkOrderType.Overdue }
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
Assert.Equal(new[] { 1, 3 }, result.Items.Select(i => i.Id).OrderBy(id => id));
Assert.All(result.Items, i => Assert.Equal(WorkOrderType.Overdue, i.WorkOrderType));
}
[Fact]
public async Task Search_TypeOverdueWithPastDue_ReturnsIntersection()
{
await using var context = NewContext();
var today = DateTime.UtcNow.Date;
context.workOrders.AddRange(
new WorkOrder
{
Id = 1,
WorkOrderType = WorkOrderType.Overdue,
ScheduledDate = today.AddDays(2),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000001"
},
new WorkOrder
{
Id = 2,
WorkOrderType = WorkOrderType.PM,
ScheduledDate = today.AddDays(-3),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000002"
},
new WorkOrder
{
Id = 3,
WorkOrderType = WorkOrderType.Overdue,
ScheduledDate = today.AddDays(-2),
LifecycleStatus = LifecycleStatus.Scheduled,
InternalWONumber = "10000000003"
});
await context.SaveChangesAsync();
var result = await NewSearchService(context).SearchAsync(new WorkOrderAdvancedSearchQueryDto
{
DatePreset = WorkOrderAdvancedSearchDatePreset.Custom,
DateFrom = DateOnly.FromDateTime(today.AddMonths(-1)),
DateTo = DateOnly.FromDateTime(today.AddMonths(1)),
Types = new List<WorkOrderType> { WorkOrderType.Overdue },
Overdue = true
}, WorkOrderAccountTestHelpers.OrgWideAdmin(), null);
var row = Assert.Single(result.Items);
Assert.Equal(3, row.Id);
Assert.True(row.IsPastDue);
}
[Fact]
public void DashboardBreakdown_CountsByTypeOnly()
{
var today = new DateOnly(2026, 9, 25);
var pastDue = today.AddDays(-3).ToDateTime(TimeOnly.MinValue);
var future = today.AddDays(3).ToDateTime(TimeOnly.MinValue);
var rows = new List<DashboardWorkOrder>
{
new(1, null, WorkOrderType.Overdue, LifecycleStatus.Scheduled, null, future, null, null),
new(2, null, WorkOrderType.PM, LifecycleStatus.Scheduled, null, pastDue, null, null),
new(3, null, WorkOrderType.Overdue, LifecycleStatus.Scheduled, null, pastDue, null, null),
};
var result = DashboardMetrics.Calculate(rows, today);
Assert.Equal(2, result.Breakdown.Overdue);
Assert.Equal(1, result.Breakdown.PM);
Assert.Equal(0, result.Breakdown.Other);
}
private static WorkOrderAdvancedSearchService NewSearchService(ApplicationDbContext context)
=> new(new WorkOrderAdvancedSearchDataService(context), WorkOrderAccountTestHelpers.Resolver(context));
}