mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +00:00
Merge pull request #184 from Sea-Haven-Industries/feat/ab/sh-322-overdue-type
feat(work-orders): add Overdue work order type
This commit is contained in:
commit
cf32dd2698
20 changed files with 593 additions and 70 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ namespace Data.SeaHavenIndustries.Enums
|
|||
Inspection = 5,
|
||||
Reactive = 6,
|
||||
AddOn = 7,
|
||||
/// <summary>Client never responded after the due date passed. Distinct from the derived past-due overlay.</summary>
|
||||
Overdue = 8,
|
||||
Other = 99
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -58,9 +58,9 @@ namespace SeaHaven.DataServices.Helpers
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters by real <paramref name="types"/> (including <see cref="WorkOrderType.Other"/>)
|
||||
/// and/or past-due rows when <paramref name="overdue"/> is true.
|
||||
/// Types and overdue are combined with OR when both are present.
|
||||
/// Filters by stored <paramref name="types"/> (including <see cref="WorkOrderType.Overdue"/> and
|
||||
/// <see cref="WorkOrderType.Other"/>) and/or past-due rows when <paramref name="overdue"/> is true.
|
||||
/// Past due is a status overlay, so types and overdue are combined with AND when both are present.
|
||||
/// </summary>
|
||||
public static IQueryable<WorkOrder> ApplyTypeAndOverdueFilter(
|
||||
IQueryable<WorkOrder> 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 =>
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ namespace SeaHaven.Services.DTOs
|
|||
public List<string>? Dispatchers { get; set; }
|
||||
public bool MyWorkOrders { get; set; }
|
||||
public List<WorkOrderType>? Types { get; set; }
|
||||
/// <summary>When true, include past-due (isPastDue) rows. Combined with Types via OR.</summary>
|
||||
/// <summary>When true, keep only past-due (isPastDue) rows. Combined with Types via AND.</summary>
|
||||
public bool Overdue { get; set; }
|
||||
public string? Search { get; set; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ namespace SeaHaven.Services.DTOs
|
|||
public List<string>? Sites { get; set; }
|
||||
public List<string>? Regions { get; set; }
|
||||
public List<WorkOrderType>? Types { get; set; }
|
||||
/// <summary>When true, include past-due (isPastDue) rows. Combined with Types via OR.</summary>
|
||||
/// <summary>When true, keep only past-due (isPastDue) rows. Combined with Types via AND.</summary>
|
||||
public bool Overdue { get; set; }
|
||||
public List<string>? Dispatchers { get; set; }
|
||||
public List<LifecycleStatus>? Statuses { get; set; }
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
17
SeaHaven.Services/Helpers/WorkOrderCatalogType.cs
Normal file
17
SeaHaven.Services/Helpers/WorkOrderCatalogType.cs
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
using Data.SeaHavenIndustries.Enums;
|
||||
|
||||
namespace SeaHaven.Services.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ namespace SeaHaven.Services.Helpers
|
|||
{
|
||||
public static class WorkOrderTypeMapper
|
||||
{
|
||||
/// <summary>Board FE types. Overdue is not a type — use <see cref="WorkOrderDerivedFields.IsPastDue(System.DateTime?, LifecycleStatus?, System.DateTime?)"/>.</summary>
|
||||
/// <summary>Board FE types. Overdue is a dispatcher-assigned type; past due is derived — see <see cref="WorkOrderDerivedFields.IsPastDue(System.DateTime?, LifecycleStatus?, System.DateTime?)"/>.</summary>
|
||||
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",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using SeaHaven.DataServices.Interfaces;
|
|||
using SeaHaven.Services.Constants;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Exceptions;
|
||||
using SeaHaven.Services.Helpers;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
|
||||
namespace SeaHaven.Services.Implementation
|
||||
|
|
@ -34,7 +35,11 @@ namespace SeaHaven.Services.Implementation
|
|||
WorkOrderType? workOrderType,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var rows = await _templateData.ListAsync(search, serviceKey, workOrderType, cancellationToken);
|
||||
var rows = await _templateData.ListAsync(
|
||||
search,
|
||||
serviceKey,
|
||||
WorkOrderCatalogType.For(workOrderType),
|
||||
cancellationToken);
|
||||
return rows.Select(Map).ToList();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IReadOnlyList<ServiceDto>> 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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}");
|
||||
|
|
|
|||
|
|
@ -93,7 +93,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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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<WorkOrderBoardValidationException>(() =>
|
||||
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]
|
||||
|
|
|
|||
509
SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs
Normal file
509
SeaHavenIndustries.Tests/WorkOrderOverdueTypeTests.cs
Normal file
|
|
@ -0,0 +1,509 @@
|
|||
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<ApplicationDbContext>()
|
||||
.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<ServiceWorkOrderType>
|
||||
{
|
||||
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<WorkOrderBoardValidationException>(() =>
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
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
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// 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()
|
||||
{
|
||||
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 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 CompletionDocTemplateService(
|
||||
new CompletionDocTemplateDataService(context),
|
||||
new TeamPermissionOverrideDataService(context),
|
||||
new TeamPermissionPolicy(),
|
||||
WorkOrderAccountTestHelpers.Resolver(context));
|
||||
|
||||
var templates = await service.ListAsync(null, null, WorkOrderType.Overdue, CancellationToken.None);
|
||||
|
||||
Assert.Equal(new[] { "PM Completion" }, templates.Select(t => t.Name).ToArray());
|
||||
}
|
||||
|
||||
[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> { 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> { 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<DashboardWorkOrder>
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue