using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using SeaHaven.DataServices.Implementation; using SeaHaven.Services.DTOs; using SeaHaven.Services.Helpers; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; using SeaHaven.Services.Validation; namespace SeaHavenIndustries.Tests; public class WorkOrderPhase7CoexistenceTests { [Fact] public void IngestFieldMapper_MapsDynamoStatus() { Assert.Equal("Open", WorkOrderIngestFieldMapper.MapStatus("new")); Assert.Equal("In Progress", WorkOrderIngestFieldMapper.MapStatus("in_progress")); Assert.Equal("Done", WorkOrderIngestFieldMapper.MapStatus("completed")); } [Fact] public async Task IngestService_CreatesThenUpdates_IdempotentByExternalId() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; await using var context = new ApplicationDbContext(options); await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 1, "Phase7 Customer"); var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context)); var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks); var ingest = new WorkOrderIngestService(new WorkOrderIngestDataService(context), new SyncFieldMergePolicy(fieldLocks), fieldLocks, audit, WorkOrderAccountTestHelpers.Resolver(context)); var payload = new WorkOrderIngestPayloadDto { ExternalWorkOrderId = "ext-100", Description = "Initial", WoStatus = "new", Severity = "2", SiteCode = "BK1", Customer = "Phase7 Customer" }; var create = await ingest.UpsertBatchAsync(new[] { payload }); Assert.Equal(1, create.Created); Assert.Equal(0, create.Updated); payload.Description = "Updated via ingest"; var update = await ingest.UpsertBatchAsync(new[] { payload }); Assert.Equal(0, update.Created); Assert.Equal(1, update.Updated); var wo = await context.workOrders.SingleAsync(); Assert.Equal("Updated via ingest", wo.Description); Assert.Equal("ext-100", wo.ExternalWorkOrderId); } [Fact] public async Task IngestRespectsFieldLock_AfterShocBoardCreate() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; await using 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 createService = new WorkOrderBoardCreateService( boardData, mutationData, boardService, audit, new WorkOrderBoardCreateValidation(), resolver, new WorkOrderPocDataService(context)); var policy = new SyncFieldMergePolicy(fieldLocks); var ingest = new WorkOrderIngestService(new WorkOrderIngestDataService(context), policy, fieldLocks, audit, WorkOrderAccountTestHelpers.Resolver(context)); await createService.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", LocationId = 1 }, WorkOrderAccountTestHelpers.AccountUser("dispatcher-1"), "dispatcher-1"); var wo = await context.workOrders.SingleAsync(); wo.ExternalWorkOrderId = "ext-shoc-1"; await context.SaveChangesAsync(); await ingest.UpsertBatchAsync(new[] { new WorkOrderIngestPayloadDto { ExternalWorkOrderId = "ext-shoc-1", SiteCode = "SYNC-SITE" } }); Assert.Equal("BK5", wo.SiteCode); Assert.True(await context.WorkOrderAuditLogs.AnyAsync(l => l.Action == "SyncRejected")); } [Fact] public async Task IngestRespectsLocationIdFieldLock() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; await using var context = new ApplicationDbContext(options); context.Locations.Add(new Locations { Id = 1, Name = "BK1", Status = "Active" }); context.Locations.Add(new Locations { Id = 2, Name = "BK2", Status = "Active" }); context.workOrders.Add(new WorkOrder { Id = 10, ExternalWorkOrderId = "ext-loc-1", LocationId = 1, SiteCode = "BK1", istemplate = false }); context.WorkOrderFieldLocks.Add(new WorkOrderFieldLock { WorkOrderId = 10, FieldName = "LocationId", LockedAt = DateTime.UtcNow, LockedByUserId = "dispatcher-1" }); await context.SaveChangesAsync(); var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context)); var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks); var ingest = new WorkOrderIngestService(new WorkOrderIngestDataService(context), new SyncFieldMergePolicy(fieldLocks), fieldLocks, audit, WorkOrderAccountTestHelpers.Resolver(context)); await ingest.UpsertBatchAsync(new[] { new WorkOrderIngestPayloadDto { ExternalWorkOrderId = "ext-loc-1", SiteCode = "BK2", Building = "BK2" } }); var wo = await context.workOrders.SingleAsync(w => w.Id == 10); Assert.Equal(1, wo.LocationId); Assert.True(await context.WorkOrderAuditLogs.AnyAsync(l => l.Action == "SyncRejected" && l.FieldName == "LocationId")); } [Fact] public async Task IngestBatch_SkipsItemsWithoutExternalId() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; await using var context = new ApplicationDbContext(options); await WorkOrderAccountTestHelpers.EnsureAccountAsync(context, 1, "Phase7 Customer"); var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context)); var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks); var ingest = new WorkOrderIngestService(new WorkOrderIngestDataService(context), new SyncFieldMergePolicy(fieldLocks), fieldLocks, audit, WorkOrderAccountTestHelpers.Resolver(context)); var result = await ingest.UpsertBatchAsync(new[] { new WorkOrderIngestPayloadDto { ExternalWorkOrderId = " ", Description = "bad" }, new WorkOrderIngestPayloadDto { ExternalWorkOrderId = "ext-ok", Description = "good", Customer = "Phase7 Customer" } }); Assert.Equal(1, result.Created); Assert.Equal(1, result.Skipped); var createdResult = Assert.Single(result.Results); Assert.Equal("ext-ok", createdResult.ExternalWorkOrderId); } [Fact] public async Task OpsHealth_ReturnsCoexistenceMetrics() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; await using var context = new ApplicationDbContext(options); context.WorkOrderFieldLocks.Add(new WorkOrderFieldLock { WorkOrderId = 1, FieldName = "SiteCode", LockedAt = DateTime.UtcNow, LockedByUserId = "u1" }); context.WorkOrderAuditLogs.Add(new WorkOrderAuditLog { WorkOrderId = 1, Action = "SyncRejected", FieldName = "Description", CreatedAt = DateTime.UtcNow.AddHours(-1) }); await context.SaveChangesAsync(); var runState = new WorkOrderJobRunStateAccessor { State = { LastWeekRolledRunUtc = DateTime.UtcNow.AddDays(-1) } }; var healthOptions = Options.Create(new WorkOrderOpsHealthOptions { SyncEnabled = true, IngestEnabled = true, LegacyDeprecationEnabled = true, LegacySunsetDate = "2026-12-31" }); var service = new WorkOrderOpsHealthService(new WorkOrderOpsHealthDataService(context), runState, healthOptions); var health = await service.GetHealthAsync(); Assert.Equal(1, health.FieldLockCount); Assert.Equal(1, health.SyncRejectedLast24h); Assert.True(health.SyncEnabled); Assert.True(health.IngestEnabled); Assert.Equal("2026-12-31", health.LegacySunsetDate); } [Fact] public async Task BoardAndLegacyEndpoints_CanCoexist_InSameDatabase() { var options = new DbContextOptionsBuilder() .UseInMemoryDatabase(Guid.NewGuid().ToString()) .Options; await using var context = new ApplicationDbContext(options); context.workOrders.Add(new WorkOrder { WorkerOrderTitle = "Legacy row", Status = "Open", InternalWONumber = "10000001", istemplate = false }); await context.SaveChangesAsync(); var boardData = new WorkOrderBoardDataService(context); var boardService = new WorkOrderBoardService(boardData, WorkOrderAccountTestHelpers.Resolver(context)); var count = await context.workOrders.CountAsync(); Assert.Equal(1, count); Assert.NotNull(boardService); } }