using Hangfire;
namespace foodmarket.Api.Background;
/// Регистрирует recurring jobs Hangfire при старте приложения.
/// Идемпотентно: RecurringJob.AddOrUpdate перетирает существующее
/// определение, так что миграции job'ов проходят без ручной очистки таблицы
/// hangfire.recurring_jobs.
///
/// Cron-выражения берутся из конфига (Hangfire:Cron:*) с дефолтом
/// «каждый день в 03:30 UTC» для cleanup-джобов — за 30 минут до бэкапа,
/// чтобы бэкап не цеплял временные блокировки PruneStockMovements.
public class HangfireJobsConfigurator : IHostedService
{
private readonly IRecurringJobManager _jobs;
private readonly IConfiguration _cfg;
public HangfireJobsConfigurator(IRecurringJobManager jobs, IConfiguration cfg)
{
_jobs = jobs;
_cfg = cfg;
}
public Task StartAsync(CancellationToken ct)
{
// Cron в Hangfire — стандартный 5-полевой (UTC по умолчанию).
var cronStock = _cfg["Hangfire:Cron:PruneStockMovements"] ?? "30 3 * * *";
var cronAudit = _cfg["Hangfire:Cron:PruneAuditLog"] ?? "45 3 * * *";
_jobs.AddOrUpdate(
recurringJobId: "prune-stock-movements",
methodCall: j => j.PruneStockMovementsAsync(CancellationToken.None),
cronExpression: cronStock,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
_jobs.AddOrUpdate(
recurringJobId: "prune-audit-log",
methodCall: j => j.PruneAuditLogAsync(CancellationToken.None),
cronExpression: cronAudit,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Sprint 20: per-tenant audit-log + drafts + revoked refresh-tokens.
// Default 03:00 UTC group (можно переопределить через конфиг).
var cronOrgAudit = _cfg["Hangfire:Cron:PruneOrgAuditLog"] ?? "0 3 * * *";
var cronDrafts = _cfg["Hangfire:Cron:PruneDrafts"] ?? "15 3 * * *";
var cronTokens = _cfg["Hangfire:Cron:PruneRevokedRefreshTokens"] ?? "20 3 * * *";
_jobs.AddOrUpdate(
recurringJobId: "prune-org-audit-log",
methodCall: j => j.PruneOrgAuditLogAsync(CancellationToken.None),
cronExpression: cronOrgAudit,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
_jobs.AddOrUpdate(
recurringJobId: "prune-drafts",
methodCall: j => j.PruneDraftsAsync(CancellationToken.None),
cronExpression: cronDrafts,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
_jobs.AddOrUpdate(
recurringJobId: "prune-revoked-refresh-tokens",
methodCall: j => j.PruneRevokedRefreshTokensAsync(CancellationToken.None),
cronExpression: cronTokens,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Sprint 25: чистка quality-{epoch}-* test-org, созданных
// ~/quality-watchdog.sh hourly. Default 02:30 UTC, до утреннего prune-блока.
var cronQualityOrgs = _cfg["Hangfire:Cron:PruneQualityTestOrgs"] ?? "30 2 * * *";
_jobs.AddOrUpdate(
recurringJobId: "prune-quality-test-orgs",
methodCall: j => j.PruneQualityTestOrgsAsync(CancellationToken.None),
cronExpression: cronQualityOrgs,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Sprint 30: широкий cleanup всех autotest-органа/юзеров (e2e, perf,
// smoke, a11y, ratetest, mt6*, cat3*, ...). Default 02:45 UTC
// (между prune-quality 02:30 и prune-stock 03:30).
var cronAutoTest = _cfg["Hangfire:Cron:PruneAutoTestEntities"] ?? "45 2 * * *";
_jobs.AddOrUpdate(
recurringJobId: "prune-autotest-entities",
methodCall: j => j.PruneAutoTestEntitiesAsync(CancellationToken.None),
cronExpression: cronAutoTest,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Sprint 20: weekly VACUUM ANALYZE топ-5 таблиц + ежечасный disk-monitor.
var cronVacuum = _cfg["Hangfire:Cron:VacuumTopTables"] ?? "0 4 * * 0"; // Воскресенье 04:00 UTC
var cronDisk = _cfg["Hangfire:Cron:DiskMonitor"] ?? "0 * * * *"; // каждый час
_jobs.AddOrUpdate(
recurringJobId: "vacuum-top-tables",
methodCall: j => j.VacuumTopTablesAsync(CancellationToken.None),
cronExpression: cronVacuum,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
_jobs.AddOrUpdate(
recurringJobId: "disk-monitor",
methodCall: j => j.CheckAsync(CancellationToken.None),
cronExpression: cronDisk,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Sprint 22: weekly DB schema documentation generation.
var cronSchema = _cfg["Hangfire:Cron:DbSchemaDocs"] ?? "0 5 * * 0"; // Воскресенье 05:00 UTC
_jobs.AddOrUpdate(
recurringJobId: "db-schema-docs",
methodCall: j => j.GenerateAsync(CancellationToken.None),
cronExpression: cronSchema,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Sprint 24: weekly API endpoint reference generation.
var cronApiRef = _cfg["Hangfire:Cron:ApiReferenceDocs"] ?? "30 5 * * 0"; // Воскресенье 05:30 UTC
_jobs.AddOrUpdate(
recurringJobId: "api-reference-docs",
methodCall: j => j.GenerateAsync(CancellationToken.None),
cronExpression: cronApiRef,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Email-уведомления: weekly-summary в понедельник 07:00 UTC,
// low-stock каждый день в 08:00 UTC (после weekly чтобы не дублить
// если оба совпадают). Cron-ы конфигурируются — на тестовом стенде
// имеет смысл сдвинуть/отключить через "off" специальное значение
// (не реализовано, пока просто кастомные cron).
var cronWeekly = _cfg["Hangfire:Cron:WeeklySummary"] ?? "0 7 * * 1";
var cronLowStock = _cfg["Hangfire:Cron:LowStockAlert"] ?? "0 8 * * *";
_jobs.AddOrUpdate(
recurringJobId: "weekly-summary",
methodCall: j => j.SendWeeklySummariesAsync(CancellationToken.None),
cronExpression: cronWeekly,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
_jobs.AddOrUpdate(
recurringJobId: "low-stock-alert",
methodCall: j => j.SendLowStockAlertsAsync(CancellationToken.None),
cronExpression: cronLowStock,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Telegram-сводка владельцу — 09:00 МСК = 06:00 UTC. По вкусу
// переопределяется через Hangfire:Cron:TelegramOwnerDailySummary.
var cronTgDaily = _cfg["Hangfire:Cron:TelegramOwnerDailySummary"] ?? "0 6 * * *";
_jobs.AddOrUpdate(
recurringJobId: "telegram-owner-daily-summary",
methodCall: j => j.RunAsync(CancellationToken.None),
cronExpression: cronTgDaily,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Уведомления об отрицательных остатках: агрегированная рассылка + эскалация.
// Бежит часто (каждые 5 минут), сам дебаунсит через NotifiedAt и настройку
// интервала/тихих часов организации.
var cronNegStock = _cfg["Hangfire:Cron:NegativeStockNotify"] ?? "*/5 * * * *";
_jobs.AddOrUpdate(
recurringJobId: "negative-stock-notify",
methodCall: j => j.RunAsync(CancellationToken.None),
cronExpression: cronNegStock,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Смены открытые >24ч — предупреждение кассиру/админам (мягкое правило).
var cronShiftWatch = _cfg["Hangfire:Cron:ShiftWatch"] ?? "7 * * * *"; // раз в час
_jobs.AddOrUpdate(
recurringJobId: "shift-watch-24h",
methodCall: j => j.RunAsync(CancellationToken.None),
cronExpression: cronShiftWatch,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// «Проверка данных» — ночной полный проход (низкий приоритет, не в часы продаж).
var cronDataQuality = _cfg["Hangfire:Cron:DataQuality"] ?? "20 2 * * *"; // 02:20 UTC = 07:20 МСК/Алматы
_jobs.AddOrUpdate(
recurringJobId: "data-quality-scan",
methodCall: j => j.RunAsync(CancellationToken.None),
cronExpression: cronDataQuality,
options: new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
// Разовый прогон при старте: чтобы после деплоя/пересева Демо (и любой новой
// организации) счётчики «Проверки данных» были посчитаны сразу, не дожидаясь
// ночного cron (иначе панель/бейдж показывают 0 и lastCheckedAt=null).
BackgroundJob.Enqueue(j => j.RunAsync(CancellationToken.None));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken ct) => Task.CompletedTask;
}