- 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.
313 lines
13 KiB
Markdown
313 lines
13 KiB
Markdown
# 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_<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 `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.
|