- Agrega tabla sat_proxy_errors con proxy_usado, error_code, stage, message. - sat-client.service.ts expone proxyInfo usado en cada conexión SAT. - sat.service.ts registra errores de bloqueo/devolución del SAT en sat_proxy_errors y guarda proxyUsed en sat_sync_jobs. - Nuevo job sat-proxy-report.job.ts: cron 8 AM CDMX, envía email a ADMIN_EMAIL con errores de las últimas 24h agrupados por proxy/error_code. - Template de email sat-proxy-report.ts y método sendSatProxyReport. - Registra el cron en src/index.ts. - Actualiza docs/SAT-SYNC-IMPLEMENTATION.md.
13 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/proxy.service.ts |
Pool rotativo de proxies SAT |
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, hasta SAT_CONCURRENT_CONTRIBUYENTES paralelos |
| 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 |
| SAT Proxy Report | 0 8 * * * |
8:00 AM | Reporte diario de errores SAT por proxy |
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. Proxies SAT (rotación por IP)
Para mitigar el bloqueo 404 Error no controlado causado por cuota de solicitudes desde una sola IP pública, el sistema soporta un pool de proxies HTTP/HTTPS rotativos.
Configuración
Variables en apps/api/.env:
# Lista de proxies separados por coma. Soporta autenticación básica.
SAT_PROXY_LIST=http://user:pass@host1:port,http://user:pass@host2:port
# Estrategia de rotación: round-robin | random (default: round-robin)
SAT_PROXY_STRATEGY=round-robin
# Si true, cuando todos los proxies fallan se intenta con la IP directa del servidor.
SAT_PROXY_FALLBACK_DIRECT=true
# Máximo de contribuyentes sincronizados en paralelo por el scheduler (default: 10).
SAT_CONCURRENT_CONTRIBUYENTES=10
Componentes
ProxyManager(apps/api/src/services/sat/proxy.service.ts): parseaSAT_PROXY_LIST, rota proxies y creaHttpsProxyAgent.sat-client.service.ts: usa el agente del proxy en cada petición SOAP al SAT.
Reporte diario de errores por proxy
Cada error de bloqueo/devolución del SAT se guarda en public.sat_proxy_errors con el proxy usado.
Un cron a las 8:00 AM CDMX envía un email a ADMIN_EMAIL con:
- Total de errores en las últimas 24 horas.
- Tabla agrupada por
proxy+error_code.
Archivos:
apps/api/src/jobs/sat-proxy-report.job.ts— cron y consulta.apps/api/src/services/email/templates/sat-proxy-report.ts— template del email.apps/api/src/services/email/email.service.ts—sendSatProxyReport.
Comportamiento
- Cada llamada a
getNextProxy()devuelve el siguiente proxy del pool (round-robin). - Cuando el scheduler lanza 10 contribuyentes en paralelo, cada uno tiende a usar una IP distinta.
- Si un proxy devuelve error de conexión, el cliente SAT puede caer a la IP directa según
SAT_PROXY_FALLBACK_DIRECT.
Recomendaciones operativas
- Tamaño mínimo del pool: 1 proxy por cada 5 RFCs que se sincronicen en paralelo.
- Con
SAT_CONCURRENT_CONTRIBUYENTES=10, un pool de 10 proxies da una IP por RFC en el peor caso. - Monitorear logs por
[ProxyManager] Usando proxy: ...y por 404 persistentes en una misma IP.
8. 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.
9. 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.
10. 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.
11. 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 |
12. 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;
"
13. Changelog reciente
2026-08-03
- Proxies SAT rotativos: integración de
ProxyManagery pool de proxies HTTP/HTTPS para evitar bloqueo por IP del SAT. - Concurrencia por contribuyente: el scheduler daily e incremental procesa hasta
SAT_CONCURRENT_CONTRIBUYENTES=10RFCs en paralelo, sin importar a cuántos tenants pertenezcan. - Reporte diario de errores por proxy: cron a las 8 AM CDMX que envía a
ADMIN_EMAILun resumen de errores SAT agrupados por proxy/IP. - Tabla
public.sat_proxy_errorspara trazabilidad de bloqueos por IP. - Variables de entorno:
SAT_PROXY_LIST,SAT_PROXY_STRATEGY,SAT_PROXY_FALLBACK_DIRECT,SAT_CONCURRENT_CONTRIBUYENTES.
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.
14. Problemas conocidos
- Bloqueo
404 Error no controladodel SAT: Aparece cuando se hacen muchas consultas desde la misma IP. Mitigación: proxies rotativos (SAT_PROXY_LIST) + concurrencia controlada por contribuyente + metadata histórica 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.
15. Próximos pasos
- Implementar proxies rotativos para evitar bloqueo por IP del SAT.
- Reporte diario de errores por proxy.
- Monitorear tasa de éxito tras proxies + concurrencia por contribuyente.
- Revisar/renovar FIELs inválidas reportadas por el monitor.
- Evaluar ampliar ventana horaria del daily (6–10 AM) si el volumen de RFCs supera el throughput con 10 paralelos.