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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user