Compare commits

..

No commits in common. "a224f883bc47b5ffcbd372fe1fb1bc9f3a08b173" and "7c097c27508adb0983b5ce17ceea3fb49829a752" have entirely different histories.

4 changed files with 9 additions and 113 deletions

View file

@ -8,10 +8,6 @@ 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)
@ -157,12 +153,9 @@ 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);

View file

@ -49,33 +49,15 @@ 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. 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.
// stored value unchanged, as does a null Procedures list. 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 => _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 WorkOrderType? WorkOrderType { get; set; }
public string? TemplateUrl { get; set; }
public bool? IsActive { get; set; }
}

View file

@ -105,7 +105,7 @@ namespace SeaHaven.Services.Implementation
entity.ExtraSafetyNote = input.ExtraSafetyNote;
if (input.ServiceKey != null)
entity.ServiceKey = input.ServiceKey;
if (request.WorkOrderTypeSpecified)
if (request.WorkOrderType.HasValue)
entity.WorkOrderType = request.WorkOrderType;
if (input.TemplateUrl != null)
entity.TemplateUrl = input.TemplateUrl;

View file

@ -248,42 +248,6 @@ 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()
{
@ -412,7 +376,6 @@ 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);
@ -420,21 +383,13 @@ 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(3, tenantOne.Count);
Assert.Equal(new[] { 101, 102, 111 }, tenantOne.WorkOrderIds);
Assert.Equal(2, tenantOne.Count);
Assert.Equal(new[] { 101, 102 }, tenantOne.WorkOrderIds);
Assert.Equal(new[] { 205 }, tenantTwo.WorkOrderIds);
Assert.Equal(new[] { 101, 102, 111, 205 }, orgWide.WorkOrderIds);
Assert.Equal(4, orgWide.Count);
Assert.Equal(new[] { 101, 102, 205 }, orgWide.WorkOrderIds);
Assert.Equal(3, 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()
{
@ -530,40 +485,6 @@ 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()
{