/** * Cron diario 8:30 AM (America/Mexico_City) que envía emails de: * - Alertas fiscales nuevas (Option B — una sola vez por alerta). * - Recordatorios próximos a vencer en ventanas 3d / 1d / 0d. * * Por-tenant try/catch: un fallo en un tenant no bloquea al resto. */ import cron from 'node-cron'; import { prisma, tenantDb } from '../config/database.js'; import { processNewAlertas, processProximosRecordatorios } from '../services/notifications.service.js'; const SCHEDULE = '30 8 * * *'; // 08:30 AM diario let task: ReturnType | null = null; /** Ejecuta ambos procesos para UN tenant. Exportado para disparo manual. */ export async function runNotificationsForTenant(tenantId: string): Promise<{ alertasNuevas: number; recordatoriosEnviados: number; }> { const tenant = await prisma.tenant.findUnique({ where: { id: tenantId }, select: { id: true, nombre: true, rfc: true, active: true, databaseName: true }, }); if (!tenant || !tenant.active) { return { alertasNuevas: 0, recordatoriosEnviados: 0 }; } const pool = await tenantDb.getPool(tenantId, tenant.databaseName); const ctx = { rfc: tenant.rfc, nombre: tenant.nombre }; const [alertasResult, recordResult] = await Promise.all([ processNewAlertas(pool, tenantId, ctx).catch(err => { console.error(`[Notifications] Alertas (${tenant.rfc}) fallo:`, err.message || err); return { contribuyentes: 0, nuevasTotal: 0 }; }), processProximosRecordatorios(pool, tenantId, ctx).catch(err => { console.error(`[Notifications] Recordatorios (${tenant.rfc}) fallo:`, err.message || err); return { enviados: 0 }; }), ]); if (alertasResult.nuevasTotal > 0 || recordResult.enviados > 0) { console.log(`[Notifications] ${tenant.rfc}: ${alertasResult.nuevasTotal} alertas nuevas, ${recordResult.enviados} recordatorios`); } return { alertasNuevas: alertasResult.nuevasTotal, recordatoriosEnviados: recordResult.enviados, }; } /** Itera todos los tenants activos. */ export async function runNotifications(): Promise<{ tenants: number; alertasNuevas: number; recordatoriosEnviados: number; }> { const tenants = await prisma.tenant.findMany({ where: { active: true }, select: { id: true, rfc: true }, }); let alertasNuevas = 0; let recordatoriosEnviados = 0; for (const t of tenants) { try { const r = await runNotificationsForTenant(t.id); alertasNuevas += r.alertasNuevas; recordatoriosEnviados += r.recordatoriosEnviados; } catch (err: any) { console.error(`[Notifications] Tenant ${t.rfc} fallo completo:`, err.message || err); } } return { tenants: tenants.length, alertasNuevas, recordatoriosEnviados }; } export function startNotificationsJob(): void { if (task) { console.warn('[Notifications Cron] Ya iniciado'); return; } task = cron.schedule(SCHEDULE, async () => { try { const result = await runNotifications(); console.log( `[Notifications Cron] ${result.tenants} tenants — ` + `${result.alertasNuevas} alertas nuevas, ${result.recordatoriosEnviados} recordatorios`, ); } catch (err: any) { console.error('[Notifications Cron] Error general:', err.message || err); } }, { timezone: 'America/Mexico_City', }); console.log(`[Notifications Cron] Programado: ${SCHEDULE} (08:30 AM diario America/Mexico_City)`); } export function stopNotificationsJob(): void { if (task) { task.stop(); task = null; } }