mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 16:33:12 +00:00
251 lines
10 KiB
C#
251 lines
10 KiB
C#
using Data.SeaHavenIndustries;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using SeaHaven.DataServices.Interfaces;
|
|
using SeaHaven.Services.Configuration;
|
|
using SeaHaven.Services.Interfaces;
|
|
|
|
namespace SeaHaven.Services.Implementation
|
|
{
|
|
public class UpliftLifecycleService : IUpliftLifecycleService
|
|
{
|
|
private readonly IUpliftDataService _upliftData;
|
|
private readonly IDispatchDataService _dispatchData;
|
|
private readonly IUserDataService _userData;
|
|
private readonly IEmailSender _emailSender;
|
|
private readonly FrontendOptions _frontendOptions;
|
|
private readonly ApprovalsOptions _approvalsOptions;
|
|
private readonly TimeProvider _timeProvider;
|
|
private readonly ILogger<UpliftLifecycleService> _logger;
|
|
|
|
public UpliftLifecycleService(
|
|
IUpliftDataService upliftData,
|
|
IDispatchDataService dispatchData,
|
|
IUserDataService userData,
|
|
IEmailSender emailSender,
|
|
IOptions<FrontendOptions> frontendOptions,
|
|
IOptions<ApprovalsOptions> approvalsOptions,
|
|
TimeProvider timeProvider,
|
|
ILogger<UpliftLifecycleService> logger)
|
|
{
|
|
_upliftData = upliftData;
|
|
_dispatchData = dispatchData;
|
|
_userData = userData;
|
|
_emailSender = emailSender;
|
|
_frontendOptions = frontendOptions.Value;
|
|
_approvalsOptions = approvalsOptions.Value;
|
|
_timeProvider = timeProvider;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<int> ExpireDueAsync(CancellationToken cancellationToken)
|
|
{
|
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
|
var due = await _upliftData.GetDueForExpiryAsync(now, 50, cancellationToken);
|
|
var expired = 0;
|
|
foreach (var req in due)
|
|
{
|
|
if (!UpliftStatus.CanTransition(req.Status, UpliftStatus.Expired))
|
|
continue;
|
|
|
|
var dispatch = await _dispatchData.GetByIdAsync(req.DispatchId);
|
|
if (dispatch == null)
|
|
{
|
|
_logger.LogError("Uplift expiry skipped for orphan request {UpliftId}; dispatch {DispatchId} was not found.", req.Id, req.DispatchId);
|
|
continue;
|
|
}
|
|
|
|
var previous = UpliftStatus.ToCanonical(req.Status);
|
|
|
|
req.Status = UpliftStatus.Expired;
|
|
req.DecidedAt = now;
|
|
req.LastModificationTime = now;
|
|
|
|
await _dispatchData.StageAuditLogAsync(new WorkOrderAuditLog
|
|
{
|
|
WorkOrderId = dispatch?.WorkOrderId ?? 0,
|
|
FieldName = $"Dispatch {dispatch?.DispatchNumber} Uplift",
|
|
OldValue = previous,
|
|
NewValue = UpliftStatus.Expired,
|
|
Action = "uplift_expired",
|
|
CreatedAt = now
|
|
}, cancellationToken);
|
|
|
|
await _upliftData.SaveChangesAsync(cancellationToken);
|
|
expired++;
|
|
}
|
|
|
|
return expired;
|
|
}
|
|
|
|
public async Task<int> SendDueInitialNotificationsAsync(CancellationToken cancellationToken)
|
|
{
|
|
var due = await _upliftData.GetDueForInitialNotificationAsync(50, cancellationToken);
|
|
var sent = 0;
|
|
foreach (var req in due)
|
|
{
|
|
var dispatch = await _dispatchData.GetByIdAsync(req.DispatchId);
|
|
if (dispatch == null) continue;
|
|
|
|
var recipients = await ResolveInitialRecipientsAsync(dispatch, req, cancellationToken);
|
|
if (recipients.Count == 0)
|
|
{
|
|
await PersistNotificationOutcomeAsync(req, delivered: false, "No notification recipients are configured for this dispatch.", cancellationToken);
|
|
continue;
|
|
}
|
|
|
|
var (subject, body) = UpliftNotificationMessage.BuildInitial(dispatch, req, req.RequestedByVendorName ?? "Vendor", _frontendOptions.FrontendBaseUrl ?? string.Empty);
|
|
var delivered = await SendToAllAsync(recipients, subject, body);
|
|
await PersistNotificationOutcomeAsync(req, delivered, delivered ? null : "The uplift notification could not be delivered.", cancellationToken);
|
|
if (delivered) sent++;
|
|
}
|
|
|
|
return sent;
|
|
}
|
|
|
|
public async Task<int> EscalateDueAsync(CancellationToken cancellationToken)
|
|
{
|
|
var candidates = await _upliftData.GetDueForEscalationAsync(50, cancellationToken);
|
|
var escalationThreshold = _approvalsOptions.EffectiveEscalation;
|
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
|
var escalated = 0;
|
|
|
|
foreach (var req in candidates)
|
|
{
|
|
if (req.InitialNotificationSentAt == null || req.EscalatedAt != null)
|
|
continue;
|
|
if (now - req.InitialNotificationSentAt.Value < escalationThreshold)
|
|
continue;
|
|
|
|
var recipients = await ResolveEscalationRecipientsAsync(req, cancellationToken);
|
|
if (recipients.Count == 0)
|
|
continue;
|
|
|
|
var dispatch = await _dispatchData.GetByIdAsync(req.DispatchId);
|
|
if (dispatch == null) continue;
|
|
|
|
var (subject, body) = UpliftNotificationMessage.BuildEscalation(dispatch, req, req.RequestedByVendorName ?? "Vendor", _frontendOptions.FrontendBaseUrl ?? string.Empty);
|
|
var delivered = await SendToAllAsync(recipients, subject, body);
|
|
if (!delivered)
|
|
{
|
|
// Escalation send failure is logged only; it does not alter workflow status.
|
|
_logger.LogWarning("Uplift escalation delivery failed for request {UpliftId}", req.Id);
|
|
continue;
|
|
}
|
|
|
|
req.EscalatedAt = now;
|
|
req.LastModificationTime = now;
|
|
|
|
await _dispatchData.StageAuditLogAsync(new WorkOrderAuditLog
|
|
{
|
|
WorkOrderId = dispatch.WorkOrderId ?? 0,
|
|
FieldName = $"Dispatch {dispatch.DispatchNumber} Uplift",
|
|
OldValue = "Pending",
|
|
NewValue = "Escalated",
|
|
Action = "uplift_escalated",
|
|
CreatedAt = now
|
|
}, cancellationToken);
|
|
|
|
await _upliftData.SaveChangesAsync(cancellationToken);
|
|
escalated++;
|
|
}
|
|
|
|
return escalated;
|
|
}
|
|
|
|
private async Task<HashSet<string>> ResolveInitialRecipientsAsync(Dispatch dispatch, DispatchUpliftRequest req, CancellationToken cancellationToken)
|
|
{
|
|
var recipients = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
var dispatcherUserId = await _dispatchData.GetDispatchDispatcherUserIdAsync(dispatch.WorkOrderId ?? 0, cancellationToken);
|
|
if (!string.IsNullOrWhiteSpace(dispatcherUserId))
|
|
{
|
|
var dispatcherEmail = await _userData.GetEmailByIdAsync(dispatcherUserId, cancellationToken);
|
|
if (!string.IsNullOrWhiteSpace(dispatcherEmail))
|
|
recipients.Add(dispatcherEmail);
|
|
}
|
|
|
|
var tierRecipients = req.RequiredTier == 2
|
|
? _approvalsOptions.Tier2NotificationRecipients
|
|
: _approvalsOptions.Tier1NotificationRecipients;
|
|
if (tierRecipients != null)
|
|
{
|
|
foreach (var recipient in tierRecipients)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(recipient))
|
|
recipients.Add(recipient.Trim());
|
|
}
|
|
}
|
|
|
|
return recipients;
|
|
}
|
|
|
|
private Task<HashSet<string>> ResolveEscalationRecipientsAsync(DispatchUpliftRequest req, CancellationToken cancellationToken)
|
|
{
|
|
var recipients = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
if (_approvalsOptions.EscalationRecipients != null)
|
|
{
|
|
foreach (var recipient in _approvalsOptions.EscalationRecipients)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(recipient))
|
|
recipients.Add(recipient.Trim());
|
|
}
|
|
}
|
|
|
|
var tierRecipients = req.RequiredTier == 2
|
|
? _approvalsOptions.Tier2NotificationRecipients
|
|
: _approvalsOptions.Tier1NotificationRecipients;
|
|
if (tierRecipients != null)
|
|
{
|
|
foreach (var recipient in tierRecipients)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(recipient))
|
|
recipients.Add(recipient.Trim());
|
|
}
|
|
}
|
|
|
|
return Task.FromResult(recipients);
|
|
}
|
|
|
|
private async Task<bool> SendToAllAsync(HashSet<string> recipients, string subject, string body)
|
|
{
|
|
try
|
|
{
|
|
foreach (var recipient in recipients)
|
|
{
|
|
var delivered = await _emailSender.SendEmailAsync(recipient, subject, body);
|
|
if (!delivered)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Never leak addresses or exception details; the persisted signal is safe text.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async Task PersistNotificationOutcomeAsync(DispatchUpliftRequest req, bool delivered, string? errorMessage, CancellationToken cancellationToken)
|
|
{
|
|
var now = _timeProvider.GetUtcNow().UtcDateTime;
|
|
if (delivered)
|
|
{
|
|
req.InitialNotificationSentAt = now;
|
|
req.NotificationStatus = UpliftNotificationStatus.Sent;
|
|
req.NotificationError = null;
|
|
}
|
|
else
|
|
{
|
|
req.NotificationStatus = UpliftNotificationStatus.Error;
|
|
req.NotificationError = errorMessage;
|
|
}
|
|
req.LastModificationTime = now;
|
|
await _upliftData.SaveChangesAsync(cancellationToken);
|
|
}
|
|
}
|
|
}
|