diff --git a/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs index 3c72403..0ae5ed7 100644 --- a/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs @@ -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? 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; } } diff --git a/SeaHaven.Services/Implementation/CompletionDocTemplateService.cs b/SeaHaven.Services/Implementation/CompletionDocTemplateService.cs index 6f95833..ab5a111 100644 --- a/SeaHaven.Services/Implementation/CompletionDocTemplateService.cs +++ b/SeaHaven.Services/Implementation/CompletionDocTemplateService.cs @@ -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; diff --git a/SeaHavenIndustries.Tests/CompletionDocTemplateServiceTests.cs b/SeaHavenIndustries.Tests/CompletionDocTemplateServiceTests.cs index cc53bbe..77e2160 100644 --- a/SeaHavenIndustries.Tests/CompletionDocTemplateServiceTests.cs +++ b/SeaHavenIndustries.Tests/CompletionDocTemplateServiceTests.cs @@ -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( + 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() {