mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-10-02 01:33:23 +00:00
Cancel pending uplifts when the legacy ingest cancels a work order
The ingest writes only the status text, so the shared helper gains a status-text form of the same rule and the ingest stages the same sync-attributed cancellation in its batch save.
This commit is contained in:
parent
99499c3281
commit
306ab159cc
5 changed files with 92 additions and 3 deletions
|
|
@ -7,7 +7,7 @@ namespace SeaHaven.DataServices.Helpers
|
|||
/// <summary>
|
||||
/// The single rule and write for "a work order that becomes Canceled cancels its pending
|
||||
/// uplifts in the same action". Every path that can cancel a work order (board update,
|
||||
/// the cancel endpoint, CRM webhook and reconciliation) asks <see cref="Applies"/> and
|
||||
/// CRM webhook and reconciliation, legacy ingest) asks <see cref="Applies"/> and
|
||||
/// stages the cancellation into its own unit of work, so the uplift change and its own
|
||||
/// audit row commit, or roll back, with the work order's status change.
|
||||
/// </summary>
|
||||
|
|
@ -22,6 +22,17 @@ namespace SeaHaven.DataServices.Helpers
|
|||
public static bool Applies(LifecycleStatus? before, LifecycleStatus? after)
|
||||
=> before != LifecycleStatus.Canceled && after == LifecycleStatus.Canceled;
|
||||
|
||||
/// <summary>
|
||||
/// True when a status-text change moves a work order into Cancelled. The legacy ingest
|
||||
/// sync writes only the status text, never the lifecycle status.
|
||||
/// </summary>
|
||||
public static bool AppliesToStatusText(string? before, string? after)
|
||||
=> !IsCancelledText(before) && IsCancelledText(after);
|
||||
|
||||
private static bool IsCancelledText(string? status)
|
||||
=> string.Equals(status, "Cancelled", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(status, "Canceled", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Uplift requests that belong to a work order: on a live dispatch that the work
|
||||
/// order owns or is linked to.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using Data.SeaHavenIndustries.Enums;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SeaHaven.DataServices.Helpers;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
|
||||
namespace SeaHaven.DataServices.Implementation
|
||||
|
|
@ -31,6 +33,15 @@ namespace SeaHaven.DataServices.Implementation
|
|||
await _context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task<int> StageCancelPendingUpliftsAsync(int workOrderId, CancellationToken cancellationToken)
|
||||
=> PendingUpliftCancellation.StageAsync(
|
||||
_context,
|
||||
workOrderId,
|
||||
actorId: null,
|
||||
actorType: AuditActorType.Sync.ToString(),
|
||||
DateTime.UtcNow,
|
||||
cancellationToken);
|
||||
|
||||
public async Task ExecuteTransactionalAsync(Func<CancellationToken, Task> work, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var transaction = _context.Database.IsRelational()
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ namespace SeaHaven.DataServices.Interfaces
|
|||
void TrackWorkOrder(WorkOrder workOrder);
|
||||
Task<Locations?> FindLocationAsync(string name, CancellationToken cancellationToken);
|
||||
Task AddAndSaveLocationAsync(Locations location, CancellationToken cancellationToken);
|
||||
// Stages (does not save) the sync-attributed cancellation of the work order's pending
|
||||
// uplifts, each with its own audit row. See PendingUpliftCancellation.
|
||||
Task<int> StageCancelPendingUpliftsAsync(int workOrderId, CancellationToken cancellationToken);
|
||||
Task ExecuteTransactionalAsync(Func<CancellationToken, Task> work, CancellationToken cancellationToken);
|
||||
Task SaveAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Data.SeaHavenIndustries;
|
||||
using SeaHaven.DataServices.Helpers;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Helpers;
|
||||
|
|
@ -134,8 +135,15 @@ namespace SeaHaven.Services.Implementation
|
|||
|
||||
var mappedStatus = WorkOrderIngestFieldMapper.MapStatus(item.WoStatus);
|
||||
if (mappedStatus != null)
|
||||
{
|
||||
var statusBefore = existing.Status;
|
||||
await _mergePolicy.TryApplyAsync(syncContext, "Status", mappedStatus);
|
||||
|
||||
// A sync cancellation cancels the work order's pending uplifts in the same save.
|
||||
if (PendingUpliftCancellation.AppliesToStatusText(statusBefore, existing.Status))
|
||||
await _ingestData.StageCancelPendingUpliftsAsync(existing.Id, cancellationToken);
|
||||
}
|
||||
|
||||
var priority = WorkOrderIngestFieldMapper.MapSeverityToPriority(item.Severity);
|
||||
if (priority != null)
|
||||
await _mergePolicy.TryApplyAsync(syncContext, "Priority", priority);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Microsoft.EntityFrameworkCore;
|
|||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using SeaHaven.DataServices.Implementation;
|
||||
using SeaHaven.DataServices.Interfaces;
|
||||
using SeaHaven.Services.DTOs;
|
||||
using SeaHaven.Services.Implementation;
|
||||
using SeaHaven.Services.Interfaces;
|
||||
using Xunit;
|
||||
|
|
@ -14,8 +15,8 @@ namespace SeaHavenIndustries.Tests;
|
|||
|
||||
/// <summary>
|
||||
/// A work order cancelled by the CRM (webhook or reconciliation, both persisted by the
|
||||
/// webhook data service) cancels its pending uplift in the same save, with an audit entry
|
||||
/// of its own, exactly as a cancel from the board does.
|
||||
/// webhook data service, or the legacy ingest) cancels its pending uplift in the same save,
|
||||
/// with an audit entry of its own, exactly as a cancel from the board does.
|
||||
/// </summary>
|
||||
public sealed class WorkOrderCrmCancelUpliftTests
|
||||
{
|
||||
|
|
@ -140,6 +141,61 @@ public sealed class WorkOrderCrmCancelUpliftTests
|
|||
Assert.Equal(AuditActorType.Sync.ToString(), upliftAudit.ActorType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyIngestCancel_CancelsPendingUpliftWithItsOwnSyncAuditAndLeavesDecidedUpliftsAlone()
|
||||
{
|
||||
var options = await SeedInMemoryAsync(
|
||||
WorkOrderBoardPatchCancelUpliftTests.Uplift(100, "Pending", 700m),
|
||||
WorkOrderBoardPatchCancelUpliftTests.Uplift(101, "Approved", 600m));
|
||||
|
||||
await using (var context = new ApplicationDbContext(options))
|
||||
{
|
||||
await NewIngestService(context).UpsertBatchAsync(new[]
|
||||
{
|
||||
new WorkOrderIngestPayloadDto { ExternalWorkOrderId = "123", WoStatus = "cancelled" }
|
||||
});
|
||||
}
|
||||
|
||||
await using var verify = new ApplicationDbContext(options);
|
||||
Assert.Equal("Cancelled", verify.workOrders.Single().Status);
|
||||
Assert.Equal(UpliftStatus.Withdrawn, verify.DispatchUpliftRequests.Single(u => u.Id == 100).Status);
|
||||
Assert.Equal(UpliftStatus.Approved, verify.DispatchUpliftRequests.Single(u => u.Id == 101).Status);
|
||||
var upliftAudit = Assert.Single(verify.WorkOrderAuditLogs, log => log.Action == "uplift_cancel");
|
||||
Assert.Equal(AuditActorType.Sync.ToString(), upliftAudit.ActorType);
|
||||
Assert.Equal("Dispatch DIS-10 Uplift", upliftAudit.FieldName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task LegacyIngestUpdateThatDoesNotCancel_LeavesPendingUpliftPending()
|
||||
{
|
||||
var options = await SeedInMemoryAsync(
|
||||
WorkOrderBoardPatchCancelUpliftTests.Uplift(100, "Pending", 700m));
|
||||
|
||||
await using (var context = new ApplicationDbContext(options))
|
||||
{
|
||||
await NewIngestService(context).UpsertBatchAsync(new[]
|
||||
{
|
||||
new WorkOrderIngestPayloadDto { ExternalWorkOrderId = "123", WoStatus = "in_progress" }
|
||||
});
|
||||
}
|
||||
|
||||
await using var verify = new ApplicationDbContext(options);
|
||||
Assert.Equal(UpliftStatus.Pending, verify.DispatchUpliftRequests.Single().Status);
|
||||
Assert.DoesNotContain(verify.WorkOrderAuditLogs, log => log.Action == "uplift_cancel");
|
||||
}
|
||||
|
||||
private static WorkOrderIngestService NewIngestService(ApplicationDbContext context)
|
||||
{
|
||||
var fieldLocks = new WorkOrderFieldLockService(new WorkOrderFieldLockDataService(context));
|
||||
var audit = new WorkOrderAuditService(new WorkOrderAuditDataService(context), fieldLocks);
|
||||
return new WorkOrderIngestService(
|
||||
new WorkOrderIngestDataService(context),
|
||||
new SyncFieldMergePolicy(fieldLocks),
|
||||
fieldLocks,
|
||||
audit,
|
||||
WorkOrderAccountTestHelpers.Resolver(context));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CrmCancel_FailureBeforeCommit_RollsBackWorkOrderAndUpliftTogether()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue