mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-05 13:02:10 +00:00
merge: integrate Phase 4 into Phase 5 and add DomainEvents Designer metadata
Resolve snapshot conflict by keeping Phase 4 search indexes and Phase 5 ledger model. Add missing Phase5_DomainEvents.Designer.cs so EF discovers the WorkOrderWeekRolledLedgers migration.
This commit is contained in:
commit
812a5e994c
10 changed files with 6298 additions and 11 deletions
101
Api.SeaHavenIndustries.Tests/WorkOrderControllerSearchTests.cs
Normal file
101
Api.SeaHavenIndustries.Tests/WorkOrderControllerSearchTests.cs
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
using Api.SeaHavenIndustries.Controllers;
|
||||||
|
using Api.SeaHavenIndustries.DTOs;
|
||||||
|
using Data.SeaHavenIndustries;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Moq;
|
||||||
|
using SeaHaven.DataServices.Interfaces;
|
||||||
|
using SeaHaven.Services.DTOs;
|
||||||
|
using SeaHaven.Services.Helpers;
|
||||||
|
using SeaHaven.Services.Implementation;
|
||||||
|
using SeaHaven.Services.Interfaces;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Api.SeaHavenIndustries.Tests;
|
||||||
|
|
||||||
|
public class WorkOrderControllerSearchTests
|
||||||
|
{
|
||||||
|
private static ApplicationDbContext NewContext()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
|
||||||
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
return new ApplicationDbContext(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static UserManager<ApplicationUser> MockUserManager()
|
||||||
|
{
|
||||||
|
var store = new Mock<IUserStore<ApplicationUser>>();
|
||||||
|
return new Mock<UserManager<ApplicationUser>>(
|
||||||
|
store.Object, null!, null!, null!, null!, null!, null!, null!, null!).Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorkOrderController NewController(
|
||||||
|
ApplicationDbContext ctx,
|
||||||
|
IWorkOrderAdvancedSearchService advancedSearch)
|
||||||
|
{
|
||||||
|
var controller = new WorkOrderController(
|
||||||
|
MockUserManager(),
|
||||||
|
Mock.Of<IWorkOrderService>(),
|
||||||
|
Mock.Of<IWorkOrderDataService>(),
|
||||||
|
Mock.Of<ICommentDataService>(),
|
||||||
|
Mock.Of<IQuotesDataService>(),
|
||||||
|
Mock.Of<IUserDataService>(),
|
||||||
|
Mock.Of<IDispatchDataService>(),
|
||||||
|
Mock.Of<IVendorDataService>(),
|
||||||
|
Mock.Of<ITaskListTemplateDataService>(),
|
||||||
|
ctx,
|
||||||
|
Mock.Of<IWebHostEnvironment>(),
|
||||||
|
Mock.Of<IHttpContextAccessor>(),
|
||||||
|
null!,
|
||||||
|
null!,
|
||||||
|
Mock.Of<IConfiguration>(),
|
||||||
|
Mock.Of<IWorkOrderBoardService>(),
|
||||||
|
Mock.Of<IWorkOrderBoardUpdateService>(),
|
||||||
|
Mock.Of<IWorkOrderBoardCreateService>(),
|
||||||
|
Mock.Of<IWorkOrderBoardCancelService>(),
|
||||||
|
advancedSearch);
|
||||||
|
|
||||||
|
controller.ControllerContext = new ControllerContext
|
||||||
|
{
|
||||||
|
HttpContext = new DefaultHttpContext
|
||||||
|
{
|
||||||
|
User = new System.Security.Claims.ClaimsPrincipal(
|
||||||
|
new System.Security.Claims.ClaimsIdentity(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new System.Security.Claims.Claim(
|
||||||
|
System.Security.Claims.ClaimTypes.NameIdentifier,
|
||||||
|
"test-user")
|
||||||
|
},
|
||||||
|
"Test"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SearchBoard_WhenCustomPresetMissingDates_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
using var ctx = NewContext();
|
||||||
|
var advancedSearch = new WorkOrderAdvancedSearchService(
|
||||||
|
Mock.Of<IWorkOrderAdvancedSearchDataService>());
|
||||||
|
var controller = NewController(ctx, advancedSearch);
|
||||||
|
|
||||||
|
var result = await controller.SearchBoard(new WorkOrderAdvancedSearchQueryDto
|
||||||
|
{
|
||||||
|
DatePreset = WorkOrderAdvancedSearchDatePreset.Custom
|
||||||
|
});
|
||||||
|
|
||||||
|
var badRequest = result.Should().BeOfType<BadRequestObjectResult>().Subject;
|
||||||
|
var body = badRequest.Value.Should().BeOfType<Response>().Subject;
|
||||||
|
body.Status.Should().Be("Error");
|
||||||
|
body.Message.Should().Contain("dateFrom");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -120,9 +120,16 @@ namespace Api.SeaHavenIndustries.Controllers
|
||||||
[HttpGet("board/search")]
|
[HttpGet("board/search")]
|
||||||
public async Task<IActionResult> SearchBoard([FromQuery] WorkOrderAdvancedSearchQueryDto query)
|
public async Task<IActionResult> SearchBoard([FromQuery] WorkOrderAdvancedSearchQueryDto query)
|
||||||
{
|
{
|
||||||
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
try
|
||||||
var result = await _advancedSearchService.SearchAsync(query, userId);
|
{
|
||||||
return Ok(result);
|
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
|
var result = await _advancedSearchService.SearchAsync(query, userId);
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
catch (ArgumentException ex)
|
||||||
|
{
|
||||||
|
return BadRequest(new Response { Status = "Error", Message = ex.Message });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("lookups/dispatchers")]
|
[HttpGet("lookups/dispatchers")]
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,18 @@ namespace Data.SeaHavenIndustries
|
||||||
.Property(w => w.RowVersion)
|
.Property(w => w.RowVersion)
|
||||||
.IsRowVersion();
|
.IsRowVersion();
|
||||||
|
|
||||||
|
builder.Entity<WorkOrder>()
|
||||||
|
.HasIndex(w => w.SiteCode)
|
||||||
|
.HasFilter("[istemplate] = 0");
|
||||||
|
|
||||||
|
builder.Entity<WorkOrder>()
|
||||||
|
.HasIndex(w => w.InternalWONumber)
|
||||||
|
.HasFilter("[istemplate] = 0");
|
||||||
|
|
||||||
|
builder.Entity<WorkOrder>()
|
||||||
|
.HasIndex(w => new { w.LifecycleStatus, w.ScheduledDate })
|
||||||
|
.HasFilter("[istemplate] = 0");
|
||||||
|
|
||||||
builder.Entity<Dispatch>()
|
builder.Entity<Dispatch>()
|
||||||
.Property(d => d.RowVersion)
|
.Property(d => d.RowVersion)
|
||||||
.IsRowVersion();
|
.IsRowVersion();
|
||||||
|
|
|
||||||
3006
Data.SeaHavenIndustries/Migrations/20260624200000_Phase4_SearchIndexes.Designer.cs
generated
Normal file
3006
Data.SeaHavenIndustries/Migrations/20260624200000_Phase4_SearchIndexes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
3046
Data.SeaHavenIndustries/Migrations/20260709170202_Phase5_DomainEvents.Designer.cs
generated
Normal file
3046
Data.SeaHavenIndustries/Migrations/20260709170202_Phase5_DomainEvents.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2031,10 +2031,25 @@ namespace Data.SeaHavenIndustries.Migrations
|
||||||
|
|
||||||
b.HasIndex("AssignTo");
|
b.HasIndex("AssignTo");
|
||||||
|
|
||||||
|
b.HasIndex("AssignTo", "ScheduledDate");
|
||||||
|
|
||||||
|
b.HasIndex("InternalWONumber")
|
||||||
|
.HasFilter("[istemplate] = 0");
|
||||||
|
|
||||||
|
b.HasIndex("LifecycleStatus", "ScheduledDate")
|
||||||
|
.HasFilter("[istemplate] = 0");
|
||||||
|
|
||||||
b.HasIndex("LocationId");
|
b.HasIndex("LocationId");
|
||||||
|
|
||||||
b.HasIndex("PrimaryDispatchId");
|
b.HasIndex("PrimaryDispatchId");
|
||||||
|
|
||||||
|
b.HasIndex("ScheduledDate");
|
||||||
|
|
||||||
|
b.HasIndex("SiteCode")
|
||||||
|
.HasFilter("[istemplate] = 0");
|
||||||
|
|
||||||
|
b.HasIndex("WorkOrderType");
|
||||||
|
|
||||||
b.ToTable("workOrders", (string)null);
|
b.ToTable("workOrders", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -123,19 +123,17 @@ namespace SeaHaven.Services.Implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
_context.workOrders.Add(workOrder);
|
_context.workOrders.Add(workOrder);
|
||||||
|
|
||||||
_auditService.StageCreated(workOrder.Id, actorId, woNumber);
|
|
||||||
StageChanges(workOrder.Id, changes, actorId, dispatch);
|
|
||||||
|
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
if (dispatch != null && workOrder.PrimaryDispatchId == null)
|
if (dispatch != null && workOrder.PrimaryDispatchId == null)
|
||||||
{
|
{
|
||||||
workOrder.PrimaryDispatchId = dispatch.Id;
|
workOrder.PrimaryDispatchId = dispatch.Id;
|
||||||
dispatch.WorkOrderId = workOrder.Id;
|
dispatch.WorkOrderId = workOrder.Id;
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_auditService.StageCreated(workOrder.Id, actorId, woNumber);
|
||||||
|
StageChanges(workOrder.Id, changes, actorId, dispatch);
|
||||||
|
|
||||||
if (request.PocContactId.HasValue)
|
if (request.PocContactId.HasValue)
|
||||||
{
|
{
|
||||||
_context.WorkOrderContacts.Add(new WorkOrderContacts
|
_context.WorkOrderContacts.Add(new WorkOrderContacts
|
||||||
|
|
@ -144,9 +142,10 @@ namespace SeaHaven.Services.Implementation
|
||||||
ContactId = request.PocContactId,
|
ContactId = request.PocContactId,
|
||||||
Notes = string.IsNullOrWhiteSpace(request.PocNotes) ? null : request.PocNotes.Trim()
|
Notes = string.IsNullOrWhiteSpace(request.PocNotes) ? null : request.PocNotes.Trim()
|
||||||
});
|
});
|
||||||
await _context.SaveChangesAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
var row = await _boardService.GetBoardRowAsync(workOrder.Id);
|
var row = await _boardService.GetBoardRowAsync(workOrder.Id);
|
||||||
return row ?? throw new WorkOrderBoardValidationException("NotFound", "Work order was created but could not be loaded.");
|
return row ?? throw new WorkOrderBoardValidationException("NotFound", "Work order was created but could not be loaded.");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.8" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.8" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||||
<PackageReference Include="xunit" Version="2.9.2" />
|
<PackageReference Include="xunit" Version="2.9.2" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
|
||||||
|
|
|
||||||
|
|
@ -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<ApplicationDbContext>()
|
||||||
|
.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<ApplicationDbContext> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -149,7 +149,7 @@ public class WorkOrderBoardCreateServiceTests
|
||||||
{
|
{
|
||||||
var (context, service) = CreateSut();
|
var (context, service) = CreateSut();
|
||||||
|
|
||||||
await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||||
{
|
{
|
||||||
WorkOrderType = WorkOrderType.PM,
|
WorkOrderType = WorkOrderType.PM,
|
||||||
SiteCode = "BK5",
|
SiteCode = "BK5",
|
||||||
|
|
@ -160,6 +160,7 @@ public class WorkOrderBoardCreateServiceTests
|
||||||
Assert.Contains(locks, l => l.FieldName == "SiteCode");
|
Assert.Contains(locks, l => l.FieldName == "SiteCode");
|
||||||
Assert.Contains(locks, l => l.FieldName == "WorkOrderType");
|
Assert.Contains(locks, l => l.FieldName == "WorkOrderType");
|
||||||
Assert.Contains(locks, l => l.FieldName == "Description");
|
Assert.Contains(locks, l => l.FieldName == "Description");
|
||||||
|
Assert.All(locks, l => Assert.Equal(result.Id, l.WorkOrderId));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -167,7 +168,7 @@ public class WorkOrderBoardCreateServiceTests
|
||||||
{
|
{
|
||||||
var (context, service) = CreateSut();
|
var (context, service) = CreateSut();
|
||||||
|
|
||||||
await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
var result = await service.CreateAsync(new WorkOrderBoardCreateRequestDto
|
||||||
{
|
{
|
||||||
WorkOrderType = WorkOrderType.PM,
|
WorkOrderType = WorkOrderType.PM,
|
||||||
SiteCode = "BK5"
|
SiteCode = "BK5"
|
||||||
|
|
@ -176,5 +177,7 @@ public class WorkOrderBoardCreateServiceTests
|
||||||
var logs = await context.WorkOrderAuditLogs.ToListAsync();
|
var logs = await context.WorkOrderAuditLogs.ToListAsync();
|
||||||
Assert.Contains(logs, l => l.Action == "Create");
|
Assert.Contains(logs, l => l.Action == "Create");
|
||||||
Assert.Contains(logs, l => l.Action == "FieldChanged" && l.FieldName == "SiteCode");
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue