using Microsoft.EntityFrameworkCore; using Quartz; using OTSSignsOrchestrator.Server.Data; using OTSSignsOrchestrator.Server.Data.Entities; using OTSSignsOrchestrator.Server.Reports; using OTSSignsOrchestrator.Server.Services; namespace OTSSignsOrchestrator.Server.Jobs; /// /// Quartz job with two triggers: /// - Weekly (Monday 08:00 UTC): fleet health PDF → operator email list /// - Monthly (1st of month 08:00 UTC): billing CSV + fleet health PDF → operators; /// per-customer usage PDF → each active customer's admin email /// [DisallowConcurrentExecution] public sealed class ScheduledReportJob : IJob { /// Quartz job data key indicating whether this is a monthly trigger. public const string IsMonthlyKey = "IsMonthly"; private readonly IServiceProvider _services; private readonly ILogger _logger; public ScheduledReportJob( IServiceProvider services, ILogger logger) { _services = services; _logger = logger; } public async Task Execute(IJobExecutionContext context) { var isMonthly = context.MergedJobDataMap.GetBoolean(IsMonthlyKey); _logger.LogInformation("ScheduledReportJob fired — isMonthly={IsMonthly}", isMonthly); await using var scope = _services.CreateAsyncScope(); var billing = scope.ServiceProvider.GetRequiredService(); var pdfService = scope.ServiceProvider.GetRequiredService(); var emailService = scope.ServiceProvider.GetRequiredService(); var db = scope.ServiceProvider.GetRequiredService(); // Get operator email list (admin operators) var operatorEmails = await db.Operators .AsNoTracking() .Where(o => o.Role == OperatorRole.Admin) .Select(o => o.Email) .ToListAsync(context.CancellationToken); if (operatorEmails.Count == 0) { _logger.LogWarning("No admin operators found — skipping report email dispatch"); return; } // ── Weekly: fleet health PDF for past 7 days ─────────────────────── var to = DateOnly.FromDateTime(DateTime.UtcNow); var weekFrom = to.AddDays(-7); byte[] fleetPdf; try { fleetPdf = await pdfService.GenerateFleetHealthPdfAsync(weekFrom, to); } catch (Exception ex) { _logger.LogError(ex, "Failed to generate weekly fleet health PDF"); return; } foreach (var email in operatorEmails) { var success = await emailService.SendReportEmailAsync( email, $"fleet-health-{weekFrom:yyyy-MM-dd}-{to:yyyy-MM-dd}.pdf", fleetPdf, "application/pdf"); if (!success) _logger.LogError("Failed to send weekly fleet health PDF to {Email}", email); } // ── Monthly: billing CSV + per-customer usage PDFs ───────────────── if (!isMonthly) return; var monthStart = new DateOnly(to.Year, to.Month, 1).AddMonths(-1); var monthEnd = monthStart.AddMonths(1).AddDays(-1); // Billing CSV byte[]? billingCsv = null; try { billingCsv = await billing.GenerateBillingCsvAsync(monthStart, monthEnd); } catch (Exception ex) { _logger.LogError(ex, "Failed to generate monthly billing CSV"); } // Monthly fleet health PDF byte[]? monthlyFleetPdf = null; try { monthlyFleetPdf = await pdfService.GenerateFleetHealthPdfAsync(monthStart, monthEnd); } catch (Exception ex) { _logger.LogError(ex, "Failed to generate monthly fleet health PDF"); } // Send monthly reports to operators foreach (var email in operatorEmails) { if (billingCsv is not null) { var ok = await emailService.SendReportEmailAsync( email, $"billing-{monthStart:yyyy-MM-dd}-{monthEnd:yyyy-MM-dd}.csv", billingCsv, "text/csv"); if (!ok) _logger.LogError("Failed to send monthly billing CSV to {Email}", email); } if (monthlyFleetPdf is not null) { var ok = await emailService.SendReportEmailAsync( email, $"fleet-health-{monthStart:yyyy-MM-dd}-{monthEnd:yyyy-MM-dd}.pdf", monthlyFleetPdf, "application/pdf"); if (!ok) _logger.LogError("Failed to send monthly fleet health PDF to {Email}", email); } } // Per-customer usage PDFs → customer admin emails var activeCustomers = await db.Customers .AsNoTracking() .Where(c => c.Status == CustomerStatus.Active) .ToListAsync(context.CancellationToken); foreach (var customer in activeCustomers) { try { var usagePdf = await pdfService.GenerateCustomerUsagePdfAsync( customer.Id, monthStart, monthEnd); var ok = await emailService.SendReportEmailAsync( customer.AdminEmail, $"usage-{customer.Abbreviation}-{monthStart:yyyy-MM-dd}-{monthEnd:yyyy-MM-dd}.pdf", usagePdf, "application/pdf"); if (!ok) _logger.LogError("Failed to send usage PDF to {Email} for customer {Abbrev}", customer.AdminEmail, customer.Abbreviation); else _logger.LogInformation("Sent usage PDF to {Email} for customer {Abbrev}", customer.AdminEmail, customer.Abbreviation); } catch (Exception ex) { _logger.LogError(ex, "Failed to generate/send usage PDF for customer {Abbrev}", customer.Abbreviation); } } _logger.LogInformation("Monthly report dispatch complete — {CustomerCount} customer reports processed", activeCustomers.Count); } }