Files
HoruxDespachosNuevo/docs/plans/2026-04-13-sat-incremental-enterprise.md

7.2 KiB
Raw Blame History

Sincronización Incremental SAT para Enterprise

Resumen

Los clientes con plan Enterprise reciben sincronización con el SAT 3 veces al día (11:00, 15:00 y 19:00, zona horaria America/Mexico_City) adicional al daily de las 03:00 que aplica a todos los planes. Cada corrida incremental descarga XMLs y metadata de una ventana fija de las últimas 8 horas.

Motivación

El daily del cron nocturno cubre el ciclo fiscal completo pero deja al cliente Enterprise con una latencia de ~24h para ver CFDIs nuevos. El incremental de 3 corridas intradiarias reduce esa latencia a ~4h en horas de oficina, que es cuando se emite la mayoría del tráfico. Fuera de ese rango (19:00 → 03:00 del día siguiente) el daily se encarga.

Decisiones

Aspecto Decisión Razón
Cron 0 11,15,19 * * * 3 disparos en horas con tráfico fiscal real
Zona horaria America/Mexico_City Consistencia con el resto de crons del proyecto
Ventana por corrida 8 horas hacia atrás Cubre el gap máximo (03:00 → 11:00) sin dejar huecos
Elegibilidad tenant.active && tenant.plan === 'enterprise' && hasFielConfigured(tenantId) tenant.plan es la fuente de verdad usada por feature-gate y plan-limits
Requisito previo Tenant debe tener initial completado El incremental no debe actuar como primera descarga
Deduplicación UNIQUE(uuid) en tabla cfdis El solape entre ventanas consecutivas no genera duplicados
Nuevo valor de enum SatSyncType.incremental Diferenciable en historial, simplifica métricas/debugging

Cobertura horaria

03:00 ── daily (XMLs últimos 7 días + metadata año fiscal completo)
        │
 11:00 ── incremental Enterprise   ventana 03:0011:00
 15:00 ── incremental Enterprise   ventana 07:0015:00  (solape con anterior)
 19:00 ── incremental Enterprise   ventana 11:0019:00  (solape con anterior)
        │
19:00 → 03:00 del día siguiente: cubierto por el daily al arrancar

El solape entre disparos consecutivos es deliberado: la ventana de 8h es mayor que el gap entre corridas (4h). Esto cubre CFDIs que llegan a los servidores del SAT con retraso respecto a su fecha de emisión, sin duplicar datos en la BD.

Arquitectura

┌────────────────────────────┐
│ cron "0 11,15,19 * * *"    │
│ (node-cron, America/MX)    │
└──────────┬─────────────────┘
           ▼
┌────────────────────────────────────────┐
│ runIncrementalSyncJob()                │
│ - Guard: isIncrementalRunning          │
│ - getEnterpriseTenantsWithFiel()       │
│ - Batch de CONCURRENT_SYNCS (3) tenants│
└──────────┬─────────────────────────────┘
           ▼
┌────────────────────────────────────────┐
│ incrementalSyncTenant(tenantId)        │
│ - Omite si hay sync activo             │
│ - Omite si no hay initial completado   │
│ - startSync(tenantId, 'incremental')   │
└──────────┬─────────────────────────────┘
           ▼
┌────────────────────────────────────────┐
│ processIncrementalSync(ctx, jobId)     │
│ - ventana [now - 8h, now]              │
│ - processDateRange × (emitidos,        │
│                        recibidos)       │
│ - processMetadataRange × (emitidos,    │
│                           recibidos)    │
└────────────────────────────────────────┘

Archivos modificados

Archivo Cambio
apps/api/prisma/schema.prisma enum SatSyncType ahora incluye incremental
packages/shared/src/types/sat.ts SatSyncType = 'initial' | 'daily' | 'incremental'
apps/api/src/services/sat/sat.service.ts Nueva función processIncrementalSync. Branches en startSync y retryTimedOutJobs
apps/api/src/jobs/sat-sync.job.ts Constante INCREMENTAL_CRON_SCHEDULE, funciones getEnterpriseTenantsWithFiel, incrementalSyncTenant, runIncrementalSyncJob. Export runIncrementalSyncJobManually. Registro/desregistro en startSatSyncJob/stopSatSyncJob

Guardrails

  • Un incremental a la vez por proceso: flag isIncrementalRunning evita reentradas si una corrida toma más de lo esperado.
  • Un sync a la vez por tenant: incrementalSyncTenant revisa getSyncStatus(tenantId) y omite si ya hay un job corriendo (initial, daily, custom u otro incremental).
  • Primera sync debe ser initial: si el tenant no tiene un SatSyncJob con type: 'initial' y status: 'completed', el incremental se omite con un log y no crea job. La backfill debe correrse explícitamente (manual desde UI o el daily la detecta).
  • Concurrencia limitada: CONCURRENT_SYNCS = 3 — mismo límite que el daily para no saturar el SAT con solicitudes simultáneas desde la misma IP.
  • Reintentos: si el incremental falla por timeout del SAT, el cron horario de retries (retryTimedOutJobs) lo reintenta hasta 3 veces con 6h de espera, igual que los otros tipos.
  • Deduplicación: constraint UNIQUE(uuid) en tabla cfdis (BD tenant) maneja solapes entre ventanas sin lógica adicional.

Deploy

Migración requerida (BD central)

Agregar el valor incremental al enum Postgres:

cd apps/api
pnpm prisma migrate dev --name add_incremental_sat_sync_type   # dev
pnpm prisma migrate deploy                                      # prod

Sin esta migración, al insertar SatSyncJob con type: 'incremental' el driver fallará con error de enum value.

Rebuild de shared

El tipo SatSyncType cambió en packages/shared:

pnpm build

Restart API

pm2 restart horux-api

Al arrancar, el log confirma el registro del cron:

[SAT Cron Inc] Incremental Enterprise programado para: 0 11,15,19 * * * (America/Mexico_City)

Testing manual

// Desde un script o endpoint de admin
import { runIncrementalSyncJobManually } from './jobs/sat-sync.job';
await runIncrementalSyncJobManually();

O en BD, verificar que se creó un SatSyncJob con type = 'incremental' para el tenant Enterprise esperado.

Futuro / pendientes

  • Ventana dinámica: podría calcularse desde el último incremental completado del tenant en vez de fijarse en 8h. Reduciría solicitudes al SAT en solape pero agrega complejidad. No se hizo porque la dedup por UUID ya hace el solape gratuito en BD.
  • Frecuencia configurable por tenant: si algún Enterprise pide cadencia distinta, el schedule actual es global para todo el plan. Se podría mover a una columna syncFrequency en tenants.
  • Alerta en dashboard Admin Global: mostrar cuándo fue el último incremental exitoso por tenant Enterprise, para detectar silencios.