using FLSBLL.Dispatch; using FLSBLL.Event; using FLSDAL.CustomCode.Instance; using FLSDAL.CustomCode.Registration; using FLSDAL.DTO.Instance; using FLSDAL.DTO.Registration; using GB5Shared.DTO.Framework.Login; using GB5Shared.Telemetry; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Quartz; namespace FLSBLL.Handlers; // Plan ยง4.1: Quartz IJob โ€” NOT BackgroundService โ€” for distributed locking and job history [DisallowConcurrentExecution] public class FlsReminderJobHandler : IJob { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; public FlsReminderJobHandler(IServiceScopeFactory scopeFactory, ILogger logger) { _scopeFactory = scopeFactory; _logger = logger; } public async Task Execute(IJobExecutionContext context) { using var activity = GB5Trace.BeginSection("fls-reminder-job"); var ct = context.CancellationToken; var tenantId = context.MergedJobDataMap.GetInt("TenantId"); if (tenantId == 0) { _logger.LogWarning("FlsReminderJobHandler: TenantId not in JobDataMap โ€” skipping"); return; } _logger.LogInformation("FlsReminderJobHandler executing for tenant {TenantId}", tenantId); try { using var scope = _scopeFactory.CreateScope(); var instanceDal = scope.ServiceProvider.GetRequiredService(); var regDal = scope.ServiceProvider.GetRequiredService(); var dispatchBll = scope.ServiceProvider.GetRequiredService(); var eventBll = scope.ServiceProvider.GetRequiredService(); var login = new LoginDTO { ClientId = tenantId, UserId = -1 }; var now = DateTime.UtcNow; var openInstances = await instanceDal.GetOpenInstancesForReminderAsync(tenantId, login, ct) .ConfigureAwait(false); foreach (var inst in openInstances) { GB5Trace.Step("process-fls-reminder", new { inst.FlsInstanceId }); var rules = await regDal.GetReminderRulesAsync(inst.FlsRegistrationId, login, ct) .ConfigureAwait(false); var groups = await instanceDal.GetGroupsAsync(inst.FlsInstanceId, login, ct) .ConfigureAwait(false); foreach (var rule in rules.Where(r => r.IsActive == 1)) { if (!ShouldTrigger(rule, inst, now)) continue; foreach (var g in groups) { if (rule.ReminderAction == 1) { await eventBll.PublishAsync( inst.FlsInstanceId, flsRespondentId: 0, g.GroupId, stepNo: 0, FlsEventType.RespondentOverdue, templateDataId: null, sourceReference: null, login, ct).ConfigureAwait(false); } else { await dispatchBll.DispatchReminderAsync( inst.FlsInstanceId, g.GroupId, rule.EipTemplCode ?? string.Empty, login, ct).ConfigureAwait(false); } } } } } catch (Exception ex) { GB5Trace.MarkFailed("fls-reminder-job-failed", ex); _logger.LogError(ex, "FlsReminderJobHandler failed for tenant {TenantId}", tenantId); throw new JobExecutionException(ex, refireImmediately: false); } } private static bool ShouldTrigger(FlsReminderRuleRow rule, FlsInstanceRow inst, DateTime now) => rule.ReminderTrigger switch { // DaysBeforeDeadline: trigger on the exact day 0 => inst.ClosesOn.HasValue && Math.Round((inst.ClosesOn.Value.Date - now.Date).TotalDays) == rule.DayOffset, // DaysAfterOpen: trigger on the exact day 1 => inst.OpensFrom.HasValue && Math.Round((now.Date - inst.OpensFrom.Value.Date).TotalDays) == rule.DayOffset, _ => false }; }