Files
HoruxDespachosNuevo/docs/SAT-SYNC-IMPLEMENTATION.md
Horux Dev dfc0183c12 feat(sat): metadata histórica solo domingos, 404 no fatal en daily, polling 9x5min y docs
- processDailySync ejecuta metadata histórica únicamente los domingos en CDMX.
- Errores 404 en daily se registran en error_message sin abortar el job.
- Timeouts/transitorios reales siguen lanzándose para reintento.
- Polling reducido a 9 intentos máximos cada 5 minutos por solicitud.
- Actualiza docs/SAT-SYNC-IMPLEMENTATION.md con arquitectura, crons, manejo de errores y comandos.
2026-08-02 19:22:49 +00:00

9.6 KiB
Raw Blame History

Sincronización SAT — Implementación y Operación

Documentación viva del sistema de sincronización de CFDIs con el SAT para Horux Despachos / Horux 360.

1. Resumen

El sistema descarga periódicamente XMLs y metadata de CFDIs emitidos y recibidos desde el servicio web de descarga masiva del SAT (@nodecfdi/sat-ws-descarga-masiva), usando la FIEL de cada contribuyente o del tenant (modo legacy Horux 360).

Los datos se almacenan en la base de datos del tenant correspondiente.

2. Arquitectura

Backend

Archivo Responsabilidad
apps/api/src/services/sat/sat.service.ts Lógica principal de sincronización, políticas de reintento, polling
apps/api/src/services/sat/sat-client.service.ts Cliente del SAT, query, verify, download
apps/api/src/services/sat/sat-parser.service.ts Parseo de XMLs y metadata
apps/api/src/services/sat/sat-crypto.service.ts Encriptación AES-256-GCM de credenciales FIEL
apps/api/src/services/fiel.service.ts FIEL a nivel tenant (legacy)
apps/api/src/services/contribuyente-fiel.service.ts FIEL por contribuyente (modelo despacho)
apps/api/src/controllers/sat.controller.ts Endpoints HTTP
apps/api/src/jobs/sat-sync.job.ts Crons de sincronización
apps/api/src/jobs/sat-sync-monitor.job.ts Watchdog de jobs atorados/fallidos

Frontend

Archivo Responsabilidad
apps/web/components/sat/FielUploadModal.tsx Subir FIEL
apps/web/components/sat/SyncStatus.tsx Estado y selector de fechas
apps/web/components/sat/SyncHistory.tsx Historial de sincronizaciones
apps/web/app/(dashboard)/configuracion/sat/page.tsx Página de configuración SAT

3. Modelo de datos

Tabla global public.sat_sync_jobs

CREATE TABLE sat_sync_jobs (
  id TEXT PRIMARY KEY,
  tenant_id TEXT NOT NULL REFERENCES tenants(id),
  contribuyente_id TEXT,            -- NULL = modo legacy Horux 360
  type "SatSyncType" NOT NULL,      -- 'initial' | 'daily' | 'incremental'
  status "SatSyncStatus" NOT NULL,  -- 'pending' | 'running' | 'completed' | 'failed'
  date_from DATE NOT NULL,
  date_to DATE NOT NULL,
  cfdi_type "CfdiSyncType",         -- 'emitidos' | 'recibidos' (no siempre usado)
  sat_request_id VARCHAR(50),       -- legacy, preferir sat_request_ids
  sat_package_ids TEXT[],
  cfdis_found INTEGER NOT NULL DEFAULT 0,
  cfdis_downloaded INTEGER NOT NULL DEFAULT 0,
  cfdis_inserted INTEGER NOT NULL DEFAULT 0,
  cfdis_updated INTEGER NOT NULL DEFAULT 0,
  progress_percent INTEGER NOT NULL DEFAULT 0,
  error_message TEXT,
  started_at TIMESTAMP(3),
  completed_at TIMESTAMP(3),
  created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  retry_count INTEGER NOT NULL DEFAULT 0,
  next_retry_at TIMESTAMP(3),
  is_custom_range BOOLEAN NOT NULL DEFAULT false,
  sat_request_ids JSONB NOT NULL DEFAULT '{}'
);

FIEL

  • Legacy: public.fiel_credentials (una por tenant).
  • Por contribuyente: fiel_contribuyente dentro de la base del tenant.

El sistema intenta primero la FIEL del contribuyente; si no existe, cae a la FIEL del tenant.

4. Tipos de sincronización

Tipo Descripción Cuándo corre
initial Primer sync de un contribuyente/tenant. Descarga XMLs + metadata en bloques. Manual o cuando no hay un initial completado
daily Sync diaria de los últimos 7 días de XMLs + metadata histórica solo los domingos Cron 610 AM CDMX y retries 9 AM / 4 PM
incremental Ventana de las últimas 8 horas Cron 11 AM, 3 PM, 7 PM CDMX (Enterprise)
Custom range daily con dateFrom/dateTo explícitos, llamado por el UI Manual

5. Cronograma de jobs

Definidos en apps/api/src/jobs/sat-sync.job.ts:

Job Expresión Horario CDMX Propósito
SAT Cron 0 6-10 * * * 6:0010:00 AM Daily sync, ~20% de tenants por hora
Recovery Cron 0 10 * * * 10:00 AM Recuperar jobs running atorados
Daily Retry 0 9 * * * y 0 16 * * * 9:00 AM y 4:00 PM Reintentar daily fallidos
Incremental Enterprise 0 11,15,19 * * * 11 AM, 3 PM, 7 PM Sync incremental
SAT Watchdog 0 */2 * * * Cada 2 horas Marcar jobs running sin heartbeat como failed
SAT Monitor 0 */2 * * * Cada 2 horas Alertar por email de jobs fallidos

6. Flujo de sincronización

