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"); 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() ?? ""; if (request.Amount <= current) { var autoApproved = new DispatchUpliftRequest { DispatchId = dispatchId, CurrentNTE = current, RequestedNTE = request.Amount, VendorReason = notes, Status = UpliftStatus.NoApprovalRequired, RequiredTier = 0, RequestedByVendorName = requesterName, CreatedDate = now, createdby = userId, NotificationStatus = UpliftNotificationStatus.Sent, }; await _upliftData.StageAsync(autoApproved, cancellationToken); await StageAuditAsync(dispatch, workOrderId, userId, current, request.Amount, "uplift_auto_approved", now, cancellationToken); await _upliftData.SaveChangesAsync(cancellationToken); return WorkOrderUpliftContractMapper.MapItem(autoApproved, requesterName, null); } if (await _upliftData.HasActiveAsync(dispatchId, cancellationToken)) throw new InvalidOperationException("An active uplift request already exists for this dispatch"); var tier1Max = _approvalsOptions.UpliftTier1MaxUsd ?? 2500m; var delta = request.Amount - current; var requiredTier = delta > tier1Max ? 2 : 1; var expiresAt = now + _approvalsOptions.EffectiveExpiration; var pending = new DispatchUpliftRequest { DispatchId = dispatchId, CurrentNTE = current, RequestedNTE = request.Amount, VendorReason = notes, Status = UpliftStatus.Pending, RequiredTier = requiredTier, RequestedByVendorName = requesterName, CreatedDate = now, createdby = userId, ExpiresAt = expiresAt, NotificationStatus = UpliftNotificationStatus.Pending, }; await _upliftData.StageAsync(pending, cancellationToken); await StageAuditAsync(dispatch, workOrderId, userId, current, request.Amount, "uplift_requested", now, cancellationToken); await _upliftData.SaveChangesAsync(cancellationToken); return WorkOrderUpliftContractMapper.MapItem(pending, requesterName, null); } 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); } 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"; } }