- 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.
9.6 KiB
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_contribuyentedentro 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 6–10 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:00–10: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
- Fecha final = ayer a medio día UTC (
getYesterdayEnd()). - Ejecuta XMLs emitidos y recibidos de los últimos 7 días.
- Metadata histórica (desde inicio de año) solo si es domingo en CDMX.
- Errores 404 se registran pero no abortan el daily.
- Errores transitorios (timeout) se reintentan según política.
processInitialSync / custom range
- Divide el rango en bloques de 3 o 6 meses para XMLs (según volumen estimado).
- Descarga XMLs emitidos y recibidos por bloque.
- Descarga metadata del rango completo.
- Pausa de 5 segundos entre bloques.
processIncrementalSync
- Ventana de 8 horas:
ahora - 10haahora - 2h. - 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 controladoen daily se guarda enerrorMessagecomocompletedWithWarnings.- 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
- Bloqueo
404 Error no controladodel SAT: Aparece cuando se hacen muchas consultas desde la misma IP. Mitigación temporal: reducir frecuencia de polling y metadata solo domingos. - FIEL inválida: Algunos tenants/contribuyentes tienen FIEL rechazada por el SAT. Requiere revisar/renovar FIEL.
- Jobs
initialatorados enrunning: 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.