shoc-backend/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs
Arthur Bassi e5bda0714a feat(work-orders): add Phase 3 board create and soft cancel API
Expose POST /api/workorders/board and POST /api/workorders/{id}/cancel for SHOC wizard/inline creation and soft cancel, with field locks, WO# normalization, and Admin-only hard delete.
2026-07-08 10:17:31 -03:00

180 lines
6 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.Implementation;
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);
var boardData = new WorkOrderBoardDataService(context);
var boardService = new WorkOrderBoardService(boardData);
var fieldLocks = new WorkOrderFieldLockService(context);
var audit = new WorkOrderAuditService(context, fieldLocks);
var validator = new WorkOrderBoardCreateValidation();
var service = new WorkOrderBoardCreateService(context, boardData, boardService, audit, validator);
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"
}, "actor-1");
Assert.Equal(LifecycleStatus.Incomplete, result.LifecycleStatus);
Assert.Equal("BK5", result.SiteCode);
Assert.Equal(WorkOrderType.PM, result.WorkOrderType);
Assert.False(string.IsNullOrWhiteSpace(result.WoNumber));
Assert.Equal(11, result.WoNumber!.Length);
}
[Fact]
public async Task Create_WithManualWoNumber_NormalizesTo11Digits()
{
var (_, service) = CreateSut();
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
{
WoNumber = "12345",
WorkOrderType = WorkOrderType.PO,
SiteCode = "DAL"
}, "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<WorkOrderBoardValidationException>(() =>
service.CreateAsync(new WorkOrderBoardCreateRequestDto
{
WoNumber = "99999",
WorkOrderType = WorkOrderType.PM,
SiteCode = "BK5"
}, "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",
AssignTo = "dispatcher-1",
ScheduledDate = new DateTime(2026, 6, 25)
}, "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",
ScheduleWeekOnly = true,
TargetWeek = new DateOnly(2026, 6, 22)
}, "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<FluentValidation.ValidationException>(() =>
service.CreateAsync(new WorkOrderBoardCreateRequestDto
{
WorkOrderType = WorkOrderType.PM,
SiteCode = "BK5",
ScheduleWeekOnly = true
}, "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",
VendorId = 5,
ApptDate = new DateTime(2026, 6, 26)
}, "actor-1");
Assert.Equal(5, result.VendorId);
Assert.NotNull(result.PrimaryDispatchId);
}
[Fact]
public async Task Create_CreatesFieldLocksForPopulatedFields()
{
var (context, service) = CreateSut();
await service.CreateAsync(new WorkOrderBoardCreateRequestDto
{
WorkOrderType = WorkOrderType.PM,
SiteCode = "BK5",
Description = "Test WO"
}, "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");
}
[Fact]
public async Task Create_WritesCreateAuditEvent()
{
var (context, service) = CreateSut();
await service.CreateAsync(new WorkOrderBoardCreateRequestDto
{
WorkOrderType = WorkOrderType.PM,
SiteCode = "BK5"
}, "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");
}
}