From 9dc17d4255a53124491c885d3bba43069d9514c8 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Fri, 10 Jul 2026 10:49:57 -0300 Subject: [PATCH] fix(work-orders): persist WO identity before staging create audit and locks --- .../WorkOrderBoardCreateService.cs | 11 +-- .../SeaHavenIndustries.Tests.csproj | 1 + .../WorkOrderBoardCreateRelationalTests.cs | 97 +++++++++++++++++++ .../WorkOrderBoardCreateServiceTests.cs | 7 +- 4 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 SeaHavenIndustries.Tests/WorkOrderBoardCreateRelationalTests.cs diff --git a/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs b/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs index 6d6c2a0..77bdb27 100644 --- a/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs +++ b/SeaHaven.Services/Implementation/WorkOrderBoardCreateService.cs @@ -123,19 +123,17 @@ namespace SeaHaven.Services.Implementation } _context.workOrders.Add(workOrder); - - _auditService.StageCreated(workOrder.Id, actorId, woNumber); - StageChanges(workOrder.Id, changes, actorId, dispatch); - await _context.SaveChangesAsync(); if (dispatch != null && workOrder.PrimaryDispatchId == null) { workOrder.PrimaryDispatchId = dispatch.Id; dispatch.WorkOrderId = workOrder.Id; - await _context.SaveChangesAsync(); } + _auditService.StageCreated(workOrder.Id, actorId, woNumber); + StageChanges(workOrder.Id, changes, actorId, dispatch); + if (request.PocContactId.HasValue) { _context.WorkOrderContacts.Add(new WorkOrderContacts @@ -144,9 +142,10 @@ namespace SeaHaven.Services.Implementation ContactId = request.PocContactId, Notes = string.IsNullOrWhiteSpace(request.PocNotes) ? null : request.PocNotes.Trim() }); - await _context.SaveChangesAsync(); } + await _context.SaveChangesAsync(); + var row = await _boardService.GetBoardRowAsync(workOrder.Id); return row ?? throw new WorkOrderBoardValidationException("NotFound", "Work order was created but could not be loaded."); } diff --git a/SeaHavenIndustries.Tests/SeaHavenIndustries.Tests.csproj b/SeaHavenIndustries.Tests/SeaHavenIndustries.Tests.csproj index 5eb8d5d..2fc3613 100644 --- a/SeaHavenIndustries.Tests/SeaHavenIndustries.Tests.csproj +++ b/SeaHavenIndustries.Tests/SeaHavenIndustries.Tests.csproj @@ -10,6 +10,7 @@ + diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardCreateRelationalTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardCreateRelationalTests.cs new file mode 100644 index 0000000..2873689 --- /dev/null +++ b/SeaHavenIndustries.Tests/WorkOrderBoardCreateRelationalTests.cs @@ -0,0 +1,97 @@ +using Data.SeaHavenIndustries; +using Data.SeaHavenIndustries.Enums; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using SeaHaven.DataServices.Implementation; +using SeaHaven.Services.DTOs; +using SeaHaven.Services.Implementation; +using SeaHaven.Services.Validation; + +namespace SeaHavenIndustries.Tests; + +public class WorkOrderBoardCreateRelationalTests +{ + [Fact] + public async Task Create_AuditAndFieldLocks_UseGeneratedWorkOrderId() + { + await using var connection = new SqliteConnection("DataSource=:memory:"); + await connection.OpenAsync(); + + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + await using var context = new SqliteBoardTestDbContext(options); + await context.Database.EnsureCreatedAsync(); + + const string actorId = "actor-1"; + context.Users.Add(new ApplicationUser + { + Id = actorId, + UserName = "actor-1", + NormalizedUserName = "ACTOR-1", + Email = "actor@test.local", + NormalizedEmail = "ACTOR@TEST.LOCAL" + }); + await context.SaveChangesAsync(); + + var boardData = new WorkOrderBoardDataService(context); + var boardService = new WorkOrderBoardService(boardData); + var fieldLocks = new WorkOrderFieldLockService(context); + var audit = new WorkOrderAuditService(context, fieldLocks); + var service = new WorkOrderBoardCreateService( + context, + boardData, + boardService, + audit, + new WorkOrderBoardCreateValidation()); + + var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto + { + WorkOrderType = WorkOrderType.PM, + SiteCode = "BK5", + Description = "Relational create" + }, actorId); + + Assert.True(result.Id > 0); + + var logs = await context.WorkOrderAuditLogs.AsNoTracking().ToListAsync(); + var locks = await context.WorkOrderFieldLocks.AsNoTracking().ToListAsync(); + + Assert.NotEmpty(logs); + Assert.NotEmpty(locks); + Assert.Contains(logs, l => l.Action == "Create"); + Assert.All(logs, l => Assert.Equal(result.Id, l.WorkOrderId)); + Assert.All(locks, l => Assert.Equal(result.Id, l.WorkOrderId)); + Assert.DoesNotContain(logs, l => l.WorkOrderId == 0); + Assert.DoesNotContain(locks, l => l.WorkOrderId == 0); + } + + private sealed class SqliteBoardTestDbContext : ApplicationDbContext + { + public SqliteBoardTestDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + // SQL Server filtered index syntax is invalid on SQLite. + foreach (var index in builder.Model.GetEntityTypes().SelectMany(e => e.GetIndexes())) + { + if (index.GetFilter() != null) + index.SetFilter(null); + } + + // SQLite has no rowversion type; treat as plain nullable blobs. + foreach (var entityType in new[] { typeof(WorkOrder), typeof(Dispatch) }) + { + var property = builder.Entity(entityType).Property("RowVersion").Metadata; + property.ValueGenerated = Microsoft.EntityFrameworkCore.Metadata.ValueGenerated.Never; + property.IsConcurrencyToken = false; + } + } + } +} diff --git a/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs b/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs index 2920da9..6910f7a 100644 --- a/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs +++ b/SeaHavenIndustries.Tests/WorkOrderBoardCreateServiceTests.cs @@ -149,7 +149,7 @@ public class WorkOrderBoardCreateServiceTests { var (context, service) = CreateSut(); - await service.CreateAsync(new WorkOrderBoardCreateRequestDto + var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5", @@ -160,6 +160,7 @@ public class WorkOrderBoardCreateServiceTests 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] @@ -167,7 +168,7 @@ public class WorkOrderBoardCreateServiceTests { var (context, service) = CreateSut(); - await service.CreateAsync(new WorkOrderBoardCreateRequestDto + var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto { WorkOrderType = WorkOrderType.PM, SiteCode = "BK5" @@ -176,5 +177,7 @@ public class WorkOrderBoardCreateServiceTests 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); } }