Files
HoruxDespachos/apps/api/src/jobs/notifications.job.ts
2026-04-27 22:09:36 -06:00

105 lines
3.5 KiB
TypeScript

/**
* 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<typeof cron.schedule> | 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;
}
}