mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 03:43:11 +00:00
The legacy ingest batch holds the per-work-order lock, taken in id order, for every work order it may cancel until the batch commits. A vendor uplift request now runs under the same lock. Uplift creation on both routes refuses a work order cancelled by either lifecycle or status text, so a request can neither slip past a cancel nor land after one.
97 lines
4.3 KiB
C#
97 lines
4.3 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Data.SeaHavenIndustries.Enums;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
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,
|
|
/// 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>
|
|
public static class PendingUpliftCancellation
|
|
{
|
|
public const string AuditAction = "uplift_cancel";
|
|
public const string CancelledStatus = "Withdrawn";
|
|
|
|
private static readonly string[] PendingStatuses = { "Pending", "ChangesRequested" };
|
|
|
|
/// <summary>True when a lifecycle change moves a work order into Canceled.</summary>
|
|
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);
|
|
|
|
/// <summary>
|
|
/// True when a work order is cancelled by either signal: its lifecycle status, or the
|
|
/// status text a sync cancel writes. No new uplift may be requested on it.
|
|
/// </summary>
|
|
public static bool IsCancelled(LifecycleStatus? lifecycle, string? statusText)
|
|
=> lifecycle == LifecycleStatus.Canceled || IsCancelledText(statusText);
|
|
|
|
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.
|
|
/// </summary>
|
|
internal static IQueryable<DispatchUpliftRequest> ForWorkOrder(ApplicationDbContext context, int workOrderId)
|
|
=> context.DispatchUpliftRequests.Where(u =>
|
|
(u.IsDeleted == null || u.IsDeleted == false)
|
|
&& u.Dispatch != null
|
|
&& (u.Dispatch.IsDeleted == null || u.Dispatch.IsDeleted == false)
|
|
&& (
|
|
u.Dispatch.WorkOrderId == workOrderId
|
|
|| u.Dispatch.DispatchWorkOrders!.Any(link => link.WorkOrderId == workOrderId)));
|
|
|
|
/// <summary>
|
|
/// Stages (does not save) the cancellation of every pending uplift on the work order,
|
|
/// with one audit row per uplift. Returns how many uplifts were cancelled.
|
|
/// </summary>
|
|
internal static async Task<int> StageAsync(
|
|
ApplicationDbContext context,
|
|
int workOrderId,
|
|
string? actorId,
|
|
string actorType,
|
|
DateTime now,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var pending = await ForWorkOrder(context, workOrderId)
|
|
.Include(u => u.Dispatch)
|
|
.Where(u => PendingStatuses.Contains(u.Status))
|
|
.ToListAsync(cancellationToken);
|
|
|
|
foreach (var request in pending)
|
|
{
|
|
var previous = request.Status;
|
|
request.Status = CancelledStatus;
|
|
request.DecidedAt = now;
|
|
request.DecidedByUserId = actorId;
|
|
request.LastModificationTime = now;
|
|
|
|
context.WorkOrderAuditLogs.Add(new WorkOrderAuditLog
|
|
{
|
|
WorkOrderId = workOrderId,
|
|
UserId = actorId,
|
|
FieldName = $"Dispatch {request.Dispatch!.DispatchNumber} Uplift",
|
|
OldValue = previous,
|
|
NewValue = CancelledStatus,
|
|
Action = AuditAction,
|
|
ActorType = actorType,
|
|
CreatedAt = now,
|
|
});
|
|
}
|
|
|
|
return pending.Count;
|
|
}
|
|
}
|
|
}
|