# 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` ```sql 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 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` 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. 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`: ```bash # 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`): parsea `SAT_PROXY_LIST`, rota proxies y crea `HttpsProxyAgent`. - **`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`: ```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 ```ts 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 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. ## 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 ```bash pm2 status horux-api pm2 logs horux-api --lines 50 --nostream ``` ### Jobs recientes ```bash 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 ```bash 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 ```bash # Reemplazar horux_ 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 `ProxyManager` y 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=10` RFCs 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_EMAIL` un resumen de errores SAT agrupados por proxy/IP. - Tabla `public.sat_proxy_errors` para 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 1. **Bloqueo `404 Error no controlado` del 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. 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. ## 15. Próximos pasos - [x] Implementar proxies rotativos para evitar bloqueo por IP del SAT. - [x] 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.