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() .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); 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); } [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(() => 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_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); } [Fact] public async Task Create_WeekOnlyWithoutTargetWeek_ThrowsValidation() { var (_, service) = CreateSut(); await Assert.ThrowsAsync(() => 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 { "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>(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(() => service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", LocationId = 1, ExtraServices = new List { "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(() => 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(() => 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 { 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() }, 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 { 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(() => service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", LocationId = 1, AdditionalContacts = new List { 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(() => service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", LocationId = 1, AdditionalContacts = new List { 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 { 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(); await Assert.ThrowsAsync(() => service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", LocationId = 1, LifecycleStatus = LifecycleStatus.Scheduled, }, WorkOrderAccountTestHelpers.AccountUser(), "actor-1")); } }