processDailySync

  1. Fecha final = ayer a medio día UTC (getYesterdayEnd()).
  2. Ejecuta XMLs emitidos y recibidos de los últimos 7 días.
  3. Metadata histórica (desde inicio de año) solo si es domingo en CDMX.
  4. Errores 404 se registran pero no abortan el daily.
  5. Errores transitorios (timeout) se reintentan según política.

processInitialSync / custom range

  1. Divide el rango en bloques de 3 o 6 meses para XMLs (según volumen estimado).
  2. Descarga XMLs emitidos y recibidos por bloque.
  3. Descarga metadata del rango completo.
  4. Pausa de 5 segundos entre bloques.

processIncrementalSync

  1. Ventana de 8 horas: ahora - 10h a ahora - 2h.
  2. Descarga XMLs + metadata de emitidos y recibidos.

7. Polling y límites

Después de crear una solicitud (query) al SAT, el sistema verifica el estado periódicamente (verify).

Constantes en sat.service.ts:

const POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutos entre verificaciones
const MAX_POLL_ATTEMPTS = 9;            // 9 intentos máximo por solicitud
const DAILY_MAX_POLL_ATTEMPTS = 9;      // igual para daily
const YEARS_TO_SYNC = 6;

Esto da un máximo de ~45 minutos por solicitud (9 × 5 min).

Cada solicitud al SAT tiene su propio polling; los intentos no se comparten entre solicitudes.

8. Políticas de reintentos

const RETRY_POLICIES = {
  daily:       { maxRetries: 2, retryAtHours: [6, 12] },
  custom:      { maxRetries: 2, retryAtHours: [6, 12] },
  initial:     { maxRetries: 3, retryAtHours: [6, 12, 24] },
  incremental: { maxRetries: 0, retryAtHours: [] },
};

const MAX_DAILY_RETRY_ATTEMPTS = 5; // original + 2 automáticos + 2 crons fijos

Los reintentos automáticos se programan desde createdAt del job. Los reintentos por cron fijo (9 AM / 4 PM) se manejan en continuePendingDailyRequests.

9. Manejo de errores

Errores no fatales (se registran, no abortan en daily)

  • 404 Error no controlado en daily se guarda en errorMessage como completedWithWarnings.
  • En syncs custom/initial los 404 se capturan por bloque pero el job puede continuar.

Errores transitorios (reintentan)

  • Timeout del polling (SatSyncTimeoutError).
  • SatTransientError (ej. rechazo transitorio del SAT).
  • Metadata aún no lista (SatMetadataPendingError).

Errores fatales (job falla)

  • FIEL inválida o vencida.
  • Errores que no son transitorios y no están en la lista de no fatales.

10. Errores comunes del SAT

Código/Mensaje Significado Acción
5000 / "Solicitud Aceptada" OK, hay que esperar Polling normal
5002 / "Solicitudes agotadas de por vida" Cuota de solicitudes agotada Omitir rango, esperar 24h
5004 / "No se encontró la información" Sin CFDIs en el rango Continuar
5005 / Duplicada Solicitud duplicada Reusar requestId existente
404 / "Error no controlado" Bloqueo/cuota del SAT Registrar y continuar en daily; reintentar en otros syncs
"Fecha final invalida" Fecha futura o mal formada Usar getYesterdayEnd()
"El certificado no es válido" FIEL rechazada por el SAT Revisar vigencia/contraseña de FIEL

11. Monitoreo y comandos útiles

Estado del API

pm2 status horux-api
pm2 logs horux-api --lines 50 --nostream

Jobs recientes

export DATABASE_URL="postgresql://postgres:PASSWORD@localhost:5432/horux360"
psql "$DATABASE_URL" -c "
  SELECT id, type, status, contribuyente_id,
         cfdis_found, cfdis_downloaded, cfdis_inserted, cfdis_updated,
         error_message, created_at, completed_at
  FROM sat_sync_jobs
  ORDER BY created_at DESC
  LIMIT 20;
"

Jobs fallidos o atorados

psql "$DATABASE_URL" -c "
  SELECT id, type, status, tenant_id, contribuyente_id, error_message, created_at, started_at
  FROM sat_sync_jobs
  WHERE status IN ('failed', 'running')
  ORDER BY created_at DESC;
"

Contribuyentes de un tenant

# Reemplazar horux_<tenant_db> por la base del tenant
psql "$DATABASE_URL" -c "
  SELECT c.entidad_id, c.rfc, e.nombre
  FROM contribuyentes c
  JOIN entidades_gestionadas e ON c.entidad_id = e.id;
"

12. Changelog reciente

2026-07-31

  • Metadata histórica en daily solo se ejecuta los domingos.
  • Errores 404 en daily no abortan el proceso; se registran en error_message.
  • Polling reducido a 9 intentos máximos cada 5 minutos por solicitud.
  • Daily retry fijo a las 9:00 AM y 4:00 PM CDMX.
  • Incremental Enterprise a las 11:00 AM, 3:00 PM y 7:00 PM CDMX.

13. Problemas conocidos

  1. Bloqueo 404 Error no controlado del SAT: Aparece cuando se hacen muchas consultas desde la misma IP. Mitigación temporal: reducir frecuencia de polling y metadata solo domingos.
  2. FIEL inválida: Algunos tenants/contribuyentes tienen FIEL rechazada por el SAT. Requiere revisar/renovar FIEL.
  3. Jobs initial atorados en running: Pueden quedar si el proceso se reinicia; el recovery cron y el watchdog los limpian.

14. Próximos pasos

  • Implementar proxies rotativos para evitar bloqueo por IP del SAT.
  • Monitorear tasa de éxito tras reducir polling y metadata solo domingos.
  • Revisar/renovar FIELs inválidas reportadas por el monitor.