shoc-backend/SeaHaven.Services/Implementation/UpliftService.cs
2026-08-10 13:22:16 -03:00

178 lines
7.7 KiB
C#

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> approvalsOptions)
{
_upliftData = upliftData;
_dispatchData = dispatchData;
_approvalsOptions = approvalsOptions.Value;
}
public async Task<UpliftListResultDTO> 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<IEnumerable<UpliftForDispatchDTO>> 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<UpliftApproveResultDTO> 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" || dispatch.Status == "Refused")
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<UpliftDenyResultDTO> 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<string>()
};
}
}