From b545d4a4fe6a735b978b9204fcb272398067e1c6 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 10:57:34 -0300 Subject: [PATCH 1/4] feat(work-orders): add Overdue work order type Overdue (8) is a dispatcher-assigned type, separate from the derived past-due overlay. It takes no severity, resolves services and completion-doc templates from the PM catalog, and filters as its own type. The past-due flag now narrows a type filter instead of widening it, and the dashboard breakdown partitions by stored type. --- .../DashboardServiceTests.cs | 4 +- .../Enums/WorkOrderEnums.cs | 2 + .../Helpers/WorkOrderBoardQueryFilters.cs | 10 +- .../Helpers/WorkOrderBoardSearchFilter.cs | 1 + SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs | 2 +- .../DTOs/WorkOrderBoardRequestDTOs.cs | 2 +- SeaHaven.Services/Helpers/DashboardMetrics.cs | 25 +- .../Helpers/WorkOrderCatalogType.cs | 17 + .../Helpers/WorkOrderTypeMapper.cs | 5 +- .../WorkOrderBoardCreateService.cs | 5 +- .../WorkOrderBoardUpdateService.cs | 13 +- .../WorkOrderCompletionService.cs | 4 +- .../Implementation/WorkOrderDetailService.cs | 2 +- .../WorkOrderBoardSearchTests.cs | 13 +- .../WorkOrderBoardUpdateServiceTests.cs | 20 +- .../WorkOrderOverdueTypeTests.cs | 393 ++++++++++++++++++ .../WorkOrderTypeMapperTests.cs | 9 +- 17 files changed, 461 insertions(+), 66 deletions(-) create mode 100644 SeaHaven.Services/Helpers/WorkOrderCatalogType.cs create mode 100644 SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs diff --git a/Api.SeaHavenIndustries.Tests/DashboardServiceTests.cs b/Api.SeaHavenIndustries.Tests/DashboardServiceTests.cs index 2cc73ee..7358083 100644 --- a/Api.SeaHavenIndustries.Tests/DashboardServiceTests.cs +++ b/Api.SeaHavenIndustries.Tests/DashboardServiceTests.cs @@ -213,10 +213,10 @@ public class DashboardServiceTests AccountUser(1), query, CancellationToken.None); stats.Total.Should().Be(4); - stats.Breakdown.Overdue.Should().Be(1); + stats.Breakdown.Overdue.Should().Be(0); stats.Breakdown.Other.Should().Be(0); stats.Breakdown.PM.Should().Be(2); - stats.Breakdown.Emergency.Should().Be(0); + stats.Breakdown.Emergency.Should().Be(1); stats.Breakdown.Reactive.Should().Be(1); (stats.Breakdown.PM + stats.Breakdown.Emergency + stats.Breakdown.Reactive + stats.Breakdown.Overdue + stats.Breakdown.Other).Should().Be(stats.Total); diff --git a/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs b/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs index f50824f..11ada94 100644 --- a/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs +++ b/Data.SeaHavenIndustries/Enums/WorkOrderEnums.cs @@ -35,6 +35,8 @@ namespace Data.SeaHavenIndustries.Enums Inspection = 5, Reactive = 6, AddOn = 7, + /// Client never responded after the due date passed. Distinct from the derived past-due overlay. + Overdue = 8, Other = 99 } diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs index 583aaa2..6557db2 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardQueryFilters.cs @@ -58,9 +58,9 @@ namespace SeaHaven.DataServices.Helpers } /// - /// Filters by real (including ) - /// and/or past-due rows when is true. - /// Types and overdue are combined with OR when both are present. + /// Filters by stored (including and + /// ) and/or past-due rows when is true. + /// Past due is a status overlay, so types and overdue are combined with AND when both are present. /// public static IQueryable ApplyTypeAndOverdueFilter( IQueryable query, @@ -94,8 +94,8 @@ namespace SeaHaven.DataServices.Helpers && w.ScheduledDate.Value.Date < today && w.LifecycleStatus != LifecycleStatus.Completed && w.LifecycleStatus != LifecycleStatus.Canceled) - || (w.WorkOrderType != null && typeFilters.Contains(w.WorkOrderType.Value)) - || (includeAddOnFlag && w.IsAddOn)); + && ((w.WorkOrderType != null && typeFilters.Contains(w.WorkOrderType.Value)) + || (includeAddOnFlag && w.IsAddOn))); } return query.Where(w => diff --git a/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs b/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs index 441802b..5232121 100644 --- a/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs +++ b/SeaHaven.DataServices/Helpers/WorkOrderBoardSearchFilter.cs @@ -57,6 +57,7 @@ namespace SeaHaven.DataServices.Helpers || (w.WorkOrderType == WorkOrderType.Project && "project".Contains(s)) || (w.WorkOrderType == WorkOrderType.Inspection && "inspection".Contains(s)) || (w.WorkOrderType == WorkOrderType.Reactive && "reactive".Contains(s)) + || (w.WorkOrderType == WorkOrderType.Overdue && "overdue".Contains(s)) || (w.WorkOrderType == WorkOrderType.AddOn && ("add-on".Contains(s) || "addon".Contains(s))) || (w.IsAddOn && ("add-on".Contains(s) || "addon".Contains(s))) || (w.WorkOrderType == WorkOrderType.Other && "other".Contains(s)) diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs index b6ca553..44938d8 100644 --- a/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderBoardDTOs.cs @@ -10,7 +10,7 @@ namespace SeaHaven.Services.DTOs public List? Dispatchers { get; set; } public bool MyWorkOrders { get; set; } public List? Types { get; set; } - /// When true, include past-due (isPastDue) rows. Combined with Types via OR. + /// When true, keep only past-due (isPastDue) rows. Combined with Types via AND. public bool Overdue { get; set; } public string? Search { get; set; } } diff --git a/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs b/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs index 38b4c24..de8f90d 100644 --- a/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs +++ b/SeaHaven.Services/DTOs/WorkOrderBoardRequestDTOs.cs @@ -98,7 +98,7 @@ namespace SeaHaven.Services.DTOs public List? Sites { get; set; } public List? Regions { get; set; } public List? Types { get; set; } - /// When true, include past-due (isPastDue) rows. Combined with Types via OR. + /// When true, keep only past-due (isPastDue) rows. Combined with Types via AND. public bool Overdue { get; set; } public List? Dispatchers { get; set; } public List? Statuses { get; set; } diff --git a/SeaHaven.Services/Helpers/DashboardMetrics.cs b/SeaHaven.Services/Helpers/DashboardMetrics.cs index 885ac7d..b75101d 100644 --- a/SeaHaven.Services/Helpers/DashboardMetrics.cs +++ b/SeaHaven.Services/Helpers/DashboardMetrics.cs @@ -21,9 +21,7 @@ public static class DashboardMetrics ?? LifecycleStatusMapper.ParseLifecycleStatus(workOrder.LegacyStatus); var completed = status == LifecycleStatus.Completed; var canceled = status == LifecycleStatus.Canceled; - var pastDue = IsPastDue(workOrder, status, today); - - AddBreakdownCount(breakdown, workOrder.WorkOrderType, pastDue); + AddBreakdownCount(breakdown, workOrder.WorkOrderType); var due = completed || (workOrder.ScheduledDate is DateTime scheduled && DateOnly.FromDateTime(scheduled.Date) <= today); @@ -81,25 +79,11 @@ public static class DashboardMetrics .ThenBy(bucket => bucket.Status, StringComparer.OrdinalIgnoreCase) .ToList(); - private static bool IsPastDue( - DashboardWorkOrder workOrder, - LifecycleStatus? status, - DateOnly today) - => workOrder.ScheduledDate is DateTime scheduled - && DateOnly.FromDateTime(scheduled.Date) < today - && status is not LifecycleStatus.Completed and not LifecycleStatus.Canceled; - + // Partition by the stored type only; past due is an overlay on any type, not a bucket. private static void AddBreakdownCount( DashboardBreakdownDTO breakdown, - WorkOrderType? type, - bool pastDue) + WorkOrderType? type) { - if (pastDue) - { - breakdown.Overdue++; - return; - } - switch (type) { case WorkOrderType.PM: @@ -111,6 +95,9 @@ public static class DashboardMetrics case WorkOrderType.Reactive: breakdown.Reactive++; break; + case WorkOrderType.Overdue: + breakdown.Overdue++; + break; default: breakdown.Other++; break; diff --git a/SeaHaven.Services/Helpers/WorkOrderCatalogType.cs b/SeaHaven.Services/Helpers/WorkOrderCatalogType.cs new file mode 100644 index 0000000..41daa0e --- /dev/null +++ b/SeaHaven.Services/Helpers/WorkOrderCatalogType.cs @@ -0,0 +1,17 @@ +using Data.SeaHavenIndustries.Enums; + +namespace SeaHaven.Services.Helpers +{ + /// + /// The services registry and completion-doc templates are keyed on PM, Reactive and Emergency. + /// An Overdue work order is PM work the client never confirmed, so it uses the PM catalog. + /// + public static class WorkOrderCatalogType + { + public static WorkOrderType For(WorkOrderType type) + => type == WorkOrderType.Overdue ? WorkOrderType.PM : type; + + public static WorkOrderType? For(WorkOrderType? type) + => type.HasValue ? For(type.Value) : null; + } +} diff --git a/SeaHaven.Services/Helpers/WorkOrderTypeMapper.cs b/SeaHaven.Services/Helpers/WorkOrderTypeMapper.cs index 4eda69f..d12f555 100644 --- a/SeaHaven.Services/Helpers/WorkOrderTypeMapper.cs +++ b/SeaHaven.Services/Helpers/WorkOrderTypeMapper.cs @@ -4,7 +4,7 @@ namespace SeaHaven.Services.Helpers { public static class WorkOrderTypeMapper { - /// Board FE types. Overdue is not a type — use . + /// Board FE types. Overdue is a dispatcher-assigned type; past due is derived — see . public static bool TryParseFeLabel(string? label, out WorkOrderType type) { type = default; @@ -20,7 +20,7 @@ namespace SeaHaven.Services.Helpers "project" => Set(WorkOrderType.Project, out type), "inspection" => Set(WorkOrderType.Inspection, out type), "other" => Set(WorkOrderType.Other, out type), - "overdue" => false, + "overdue" => Set(WorkOrderType.Overdue, out type), _ => Enum.TryParse(label, true, out type) }; } @@ -30,6 +30,7 @@ namespace SeaHaven.Services.Helpers WorkOrderType.PM => "PM", WorkOrderType.Reactive => "Reactive", WorkOrderType.Emergency => "Emergency", + WorkOrderType.Overdue => "Overdue", WorkOrderType.AddOn => "Add-On", WorkOrderType.PO => "PO", WorkOrderType.Project => "Project", diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs index 149a47b..6218c9b 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs @@ -298,7 +298,8 @@ namespace SeaHaven.Services.Implementation return null; } - if (!ServicesRegistryService.AllowedWorkOrderTypes.Contains(workOrderType)) + var catalogType = WorkOrderCatalogType.For(workOrderType); + if (!ServicesRegistryService.AllowedWorkOrderTypes.Contains(catalogType)) { if (serviceId.HasValue) throw new WorkOrderBoardValidationException( @@ -307,7 +308,7 @@ namespace SeaHaven.Services.Implementation return null; } - var services = await _servicesRegistryService.GetAllAsync(true, workOrderType, cancellationToken); + var services = await _servicesRegistryService.GetAllAsync(true, catalogType, cancellationToken); if (serviceId.HasValue) { var selected = services.FirstOrDefault(service => service.Id == serviceId.Value); diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs index 5a04011..fcd8721 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardUpdateService.cs @@ -370,11 +370,6 @@ namespace SeaHaven.Services.Implementation if (string.IsNullOrWhiteSpace(value)) throw new WorkOrderBoardValidationException("InvalidValue", "workOrderType value is required."); - if (string.Equals(value.Trim(), "Overdue", StringComparison.OrdinalIgnoreCase)) - throw new WorkOrderBoardValidationException( - "InvalidValue", - "Overdue is not a work order type; it is derived from dueDate (isPastDue)."); - if (!WorkOrderTypeMapper.TryParseFeLabel(value, out var parsed)) throw new WorkOrderBoardValidationException("InvalidValue", $"Invalid workOrderType: {value}"); @@ -426,10 +421,11 @@ namespace SeaHaven.Services.Implementation throw new WorkOrderBoardValidationException("ServiceInvalid", "serviceId must be a positive integer."); var service = await _servicesRegistryService.GetByIdAsync(serviceId, cancellationToken); - if (!ServicesRegistryService.AllowedWorkOrderTypes.Contains(workOrder.WorkOrderType ?? WorkOrderType.PM) + var catalogType = WorkOrderCatalogType.For(workOrder.WorkOrderType ?? WorkOrderType.PM); + if (!ServicesRegistryService.AllowedWorkOrderTypes.Contains(catalogType) || service == null || !service.IsActive - || !service.SupportedWorkOrderTypes.Contains(workOrder.WorkOrderType ?? WorkOrderType.PM)) + || !service.SupportedWorkOrderTypes.Contains(catalogType)) { throw new WorkOrderBoardValidationException( "ServiceInvalid", @@ -458,7 +454,7 @@ namespace SeaHaven.Services.Implementation throw new WorkOrderBoardValidationException("ServiceTypeInvalid", "Service registry is unavailable."); var service = await _servicesRegistryService.GetByIdAsync(serviceId, cancellationToken); - if (service == null || !service.SupportedWorkOrderTypes.Contains(workOrderType)) + if (service == null || !service.SupportedWorkOrderTypes.Contains(WorkOrderCatalogType.For(workOrderType))) throw new WorkOrderBoardValidationException( "ServiceTypeInvalid", "The selected service does not support this work order type."); @@ -467,7 +463,6 @@ namespace SeaHaven.Services.Implementation private static WorkOrderType ParseWorkOrderType(string? value) { if (string.IsNullOrWhiteSpace(value) - || string.Equals(value.Trim(), "Overdue", StringComparison.OrdinalIgnoreCase) || !WorkOrderTypeMapper.TryParseFeLabel(value, out var parsed)) { throw new WorkOrderBoardValidationException("InvalidValue", $"Invalid workOrderType: {value}"); diff --git a/SeaHaven.Services/Implementation/WorkOrderCompletionService.cs b/SeaHaven.Services/Implementation/WorkOrderCompletionService.cs index 534cd6b..f2f0895 100644 --- a/SeaHaven.Services/Implementation/WorkOrderCompletionService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderCompletionService.cs @@ -105,7 +105,9 @@ namespace SeaHaven.Services.Implementation await _completionData.SaveAsync(cancellationToken); var extended = await _detailData.GetExtendedFieldsAsync(workOrderId); - var template = await _templateData.ResolveForWorkOrderAsync(workOrder.Trade, workOrder.WorkOrderType); + var template = await _templateData.ResolveForWorkOrderAsync( + workOrder.Trade, + WorkOrderCatalogType.For(workOrder.WorkOrderType)); var signoffs = await _detailData.GetDispatchSignoffsAsync(workOrderId); return new WorkOrderCompletionDto diff --git a/SeaHaven.Services/Implementation/WorkOrderDetailService.cs b/SeaHaven.Services/Implementation/WorkOrderDetailService.cs index 9e77a69..432996d 100644 --- a/SeaHaven.Services/Implementation/WorkOrderDetailService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderDetailService.cs @@ -48,7 +48,7 @@ namespace SeaHaven.Services.Implementation var template = await _templateData.ResolveForWorkOrderAsync( extended?.Trade ?? boardRow.Pm, - boardRow.WorkOrderType); + WorkOrderCatalogType.For(boardRow.WorkOrderType)); var authorIds = WorkOrderCommentProjection.CollectAuthorUserIds(comments); var authorNames = await _userDataService.GetDisplayNamesByIdsAsync(authorIds); diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs index 6a1f2e3..1e2ce22 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardSearchTests.cs @@ -601,7 +601,7 @@ public class WorkOrderAdvancedSearchServiceTests } [Fact] - public async Task SearchAsync_OtherPlusOverdue_ReturnsUnion() + public async Task SearchAsync_OtherPlusOverdue_ReturnsIntersection() { await using var context = CreateContext(); var today = DateTime.UtcNow.Date; @@ -610,7 +610,7 @@ public class WorkOrderAdvancedSearchServiceTests new WorkOrder { Id = 1, - WorkOrderType = WorkOrderType.PM, + WorkOrderType = WorkOrderType.Other, DueDate = today.AddDays(5), ScheduledDate = today.AddDays(-3), LifecycleStatus = LifecycleStatus.Scheduled, @@ -648,10 +648,11 @@ public class WorkOrderAdvancedSearchServiceTests Overdue = true }, WorkOrderAccountTestHelpers.OrgWideAdmin(), null); - Assert.Equal(2, result.TotalCount); - Assert.Contains(result.Items, i => i.Id == 1 && i.IsPastDue); - Assert.Contains(result.Items, i => i.Id == 2 && i.WorkOrderType == WorkOrderType.Other); - Assert.DoesNotContain(result.Items, i => i.Id == 3); + // Past due is a status overlay: it narrows the type filter instead of widening it. + var row = Assert.Single(result.Items); + Assert.Equal(1, row.Id); + Assert.True(row.IsPastDue); + Assert.Equal(WorkOrderType.Other, row.WorkOrderType); } [Fact] diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs index b2c715e..fcf096e 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardUpdateServiceTests.cs @@ -581,7 +581,7 @@ public class WorkOrderBoardUpdateServiceTests } [Fact] - public async Task PatchField_RejectsOverdueAsWorkOrderType() + public async Task PatchField_AcceptsOverdueAsWorkOrderType() { var (context, service) = CreateSut(); var wo = new WorkOrder @@ -594,17 +594,15 @@ public class WorkOrderBoardUpdateServiceTests context.workOrders.Add(wo); await context.SaveChangesAsync(); - var ex = await Assert.ThrowsAsync(() => - service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto - { - Field = WorkOrderBoardFieldNames.WorkOrderType, - Value = "Overdue", - WorkOrderVersion = ToVersion(wo) - }, "actor-1")); + var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto + { + Field = WorkOrderBoardFieldNames.WorkOrderType, + Value = "Overdue", + WorkOrderVersion = ToVersion(wo) + }, "actor-1"); - Assert.Equal("InvalidValue", ex.Code); - Assert.Contains("isPastDue", ex.Message, StringComparison.Ordinal); - Assert.Contains("dueDate", ex.Message, StringComparison.OrdinalIgnoreCase); + Assert.Equal(WorkOrderType.Overdue, result.WorkOrderType); + Assert.Equal(WorkOrderType.Overdue, (await context.workOrders.SingleAsync(w => w.Id == 1)).WorkOrderType); } [Fact] diff --git a/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs b/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs new file mode 100644 index 0000000..0b3657b --- /dev/null +++ b/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs @@ -0,0 +1,393 @@ +using Data.SeaHavenIndustries; +using Data.SeaHavenIndustries.Enums; +using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Implementation; +using SeaHaven.DataServices.Interfaces; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Exceptions; +using SeaHaven.Services.Helpers; +using SeaHaven.Services.Implementation; +using SeaHaven.Services.Validation; + +namespace SeaHavenIndustries.Tests; + +// Overdue is a dispatcher-assigned work-order type: no severity, PM catalog +// (services and completion-doc templates), and distinct from the derived +// past-due overlay. +public class WorkOrderOverdueTypeTests +{ + private const int PmServiceId = 10; + private const int ReactiveServiceId = 11; + + private static ApplicationDbContext NewContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .Options; + return new ApplicationDbContext(options); + } + + private static void SeedServices(ApplicationDbContext context) + { + context.Services.AddRange( + NewService(PmServiceId, "Backflow Preventers", WorkOrderType.PM), + NewService(ReactiveServiceId, "Leak Repair", WorkOrderType.Reactive)); + } + + private static Service NewService(int id, string name, WorkOrderType type) => new() + { + Id = id, + Name = name, + NormalizedName = name.ToUpperInvariant(), + Trade = "Plumbing & Water Systems", + IconKey = "plumbing-water-systems", + IsActive = true, + SupportedWorkOrderTypes = new List + { + new() { WorkOrderType = type } + } + }; + + private static WorkOrderBoardCreateService NewCreateService(ApplicationDbContext context) + { + WorkOrderAccountTestHelpers.SeedBoardCreateScope(context); + var boardData = new WorkOrderBoardDataService(context); + var resolver = WorkOrderAccountTestHelpers.Resolver(context); + var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context)); + return new WorkOrderBoardCreateService( + boardData, + new WorkOrderBoardMutationDataService(context), + new WorkOrderBoardService(boardData, resolver), + new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks), + new WorkOrderBoardCreateValidation(), + resolver, + new WorkOrderPocDataService(context), + new ServicesRegistryService(new ServicesRegistryDataService(context))); + } + + private static WorkOrderBoardUpdateService NewUpdateService(ApplicationDbContext context) + { + var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context)); + return new WorkOrderBoardUpdateService( + new WorkOrderBoardDataService(context), + new WorkOrderBoardMutationDataService(context), + new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks), + new ServicesRegistryService(new ServicesRegistryDataService(context))); + } + + private static string ToVersion(WorkOrder workOrder) + => Convert.ToBase64String(workOrder.RowVersion ?? new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }); + + [Fact] + public async Task Create_Overdue_WithoutSeverity_PersistsOverdueType() + { + await using var context = NewContext(); + var service = NewCreateService(context); + + var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto + { + WorkOrderType = WorkOrderType.Overdue, + SiteCode = "BK5", + LocationId = 1 + }, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"); + + Assert.Equal(WorkOrderType.Overdue, result.WorkOrderType); + Assert.Null(result.Severity); + var persisted = await context.workOrders.SingleAsync(); + Assert.Equal(WorkOrderType.Overdue, persisted.WorkOrderType); + Assert.Equal(8, (int)persisted.WorkOrderType!.Value); + } + + [Fact] + public async Task Create_Overdue_DropsSuppliedSeverity() + { + await using var context = NewContext(); + var service = NewCreateService(context); + + var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto + { + WorkOrderType = WorkOrderType.Overdue, + SiteCode = "BK5", + LocationId = 1, + Severity = 2 + }, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"); + + Assert.Null(result.Severity); + Assert.Null((await context.workOrders.SingleAsync()).Severity); + } + + [Fact] + public async Task Create_Overdue_AcceptsPmCatalogService() + { + await using var context = NewContext(); + SeedServices(context); + var service = NewCreateService(context); + + var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto + { + WorkOrderType = WorkOrderType.Overdue, + SiteCode = "BK5", + LocationId = 1, + ServiceId = PmServiceId + }, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"); + + Assert.Equal(PmServiceId, result.ServiceId); + Assert.Equal("Backflow Preventers", (await context.workOrders.SingleAsync()).ServiceNameSnapshot); + } + + [Fact] + public async Task Create_Overdue_RejectsReactiveOnlyService() + { + await using var context = NewContext(); + SeedServices(context); + var service = NewCreateService(context); + + var ex = await Assert.ThrowsAsync(() => + service.CreateAsync(new WorkOrderBoardCreateRequestDto + { + WorkOrderType = WorkOrderType.Overdue, + SiteCode = "BK5", + LocationId = 1, + ServiceId = ReactiveServiceId + }, WorkOrderAccountTestHelpers.AccountUser(), "actor-1")); + + Assert.Equal("ServiceInvalid", ex.Code); + } + + [Fact] + public async Task PatchType_ToOverdue_ClearsSeverity() + { + await using var context = NewContext(); + var wo = new WorkOrder + { + Id = 1, + LifecycleStatus = LifecycleStatus.Scheduled, + WorkOrderType = WorkOrderType.Reactive, + Severity = "3", + RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } + }; + context.workOrders.Add(wo); + await context.SaveChangesAsync(); + + var result = await NewUpdateService(context).PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto + { + Field = WorkOrderBoardFieldNames.WorkOrderType, + Value = "Overdue", + WorkOrderVersion = ToVersion(wo) + }, "actor-1"); + + Assert.Equal(WorkOrderType.Overdue, result.WorkOrderType); + Assert.Null(result.Severity); + var persisted = await context.workOrders.SingleAsync(w => w.Id == 1); + Assert.Equal(WorkOrderType.Overdue, persisted.WorkOrderType); + Assert.Null(persisted.Severity); + } + + [Fact] + public async Task PatchType_PmWithPmService_ToOverdue_KeepsService() + { + await using var context = NewContext(); + SeedServices(context); + var wo = new WorkOrder + { + Id = 1, + LifecycleStatus = LifecycleStatus.Scheduled, + WorkOrderType = WorkOrderType.PM, + ServiceId = PmServiceId, + RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } + }; + context.workOrders.Add(wo); + await context.SaveChangesAsync(); + + var result = await NewUpdateService(context).PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto + { + Field = WorkOrderBoardFieldNames.WorkOrderType, + Value = "Overdue", + WorkOrderVersion = ToVersion(wo) + }, "actor-1"); + + Assert.Equal(WorkOrderType.Overdue, result.WorkOrderType); + Assert.Equal(PmServiceId, (await context.workOrders.SingleAsync(w => w.Id == 1)).ServiceId); + } + + [Fact] + public async Task PatchServiceId_OnOverdue_AcceptsPmCatalogService() + { + await using var context = NewContext(); + SeedServices(context); + var wo = new WorkOrder + { + Id = 1, + LifecycleStatus = LifecycleStatus.Scheduled, + WorkOrderType = WorkOrderType.Overdue, + RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } + }; + context.workOrders.Add(wo); + await context.SaveChangesAsync(); + + await NewUpdateService(context).PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto + { + Field = WorkOrderBoardFieldNames.ServiceId, + Value = PmServiceId.ToString(), + WorkOrderVersion = ToVersion(wo) + }, "actor-1"); + + var persisted = await context.workOrders.SingleAsync(w => w.Id == 1); + Assert.Equal(PmServiceId, persisted.ServiceId); + Assert.Equal("Backflow Preventers", persisted.Trade); + } + + [Fact] + public async Task GetDetail_Overdue_ResolvesPmCompletionTemplate() + { + await using var context = NewContext(); + context.workOrders.Add(new WorkOrder + { + Id = 1, + InternalWONumber = "10000000001", + LifecycleStatus = LifecycleStatus.Scheduled, + WorkOrderType = WorkOrderType.Overdue, + ScheduledDate = DateTime.UtcNow.AddDays(1), + DocStatus = DocStatus.No, + RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } + }); + context.CompletionDocTemplates.Add(new CompletionDocTemplate + { + Name = "PM Completion", + ServiceKey = "pm-generic", + WorkOrderType = WorkOrderType.PM, + TemplateUrl = "https://example.com/pm.pdf", + IsActive = true + }); + await context.SaveChangesAsync(); + + var boardData = new WorkOrderBoardDataService(context); + var resolver = WorkOrderAccountTestHelpers.Resolver(context); + var service = new WorkOrderDetailService( + new WorkOrderBoardService(boardData, resolver), + new WorkOrderDetailDataService(context), + new CompletionDocTemplateDataService(context), + new UserDataService(context), + resolver); + + var detail = await service.GetDetailAsync(1, WorkOrderAccountTestHelpers.OrgWideAdmin()); + + Assert.NotNull(detail); + Assert.Equal(DocStatus.No, detail!.Completion.DocStatus); + Assert.NotNull(detail.Completion.Template); + Assert.Equal("PM Completion", detail.Completion.Template!.Name); + } + + [Fact] + public async Task Search_TypeOverdue_ReturnsOverdueTypeOnly_NotPastDueRows() + { + await using var context = NewContext(); + var today = DateTime.UtcNow.Date; + context.workOrders.AddRange( + new WorkOrder + { + Id = 1, + WorkOrderType = WorkOrderType.Overdue, + ScheduledDate = today.AddDays(2), + LifecycleStatus = LifecycleStatus.Scheduled, + InternalWONumber = "10000000001" + }, + new WorkOrder + { + Id = 2, + WorkOrderType = WorkOrderType.PM, + ScheduledDate = today.AddDays(-3), + LifecycleStatus = LifecycleStatus.Scheduled, + InternalWONumber = "10000000002" + }, + new WorkOrder + { + Id = 3, + WorkOrderType = WorkOrderType.Overdue, + ScheduledDate = today.AddDays(-2), + LifecycleStatus = LifecycleStatus.Scheduled, + InternalWONumber = "10000000003" + }); + await context.SaveChangesAsync(); + + var result = await NewSearchService(context).SearchAsync(new WorkOrderAdvancedSearchQueryDto + { + DatePreset = WorkOrderAdvancedSearchDatePreset.Custom, + DateFrom = DateOnly.FromDateTime(today.AddMonths(-1)), + DateTo = DateOnly.FromDateTime(today.AddMonths(1)), + Types = new List { WorkOrderType.Overdue } + }, WorkOrderAccountTestHelpers.OrgWideAdmin(), null); + + Assert.Equal(new[] { 1, 3 }, result.Items.Select(i => i.Id).OrderBy(id => id)); + Assert.All(result.Items, i => Assert.Equal(WorkOrderType.Overdue, i.WorkOrderType)); + } + + [Fact] + public async Task Search_TypeOverdueWithPastDue_ReturnsIntersection() + { + await using var context = NewContext(); + var today = DateTime.UtcNow.Date; + context.workOrders.AddRange( + new WorkOrder + { + Id = 1, + WorkOrderType = WorkOrderType.Overdue, + ScheduledDate = today.AddDays(2), + LifecycleStatus = LifecycleStatus.Scheduled, + InternalWONumber = "10000000001" + }, + new WorkOrder + { + Id = 2, + WorkOrderType = WorkOrderType.PM, + ScheduledDate = today.AddDays(-3), + LifecycleStatus = LifecycleStatus.Scheduled, + InternalWONumber = "10000000002" + }, + new WorkOrder + { + Id = 3, + WorkOrderType = WorkOrderType.Overdue, + ScheduledDate = today.AddDays(-2), + LifecycleStatus = LifecycleStatus.Scheduled, + InternalWONumber = "10000000003" + }); + await context.SaveChangesAsync(); + + var result = await NewSearchService(context).SearchAsync(new WorkOrderAdvancedSearchQueryDto + { + DatePreset = WorkOrderAdvancedSearchDatePreset.Custom, + DateFrom = DateOnly.FromDateTime(today.AddMonths(-1)), + DateTo = DateOnly.FromDateTime(today.AddMonths(1)), + Types = new List { WorkOrderType.Overdue }, + Overdue = true + }, WorkOrderAccountTestHelpers.OrgWideAdmin(), null); + + var row = Assert.Single(result.Items); + Assert.Equal(3, row.Id); + Assert.True(row.IsPastDue); + } + + [Fact] + public void DashboardBreakdown_CountsByTypeOnly() + { + var today = new DateOnly(2026, 9, 25); + var pastDue = today.AddDays(-3).ToDateTime(TimeOnly.MinValue); + var future = today.AddDays(3).ToDateTime(TimeOnly.MinValue); + var rows = new List + { + new(1, null, WorkOrderType.Overdue, LifecycleStatus.Scheduled, null, future, null, null), + new(2, null, WorkOrderType.PM, LifecycleStatus.Scheduled, null, pastDue, null, null), + new(3, null, WorkOrderType.Overdue, LifecycleStatus.Scheduled, null, pastDue, null, null), + }; + + var result = DashboardMetrics.Calculate(rows, today); + + Assert.Equal(2, result.Breakdown.Overdue); + Assert.Equal(1, result.Breakdown.PM); + Assert.Equal(0, result.Breakdown.Other); + } + + private static WorkOrderAdvancedSearchService NewSearchService(ApplicationDbContext context) + => new(new WorkOrderAdvancedSearchDataService(context), WorkOrderAccountTestHelpers.Resolver(context)); +} diff --git a/SeaHavenIndustries.Tests/WorkOrderTypeMapperTests.cs b/SeaHavenIndustries.Tests/WorkOrderTypeMapperTests.cs index b25bcd5..e56b28c 100644 --- a/SeaHavenIndustries.Tests/WorkOrderTypeMapperTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderTypeMapperTests.cs @@ -10,21 +10,18 @@ public class WorkOrderTypeMapperTests [InlineData("Reactive", WorkOrderType.Reactive)] [InlineData("Emergency", WorkOrderType.Emergency)] [InlineData("Add-On", WorkOrderType.AddOn)] + [InlineData("Overdue", WorkOrderType.Overdue)] + [InlineData("overdue", WorkOrderType.Overdue)] public void TryParseFeLabel_BoardTypes(string label, WorkOrderType expected) { Assert.True(WorkOrderTypeMapper.TryParseFeLabel(label, out var type)); Assert.Equal(expected, type); } - [Fact] - public void TryParseFeLabel_Overdue_ReturnsFalse() - { - Assert.False(WorkOrderTypeMapper.TryParseFeLabel("Overdue", out _)); - } - [Theory] [InlineData(WorkOrderType.AddOn, "Add-On")] [InlineData(WorkOrderType.Reactive, "Reactive")] + [InlineData(WorkOrderType.Overdue, "Overdue")] public void ToFeLabel_ReturnsBoardLabel(WorkOrderType type, string expected) { Assert.Equal(expected, WorkOrderTypeMapper.ToFeLabel(type)); From d82fb18a3d2946b4cb130fb3b7232da8c26d3f26 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 11:30:34 -0300 Subject: [PATCH 2/4] fix(work-orders): map Overdue to PM catalog in service and template lists --- .../Implementation/ServicesRegistryService.cs | 6 ++- .../WorkOrderCompletionService.cs | 2 +- .../WorkOrderOverdueTypeTests.cs | 50 +++++++++++++++++++ 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/SeaHaven.Services/Implementation/ServicesRegistryService.cs b/SeaHaven.Services/Implementation/ServicesRegistryService.cs index 1a702bb..29bd32b 100644 --- a/SeaHaven.Services/Implementation/ServicesRegistryService.cs +++ b/SeaHaven.Services/Implementation/ServicesRegistryService.cs @@ -5,6 +5,7 @@ using SeaHaven.DataServices.Interfaces; using SeaHaven.DataServices.Models; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; +using SeaHaven.Services.Helpers; using SeaHaven.Services.Interfaces; namespace SeaHaven.Services.Implementation @@ -34,8 +35,9 @@ namespace SeaHaven.Services.Implementation public async Task> GetAllAsync(bool? isActive, WorkOrderType? workOrderType, CancellationToken cancellationToken) { - ValidateWorkOrderTypeFilter(workOrderType); - var rows = await _dataService.GetServiceRowsAsync(isActive, workOrderType, cancellationToken); + var catalogType = WorkOrderCatalogType.For(workOrderType); + ValidateWorkOrderTypeFilter(catalogType); + var rows = await _dataService.GetServiceRowsAsync(isActive, catalogType, cancellationToken); return rows.Select(MapToDto).ToList(); } diff --git a/SeaHaven.Services/Implementation/WorkOrderCompletionService.cs b/SeaHaven.Services/Implementation/WorkOrderCompletionService.cs index f2f0895..c748c3d 100644 --- a/SeaHaven.Services/Implementation/WorkOrderCompletionService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderCompletionService.cs @@ -33,7 +33,7 @@ namespace SeaHaven.Services.Implementation public async Task> GetTemplatesAsync(string? serviceKey, WorkOrderType? workOrderType) { - var rows = await _templateData.GetActiveAsync(serviceKey, workOrderType); + var rows = await _templateData.GetActiveAsync(serviceKey, WorkOrderCatalogType.For(workOrderType)); return rows.Select(WorkOrderDetailService.MapTemplate).ToList(); } diff --git a/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs b/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs index 0b3657b..35c04e6 100644 --- a/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs @@ -278,6 +278,56 @@ public class WorkOrderOverdueTypeTests Assert.Equal("PM Completion", detail.Completion.Template!.Name); } + [Fact] + public async Task ListServices_Overdue_ReturnsPmCatalog() + { + await using var context = NewContext(); + SeedServices(context); + await context.SaveChangesAsync(); + + var services = await new ServicesRegistryService(new ServicesRegistryDataService(context)) + .GetAllAsync(true, WorkOrderType.Overdue, CancellationToken.None); + + Assert.Equal(new[] { PmServiceId }, services.Select(s => s.Id).ToArray()); + } + + [Fact] + public async Task ListTemplates_Overdue_ReturnsPmTemplates() + { + await using var context = NewContext(); + context.CompletionDocTemplates.AddRange( + new CompletionDocTemplate + { + Name = "PM Completion", + ServiceKey = "pm-generic", + WorkOrderType = WorkOrderType.PM, + TemplateUrl = "https://example.com/pm.pdf", + IsActive = true + }, + new CompletionDocTemplate + { + Name = "Reactive Completion", + ServiceKey = "reactive-generic", + WorkOrderType = WorkOrderType.Reactive, + TemplateUrl = "https://example.com/reactive.pdf", + IsActive = true + }); + await context.SaveChangesAsync(); + + var service = new WorkOrderCompletionService( + new WorkOrderCompletionDataService(context), + new CompletionDocTemplateDataService(context), + new WorkOrderDetailDataService(context), + new WorkOrderAuditService( + new WorkOrderAuditDataService(context), + new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context))), + WorkOrderAccountTestHelpers.Resolver(context)); + + var templates = await service.GetTemplatesAsync(null, WorkOrderType.Overdue); + + Assert.Equal(new[] { "PM Completion" }, templates.Select(t => t.Name).ToArray()); + } + [Fact] public async Task Search_TypeOverdue_ReturnsOverdueTypeOnly_NotPastDueRows() { From c1910e53102c8f8453ac068d37997267de0ab407 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 11:41:33 -0300 Subject: [PATCH 3/4] test(work-orders): pin severity-then-type correction from Overdue The board sends one PATCH per field, severity before workOrderType, so correcting an Overdue work order to Emergency or Reactive patches the severity while the stored type is still Overdue. Dropping or rejecting a severity patch for the current type would leave the corrected Emergency work order with no severity. Overdue's no-severity rule is enforced when the type changes, not on the severity patch. --- .../WorkOrderOverdueTypeTests.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs b/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs index 35c04e6..7a1ee1e 100644 --- a/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs @@ -183,6 +183,47 @@ public class WorkOrderOverdueTypeTests Assert.Null(persisted.Severity); } + // The board sends one request per field, severity before type, so correcting + // Overdue to Emergency patches severity while the stored type is still Overdue. + // A severity patch must not be dropped or rejected for the current type; the + // no-severity invariant is enforced when the type itself changes. + [Fact] + public async Task PatchSeverityThenType_OverdueToEmergency_KeepsSeverity() + { + await using var context = NewContext(); + var wo = new WorkOrder + { + Id = 1, + LifecycleStatus = LifecycleStatus.Scheduled, + WorkOrderType = WorkOrderType.Overdue, + RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } + }; + context.workOrders.Add(wo); + await context.SaveChangesAsync(); + var service = NewUpdateService(context); + + await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto + { + Field = WorkOrderBoardFieldNames.Severity, + Value = "2", + WorkOrderVersion = ToVersion(wo) + }, "actor-1"); + + var updated = await context.workOrders.SingleAsync(w => w.Id == 1); + var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto + { + Field = WorkOrderBoardFieldNames.WorkOrderType, + Value = "Emergency", + WorkOrderVersion = ToVersion(updated) + }, "actor-1"); + + Assert.Equal(WorkOrderType.Emergency, result.WorkOrderType); + Assert.Equal(2, result.Severity); + var persisted = await context.workOrders.SingleAsync(w => w.Id == 1); + Assert.Equal(WorkOrderType.Emergency, persisted.WorkOrderType); + Assert.Equal("2", persisted.Severity); + } + [Fact] public async Task PatchType_PmWithPmService_ToOverdue_KeepsService() { From 47060f6a73bae943aa6ad6ffe4c9020afd1a8f4e Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Fri, 25 Sep 2026 13:41:19 -0300 Subject: [PATCH 4/4] fix(work-orders): never return a severity on an Overdue board row A standalone severity patch on an Overdue WO still stores the value, because the board patches severity before type when correcting Overdue to Emergency/Reactive and that write must not be dropped or rejected. Project the severity as null for Overdue in the board row mapping, which also feeds the PATCH response, search results and the detail view, so a stored value never surfaces on a type that carries no severity. --- .../Implementation/WorkOrderBoardService.cs | 7 ++++- .../WorkOrderOverdueTypeTests.cs | 30 ++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs index f21a21c..9341fa8 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardService.cs @@ -147,7 +147,12 @@ namespace SeaHaven.Services.Implementation MediaCount = row.MediaCount, CompletedDate = row.CompletedDate, FlagColor = row.FlagColor, - Severity = WorkOrderSeverityRules.ParseLevel(row.Severity), + // Overdue never carries a severity. A standalone severity patch can still + // store one (the board patches severity before type when correcting Overdue + // to Emergency/Reactive), so the row never returns it for an Overdue WO. + Severity = row.WorkOrderType == WorkOrderType.Overdue + ? null + : WorkOrderSeverityRules.ParseLevel(row.Severity), PrimaryDispatchId = row.PrimaryDispatchId, RowVersion = row.RowVersion, DispatchRowVersion = row.DispatchRowVersion, diff --git a/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs b/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs index 7a1ee1e..fe3b61b 100644 --- a/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs @@ -202,12 +202,13 @@ public class WorkOrderOverdueTypeTests await context.SaveChangesAsync(); var service = NewUpdateService(context); - await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto + var severityStep = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto { Field = WorkOrderBoardFieldNames.Severity, Value = "2", WorkOrderVersion = ToVersion(wo) }, "actor-1"); + Assert.Null(severityStep.Severity); var updated = await context.workOrders.SingleAsync(w => w.Id == 1); var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto @@ -224,6 +225,33 @@ public class WorkOrderOverdueTypeTests Assert.Equal("2", persisted.Severity); } + // A severity patch with no following type change leaves a stored value on an + // Overdue WO; the board row still reports no severity. + [Fact] + public async Task PatchSeverity_OnOverdue_RowShowsNoSeverity() + { + await using var context = NewContext(); + var wo = new WorkOrder + { + Id = 1, + LifecycleStatus = LifecycleStatus.Scheduled, + WorkOrderType = WorkOrderType.Overdue, + RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 } + }; + context.workOrders.Add(wo); + await context.SaveChangesAsync(); + + var result = await NewUpdateService(context).PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto + { + Field = WorkOrderBoardFieldNames.Severity, + Value = "3", + WorkOrderVersion = ToVersion(wo) + }, "actor-1"); + + Assert.Equal(WorkOrderType.Overdue, result.WorkOrderType); + Assert.Null(result.Severity); + } + [Fact] public async Task PatchType_PmWithPmService_ToOverdue_KeepsService() {