From a224f883bc47b5ffcbd372fe1fb1bc9f3a08b173 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 11:40:49 -0300 Subject: [PATCH] fix(completion-templates): let PUT clear workOrderType with an explicit null UpdateAsync only applied WorkOrderType when it had a value, so once a template was restricted to one work order type no request could make it trade-generic again. The DTO now records whether workOrderType was present in the body: omitting it keeps the stored value, an explicit null clears it, and a concrete value sets it. The templates page echoes the stored legacy fields on PUT, so its behaviour is unchanged. --- SeaHaven.Services/DTOs/WorkOrderDetailDTOs.cs | 24 +++++++++++-- .../CompletionDocTemplateService.cs | 2 +- .../CompletionDocTemplateServiceTests.cs | 36 +++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) 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() {