mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 08:23:12 +00:00
394 lines
15 KiB
C#
394 lines
15 KiB
C#
|
|
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));
|
||
|
|
}
|