using EntitlementBLL.Common; using EntitlementDAL.DTOs; using EntitlementDAL.Interfaces; using GB5Shared.QueryExecutor; using GB5Shared.Telemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Quartz; namespace EntitlementBLL.Handlers; // Daily 08:00: writes a 30-day warning audit entry, then a 7-day escalation entry, for every // subscription whose LICENSEVALIDTILL is approaching. Actual notification delivery (email/push) // is out of scope for this phase — the audit rows are the durable record the notification // pipeline (a later phase) will read from. [DisallowConcurrentExecution] public class LicenseExpiryWarningJob : IJob { private readonly IServiceScopeFactory _ScopeFactory; private readonly ILogger _Logger; public LicenseExpiryWarningJob(IServiceScopeFactory scopeFactory, ILogger logger) { _ScopeFactory = scopeFactory; _Logger = logger; } public async Task Execute(IJobExecutionContext context) { using var activity = GB5Trace.BeginSection("license-expiry-warning-job"); var ct = context.CancellationToken; using var scope = _ScopeFactory.CreateScope(); var subscriptionDal = scope.ServiceProvider.GetRequiredService(); var auditDal = scope.ServiceProvider.GetRequiredService(); var loginFactory = scope.ServiceProvider.GetRequiredService(); var login = loginFactory.Create(-1); var now = DateTime.UtcNow; // Active subscriptions expiring in the next 30 days (paged 0..int.MaxValue since this is // an infrequent batch job, not a user-facing list — acceptable one-shot per CLAUDE.md's // "paginate all list endpoints" rule, which targets interactive endpoints specifically). var page = await subscriptionDal.GetListPagedAsync( subscriptionStatus: 1, planId: -1, clientId: -1, offset: 0, pageSize: 5000, login, ct).ConfigureAwait(false); foreach (var sub in page.Items ?? Enumerable.Empty()) { var daysToExpiry = (sub.LicenseValidTill - now).TotalDays; string? action = daysToExpiry switch { <= 7 and > 0 => "EXPIRY_ESCALATION_7D", <= 30 and > 7 => "EXPIRY_WARNING_30D", _ => null }; if (action is null) continue; try { GB5Trace.Step("license-expiry-audit", new { sub.SubscriptionId, action }); await auditDal.InsertEntitlementAuditAsync(new EntitlementAuditDTO { ClientId = sub.ClientId, SubscriptionId = sub.SubscriptionId, Action = action, NewValue = $"LicenseValidTill={sub.LicenseValidTill:O}", CreatedById = -1, CreatedOn = now }, login, null, ct).ConfigureAwait(false); } catch (Exception ex) { GB5Trace.MarkFailed("license-expiry-warning-failed", ex); _Logger.LogError(ex, "LicenseExpiryWarningJob failed for SubscriptionId {SubscriptionId}", sub.SubscriptionId); } } } }