using Data.SeaHavenIndustries; using Microsoft.Extensions.Options; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.Configuration; using SeaHaven.Services.DTOs; using SeaHaven.Services.Interfaces; using System.Security.Claims; namespace SeaHaven.Services.Implementation { public class UpliftService : IUpliftService { private readonly IUpliftDataService _upliftData; private readonly IDispatchDataService _dispatchData; private readonly ApprovalsOptions _approvalsOptions; public UpliftService(IUpliftDataService upliftData, IDispatchDataService dispatchData, IOptions approvalsOptions) { _upliftData = upliftData; _dispatchData = dispatchData; _approvalsOptions = approvalsOptions.Value; } public async Task ListAsync(ClaimsPrincipal user, string? status, int? tier, int page, int pageSize, CancellationToken cancellationToken) { var (total, items) = await _upliftData.GetPagedAsync(status, tier, page, pageSize, cancellationToken); var mapped = items.Select(r => new UpliftListItemDTO { Id = r.Id, DispatchId = r.DispatchId, DispatchNumber = r.DispatchNumber, PONumber = r.PONumber, WorkOrderId = r.WorkOrderId, VendorCompanyName = r.VendorCompanyName, CurrentNTE = r.CurrentNTE, RequestedNTE = r.RequestedNTE, Delta = r.RequestedNTE - (r.CurrentNTE ?? 0m), VendorReason = r.VendorReason, RequiredTier = r.RequiredTier, Status = r.Status, RequestedAt = r.CreatedDate, DecidedAt = r.DecidedAt, DecisionNote = r.DecisionNote, CanDecide = UserCanApprove(user, r.RequiredTier) }).ToList(); return new UpliftListResultDTO { Total = total, Page = page, PageSize = pageSize, Items = mapped }; } public async Task> ListForDispatchAsync(ClaimsPrincipal user, int dispatchId, CancellationToken cancellationToken) { var rows = await _upliftData.GetForDispatchAsync(dispatchId, cancellationToken); return rows.Select(r => new UpliftForDispatchDTO { Id = r.Id, DispatchId = r.DispatchId, CurrentNTE = r.CurrentNTE, RequestedNTE = r.RequestedNTE, Delta = r.RequestedNTE - (r.CurrentNTE ?? 0m), VendorReason = r.VendorReason, Status = r.Status, RequiredTier = r.RequiredTier, RequestedByVendorName = r.RequestedByVendorName, RequestedAt = r.CreatedDate, DecidedAt = r.DecidedAt, DecisionNote = r.DecisionNote, DecidedByName = string.Join(" ", new[] { r.DecidedByFirstName, r.DecidedByLastName } .Where(s => !string.IsNullOrWhiteSpace(s))).Trim(), CanDecide = UserCanApprove(user, r.RequiredTier) }).ToList(); } public async Task ApproveAsync(ClaimsPrincipal user, int id, string? note, CancellationToken cancellationToken) { var req = await _upliftData.GetByIdAsync(id, cancellationToken); if (req == null) throw new KeyNotFoundException("Uplift request not found"); if (req.Status != "Pending") throw new InvalidOperationException($"Cannot approve a '{req.Status}' request"); if (!UserCanApprove(user, req.RequiredTier)) throw new UpliftForbiddenException($"Approval requires a Tier {req.RequiredTier} role"); var dispatch = await _dispatchData.GetByIdAsync(req.DispatchId); if (dispatch == null) throw new KeyNotFoundException("Dispatch not found"); if (dispatch.Status == "Verified" || dispatch.Status == "Cancelled") throw new InvalidOperationException($"Cannot approve uplift on a '{dispatch.Status}' dispatch"); var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value; var now = DateTime.UtcNow; var oldNTE = dispatch.NTEAmount ?? 0m; dispatch.NTEAmount = req.RequestedNTE; dispatch.LastModificationTime = now; req.Status = "Approved"; req.DecidedAt = now; req.DecidedByUserId = userId; req.DecisionNote = string.IsNullOrWhiteSpace(note) ? null : note!.Trim(); req.LastModificationTime = now; await _dispatchData.StageAuditLogAsync(new WorkOrderAuditLog { WorkOrderId = dispatch.WorkOrderId ?? 0, UserId = userId, FieldName = $"Dispatch {dispatch.DispatchNumber} NTE", OldValue = $"${oldNTE:F2}", NewValue = $"${req.RequestedNTE:F2}", Action = "uplift_approved", CreatedAt = now }, cancellationToken); await _upliftData.SaveChangesAsync(cancellationToken); return new UpliftApproveResultDTO { Id = req.Id, Status = req.Status, NTEAmount = dispatch.NTEAmount }; } public async Task DenyAsync(ClaimsPrincipal user, int id, string? note, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(note)) throw new InvalidOperationException("A note is required when denying"); var req = await _upliftData.GetByIdAsync(id, cancellationToken); if (req == null) throw new KeyNotFoundException("Uplift request not found"); if (req.Status != "Pending") throw new InvalidOperationException($"Cannot deny a '{req.Status}' request"); if (!UserCanApprove(user, req.RequiredTier)) throw new UpliftForbiddenException($"Decision requires a Tier {req.RequiredTier} role"); var dispatch = await _dispatchData.GetByIdAsync(req.DispatchId); var userId = user.FindFirst(ClaimTypes.NameIdentifier)?.Value; var now = DateTime.UtcNow; req.Status = "Denied"; req.DecidedAt = now; req.DecidedByUserId = userId; req.DecisionNote = note!.Trim(); req.LastModificationTime = now; await _dispatchData.StageAuditLogAsync(new WorkOrderAuditLog { WorkOrderId = dispatch?.WorkOrderId ?? 0, UserId = userId, FieldName = $"Dispatch {dispatch?.DispatchNumber} Uplift", OldValue = "Pending", NewValue = "Denied", Action = "uplift_denied", CreatedAt = now }, cancellationToken); await _upliftData.SaveChangesAsync(cancellationToken); return new UpliftDenyResultDTO { Id = req.Id, Status = req.Status }; } public bool CanApprove(ClaimsPrincipal user, int tier) => UserCanApprove(user, tier); private bool UserCanApprove(ClaimsPrincipal user, int requiredTier) { var roles = RolesForTier(requiredTier); foreach (var r in roles) { if (!string.IsNullOrWhiteSpace(r) && user.IsInRole(r)) return true; } return false; } private string[] RolesForTier(int requiredTier) => requiredTier switch { 1 => _approvalsOptions.Tier1Roles, 2 => _approvalsOptions.Tier2Roles, _ => Array.Empty() }; } }