using EntitlementBLL.Common; using EntitlementBLL.Interfaces; using EntitlementBLL.Payment; using EntitlementDAL.DTOs; using EntitlementDAL.Interfaces; using GB5Shared.QueryExecutor; using GB5Shared.Telemetry; using Microsoft.Extensions.Logging; namespace EntitlementBLL.Handlers; public class SchedulerTasksBLL : ISchedulerTasksBLL { private static readonly TimeSpan StaleKillSwitchThreshold = TimeSpan.FromHours(48); private readonly IFeatureFlagDAL _FeatureFlagDAL; private readonly IAuditDAL _AuditDAL; private readonly IQueryExecutor _QueryExecutor; private readonly ISubscriptionDAL _SubscriptionDAL; private readonly ISubscriptionService _SubscriptionService; private readonly IPaySubscriptionClient _PayClient; private readonly IEntitlementLoginFactory _LoginFactory; private readonly ILogger _Logger; public SchedulerTasksBLL( IFeatureFlagDAL featureFlagDAL, IAuditDAL auditDAL, IQueryExecutor queryExecutor, ISubscriptionDAL subscriptionDAL, ISubscriptionService subscriptionService, IPaySubscriptionClient payClient, IEntitlementLoginFactory loginFactory, ILogger logger) { _FeatureFlagDAL = featureFlagDAL; _AuditDAL = auditDAL; _QueryExecutor = queryExecutor; _SubscriptionDAL = subscriptionDAL; _SubscriptionService = subscriptionService; _PayClient = payClient; _LoginFactory = loginFactory; _Logger = logger; } public async Task RunKillSwitchMonitorAsync(CancellationToken ct) { using var activity = GB5Trace.BeginSection("kill-switch-monitor-job"); var login = _LoginFactory.Create(-1); var now = DateTime.UtcNow; var stale = await _FeatureFlagDAL.GetStaleKillSwitchesAsync(now - StaleKillSwitchThreshold, login, ct).ConfigureAwait(false); foreach (var flag in stale) { GB5Trace.Step("stale-kill-switch-alert", new { flag.FeatureFlagId, flag.FlagName }); _Logger.LogWarning( "Kill switch STALE ALERT: FeatureFlagId {FeatureFlagId} ({FlagName}) has been active since {ModifiedOn} — over 48h", flag.FeatureFlagId, flag.FlagName, flag.ModifiedOn); try { await _AuditDAL.InsertFeatureFlagAuditAsync(new FeatureFlagAuditDTO { FeatureFlagId = flag.FeatureFlagId, FeatureId = flag.FeatureId, Action = "KILL_SWITCH_STALE_ALERT", OldFlagStatus = flag.FlagStatus, NewFlagStatus = flag.FlagStatus, CreatedById = -1, CreatedOn = now }, login, null, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("kill-switch-monitor-failed", ex); _Logger.LogError(ex, "RunKillSwitchMonitorAsync failed logging alert for FeatureFlagId {FeatureFlagId}", flag.FeatureFlagId); } } } public async Task RunFeatureFlagSchedulerAsync(CancellationToken ct) { using var activity = GB5Trace.BeginSection("featureflag-scheduler-job"); var login = _LoginFactory.Create(-1); var now = DateTime.UtcNow; var due = await _FeatureFlagDAL.GetDueScheduledTransitionsAsync(now, login, ct).ConfigureAwait(false); foreach (var flag in due) { GB5Trace.Step("process-scheduled-flag-transition", new { flag.FeatureFlagId }); var tx = await _QueryExecutor.BeginTransactionAsync(login).ConfigureAwait(false); try { if (flag.ScheduledOffOn is not null && flag.ScheduledOffOn <= now && flag.FlagStatus != 1) { await _FeatureFlagDAL.ClearScheduledOnAsync(flag.FeatureFlagId, now, login, tx, ct).ConfigureAwait(false); await _AuditDAL.InsertFeatureFlagAuditAsync(new FeatureFlagAuditDTO { FeatureFlagId = flag.FeatureFlagId, FeatureId = flag.FeatureId, Action = "FLAG_ON_SCHEDULED", OldFlagStatus = flag.FlagStatus, NewFlagStatus = 1, CreatedById = -1, CreatedOn = now }, login, tx, ct).ConfigureAwait(false); } else if (flag.ScheduledOffOff is not null && flag.ScheduledOffOff <= now && flag.FlagStatus != 0) { await _FeatureFlagDAL.ClearScheduledOffAsync(flag.FeatureFlagId, now, login, tx, ct).ConfigureAwait(false); await _AuditDAL.InsertFeatureFlagAuditAsync(new FeatureFlagAuditDTO { FeatureFlagId = flag.FeatureFlagId, FeatureId = flag.FeatureId, Action = "FLAG_OFF_SCHEDULED", OldFlagStatus = flag.FlagStatus, NewFlagStatus = 0, CreatedById = -1, CreatedOn = now }, login, tx, ct).ConfigureAwait(false); } await _QueryExecutor.CommitAsync(tx).ConfigureAwait(false); } catch (Exception ex) { await _QueryExecutor.RollbackAsync(tx).ConfigureAwait(false); GB5Trace.MarkFailed("featureflag-scheduler-job-failed", ex); _Logger.LogError(ex, "RunFeatureFlagSchedulerAsync failed for FeatureFlagId {FeatureFlagId}", flag.FeatureFlagId); } } } public async Task RunPayOrderReconciliationAsync(CancellationToken ct) { using var activity = GB5Trace.BeginSection("payorder-reconciliation-job"); var login = _LoginFactory.Create(-1); var pending = await _SubscriptionDAL.GetPendingWithPayOrderAsync(login, ct).ConfigureAwait(false); foreach (var sub in pending) { ct.ThrowIfCancellationRequested(); if (sub.PayOrderId is not int payOrderId) continue; // defensive — query already filters NOT NULL try { GB5Trace.Step("reconcile-payorder", new { sub.SubscriptionId, payOrderId }); var order = await _PayClient.GetPayOrderAsync(payOrderId, login, ct).ConfigureAwait(false); if (order is null) continue; // nothing to reconcile against await _SubscriptionService.HandlePaymentOutcomeAsync(order.PayOrderId, order.OrderStatus, ct) .ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("payorder-reconciliation-failed", ex); _Logger.LogError(ex, "RunPayOrderReconciliationAsync failed for SubscriptionId {SubscriptionId} PayOrderId {PayOrderId}", sub.SubscriptionId, payOrderId); } } } }