mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 10:43:13 +00:00
Editing a work order's POC never reached the backend: no update path wrote
PocName/PocPhone/PocNotes, so the optimistic UI edit was lost on refetch and
the completion freeze captured the Site contact instead of the manual value,
and nothing was audited.
- Add tenant-scoped PATCH api/workorders/{id}/poc via new WorkOrderPocService
+ WorkOrderPocDataService: persists the override, stages FieldChanged audit
entries (which also write field locks so sync never overwrites a manual POC),
and enforces row-version concurrency and terminal-status read-only rules.
- Lock semantics (SH-190): a manual POC away from the Site's live contacts is
stored WO-level; an edit equal to a live Site contact (or blanking name+phone)
stores nothing so the WO follows the Site. PocCustomized exposes the state.
- Board projection, completion freeze and create now share one Site-contact
fallback (first non-deleted contact by SiteContactOrder) so a never-overridden
WO keeps following the Site, including at create when the wizard prefills it.
- Route contract baseline gains PATCH {id:int}/poc.
833 lines
30 KiB
C#
833 lines
30 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using SeaHaven.DataServices.Implementation;
|
|
using SeaHaven.Services.DTOs;
|
|
using SeaHaven.Services.Exceptions;
|
|
using SeaHaven.Services.Helpers;
|
|
using SeaHaven.Services.Implementation;
|
|
using SeaHaven.Services.Interfaces;
|
|
using SeaHaven.Services.Validation;
|
|
|
|
namespace SeaHavenIndustries.Tests;
|
|
|
|
public class WorkOrderBoardCreateServiceTests
|
|
{
|
|
private static (ApplicationDbContext Context, WorkOrderBoardCreateService Service) CreateSut()
|
|
{
|
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
|
.Options;
|
|
var context = new ApplicationDbContext(options);
|
|
WorkOrderAccountTestHelpers.SeedBoardCreateScope(context);
|
|
var boardData = new WorkOrderBoardDataService(context);
|
|
var mutationData = new WorkOrderBoardMutationDataService(context);
|
|
var resolver = WorkOrderAccountTestHelpers.Resolver(context);
|
|
var boardService = new WorkOrderBoardService(boardData, resolver);
|
|
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
|
var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks);
|
|
var validator = new WorkOrderBoardCreateValidation();
|
|
var service = new WorkOrderBoardCreateService(boardData, mutationData, boardService, audit, validator, resolver, new WorkOrderPocDataService(context));
|
|
return (context, service);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_Minimal_ReturnsIncomplete()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal(LifecycleStatus.Incomplete, result.LifecycleStatus);
|
|
Assert.Equal("BK5", result.SiteCode);
|
|
Assert.Equal(WorkOrderType.PM, result.WorkOrderType);
|
|
Assert.Equal("SH00001", result.WoNumber);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_BlankWoNumber_PreservesExistingNumericPlaceholderAndGeneratesShNumber()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
context.workOrders.Add(new WorkOrder
|
|
{
|
|
InternalWONumber = "00000000123",
|
|
WorkerOrderNumber = "00000000123"
|
|
});
|
|
await context.SaveChangesAsync();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal("SH00002", result.WoNumber);
|
|
Assert.True(await context.workOrders.AnyAsync(w => w.InternalWONumber == "00000000123"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_WithManualWoNumber_NormalizesTo11Digits()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WoNumber = "12345",
|
|
WorkOrderType = WorkOrderType.PO,
|
|
SiteCode = "DAL",
|
|
LocationId = 1
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal("00000012345", result.WoNumber);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("SH00001", "SH00001")]
|
|
[InlineData("sh00042", "SH00042")]
|
|
public async Task Create_WithManualShPlaceholderWoNumber_KeepsShPrefix(string requested, string expected)
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WoNumber = requested,
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "DAL",
|
|
LocationId = 1
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal(expected, result.WoNumber);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_DuplicateWoNumber_Throws()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
context.workOrders.Add(new WorkOrder { InternalWONumber = "00000099999", WorkerOrderNumber = "00000099999" });
|
|
await context.SaveChangesAsync();
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WoNumber = "99999",
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
|
|
|
Assert.Equal("DuplicateWoNumber", ex.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_AutoSchedulesWhenDateWithoutAssignee()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
ScheduledDate = new DateTime(2026, 6, 25)
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal(LifecycleStatus.Scheduled, result.LifecycleStatus);
|
|
Assert.Equal(new DateTime(2026, 6, 25), result.ScheduledDate);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_PendingWithDate_PromotesToScheduled()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
LifecycleStatus = LifecycleStatus.Pending,
|
|
ScheduledDate = new DateTime(2026, 6, 25)
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal(LifecycleStatus.Scheduled, result.LifecycleStatus);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_AutoSchedulesWhenDateAndAssignee()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
AssignTo = "dispatcher-1",
|
|
ScheduledDate = new DateTime(2026, 6, 25)
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal(LifecycleStatus.Scheduled, result.LifecycleStatus);
|
|
Assert.Equal(new DateTime(2026, 6, 25), result.ScheduledDate);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_WeekOnlyScheduling_SetsTargetWeek()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
ScheduleWeekOnly = true,
|
|
TargetWeek = new DateOnly(2026, 6, 22)
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.True(result.ScheduleWeekOnly);
|
|
Assert.Equal(new DateOnly(2026, 6, 22), result.TargetWeek);
|
|
Assert.Equal(LifecycleStatus.Incomplete, result.LifecycleStatus);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_WeekOnlyWithoutTargetWeek_ThrowsValidation()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
await Assert.ThrowsAsync<FluentValidation.ValidationException>(() =>
|
|
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
ScheduleWeekOnly = true
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_WithVendor_CreatesPrimaryDispatch()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
context.Vendors.Add(new Vendor { Id = 5, CompanyName = "Acme HVAC" });
|
|
await context.SaveChangesAsync();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
VendorId = 5,
|
|
ApptDate = new DateTime(2026, 6, 26)
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal(5, result.VendorId);
|
|
Assert.NotNull(result.PrimaryDispatchId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_CreatesFieldLocksForPopulatedFields()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
Description = "Test WO"
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
var locks = await context.WorkOrderFieldLocks.ToListAsync();
|
|
Assert.Contains(locks, l => l.FieldName == "SiteCode");
|
|
Assert.Contains(locks, l => l.FieldName == "WorkOrderType");
|
|
Assert.Contains(locks, l => l.FieldName == "Description");
|
|
Assert.All(locks, l => Assert.Equal(result.Id, l.WorkOrderId));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_WritesCreateAuditEvent()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
var logs = await context.WorkOrderAuditLogs.ToListAsync();
|
|
Assert.Contains(logs, l => l.Action == "Create");
|
|
Assert.Contains(logs, l => l.Action == "FieldChanged" && l.FieldName == "SiteCode");
|
|
Assert.All(logs, l => Assert.Equal(result.Id, l.WorkOrderId));
|
|
Assert.True(result.Id > 0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_StructuredServiceNotes_PersistsAndReturnsPm()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
PrimaryService = "HVAC PM",
|
|
ExtraServices = new List<string> { "Filter change", "Coil clean", "Filter change" },
|
|
ServiceNotes = "Unit on roof"
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal("HVAC PM", result.Pm);
|
|
Assert.Equal("Unit on roof", result.ServiceNotes);
|
|
Assert.Equal(new[] { "Filter change", "Coil clean" }, result.ExtraServices);
|
|
|
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
|
Assert.Equal("HVAC PM", wo.Trade);
|
|
Assert.Equal("Unit on roof", wo.ServiceNotes);
|
|
Assert.Contains("Filter change", wo.ExtraServices);
|
|
Assert.Contains("Coil clean", wo.ExtraServices);
|
|
Assert.Equal(1, System.Text.Json.JsonSerializer.Deserialize<List<string>>(wo.ExtraServices!)!
|
|
.Count(s => s == "Filter change"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_PrimaryServiceTakesPrecedenceOverTrade()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.Reactive,
|
|
SiteCode = "CHI",
|
|
LocationId = 1,
|
|
Trade = "Legacy Trade",
|
|
PrimaryService = "Plumbing"
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal("Plumbing", result.Pm);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_ExtraServicesWithoutPrimary_ThrowsValidation()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
await Assert.ThrowsAsync<FluentValidation.ValidationException>(() =>
|
|
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
ExtraServices = new List<string> { "Filter change" }
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_PocFreeText_WithoutContact_AppearsOnBoardRow()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.Reactive,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
PocName = "Jane Site Lead",
|
|
PocPhone = "+1 555-0100",
|
|
PocNotes = "Call 30 min before"
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal("Jane Site Lead", result.PocName);
|
|
Assert.Equal("+1 555-0100", result.PocPhone);
|
|
Assert.Equal("Call 30 min before", result.PocNotes);
|
|
|
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
|
Assert.Equal("Jane Site Lead", wo.PocName);
|
|
Assert.Empty(context.WorkOrderContacts);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_WithoutVendor_PersistsTechPhoneAndVendorNotesOnWorkOrder()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
TechPhone = "+1 555-0199",
|
|
VendorNotes = "Gate code 4421"
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal("+1 555-0199", result.TechPhone);
|
|
|
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
|
Assert.Equal("+1 555-0199", wo.TechPhone);
|
|
Assert.Equal("Gate code 4421", wo.VendorNotes);
|
|
Assert.Null(wo.PrimaryDispatchId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_WithVendor_PersistsTechPhoneAndVendorNotesOnDispatch()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
context.Vendors.Add(new Vendor { Id = 55, CompanyName = "Acme", Phone = "+1 555-0000" });
|
|
await context.SaveChangesAsync();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
VendorId = 55,
|
|
ApptDate = new DateTime(2026, 7, 18),
|
|
TechPhone = "+1 555-0199",
|
|
VendorNotes = "Park behind dock"
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal("+1 555-0199", result.TechPhone);
|
|
Assert.Equal(55, result.VendorId);
|
|
|
|
var wo = await context.workOrders.Include(w => w.PrimaryDispatch).SingleAsync(w => w.Id == result.Id);
|
|
Assert.Null(wo.TechPhone);
|
|
Assert.Null(wo.VendorNotes);
|
|
Assert.NotNull(wo.PrimaryDispatch);
|
|
Assert.Equal("+1 555-0199", wo.PrimaryDispatch!.TechPhone);
|
|
Assert.Equal("Park behind dock", wo.PrimaryDispatch.VendorNotes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_LegacyTradeAndDescription_StillWorks()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
Trade = "HVAC",
|
|
Description = "pmNote\nPOC: Jane · +1 555\nVendor notes: gate"
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal("HVAC", result.Pm);
|
|
Assert.Null(result.PocName);
|
|
|
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
|
Assert.Equal("HVAC", wo.Trade);
|
|
Assert.Contains("Vendor notes:", wo.Description);
|
|
Assert.Null(wo.PocName);
|
|
Assert.Null(wo.ExtraServices);
|
|
Assert.Null(wo.ServiceNotes);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_UnknownVendorId_Throws()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
VendorId = 999
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
|
|
|
Assert.Equal("VendorNotFound", ex.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_ScheduledAfterCutoff_SetsIsAddOnTrue_KeepsRealType()
|
|
{
|
|
// UtcNow (test run) is after cutoff for a 2026-06 schedule week.
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
ScheduledDate = new DateTime(2026, 6, 24),
|
|
IsAddOn = false
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.True(result.IsAddOn);
|
|
Assert.Equal(WorkOrderType.PM, result.WorkOrderType);
|
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
|
Assert.True(wo.IsAddOn);
|
|
Assert.Equal(WorkOrderType.PM, wo.WorkOrderType);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_ScheduledBeforeCutoff_SetsIsAddOnFalse_IgnoresClientTrue()
|
|
{
|
|
// Far-future schedule → cutoff after UtcNow → not an add-on.
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.Reactive,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
ScheduledDate = new DateTime(2099, 3, 10),
|
|
IsAddOn = true
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.False(result.IsAddOn);
|
|
Assert.Equal(WorkOrderType.Reactive, result.WorkOrderType);
|
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
|
Assert.False(wo.IsAddOn);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_NoSchedule_ManualIsAddOnTrue()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
IsAddOn = true
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.True(result.IsAddOn);
|
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
|
Assert.True(wo.IsAddOn);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_AvetaRequiredTrue_PersistsAndReturns()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
AvetaRequired = true
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.True(result.AvetaRequired);
|
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
|
Assert.True(wo.AvetaRequired);
|
|
Assert.Contains(
|
|
await context.WorkOrderAuditLogs.ToListAsync(),
|
|
l => l.FieldName == "AvetaRequired" && l.Action == "FieldChanged");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_AvetaRequiredOmitted_DefaultsFalse()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.False(result.AvetaRequired);
|
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
|
Assert.False(wo.AvetaRequired);
|
|
Assert.DoesNotContain(
|
|
await context.WorkOrderAuditLogs.ToListAsync(),
|
|
l => l.FieldName == "AvetaRequired");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_WorkOrderTypeAddOn_ThrowsValidation()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var ex = await Assert.ThrowsAsync<FluentValidation.ValidationException>(() =>
|
|
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.AddOn,
|
|
SiteCode = "BK5",
|
|
LocationId = 1
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
|
|
|
Assert.Contains(ex.Errors, e => e.PropertyName == "WorkOrderType");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_AdditionalContacts_PersistsAndReturnsOnBoardRow()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.Reactive,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
PocName = "Primary Lead",
|
|
AdditionalContacts = new List<WorkOrderAdditionalContactDto>
|
|
{
|
|
new("Backup Lead", "+1 555-0101", "After hours"),
|
|
new("Security Desk", "+1 555-0102", null)
|
|
}
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal("Primary Lead", result.PocName);
|
|
Assert.NotNull(result.AdditionalContacts);
|
|
Assert.Equal(2, result.AdditionalContacts!.Count);
|
|
Assert.Equal("Backup Lead", result.AdditionalContacts[0].Name);
|
|
Assert.Equal("+1 555-0101", result.AdditionalContacts[0].Phone);
|
|
Assert.Equal("After hours", result.AdditionalContacts[0].Notes);
|
|
|
|
var wo = await context.workOrders.SingleAsync(w => w.Id == result.Id);
|
|
Assert.NotNull(wo.AdditionalContacts);
|
|
Assert.Contains("Backup Lead", wo.AdditionalContacts);
|
|
Assert.Contains("Security Desk", wo.AdditionalContacts);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_AdditionalContacts_EmptyList_SucceedsWithoutContacts()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
AdditionalContacts = new List<WorkOrderAdditionalContactDto>()
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Null(result.AdditionalContacts);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_AdditionalContacts_DropsBlankRows_PreservesOrder()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.Reactive,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
AdditionalContacts = new List<WorkOrderAdditionalContactDto>
|
|
{
|
|
new("", "", null),
|
|
new("Backup Lead", "+1 555-0101", "After hours"),
|
|
new(" ", " ", null),
|
|
new("Security Desk", "+1 555-0102", null)
|
|
}
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.NotNull(result.AdditionalContacts);
|
|
Assert.Equal(2, result.AdditionalContacts!.Count);
|
|
Assert.Equal("Backup Lead", result.AdditionalContacts[0].Name);
|
|
Assert.Equal("Security Desk", result.AdditionalContacts[1].Name);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_AdditionalContacts_NameOnly_Throws()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var ex = await Assert.ThrowsAsync<FluentValidation.ValidationException>(() =>
|
|
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
AdditionalContacts = new List<WorkOrderAdditionalContactDto>
|
|
{
|
|
new("Backup Lead", "", null)
|
|
}
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
|
|
|
Assert.Contains("name and phone", ex.Message, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_AdditionalContacts_PhoneOnly_Throws()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
await Assert.ThrowsAsync<FluentValidation.ValidationException>(() =>
|
|
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
AdditionalContacts = new List<WorkOrderAdditionalContactDto>
|
|
{
|
|
new("", "+1 555-0101", null)
|
|
}
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_AdditionalContacts_PersistsAndReturnsOnDetail()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
var created = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.Reactive,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
AdditionalContacts = new List<WorkOrderAdditionalContactDto>
|
|
{
|
|
new("Backup Lead", "+1 555-0101", "After hours"),
|
|
new("Security Desk", "+1 555-0102", null)
|
|
}
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
var resolver = WorkOrderAccountTestHelpers.Resolver(context);
|
|
var detail = new WorkOrderDetailService(
|
|
new WorkOrderBoardService(new WorkOrderBoardDataService(context), resolver),
|
|
new WorkOrderDetailDataService(context),
|
|
new CompletionDocTemplateDataService(context),
|
|
new UserDataService(context),
|
|
resolver);
|
|
|
|
var result = await detail.GetDetailAsync(created.Id, WorkOrderAccountTestHelpers.AccountUser());
|
|
|
|
Assert.NotNull(result);
|
|
Assert.NotNull(result!.Info.AdditionalContacts);
|
|
Assert.Equal(2, result.Info.AdditionalContacts!.Count);
|
|
Assert.Equal("Backup Lead", result.Info.AdditionalContacts[0].Name);
|
|
Assert.Equal("+1 555-0101", result.Info.AdditionalContacts[0].Phone);
|
|
Assert.Equal("Security Desk", result.Info.AdditionalContacts[1].Name);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_WithPendingLifecycle_PersistsPending()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
LifecycleStatus = LifecycleStatus.Pending,
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal(LifecycleStatus.Pending, result.LifecycleStatus);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_WithScheduledLifecycleAndDate_PersistsWithoutAssignee()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
|
ScheduledDate = new DateTime(2026, 7, 15),
|
|
ScheduleWeekOnly = false,
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal(LifecycleStatus.Scheduled, result.LifecycleStatus);
|
|
Assert.Equal(new DateTime(2026, 7, 15), result.ScheduledDate?.Date);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_WithScheduledLifecycleWithoutDate_Throws()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
|
|
|
Assert.Equal("ScheduledRequiresDate", ex.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_WithScheduledLifecycleWeekOnly_Throws()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
|
ScheduledDate = new DateTime(2026, 7, 15),
|
|
ScheduleWeekOnly = true,
|
|
TargetWeek = new DateOnly(2026, 7, 13),
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
|
|
|
Assert.Equal("ScheduledRequiresDate", ex.Code);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_EmergencyWithSeverity_Persists()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.Emergency,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
Severity = 2
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1");
|
|
|
|
Assert.Equal(2, result.Severity);
|
|
Assert.Equal("2", (await context.workOrders.SingleAsync()).Severity);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Create_PmWithSeverity_Ignores()
|
|
{
|
|
var (context, service) = CreateSut();
|
|
|
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.PM,
|
|
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_SeverityOutOfRange_ThrowsInvalidValue()
|
|
{
|
|
var (_, service) = CreateSut();
|
|
|
|
var ex = await Assert.ThrowsAsync<WorkOrderBoardValidationException>(() =>
|
|
service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
|
{
|
|
WorkOrderType = WorkOrderType.Emergency,
|
|
SiteCode = "BK5",
|
|
LocationId = 1,
|
|
Severity = 6
|
|
}, WorkOrderAccountTestHelpers.AccountUser(), "actor-1"));
|
|
|
|
Assert.Equal("InvalidValue", ex.Code);
|
|
}
|
|
}
|