mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-05 16:32:11 +00:00
Compare commits
2 commits
7c097c2750
...
a224f883bc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a224f883bc | ||
|
|
cd23ad5b68 |
4 changed files with 113 additions and 9 deletions
|
|
@ -8,6 +8,10 @@ namespace SeaHaven.DataServices.Implementation
|
|||
{
|
||||
public class CompletionDocTemplateDataService : ICompletionDocTemplateDataService
|
||||
{
|
||||
private static readonly LifecycleStatus[] OpenStatuses = Enum.GetValues<LifecycleStatus>()
|
||||
.Where(status => !LifecycleStatusSets.Terminal.Contains(status))
|
||||
.ToArray();
|
||||
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public CompletionDocTemplateDataService(ApplicationDbContext context)
|
||||
|
|
@ -153,9 +157,12 @@ namespace SeaHaven.DataServices.Implementation
|
|||
.Where(s => s.CompletionDocTemplateId == templateId)
|
||||
.Select(s => (int?)s.Id);
|
||||
|
||||
// Shared status filter so legacy rows without a LifecycleStatus are read
|
||||
// through their legacy status text (a legacy "completed" row is not open).
|
||||
workOrders = WorkOrderBoardQueryFilters.ApplyStatusFilter(workOrders, OpenStatuses);
|
||||
|
||||
return await workOrders
|
||||
.Where(w => w.ServiceId != null && linkedServiceIds.Contains(w.ServiceId))
|
||||
.Where(w => !LifecycleStatusSets.Terminal.Contains(w.LifecycleStatus))
|
||||
.OrderBy(w => w.Id)
|
||||
.Select(w => w.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
|
|
|||
|
|
@ -49,15 +49,33 @@ namespace SeaHaven.Services.DTOs
|
|||
}
|
||||
|
||||
// Create/update body. Legacy fields are optional: on update, null leaves the
|
||||
// stored value unchanged, as does a null Procedures list. ExtraSafetyNote is
|
||||
// always applied, so null removes the note.
|
||||
// stored value unchanged, as does a null Procedures list. WorkOrderType is the
|
||||
// exception: omitting it keeps the stored value, while an explicit
|
||||
// "workOrderType": null clears it so the template applies to every work order
|
||||
// type of its trade. ExtraSafetyNote is always applied, so null removes the note.
|
||||
public class CompletionDocTemplateCreateDto
|
||||
{
|
||||
private WorkOrderType? _workOrderType;
|
||||
|
||||
public string? Name { get; set; }
|
||||
public string? ExtraSafetyNote { get; set; }
|
||||
public List<CompletionDocTemplateProcedureInputDto>? Procedures { get; set; }
|
||||
public string? ServiceKey { get; set; }
|
||||
public WorkOrderType? WorkOrderType { get; set; }
|
||||
|
||||
public WorkOrderType? WorkOrderType
|
||||
{
|
||||
get => _workOrderType;
|
||||
set
|
||||
{
|
||||
_workOrderType = value;
|
||||
WorkOrderTypeSpecified = true;
|
||||
}
|
||||
}
|
||||
|
||||
// True when the body carried workOrderType at all, including as null.
|
||||
[System.Text.Json.Serialization.JsonIgnore]
|
||||
public bool WorkOrderTypeSpecified { get; private set; }
|
||||
|
||||
public string? TemplateUrl { get; set; }
|
||||
public bool? IsActive { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ namespace SeaHaven.Services.Implementation
|
|||
entity.ExtraSafetyNote = input.ExtraSafetyNote;
|
||||
if (input.ServiceKey != null)
|
||||
entity.ServiceKey = input.ServiceKey;
|
||||
if (request.WorkOrderType.HasValue)
|
||||
if (request.WorkOrderTypeSpecified)
|
||||
entity.WorkOrderType = request.WorkOrderType;
|
||||
if (input.TemplateUrl != null)
|
||||
entity.TemplateUrl = input.TemplateUrl;
|
||||
|
|
|
|||
|
|
@ -248,6 +248,42 @@ public class CompletionDocTemplateServiceTests
|
|||
Assert.Null(updated.CreatedByName);
|
||||
}
|
||||
|
||||
// Bodies are bound by System.Text.Json with the web defaults, as MVC does.
|
||||
private static CompletionDocTemplateCreateDto JsonBody(string json) =>
|
||||
System.Text.Json.JsonSerializer.Deserialize<CompletionDocTemplateCreateDto>(
|
||||
json, new System.Text.Json.JsonSerializerOptions(System.Text.Json.JsonSerializerDefaults.Web))!;
|
||||
|
||||
[Fact]
|
||||
public async Task Update_WorkOrderTypeExplicitNullClears_OmittedKeeps_ValueSets()
|
||||
{
|
||||
using var context = NewContext();
|
||||
context.CompletionDocTemplates.Add(new CompletionDocTemplate
|
||||
{
|
||||
Id = 8,
|
||||
Name = "HVAC PM Completion",
|
||||
ServiceKey = "HVAC",
|
||||
WorkOrderType = WorkOrderType.PM,
|
||||
TemplateUrl = "",
|
||||
IsActive = true
|
||||
});
|
||||
await context.SaveChangesAsync();
|
||||
var service = NewService(context);
|
||||
|
||||
var omitted = JsonBody("""{"name":"HVAC Completion"}""");
|
||||
Assert.False(omitted.WorkOrderTypeSpecified);
|
||||
var kept = await service.UpdateAsync(Admin(), 8, omitted, CancellationToken.None);
|
||||
Assert.Equal(WorkOrderType.PM, kept.WorkOrderType);
|
||||
|
||||
var cleared = await service.UpdateAsync(
|
||||
Admin(), 8, JsonBody("""{"name":"HVAC Completion","workOrderType":null}"""), CancellationToken.None);
|
||||
Assert.Null(cleared.WorkOrderType);
|
||||
Assert.Null((await context.CompletionDocTemplates.AsNoTracking().SingleAsync(t => t.Id == 8)).WorkOrderType);
|
||||
|
||||
var set = await service.UpdateAsync(
|
||||
Admin(), 8, JsonBody("""{"name":"HVAC Completion","workOrderType":6}"""), CancellationToken.None);
|
||||
Assert.Equal(WorkOrderType.Reactive, set.WorkOrderType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_MissingOrDeletedTemplate_ThrowsNotFound()
|
||||
{
|
||||
|
|
@ -376,6 +412,7 @@ public class CompletionDocTemplateServiceTests
|
|||
new WorkOrder { Id = 107, ServiceId = 1, AccountId = 1, LifecycleStatus = LifecycleStatus.Scheduled, IsDeleted = true },
|
||||
new WorkOrder { Id = 108, ServiceId = 1, AccountId = 1, LifecycleStatus = LifecycleStatus.Scheduled, istemplate = true },
|
||||
new WorkOrder { Id = 205, ServiceId = 1, AccountId = 2, LifecycleStatus = LifecycleStatus.InProgress });
|
||||
context.workOrders.AddRange(LegacyStatusWorkOrders(serviceId: 1, accountId: 1));
|
||||
await context.SaveChangesAsync();
|
||||
var service = NewService(context);
|
||||
|
||||
|
|
@ -383,13 +420,21 @@ public class CompletionDocTemplateServiceTests
|
|||
var tenantTwo = await service.GetLinkedWorkOrdersAsync(Admin(accountId: 2), 10, CancellationToken.None);
|
||||
var orgWide = await service.GetLinkedWorkOrdersAsync(Admin(), 10, CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, tenantOne.Count);
|
||||
Assert.Equal(new[] { 101, 102 }, tenantOne.WorkOrderIds);
|
||||
Assert.Equal(3, tenantOne.Count);
|
||||
Assert.Equal(new[] { 101, 102, 111 }, tenantOne.WorkOrderIds);
|
||||
Assert.Equal(new[] { 205 }, tenantTwo.WorkOrderIds);
|
||||
Assert.Equal(new[] { 101, 102, 205 }, orgWide.WorkOrderIds);
|
||||
Assert.Equal(3, orgWide.Count);
|
||||
Assert.Equal(new[] { 101, 102, 111, 205 }, orgWide.WorkOrderIds);
|
||||
Assert.Equal(4, orgWide.Count);
|
||||
}
|
||||
|
||||
// Legacy rows carry no LifecycleStatus; their legacy status text decides whether they are open.
|
||||
private static WorkOrder[] LegacyStatusWorkOrders(int serviceId, int accountId) => new[]
|
||||
{
|
||||
new WorkOrder { Id = 109, ServiceId = serviceId, AccountId = accountId, LegacyStatus = "Completed" },
|
||||
new WorkOrder { Id = 110, ServiceId = serviceId, AccountId = accountId, Status = " Cancelled " },
|
||||
new WorkOrder { Id = 111, ServiceId = serviceId, AccountId = accountId, LegacyStatus = "On Hold" }
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public async Task LinkedWorkOrders_UnknownTemplateOrMissingScope_Throws()
|
||||
{
|
||||
|
|
@ -485,6 +530,40 @@ public class CompletionDocTemplateServiceTests
|
|||
Assert.True((await context.CompletionDocTemplates.SingleAsync()).IsDeleted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Relational_LinkedWorkOrdersExcludeTerminalAndLegacyTerminalRows()
|
||||
{
|
||||
await using var connection = new SqliteConnection("DataSource=:memory:");
|
||||
await connection.OpenAsync();
|
||||
await using var context = await NewSqliteContextAsync(connection);
|
||||
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 1);
|
||||
await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 2, "Other Corp");
|
||||
context.CompletionDocTemplates.Add(new CompletionDocTemplate { Id = 10, Name = "Backflow" });
|
||||
context.Services.Add(new Service
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Backflow",
|
||||
NormalizedName = "BACKFLOW",
|
||||
RequiresCompletionDocument = true,
|
||||
CompletionDocTemplateId = 10
|
||||
});
|
||||
context.workOrders.AddRange(
|
||||
new WorkOrder { Id = 101, ServiceId = 1, AccountId = 1, LifecycleStatus = LifecycleStatus.Scheduled },
|
||||
new WorkOrder { Id = 102, ServiceId = 1, AccountId = 1 },
|
||||
new WorkOrder { Id = 103, ServiceId = 1, AccountId = 1, LifecycleStatus = LifecycleStatus.Completed },
|
||||
new WorkOrder { Id = 104, ServiceId = 1, AccountId = 1, LifecycleStatus = LifecycleStatus.Canceled },
|
||||
new WorkOrder { Id = 205, ServiceId = 1, AccountId = 2, LifecycleStatus = LifecycleStatus.OnSite });
|
||||
context.workOrders.AddRange(LegacyStatusWorkOrders(serviceId: 1, accountId: 1));
|
||||
await context.SaveChangesAsync();
|
||||
var data = new CompletionDocTemplateDataService(context);
|
||||
|
||||
var tenantOne = await data.GetOpenLinkedWorkOrderIdsAsync(10, 1, CancellationToken.None);
|
||||
var orgWide = await data.GetOpenLinkedWorkOrderIdsAsync(10, null, CancellationToken.None);
|
||||
|
||||
Assert.Equal(new[] { 101, 102, 111 }, tenantOne);
|
||||
Assert.Equal(new[] { 101, 102, 111, 205 }, orgWide);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Relational_UpdateReplacesProceduresInOrder()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue