using System.Security.Claims; using Data.SeaHavenIndustries; using Microsoft.Extensions.Options; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.Configuration; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Helpers; using SeaHaven.Services.Implementation; using SeaHaven.Services.Interfaces; namespace SeaHaven.Services.Implementation { public class WorkOrderUpliftService : IWorkOrderUpliftService { private readonly IUpliftDataService _upliftData; private readonly IDispatchDataService _dispatchData; private readonly IWorkOrderDetailDataService _detailData; private readonly IWorkOrderAccountResolver _accountResolver; private readonly IUserDataService _userData; private readonly TimeProvider _timeProvider; private readonly ApprovalsOptions _approvalsOptions; public WorkOrderUpliftService( IUpliftDataService upliftData, IDispatchDataService dispatchData, IWorkOrderDetailDataService detailData, IWorkOrderAccountResolver accountResolver, IUserDataService userData, TimeProvider timeProvider, IOptions approvalsOptions) { _upliftData = upliftData; _dispatchData = dispatchData; _detailData = detailData; _accountResolver = accountResolver; _userData = userData; _timeProvider = timeProvider; _approvalsOptions = approvalsOptions.Value; } public async Task?> ListAsync( int workOrderId, ClaimsPrincipal user, CancellationToken cancellationToken) { if (!await HasWorkOrderAccessAsync(workOrderId, user, cancellationToken)) return null; var rows = await _upliftData.GetForWorkOrderAsync(workOrderId, cancellationToken); return rows.Select(WorkOrderUpliftContractMapper.MapItem).ToList(); } public async Task CreateAsync( int workOrderId, CreateWorkOrderUpliftRequestDto request, ClaimsPrincipal user, CancellationToken cancellationToken) { if (!await HasWorkOrderAccessAsync(workOrderId, user, cancellationToken)) return null; if (request.Amount <= 0) throw new InvalidOperationException("Uplift amount must be greater than zero"); var workOrder = await _detailData.GetWorkOrderForMediaAsync( workOrderId, cancellationToken, _accountResolver.ResolveAccountFilter(user)); if (workOrder?.PrimaryDispatchId is not int dispatchId) throw new InvalidOperationException("Work order has no primary dispatch for uplift requests"); var dispatch = await _dispatchData.GetByIdAsync(dispatchId); if (dispatch == null) throw new KeyNotFoundException("Dispatch not found"); if (IsTerminalForUplift(dispatch.Status)) throw new InvalidOperationException($"Cannot request uplift on a '{dispatch.Status}' dispatch"); if (await _upliftData.HasPendingForWorkOrderAsync(workOrderId, cancellationToken)) throw new InvalidOperationException("An open uplift request already exists for this work order"); var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); var requesterName = await ResolveUserDisplayNameAsync(userId, cancellationToken); var now = _timeProvider.GetUtcNow().UtcDateTime; var current = dispatch.NTEAmount ?? 0m; var notes = request.Notes?.Trim() ?? ""; var consumed = await _upliftData.SumAutoApprovedAmountForWorkOrderAsync(workOrderId, cancellationToken); var remaining = WorkOrderUpliftAllowance.Remaining( WorkOrderUpliftAllowance.CapFor(workOrder.WorkOrderType), consumed); if (WorkOrderUpliftAllowance.AutoApproves(request.Amount, remaining)) { return await PersistCreatedAsync( dispatch, workOrderId, userId, requesterName, current, request.Amount, notes, UpliftStatus.NoApprovalRequired, requiredTier: 0, expiresAt: null, UpliftNotificationStatus.Sent, "uplift_auto_approved", now, cancellationToken); } return await PersistCreatedAsync( dispatch, workOrderId, userId, requesterName, current, request.Amount, notes, UpliftStatus.Pending, requiredTier: 1, now + _approvalsOptions.EffectiveExpiration, UpliftNotificationStatus.Pending, "uplift_requested", now, cancellationToken); } public async Task CancelAsync( int workOrderId, int upliftId, ClaimsPrincipal user, CancellationToken cancellationToken) { if (!await HasWorkOrderAccessAsync(workOrderId, user, cancellationToken)) return null; var req = await _upliftData.GetByIdAndWorkOrderAsync(upliftId, workOrderId, cancellationToken); if (req == null) throw new KeyNotFoundException("Uplift request not found"); if (!UpliftStatus.CanTransition(req.Status, UpliftStatus.Withdrawn)) throw new InvalidOperationException($"Cannot cancel a '{UpliftStatus.ToCanonical(req.Status)}' uplift request"); var dispatch = await _dispatchData.GetByIdAsync(req.DispatchId); if (dispatch == null) throw new KeyNotFoundException("Dispatch not found"); var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); var now = _timeProvider.GetUtcNow().UtcDateTime; var previous = UpliftStatus.ToCanonical(req.Status); req.Status = UpliftStatus.Withdrawn; req.DecidedAt = now; req.DecidedByUserId = userId; req.LastModificationTime = now; await StageAuditAsync( dispatch, workOrderId, userId, previous, UpliftStatus.Withdrawn, "uplift_cancel", now, cancellationToken, isStatusTransition: true); await _upliftData.SaveChangesAsync(cancellationToken); var requesterName = req.RequestedByVendorName ?? await ResolveUserDisplayNameAsync(req.createdby, cancellationToken); return WorkOrderUpliftContractMapper.MapItem(req, requesterName, await ResolveUserDisplayNameAsync(userId, cancellationToken)); } public async Task RevokeAsync( int workOrderId, int upliftId, RevokeWorkOrderUpliftRequestDto request, ClaimsPrincipal user, CancellationToken cancellationToken) { if (!await HasWorkOrderAccessAsync(workOrderId, user, cancellationToken)) return null; var req = await _upliftData.GetByIdAndWorkOrderAsync(upliftId, workOrderId, cancellationToken); if (req == null) throw new KeyNotFoundException("Uplift request not found"); var canonical = UpliftStatus.ToCanonical(req.Status); var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); if (canonical == UpliftStatus.NoApprovalRequired) { if (string.IsNullOrWhiteSpace(userId) || !string.Equals(req.createdby, userId, StringComparison.Ordinal)) { throw new UpliftForbiddenException("Only the request owner can revoke an auto-approved uplift"); } } else if (canonical == UpliftStatus.Approved) { if (!user.IsInRole("Admin")) throw new UpliftForbiddenException("Admin role is required to revoke an approved uplift"); if (string.IsNullOrWhiteSpace(request.Reason)) throw new InvalidOperationException("A reason is required when revoking an approved uplift"); } else { throw new InvalidOperationException($"Cannot revoke a '{canonical}' uplift request"); } var dispatch = await _dispatchData.GetByIdAsync(req.DispatchId); if (dispatch == null) throw new KeyNotFoundException("Dispatch not found"); var now = _timeProvider.GetUtcNow().UtcDateTime; var previous = canonical; if (canonical == UpliftStatus.Approved) { var oldNte = dispatch.NTEAmount ?? 0m; dispatch.NTEAmount = req.CurrentNTE ?? oldNte; dispatch.LastModificationTime = now; } req.Status = UpliftStatus.Revoked; req.DecidedAt = now; req.DecidedByUserId = userId; req.DecisionNote = string.IsNullOrWhiteSpace(request.Reason) ? null : request.Reason.Trim(); req.LastModificationTime = now; await StageAuditAsync( dispatch, workOrderId, userId, previous, UpliftStatus.Revoked, "uplift_revoke", now, cancellationToken, isStatusTransition: true); await _upliftData.SaveChangesAsync(cancellationToken); var decidedByName = await ResolveUserDisplayNameAsync(userId, cancellationToken); var requesterName = req.RequestedByVendorName ?? await ResolveUserDisplayNameAsync(req.createdby, cancellationToken); return WorkOrderUpliftContractMapper.MapRevokedItem(req, requesterName, decidedByName); } public async Task WithdrawPendingForWorkOrderAsync( int workOrderId, string? actorId, CancellationToken cancellationToken) { var pending = await _upliftData.GetPendingForWorkOrderAsync(workOrderId, cancellationToken); if (pending.Count == 0) return; var now = _timeProvider.GetUtcNow().UtcDateTime; foreach (var req in pending) { if (!UpliftStatus.CanTransition(req.Status, UpliftStatus.Withdrawn)) continue; var dispatch = req.Dispatch ?? await _dispatchData.GetByIdAsync(req.DispatchId); if (dispatch == null) continue; var previous = UpliftStatus.ToCanonical(req.Status); req.Status = UpliftStatus.Withdrawn; req.DecidedAt = now; req.DecidedByUserId = actorId; req.LastModificationTime = now; await StageAuditAsync( dispatch, workOrderId, actorId, previous, UpliftStatus.Withdrawn, "uplift_cancel", now, cancellationToken, isStatusTransition: true); } await _upliftData.SaveChangesAsync(cancellationToken); } private async Task PersistCreatedAsync( Dispatch dispatch, int workOrderId, string? userId, string requesterName, decimal currentNte, decimal amount, string notes, string status, int requiredTier, DateTime? expiresAt, string notificationStatus, string auditAction, DateTime now, CancellationToken cancellationToken) { var created = new DispatchUpliftRequest { DispatchId = dispatch.Id, CurrentNTE = currentNte, RequestedNTE = amount, VendorReason = notes, Status = status, RequiredTier = requiredTier, RequestedByVendorName = requesterName, CreatedDate = now, createdby = userId, ExpiresAt = expiresAt, NotificationStatus = notificationStatus, }; await _upliftData.StageAsync(created, cancellationToken); await StageAuditAsync(dispatch, workOrderId, userId, currentNte, amount, auditAction, now, cancellationToken); await _upliftData.SaveChangesAsync(cancellationToken); return WorkOrderUpliftContractMapper.MapItem(created, requesterName, null); } private async Task HasWorkOrderAccessAsync( int workOrderId, ClaimsPrincipal user, CancellationToken cancellationToken) { var accountId = _accountResolver.ResolveAccountFilter(user); return await _detailData.ExistsAsync(workOrderId, cancellationToken, accountId); } private async Task ResolveUserDisplayNameAsync(string? userId, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(userId)) return ""; var names = await _userData.GetDisplayNamesByIdsAsync(new[] { userId }); return names.TryGetValue(userId, out var name) ? name : ""; } private async Task StageAuditAsync( Dispatch dispatch, int workOrderId, string? userId, object oldValue, object newValue, string action, DateTime now, CancellationToken cancellationToken, bool isStatusTransition = false) { await _dispatchData.StageAuditLogAsync(new WorkOrderAuditLog { WorkOrderId = dispatch.WorkOrderId ?? workOrderId, UserId = userId, FieldName = $"Dispatch {dispatch.DispatchNumber} Uplift", OldValue = isStatusTransition ? oldValue.ToString() : $"${Convert.ToDecimal(oldValue):F2}", NewValue = isStatusTransition ? newValue.ToString() : $"${Convert.ToDecimal(newValue):F2}", Action = action, ActorType = "internal", CreatedAt = now, }, cancellationToken); } private static bool IsTerminalForUplift(string? status) => status is "Verified" or "Cancelled" or "Canceled" or "Refused"; } }