mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 07:13:12 +00:00
fix(work-orders): create a new dispatch when patching vendor on an inactive primary
Cancelled, Canceled, and Refused primaries are not live company assignments. VendorId PATCH now inserts a Pending dispatch instead of mutating the refused row.
This commit is contained in:
parent
737c137a82
commit
af593fe2d2
2 changed files with 166 additions and 28 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Data.SeaHavenIndustries;
|
using Data.SeaHavenIndustries;
|
||||||
using Data.SeaHavenIndustries.Enums;
|
using Data.SeaHavenIndustries.Enums;
|
||||||
|
using SeaHaven.DataServices.Helpers;
|
||||||
using SeaHaven.DataServices.Interfaces;
|
using SeaHaven.DataServices.Interfaces;
|
||||||
using SeaHaven.Services.Constants;
|
using SeaHaven.Services.Constants;
|
||||||
using SeaHaven.Services.DTOs;
|
using SeaHaven.Services.DTOs;
|
||||||
|
|
@ -75,18 +76,34 @@ namespace SeaHaven.Services.Implementation
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
var hadPrimaryDispatch = workOrder.PrimaryDispatchId.HasValue;
|
var hadPrimaryDispatch = workOrder.PrimaryDispatchId.HasValue;
|
||||||
dispatch = await ResolveDispatchAsync(workOrder, request.PrimaryDispatchId, vendorIdHint, ct);
|
var replaceInactivePrimary = vendorIdHint.HasValue;
|
||||||
|
var resolved = await ResolveDispatchAsync(
|
||||||
if (dispatchVersion != null && !RowVersionsMatch(dispatch.RowVersion, dispatchVersion))
|
workOrder,
|
||||||
{
|
request.PrimaryDispatchId,
|
||||||
var staleState = await LoadBoardRowAsync(workOrderId);
|
vendorIdHint,
|
||||||
throw new WorkOrderBoardConcurrencyException(staleState);
|
replaceInactivePrimary,
|
||||||
}
|
ct);
|
||||||
|
dispatch = resolved.Dispatch;
|
||||||
|
|
||||||
|
var versionSource = resolved.Created ? resolved.Previous : dispatch;
|
||||||
if (dispatchVersion != null)
|
if (dispatchVersion != null)
|
||||||
_mutationData.SetExpectedDispatchVersion(dispatch, dispatchVersion);
|
{
|
||||||
else if (hadPrimaryDispatch && dispatch.RowVersion != null)
|
if (versionSource != null
|
||||||
throw new WorkOrderBoardValidationException("DispatchVersionRequired", "dispatchVersion is required for dispatch fields.");
|
&& !RowVersionsMatch(versionSource.RowVersion, dispatchVersion))
|
||||||
|
{
|
||||||
|
var staleState = await LoadBoardRowAsync(workOrderId);
|
||||||
|
throw new WorkOrderBoardConcurrencyException(staleState);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resolved.Created)
|
||||||
|
_mutationData.SetExpectedDispatchVersion(dispatch, dispatchVersion);
|
||||||
|
}
|
||||||
|
else if (hadPrimaryDispatch && versionSource?.RowVersion != null)
|
||||||
|
{
|
||||||
|
throw new WorkOrderBoardValidationException(
|
||||||
|
"DispatchVersionRequired",
|
||||||
|
"dispatchVersion is required for dispatch fields.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var auditField = WorkOrderBoardFieldNames.ToAuditFieldName(canonicalField);
|
var auditField = WorkOrderBoardFieldNames.ToAuditFieldName(canonicalField);
|
||||||
|
|
@ -163,25 +180,22 @@ namespace SeaHaven.Services.Implementation
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Dispatch> ResolveDispatchAsync(WorkOrder workOrder, int? primaryDispatchId, int? vendorIdForCreate, CancellationToken cancellationToken)
|
private sealed record ResolvedDispatch(Dispatch Dispatch, bool Created, Dispatch? Previous);
|
||||||
|
|
||||||
|
private async Task<ResolvedDispatch> ResolveDispatchAsync(
|
||||||
|
WorkOrder workOrder,
|
||||||
|
int? primaryDispatchId,
|
||||||
|
int? vendorIdForCreate,
|
||||||
|
bool replaceInactivePrimary,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (primaryDispatchId.HasValue)
|
var existing = await LoadExistingDispatchAsync(workOrder, primaryDispatchId, cancellationToken);
|
||||||
|
if (existing != null
|
||||||
|
&& !(replaceInactivePrimary
|
||||||
|
&& WorkOrderBoardDispatchAssignment.IsInactivePrimaryDispatch(existing.Status)))
|
||||||
{
|
{
|
||||||
var dispatch = await _mutationData.GetTrackedDispatchAsync(primaryDispatchId.Value, workOrder.Id, cancellationToken);
|
workOrder.PrimaryDispatchId = existing.Id;
|
||||||
|
return new ResolvedDispatch(existing, Created: false, Previous: null);
|
||||||
if (dispatch == null)
|
|
||||||
throw new WorkOrderBoardValidationException("InvalidDispatch", "Primary dispatch does not belong to this work order.");
|
|
||||||
|
|
||||||
workOrder.PrimaryDispatchId = dispatch.Id;
|
|
||||||
return dispatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workOrder.PrimaryDispatchId.HasValue)
|
|
||||||
{
|
|
||||||
var existing = await _mutationData.GetTrackedDispatchByIdAsync(workOrder.PrimaryDispatchId.Value, cancellationToken);
|
|
||||||
|
|
||||||
if (existing != null)
|
|
||||||
return existing;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!vendorIdForCreate.HasValue || vendorIdForCreate.Value <= 0)
|
if (!vendorIdForCreate.HasValue || vendorIdForCreate.Value <= 0)
|
||||||
|
|
@ -199,7 +213,35 @@ namespace SeaHaven.Services.Implementation
|
||||||
_mutationData.TrackNewDispatch(created);
|
_mutationData.TrackNewDispatch(created);
|
||||||
await _mutationData.SaveAsync(cancellationToken);
|
await _mutationData.SaveAsync(cancellationToken);
|
||||||
workOrder.PrimaryDispatchId = created.Id;
|
workOrder.PrimaryDispatchId = created.Id;
|
||||||
return created;
|
return new ResolvedDispatch(created, Created: true, Previous: existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dispatch?> LoadExistingDispatchAsync(
|
||||||
|
WorkOrder workOrder,
|
||||||
|
int? primaryDispatchId,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (primaryDispatchId.HasValue)
|
||||||
|
{
|
||||||
|
var dispatch = await _mutationData.GetTrackedDispatchAsync(
|
||||||
|
primaryDispatchId.Value,
|
||||||
|
workOrder.Id,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
if (dispatch == null)
|
||||||
|
throw new WorkOrderBoardValidationException(
|
||||||
|
"InvalidDispatch",
|
||||||
|
"Primary dispatch does not belong to this work order.");
|
||||||
|
|
||||||
|
return dispatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!workOrder.PrimaryDispatchId.HasValue)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return await _mutationData.GetTrackedDispatchByIdAsync(
|
||||||
|
workOrder.PrimaryDispatchId.Value,
|
||||||
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureVendorExistsAsync(int vendorId)
|
private async Task EnsureVendorExistsAsync(int vendorId)
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,102 @@ public class WorkOrderBoardUpdateServiceTests
|
||||||
Assert.Equal(1, await context.Dispatches.CountAsync());
|
Assert.Equal(1, await context.Dispatches.CountAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Cancelled")]
|
||||||
|
[InlineData("Canceled")]
|
||||||
|
[InlineData("Refused")]
|
||||||
|
[InlineData(" refused ")]
|
||||||
|
public async Task PatchField_VendorId_CreatesNewDispatchWhenPrimaryIsInactive(string inactiveStatus)
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
context.Vendors.AddRange(
|
||||||
|
new Vendor { Id = 1, CompanyName = "Old Co" },
|
||||||
|
new Vendor { Id = 5, CompanyName = "New Co" });
|
||||||
|
var dispatch = new Dispatch
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
WorkOrderId = 1,
|
||||||
|
VendorId = 1,
|
||||||
|
Status = inactiveStatus,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 }
|
||||||
|
};
|
||||||
|
var wo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
PrimaryDispatchId = 10,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
};
|
||||||
|
context.Dispatches.Add(dispatch);
|
||||||
|
context.workOrders.Add(wo);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||||
|
{
|
||||||
|
Field = WorkOrderBoardFieldNames.VendorId,
|
||||||
|
Value = "5",
|
||||||
|
WorkOrderVersion = ToVersion(wo),
|
||||||
|
DispatchVersion = ToVersion(dispatch),
|
||||||
|
PrimaryDispatchId = 10
|
||||||
|
}, "actor-1");
|
||||||
|
|
||||||
|
Assert.Equal(5, result.VendorId);
|
||||||
|
Assert.Equal("New Co", result.VendorName);
|
||||||
|
Assert.Equal("Pending", result.PrimaryDispatchStatus);
|
||||||
|
Assert.NotEqual(10, result.PrimaryDispatchId);
|
||||||
|
Assert.Equal(2, await context.Dispatches.CountAsync());
|
||||||
|
var leftover = await context.Dispatches.SingleAsync(d => d.Id == 10);
|
||||||
|
Assert.Equal(1, leftover.VendorId);
|
||||||
|
Assert.Equal(inactiveStatus, leftover.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Sent")]
|
||||||
|
[InlineData("Verified")]
|
||||||
|
[InlineData("Completed")]
|
||||||
|
public async Task PatchField_VendorId_UpdatesExistingLivePrimaryDispatch(string liveStatus)
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
context.Vendors.AddRange(
|
||||||
|
new Vendor { Id = 1, CompanyName = "Old Co" },
|
||||||
|
new Vendor { Id = 5, CompanyName = "New Co" });
|
||||||
|
var dispatch = new Dispatch
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
WorkOrderId = 1,
|
||||||
|
VendorId = 1,
|
||||||
|
Status = liveStatus,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 }
|
||||||
|
};
|
||||||
|
var wo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
PrimaryDispatchId = 10,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
};
|
||||||
|
context.Dispatches.Add(dispatch);
|
||||||
|
context.workOrders.Add(wo);
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||||
|
{
|
||||||
|
Field = WorkOrderBoardFieldNames.VendorId,
|
||||||
|
Value = "5",
|
||||||
|
WorkOrderVersion = ToVersion(wo),
|
||||||
|
DispatchVersion = ToVersion(dispatch),
|
||||||
|
PrimaryDispatchId = 10
|
||||||
|
}, "actor-1");
|
||||||
|
|
||||||
|
Assert.Equal(5, result.VendorId);
|
||||||
|
Assert.Equal(10, result.PrimaryDispatchId);
|
||||||
|
Assert.Equal(liveStatus, result.PrimaryDispatchStatus);
|
||||||
|
Assert.Equal(1, await context.Dispatches.CountAsync());
|
||||||
|
var updated = await context.Dispatches.SingleAsync();
|
||||||
|
Assert.Equal(5, updated.VendorId);
|
||||||
|
Assert.Equal(liveStatus, updated.Status);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PatchField_CreatesFieldLockAndAuditPerField()
|
public async Task PatchField_CreatesFieldLockAndAuditPerField()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue