mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-06 10:02:09 +00:00
fix(work-orders): detach shared dispatch on vendor fork
Keep same-vendor saves idempotent and drop stale DispatchWorkOrders so listing and uplift follow the new primary.
This commit is contained in:
parent
ca0404272e
commit
03c069d10d
5 changed files with 212 additions and 1 deletions
|
|
@ -38,6 +38,45 @@ namespace SeaHaven.DataServices.Implementation
|
||||||
.Include(d => d.DispatchWorkOrders)
|
.Include(d => d.DispatchWorkOrders)
|
||||||
.FirstOrDefaultAsync(d => d.Id == dispatchId, cancellationToken);
|
.FirstOrDefaultAsync(d => d.Id == dispatchId, cancellationToken);
|
||||||
|
|
||||||
|
public void DetachWorkOrderFromDispatch(Dispatch dispatch, int workOrderId)
|
||||||
|
{
|
||||||
|
var links = dispatch.DispatchWorkOrders?
|
||||||
|
.Where(link => link.WorkOrderId == workOrderId)
|
||||||
|
.ToList();
|
||||||
|
if (links == null || links.Count == 0)
|
||||||
|
{
|
||||||
|
links = _context.DispatchWorkOrders
|
||||||
|
.Where(link => link.DispatchId == dispatch.Id && link.WorkOrderId == workOrderId)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var link in links)
|
||||||
|
_context.DispatchWorkOrders.Remove(link);
|
||||||
|
|
||||||
|
if (dispatch.WorkOrderId != workOrderId)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var remainingIds = (dispatch.DispatchWorkOrders ?? Enumerable.Empty<DispatchWorkOrder>())
|
||||||
|
.Where(link => link.WorkOrderId != workOrderId)
|
||||||
|
.Select(link => link.WorkOrderId)
|
||||||
|
.Distinct()
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (remainingIds.Count == 0)
|
||||||
|
{
|
||||||
|
dispatch.WorkOrderId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var preferredOwnerId = _context.workOrders
|
||||||
|
.Where(workOrder => remainingIds.Contains(workOrder.Id)
|
||||||
|
&& workOrder.PrimaryDispatchId == dispatch.Id)
|
||||||
|
.Select(workOrder => (int?)workOrder.Id)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
dispatch.WorkOrderId = preferredOwnerId ?? remainingIds[0];
|
||||||
|
}
|
||||||
|
|
||||||
public void TrackNewWorkOrder(WorkOrder workOrder)
|
public void TrackNewWorkOrder(WorkOrder workOrder)
|
||||||
=> _context.workOrders.Add(workOrder);
|
=> _context.workOrders.Add(workOrder);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ namespace SeaHaven.DataServices.Interfaces
|
||||||
Task<WorkOrder?> GetTrackedWorkOrderAsync(int workOrderId, CancellationToken cancellationToken);
|
Task<WorkOrder?> GetTrackedWorkOrderAsync(int workOrderId, CancellationToken cancellationToken);
|
||||||
Task<Dispatch?> GetTrackedDispatchAsync(int dispatchId, int workOrderId, CancellationToken cancellationToken);
|
Task<Dispatch?> GetTrackedDispatchAsync(int dispatchId, int workOrderId, CancellationToken cancellationToken);
|
||||||
Task<Dispatch?> GetTrackedDispatchByIdAsync(int dispatchId, CancellationToken cancellationToken);
|
Task<Dispatch?> GetTrackedDispatchByIdAsync(int dispatchId, CancellationToken cancellationToken);
|
||||||
|
void DetachWorkOrderFromDispatch(Dispatch dispatch, int workOrderId);
|
||||||
void TrackNewWorkOrder(WorkOrder workOrder);
|
void TrackNewWorkOrder(WorkOrder workOrder);
|
||||||
void TrackNewDispatch(Dispatch dispatch);
|
void TrackNewDispatch(Dispatch dispatch);
|
||||||
void TrackWorkOrderContact(int workOrderId, int contactId, string? notes);
|
void TrackWorkOrderContact(int workOrderId, int contactId, string? notes);
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,9 @@ namespace SeaHaven.Services.Implementation
|
||||||
"DispatchVersionRequired",
|
"DispatchVersionRequired",
|
||||||
"dispatchVersion is required for dispatch fields.");
|
"dispatchVersion is required for dispatch fields.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resolved is { Created: true, Previous: { } previousDispatch })
|
||||||
|
_mutationData.DetachWorkOrderFromDispatch(previousDispatch, workOrder.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
var auditField = WorkOrderBoardFieldNames.ToAuditFieldName(canonicalField);
|
var auditField = WorkOrderBoardFieldNames.ToAuditFieldName(canonicalField);
|
||||||
|
|
@ -215,10 +218,13 @@ namespace SeaHaven.Services.Implementation
|
||||||
primaryDispatchId,
|
primaryDispatchId,
|
||||||
relaxInvalidDispatch,
|
relaxInvalidDispatch,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
var vendorChanged = existing != null
|
||||||
|
&& vendorIdForCreate.HasValue
|
||||||
|
&& existing.VendorId != vendorIdForCreate.Value;
|
||||||
var mustReplace = existing != null
|
var mustReplace = existing != null
|
||||||
&& replaceInactivePrimary
|
&& replaceInactivePrimary
|
||||||
&& (WorkOrderBoardDispatchAssignment.IsInactivePrimaryDispatch(existing.Status)
|
&& (WorkOrderBoardDispatchAssignment.IsInactivePrimaryDispatch(existing.Status)
|
||||||
|| IsSharedDispatch(existing, workOrder.Id));
|
|| (IsSharedDispatch(existing, workOrder.Id) && vendorChanged));
|
||||||
if (existing != null && !mustReplace)
|
if (existing != null && !mustReplace)
|
||||||
{
|
{
|
||||||
workOrder.PrimaryDispatchId = existing.Id;
|
workOrder.PrimaryDispatchId = existing.Id;
|
||||||
|
|
|
||||||
|
|
@ -392,6 +392,9 @@ public class WorkOrderBoardCancelServiceTests
|
||||||
public Task<Dispatch?> GetTrackedDispatchByIdAsync(int dispatchId, CancellationToken cancellationToken)
|
public Task<Dispatch?> GetTrackedDispatchByIdAsync(int dispatchId, CancellationToken cancellationToken)
|
||||||
=> _inner.GetTrackedDispatchByIdAsync(dispatchId, cancellationToken);
|
=> _inner.GetTrackedDispatchByIdAsync(dispatchId, cancellationToken);
|
||||||
|
|
||||||
|
public void DetachWorkOrderFromDispatch(Dispatch dispatch, int workOrderId)
|
||||||
|
=> _inner.DetachWorkOrderFromDispatch(dispatch, workOrderId);
|
||||||
|
|
||||||
public void TrackNewWorkOrder(WorkOrder workOrder) => _inner.TrackNewWorkOrder(workOrder);
|
public void TrackNewWorkOrder(WorkOrder workOrder) => _inner.TrackNewWorkOrder(workOrder);
|
||||||
|
|
||||||
public void TrackNewDispatch(Dispatch dispatch) => _inner.TrackNewDispatch(dispatch);
|
public void TrackNewDispatch(Dispatch dispatch) => _inner.TrackNewDispatch(dispatch);
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,27 @@ public class WorkOrderBoardUpdateServiceTests
|
||||||
private static string ToVersion(Dispatch dispatch)
|
private static string ToVersion(Dispatch dispatch)
|
||||||
=> Convert.ToBase64String(dispatch.RowVersion ?? new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 });
|
=> Convert.ToBase64String(dispatch.RowVersion ?? new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 });
|
||||||
|
|
||||||
|
private static Task<int[]> DispatchIdsForWorkOrderAsync(ApplicationDbContext context, int workOrderId)
|
||||||
|
=> context.Dispatches
|
||||||
|
.Where(dispatch =>
|
||||||
|
dispatch.WorkOrderId == workOrderId
|
||||||
|
|| context.DispatchWorkOrders.Any(link =>
|
||||||
|
link.DispatchId == dispatch.Id && link.WorkOrderId == workOrderId))
|
||||||
|
.Select(dispatch => dispatch.Id)
|
||||||
|
.OrderBy(id => id)
|
||||||
|
.ToArrayAsync();
|
||||||
|
|
||||||
|
private static Task<bool> UpliftAssociatedAsync(
|
||||||
|
ApplicationDbContext context,
|
||||||
|
int workOrderId,
|
||||||
|
int dispatchId)
|
||||||
|
=> context.DispatchUpliftRequests.AnyAsync(uplift =>
|
||||||
|
uplift.DispatchId == dispatchId
|
||||||
|
&& (context.Dispatches.Any(dispatch =>
|
||||||
|
dispatch.Id == dispatchId && dispatch.WorkOrderId == workOrderId)
|
||||||
|
|| context.DispatchWorkOrders.Any(link =>
|
||||||
|
link.DispatchId == dispatchId && link.WorkOrderId == workOrderId)));
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task PatchField_AutoSchedulesIncompleteToScheduled()
|
public async Task PatchField_AutoSchedulesIncompleteToScheduled()
|
||||||
{
|
{
|
||||||
|
|
@ -613,6 +634,15 @@ public class WorkOrderBoardUpdateServiceTests
|
||||||
context.DispatchWorkOrders.AddRange(
|
context.DispatchWorkOrders.AddRange(
|
||||||
new DispatchWorkOrder { DispatchId = 10, WorkOrderId = 1 },
|
new DispatchWorkOrder { DispatchId = 10, WorkOrderId = 1 },
|
||||||
new DispatchWorkOrder { DispatchId = 10, WorkOrderId = 2 });
|
new DispatchWorkOrder { DispatchId = 10, WorkOrderId = 2 });
|
||||||
|
context.DispatchUpliftRequests.Add(new DispatchUpliftRequest
|
||||||
|
{
|
||||||
|
Id = 200,
|
||||||
|
DispatchId = 10,
|
||||||
|
RequestedNTE = 900m,
|
||||||
|
Status = "Pending",
|
||||||
|
RequiredTier = 1,
|
||||||
|
NotificationStatus = "Pending"
|
||||||
|
});
|
||||||
await context.SaveChangesAsync();
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
var result = await service.PatchFieldAsync(2, new WorkOrderBoardPatchRequestDto
|
var result = await service.PatchFieldAsync(2, new WorkOrderBoardPatchRequestDto
|
||||||
|
|
@ -630,11 +660,143 @@ public class WorkOrderBoardUpdateServiceTests
|
||||||
var original = await context.Dispatches.SingleAsync(d => d.Id == 10);
|
var original = await context.Dispatches.SingleAsync(d => d.Id == 10);
|
||||||
Assert.Equal(1, original.VendorId);
|
Assert.Equal(1, original.VendorId);
|
||||||
Assert.Equal("Sent", original.Status);
|
Assert.Equal("Sent", original.Status);
|
||||||
|
Assert.Equal(1, original.WorkOrderId);
|
||||||
var created = await context.Dispatches.SingleAsync(d => d.Id == result.PrimaryDispatchId);
|
var created = await context.Dispatches.SingleAsync(d => d.Id == result.PrimaryDispatchId);
|
||||||
Assert.Equal(2, created.WorkOrderId);
|
Assert.Equal(2, created.WorkOrderId);
|
||||||
Assert.Equal(5, created.VendorId);
|
Assert.Equal(5, created.VendorId);
|
||||||
var primaryUnchanged = await context.workOrders.SingleAsync(w => w.Id == 1);
|
var primaryUnchanged = await context.workOrders.SingleAsync(w => w.Id == 1);
|
||||||
Assert.Equal(10, primaryUnchanged.PrimaryDispatchId);
|
Assert.Equal(10, primaryUnchanged.PrimaryDispatchId);
|
||||||
|
Assert.False(await context.DispatchWorkOrders.AnyAsync(link =>
|
||||||
|
link.DispatchId == 10 && link.WorkOrderId == 2));
|
||||||
|
Assert.True(await context.DispatchWorkOrders.AnyAsync(link =>
|
||||||
|
link.DispatchId == 10 && link.WorkOrderId == 1));
|
||||||
|
var secondaryDispatchIds = await DispatchIdsForWorkOrderAsync(context, 2);
|
||||||
|
var primaryDispatchIds = await DispatchIdsForWorkOrderAsync(context, 1);
|
||||||
|
Assert.Equal(new int[] { result.PrimaryDispatchId!.Value }, secondaryDispatchIds);
|
||||||
|
Assert.Equal(new int[] { 10 }, primaryDispatchIds);
|
||||||
|
Assert.False(await UpliftAssociatedAsync(context, 2, 10));
|
||||||
|
Assert.True(await UpliftAssociatedAsync(context, 1, 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PatchField_VendorId_SharedDispatch_OwnerChange_RehomesDirectOwnership()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
context.Vendors.AddRange(
|
||||||
|
new Vendor { Id = 1, CompanyName = "Old Co" },
|
||||||
|
new Vendor { Id = 5, CompanyName = "New Co" });
|
||||||
|
var shared = new Dispatch
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
WorkOrderId = 1,
|
||||||
|
VendorId = 1,
|
||||||
|
Status = "Sent",
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 }
|
||||||
|
};
|
||||||
|
var primaryWo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
PrimaryDispatchId = 10,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
};
|
||||||
|
var secondaryWo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
PrimaryDispatchId = 10,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 5 }
|
||||||
|
};
|
||||||
|
context.Dispatches.Add(shared);
|
||||||
|
context.workOrders.AddRange(primaryWo, secondaryWo);
|
||||||
|
context.DispatchWorkOrders.AddRange(
|
||||||
|
new DispatchWorkOrder { DispatchId = 10, WorkOrderId = 1 },
|
||||||
|
new DispatchWorkOrder { DispatchId = 10, WorkOrderId = 2 });
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await service.PatchFieldAsync(1, new WorkOrderBoardPatchRequestDto
|
||||||
|
{
|
||||||
|
Field = WorkOrderBoardFieldNames.VendorId,
|
||||||
|
Value = "5",
|
||||||
|
WorkOrderVersion = ToVersion(primaryWo),
|
||||||
|
DispatchVersion = ToVersion(shared),
|
||||||
|
PrimaryDispatchId = 10
|
||||||
|
}, "actor-1");
|
||||||
|
|
||||||
|
Assert.Equal(5, result.VendorId);
|
||||||
|
Assert.NotEqual(10, result.PrimaryDispatchId);
|
||||||
|
var original = await context.Dispatches.SingleAsync(d => d.Id == 10);
|
||||||
|
Assert.Equal(1, original.VendorId);
|
||||||
|
Assert.Equal(2, original.WorkOrderId);
|
||||||
|
Assert.False(await context.DispatchWorkOrders.AnyAsync(link =>
|
||||||
|
link.DispatchId == 10 && link.WorkOrderId == 1));
|
||||||
|
Assert.True(await context.DispatchWorkOrders.AnyAsync(link =>
|
||||||
|
link.DispatchId == 10 && link.WorkOrderId == 2));
|
||||||
|
var secondaryUnchanged = await context.workOrders.SingleAsync(w => w.Id == 2);
|
||||||
|
Assert.Equal(10, secondaryUnchanged.PrimaryDispatchId);
|
||||||
|
var ownerDispatchIds = await DispatchIdsForWorkOrderAsync(context, 1);
|
||||||
|
var remainingDispatchIds = await DispatchIdsForWorkOrderAsync(context, 2);
|
||||||
|
Assert.Equal(new int[] { result.PrimaryDispatchId!.Value }, ownerDispatchIds);
|
||||||
|
Assert.Equal(new int[] { 10 }, remainingDispatchIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PatchField_VendorId_SharedDispatch_SameVendor_IsIdempotent()
|
||||||
|
{
|
||||||
|
var (context, service) = CreateSut();
|
||||||
|
context.Vendors.Add(new Vendor { Id = 1, CompanyName = "Old Co" });
|
||||||
|
var appointment = new DateTime(2026, 6, 26);
|
||||||
|
var shared = new Dispatch
|
||||||
|
{
|
||||||
|
Id = 10,
|
||||||
|
WorkOrderId = 1,
|
||||||
|
VendorId = 1,
|
||||||
|
Status = "Sent",
|
||||||
|
ScheduledDate = appointment,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 2 }
|
||||||
|
};
|
||||||
|
var primaryWo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
PrimaryDispatchId = 10,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 1 }
|
||||||
|
};
|
||||||
|
var secondaryWo = new WorkOrder
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
LifecycleStatus = LifecycleStatus.Scheduled,
|
||||||
|
PrimaryDispatchId = 10,
|
||||||
|
RowVersion = new byte[] { 1, 0, 0, 0, 0, 0, 0, 5 }
|
||||||
|
};
|
||||||
|
context.Dispatches.Add(shared);
|
||||||
|
context.workOrders.AddRange(primaryWo, secondaryWo);
|
||||||
|
context.DispatchWorkOrders.AddRange(
|
||||||
|
new DispatchWorkOrder { DispatchId = 10, WorkOrderId = 1 },
|
||||||
|
new DispatchWorkOrder { DispatchId = 10, WorkOrderId = 2 });
|
||||||
|
await context.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await service.PatchFieldAsync(2, new WorkOrderBoardPatchRequestDto
|
||||||
|
{
|
||||||
|
Field = WorkOrderBoardFieldNames.VendorId,
|
||||||
|
Value = "1",
|
||||||
|
WorkOrderVersion = ToVersion(secondaryWo),
|
||||||
|
DispatchVersion = ToVersion(shared),
|
||||||
|
PrimaryDispatchId = 10
|
||||||
|
}, "actor-1");
|
||||||
|
|
||||||
|
Assert.Equal(1, result.VendorId);
|
||||||
|
Assert.Equal(10, result.PrimaryDispatchId);
|
||||||
|
Assert.Equal("Sent", result.PrimaryDispatchStatus);
|
||||||
|
Assert.Equal(1, await context.Dispatches.CountAsync());
|
||||||
|
var unchanged = await context.Dispatches.SingleAsync(d => d.Id == 10);
|
||||||
|
Assert.Equal(appointment, unchanged.ScheduledDate);
|
||||||
|
Assert.Equal("Sent", unchanged.Status);
|
||||||
|
Assert.Equal(1, unchanged.VendorId);
|
||||||
|
Assert.Equal(2, await context.DispatchWorkOrders.CountAsync());
|
||||||
|
Assert.Empty(await context.WorkOrderAuditLogs
|
||||||
|
.Where(a => a.FieldName == "VendorId")
|
||||||
|
.ToListAsync());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue