feat(sat-sync): excluir tenant DEMO2501019X2 de sincronizaciones SAT

- Agrega SAT_SYNC_EXCLUDED_TENANTS en ecosystem.config.js con DEMO2501019X2.
- Filtra tenants excluidos en getTenantsWithFiel() para crons automáticos.
- Rechaza startSync() manual para tenants excluidos.
- Incluye script check-daily-sync.js para monitorear cobertura diaria.
This commit is contained in:
Horux Dev
2026-08-26 16:45:33 +00:00
parent a5636427be
commit 97f3991cb0
4 changed files with 377 additions and 103 deletions

View File

@@ -11,18 +11,40 @@ import { consultarConstancia, purgeConstanciasAntiguas } from '../services/const
import { tenantDb } from '../config/database.js';
import type { Pool } from 'pg';
const SYNC_CRON_SCHEDULE = '0 6-10 * * *'; // 6:00–10:00 AM CDMX — ~20% de tenants por hora (5 grupos); el SAT cierra el servicio en la noche
const RECOVERY_CRON_SCHEDULE = '0 10 * * *'; // 10:00 AM todos los días
const RETRY_9AM_CRON_SCHEDULE = '0 9 * * *'; // 9:00 AM todos los días
const SYNC_CRON_SCHEDULE = '0 3-7 * * *'; // 3:00–7:00 AM CDMX — ~20% de tenants por hora (5 grupos); ventana de menor carga
const RECOVERY_CRON_SCHEDULE = '0 14 * * *'; // 2:00 PM todos los días
const RETRY_4PM_CRON_SCHEDULE = '0 16 * * *'; // 4:00 PM todos los días
const CONCURRENT_SYNCS = 3; // Máximo de sincronizaciones simultáneas (legacy, se mantiene por compatibilidad)
const CONCURRENT_CONTRIBUYENTES = Number(process.env.SAT_CONCURRENT_CONTRIBUYENTES || '10'); // Máximo de contribuyentes en paralelo
const CONCURRENT_CONTRIBUYENTES = Number(process.env.SAT_CONCURRENT_CONTRIBUYENTES || '5'); // Máximo de contribuyentes en paralelo
const OPINION_CRON_SCHEDULE = '0 4 * * 0'; // Sundays 4:00 AM
const CSF_CRON_SCHEDULE = '0 4 1 * *'; // Día 1 de cada mes 04:00 AM (CSF mensual)
const INCREMENTAL_CRON_SCHEDULE = '0 11,15,19 * * *'; // 11:00, 15:00 y 19:00; fuera de ese rango el daily (6-10 AM) cubre
const INCREMENTAL_CRON_SCHEDULE = '0 12,16 * * *'; // 12:00 PM y 4:00 PM CDMX
const BATCH_JITTER_MS = Number(process.env.SAT_BATCH_JITTER_MS || '60000'); // Jitter máximo entre inicios de sync en un batch (default 60s)
const SUBSCRIPTION_LIFECYCLE_CRON = '30 2 * * *'; // 2:30 AM diario — aplica pending changes + expira trials
const EXPIRY_REMINDERS_CRON = '0 9 * * *'; // 9:00 AM diario — avisos pre-vencimiento (7d/3d/1d/0d)
// Tenants excluidos de sincronizaciones SAT (cuentas demo/ficticias).
// Ejemplo: SAT_SYNC_EXCLUDED_TENANTS=DEMO2501019X2,OTRO_RFC
const EXCLUDED_TENANTS = new Set(
(process.env.SAT_SYNC_EXCLUDED_TENANTS || '')
.split(',')
.map(s => s.trim().toUpperCase())
.filter(Boolean)
);
function isTenantExcluded(tenantId: string, tenantRfc?: string): boolean {
return EXCLUDED_TENANTS.has(tenantId) || (tenantRfc ? EXCLUDED_TENANTS.has(tenantRfc.toUpperCase()) : false);
}
/**
* Pausa aleatoria para desalinear el inicio de syncs dentro de un batch.
* El jitter se aplica antes de cada syncUnit para evitar ráfagas.
*/
function jitterSleep(): Promise<void> {
const ms = Math.floor(Math.random() * BATCH_JITTER_MS);
return new Promise(resolve => setTimeout(resolve, ms));
}
let isRunning = false;
let isIncrementalRunning = false;
let isRecoveryRunning = false;
@@ -65,12 +87,16 @@ async function hasAnyFielConfigured(tenantId: string, databaseName?: string | nu
async function getTenantsWithFiel(): Promise<string[]> {
const tenants = await prisma.tenant.findMany({
where: { active: true },
select: { id: true, databaseName: true },
select: { id: true, rfc: true, databaseName: true },
});
const tenantsWithFiel: string[] = [];
for (const tenant of tenants) {
if (isTenantExcluded(tenant.id, tenant.rfc)) {
console.log(`[SAT Cron] Tenant ${tenant.rfc || tenant.id} excluido de sincronizaciones SAT`);
continue;
}
const hasFiel = await hasAnyFielConfigured(tenant.id, tenant.databaseName);
if (hasFiel) {
tenantsWithFiel.push(tenant.id);
@@ -288,8 +314,8 @@ async function syncTenant(tenantId: string): Promise<void> {
/**
* Ejecuta el job de sincronización para todos los tenants
*/
const DAILY_GROUPS = 5; // ventanas 6,7,8,9,10 AM
const DAILY_WINDOW_START = 6; // primera ventana CDMX
const DAILY_GROUPS = 5; // ventanas 3,4,5,6,7 AM
const DAILY_WINDOW_START = 3; // primera ventana CDMX
/** Hash estable del tenantId → grupo 0..DAILY_GROUPS-1 (reparte ~20% por ventana) */
function tenantGroup(tenantId: string): number {
@@ -328,7 +354,7 @@ async function runSyncJob(): Promise<void> {
}
const hour = cdmxHour();
const groupIndex = hour - DAILY_WINDOW_START; // 6→0 … 10→4
const groupIndex = hour - DAILY_WINDOW_START; // 3→0 … 7→4
if (groupIndex < 0 || groupIndex >= DAILY_GROUPS) {
console.log(`[SAT Cron] Hora CDMX ${hour} fuera de ventana 6-10 AM, omitiendo`);
return;
@@ -353,7 +379,10 @@ async function runSyncJob(): Promise<void> {
// Procesar en lotes de contribuyentes para aprovechar los proxies
for (let i = 0; i < units.length; i += CONCURRENT_CONTRIBUYENTES) {
const batch = units.slice(i, i + CONCURRENT_CONTRIBUYENTES);
await Promise.all(batch.map(unit => syncUnit(unit, '[SAT Cron]')));
await Promise.all(batch.map(async (unit) => {
await jitterSleep(); // desalinear inicios y reducir ráfagas
return syncUnit(unit, '[SAT Cron]');
}));
// Pequeña pausa entre lotes
if (i + CONCURRENT_CONTRIBUYENTES < units.length) {
@@ -499,7 +528,10 @@ async function runIncrementalSyncJob(): Promise<void> {
for (let i = 0; i < units.length; i += CONCURRENT_CONTRIBUYENTES) {
const batch = units.slice(i, i + CONCURRENT_CONTRIBUYENTES);
await Promise.all(batch.map(unit => syncUnit(unit, '[SAT Cron Inc]')));
await Promise.all(batch.map(async (unit) => {
await jitterSleep(); // desalinear inicios y reducir ráfagas
return syncUnit(unit, '[SAT Cron Inc]');
}));
if (i + CONCURRENT_CONTRIBUYENTES < units.length) {
await new Promise(resolve => setTimeout(resolve, 5000));
@@ -742,7 +774,6 @@ async function runDailyRetryJob(): Promise<void> {
let scheduledTask: ReturnType<typeof cron.schedule> | null = null;
let retryTask: ReturnType<typeof cron.schedule> | null = null;
let recoveryTask: ReturnType<typeof cron.schedule> | null = null;
let retry9amTask: ReturnType<typeof cron.schedule> | null = null;
let retry4pmTask: ReturnType<typeof cron.schedule> | null = null;
let opinionTask: ReturnType<typeof cron.schedule> | null = null;
let csfTask: ReturnType<typeof cron.schedule> | null = null;
@@ -798,17 +829,7 @@ export function startSatSyncJob(): void {
});
// Retomar jobs diarios que quedaron pending por timeout de polling.
// 9:00 AM y 4:00 PM CDMX, complemento a los retries automáticos de 6h/12h.
retry9amTask = cron.schedule(RETRY_9AM_CRON_SCHEDULE, async () => {
try {
await runDailyRetryJob();
} catch (error: any) {
console.error('[SAT Daily Retry 9AM] Error:', error.message);
}
}, {
timezone: 'America/Mexico_City',
});
// 4:00 PM CDMX, complemento a los retries automáticos de 6h/12h.
retry4pmTask = cron.schedule(RETRY_4PM_CRON_SCHEDULE, async () => {
try {
await runDailyRetryJob();
@@ -925,7 +946,7 @@ export function startSatSyncJob(): void {
console.log(`[SAT Cron] Job programado para: ${SYNC_CRON_SCHEDULE} (America/Mexico_City)`);
console.log(`[SAT Cron] Retry programado cada hora`);
console.log(`[SAT Recovery Cron] Programado para: ${RECOVERY_CRON_SCHEDULE} (America/Mexico_City)`);
console.log(`[SAT Daily Retry] Programado para: ${RETRY_9AM_CRON_SCHEDULE} y ${RETRY_4PM_CRON_SCHEDULE} (America/Mexico_City)`);
console.log(`[SAT Daily Retry] Programado para: ${RETRY_4PM_CRON_SCHEDULE} (America/Mexico_City)`);
console.log(`[Opinion Cron] Programado para: ${OPINION_CRON_SCHEDULE} (America/Mexico_City)`);
console.log(`[CSF Cron] Programado para: ${CSF_CRON_SCHEDULE} (America/Mexico_City)`);
console.log(`[SAT Cron Inc] Incremental Enterprise programado para: ${INCREMENTAL_CRON_SCHEDULE} (America/Mexico_City)`);
@@ -949,10 +970,6 @@ export function stopSatSyncJob(): void {
recoveryTask.stop();
recoveryTask = null;
}
if (retry9amTask) {
retry9amTask.stop();
retry9amTask = null;
}
if (retry4pmTask) {
retry4pmTask.stop();
retry4pmTask = null;