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.
This commit is contained in:
Alexandre Brandizzi 2026-09-25 11:40:49 -03:00
parent cd23ad5b68
commit a224f883bc
3 changed files with 58 additions and 4 deletions

View file

@ -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; }
}

View file

@ -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;

View file

@ -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()
{