shoc-backend/SeaHavenIndustries.Tests/WorkOrderPhase7CoexistenceTests.cs
Arthur Bassi 61923b2a7d feat(work-orders): stamp board create account from location
Org-wide create no longer depends on customer name. POST /workorders/board requires locationId and stamps WorkOrder.AccountId from Location.AccountId.
2026-08-26 09:12:48 -03:00

253 lines
10 KiB
C#

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<ApplicationDbContext>()
.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<ApplicationDbContext>()
.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);
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<ApplicationDbContext>()
.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<ApplicationDbContext>()
.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<ApplicationDbContext>()
.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<ApplicationDbContext>()
.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);
}
}