Files
OTSSignsOrchestrator/OTSSignsOrchestrator.Server/Jobs/ScheduledReportJob.cs

176 lines
6.4 KiB
C#
Raw Normal View History

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;
/// <summary>
/// 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
/// </summary>
[DisallowConcurrentExecution]
public sealed class ScheduledReportJob : IJob
{
/// <summary>Quartz job data key indicating whether this is a monthly trigger.</summary>
public const string IsMonthlyKey = "IsMonthly";
private readonly IServiceProvider _services;
private readonly ILogger<ScheduledReportJob> _logger;
public ScheduledReportJob(
IServiceProvider services,
ILogger<ScheduledReportJob> 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<BillingReportService>();
var pdfService = scope.ServiceProvider.GetRequiredService<FleetHealthPdfService>();
var emailService = scope.ServiceProvider.GetRequiredService<EmailService>();
var db = scope.ServiceProvider.GetRequiredService<OrchestratorDbContext>();
// 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);
}
}