using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.EntityFrameworkCore; namespace SeaHaven.DataServices.Helpers { /// /// 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 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. /// public static class PendingUpliftCancellation { public const string AuditAction = "uplift_cancel"; public const string CancelledStatus = "Withdrawn"; private static readonly string[] PendingStatuses = { "Pending", "ChangesRequested" }; /// True when a lifecycle change moves a work order into Canceled. public static bool Applies(LifecycleStatus? before, LifecycleStatus? after) => before != LifecycleStatus.Canceled && after == LifecycleStatus.Canceled; /// /// 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. /// public static bool AppliesToStatusText(string? before, string? after) => !IsCancelledText(before) && IsCancelledText(after); /// /// 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. /// 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); /// /// Uplift requests that belong to a work order: on a live dispatch that the work /// order owns or is linked to. /// internal static IQueryable 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))); /// /// 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. /// internal static async Task 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; } } }