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:
Arthur Bassi 2026-09-01 09:42:41 -03:00
parent ca0404272e
commit 03c069d10d
5 changed files with 212 additions and 1 deletions

View file

@ -38,6 +38,45 @@ namespace SeaHaven.DataServices.Implementation
.Include(d => d.DispatchWorkOrders)
.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)
=> _context.workOrders.Add(workOrder);

View file

@ -15,6 +15,7 @@ namespace SeaHaven.DataServices.Interfaces
Task<WorkOrder?> GetTrackedWorkOrderAsync(int workOrderId, CancellationToken cancellationToken);
Task<Dispatch?> GetTrackedDispatchAsync(int dispatchId, int workOrderId, CancellationToken cancellationToken);
Task<Dispatch?> GetTrackedDispatchByIdAsync(int dispatchId, CancellationToken cancellationToken);
void DetachWorkOrderFromDispatch(Dispatch dispatch, int workOrderId);
void TrackNewWorkOrder(WorkOrder workOrder);
void TrackNewDispatch(Dispatch dispatch);
void TrackWorkOrderContact(int workOrderId, int contactId, string? notes);

View file

@ -111,6 +111,9 @@ namespace SeaHaven.Services.Implementation
"DispatchVersionRequired",
"dispatchVersion is required for dispatch fields.");
}
if (resolved is { Created: true, Previous: { } previousDispatch })
_mutationData.DetachWorkOrderFromDispatch(previousDispatch, workOrder.Id);
}
var auditField = WorkOrderBoardFieldNames.ToAuditFieldName(canonicalField);
@ -215,10 +218,13 @@ namespace SeaHaven.Services.Implementation
primaryDispatchId,
relaxInvalidDispatch,
cancellationToken);
var vendorChanged = existing != null
&& vendorIdForCreate.HasValue
&& existing.VendorId != vendorIdForCreate.Value;
var mustReplace = existing != null
&& replaceInactivePrimary
&& (WorkOrderBoardDispatchAssignment.IsInactivePrimaryDispatch(existing.Status)
|| IsSharedDispatch(existing, workOrder.Id));
|| (IsSharedDispatch(existing, workOrder.Id) && vendorChanged));
if (existing != null && !mustReplace)
{
workOrder.PrimaryDispatchId = existing.Id;

View file

@ -392,6 +392,9 @@ public class WorkOrderBoardCancelServiceTests
public Task<Dispatch?> GetTrackedDispatchByIdAsync(int dispatchId, CancellationToken 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 TrackNewDispatch(Dispatch dispatch) => _inner.TrackNewDispatch(dispatch);

View file

@ -33,6 +33,27 @@ public class WorkOrderBoardUpdateServiceTests
private static string ToVersion(Dispatch dispatch)
=> 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]
public async Task PatchField_AutoSchedulesIncompleteToScheduled()
{
@ -613,6 +634,15 @@ public class WorkOrderBoardUpdateServiceTests
context.DispatchWorkOrders.AddRange(
new DispatchWorkOrder { DispatchId = 10, WorkOrderId = 1 },
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();
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);
Assert.Equal(1, original.VendorId);
Assert.Equal("Sent", original.Status);
Assert.Equal(1, original.WorkOrderId);
var created = await context.Dispatches.SingleAsync(d => d.Id == result.PrimaryDispatchId);
Assert.Equal(2, created.WorkOrderId);
Assert.Equal(5, created.VendorId);
var primaryUnchanged = await context.workOrders.SingleAsync(w => w.Id == 1);
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]