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.
This commit is contained in:
Horux Dev
2026-08-02 19:22:49 +00:00
parent b5701b603c
commit dfc0183c12
2 changed files with 782 additions and 320 deletions

View File

@@ -1,298 +1,246 @@
# Implementación de Sincronización SAT
# Sincronización SAT — Implementación y Operación
## Resumen
Documentación viva del sistema de sincronización de CFDIs con el SAT para Horux Despachos / Horux 360.
Sistema de sincronización automática de CFDIs con el SAT (Servicio de Administración Tributaria de México) para Horux360.
## 1. Resumen
## Componentes Implementados
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).
### 1. Backend (API)
Los datos se almacenan en la base de datos del tenant correspondiente.
#### Servicios
## 2. Arquitectura
| Archivo | Descripción |
|---------|-------------|
| `src/services/fiel.service.ts` | Gestión de credenciales FIEL (e.firma) |
| `src/services/sat/sat-client.service.ts` | Cliente para el servicio web del SAT |
| `src/services/sat/sat.service.ts` | Lógica principal de sincronización |
| `src/services/sat/sat-crypto.service.ts` | Encriptación AES-256-GCM para credenciales |
| `src/services/sat/sat-parser.service.ts` | Parser de XMLs de CFDI |
### Backend
#### Controladores
| 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 |
| Archivo | Descripción |
|---------|-------------|
| `src/controllers/fiel.controller.ts` | Endpoints para gestión de FIEL |
| `src/controllers/sat.controller.ts` | Endpoints para sincronización SAT |
### Frontend
#### Job Programado
| 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 |
| Archivo | Descripción |
|---------|-------------|
| `src/jobs/sat-sync.job.ts` | Cron job para sincronización diaria (3:00 AM) |
## 3. Modelo de datos
### 2. Frontend (Web)
#### Componentes
| Archivo | Descripción |
|---------|-------------|
| `components/sat/FielUploadModal.tsx` | Modal para subir certificado y llave FIEL |
| `components/sat/SyncStatus.tsx` | Estado de sincronización con selector de fechas |
| `components/sat/SyncHistory.tsx` | Historial de sincronizaciones |
#### Página
| Archivo | Descripción |
|---------|-------------|
| `app/(dashboard)/configuracion/sat/page.tsx` | Página de configuración SAT |
### 3. Base de Datos
#### Tabla Principal (schema public)
### Tabla global `public.sat_sync_jobs`
```sql
-- sat_sync_jobs: Almacena los trabajos de sincronización
CREATE TABLE sat_sync_jobs (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
type VARCHAR(20) NOT NULL, -- 'initial' | 'daily'
status VARCHAR(20) NOT NULL, -- 'pending' | 'running' | 'completed' | 'failed'
date_from TIMESTAMP NOT NULL,
date_to TIMESTAMP NOT NULL,
cfdi_type VARCHAR(20),
sat_request_id VARCHAR(100),
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 DEFAULT 0,
cfdis_downloaded INTEGER DEFAULT 0,
cfdis_inserted INTEGER DEFAULT 0,
cfdis_updated INTEGER DEFAULT 0,
progress_percent INTEGER DEFAULT 0,
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,
completed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
retry_count INTEGER DEFAULT 0
);
-- fiel_credentials: Almacena las credenciales FIEL encriptadas
CREATE TABLE fiel_credentials (
id UUID PRIMARY KEY,
tenant_id UUID UNIQUE NOT NULL,
rfc VARCHAR(13) NOT NULL,
cer_data BYTEA NOT NULL,
key_data BYTEA NOT NULL,
key_password_encrypted BYTEA NOT NULL,
encryption_iv BYTEA NOT NULL,
encryption_tag BYTEA NOT NULL,
serial_number VARCHAR(100),
valid_from TIMESTAMP NOT NULL,
valid_until TIMESTAMP NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
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 '{}'
);
```
#### Columnas agregadas a tabla cfdis (por tenant)
```sql
ALTER TABLE tenant_xxx.cfdis ADD COLUMN xml_original TEXT;
ALTER TABLE tenant_xxx.cfdis ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE tenant_xxx.cfdis ADD COLUMN last_sat_sync TIMESTAMP;
ALTER TABLE tenant_xxx.cfdis ADD COLUMN sat_sync_job_id UUID;
ALTER TABLE tenant_xxx.cfdis ADD COLUMN source VARCHAR(20) DEFAULT 'manual';
```
## Dependencias
```json
{
"@nodecfdi/sat-ws-descarga-masiva": "^2.0.0",
"@nodecfdi/credentials": "^2.0.0",
"@nodecfdi/cfdi-core": "^1.0.1"
}
```
## Flujo de Sincronización
```
1. Usuario configura FIEL (certificado .cer + llave .key + contraseña)
2. Sistema valida y encripta credenciales (AES-256-GCM)
3. Usuario inicia sincronización (manual o automática 3:00 AM)
4. Sistema desencripta FIEL y crea cliente SAT
5. Por cada mes en el rango:
a. Solicitar CFDIs emitidos al SAT
b. Esperar respuesta (polling cada 30s)
c. Descargar paquetes ZIP
d. Extraer y parsear XMLs
e. Guardar en BD del tenant
f. Repetir para CFDIs recibidos
6. Marcar job como completado
```
## API Endpoints
### FIEL
| Método | Ruta | Descripción |
|--------|------|-------------|
| GET | `/api/fiel/status` | Estado de la FIEL configurada |
| POST | `/api/fiel/upload` | Subir nueva FIEL |
| DELETE | `/api/fiel` | Eliminar FIEL |
- **Legacy:** `public.fiel_credentials` (una por tenant).
- **Por contribuyente:** `fiel_contribuyente` dentro de la base del tenant.
### Sincronización SAT
El sistema intenta primero la FIEL del contribuyente; si no existe, cae a la FIEL del tenant.
| Método | Ruta | Descripción |
|--------|------|-------------|
| POST | `/api/sat/sync` | Iniciar sincronización |
| GET | `/api/sat/sync/status` | Estado actual |
| GET | `/api/sat/sync/history` | Historial de syncs |
| GET | `/api/sat/sync/:id` | Detalle de un job |
| POST | `/api/sat/sync/:id/retry` | Reintentar job fallido |
## 4. Tipos de sincronización
### Parámetros 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 |
```typescript
interface StartSyncRequest {
type?: 'initial' | 'daily'; // default: 'daily'
dateFrom?: string; // ISO date, ej: "2025-01-01T00:00:00"
dateTo?: string; // ISO date, ej: "2025-12-31T23:59:59"
}
## 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`:
```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;
```
## Configuración
Esto da un máximo de **~45 minutos por solicitud** (9 × 5 min).
### Variables de entorno
Cada solicitud al SAT tiene su propio polling; los intentos no se comparten entre solicitudes.
```env
# Clave para encriptar credenciales FIEL (32 bytes hex)
FIEL_ENCRYPTION_KEY=tu_clave_de_32_bytes_en_hexadecimal
## 8. Políticas de reintentos
# Zona horaria para el cron
TZ=America/Mexico_City
```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
```
### Límites del SAT
Los reintentos automáticos se programan desde `createdAt` del job. Los reintentos por cron fijo (9 AM / 4 PM) se manejan en `continuePendingDailyRequests`.
- **Antigüedad máxima**: 6 años
- **Solicitudes por día**: Limitadas (se reinicia cada 24h)
- **Tamaño de paquete**: Variable
## 9. Manejo de errores
## Errores Comunes del SAT
### Errores no fatales (se registran, no abortan en daily)
| Código | Mensaje | Solución |
|--------|---------|----------|
| 5000 | Solicitud Aceptada | OK - esperar verificación |
| 5002 | Límite de solicitudes agotado | Esperar 24 horas |
| 5004 | No se encontraron CFDIs | Normal si no hay facturas en el rango |
| 5005 | Solicitud duplicada | Ya existe una solicitud pendiente |
| - | Información mayor a 6 años | Ajustar rango de fechas |
| - | No se permite descarga de cancelados | Facturas canceladas no disponibles |
- `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.
## Seguridad
### Errores transitorios (reintentan)
1. **Encriptación de credenciales**: AES-256-GCM con IV único
2. **Almacenamiento seguro**: Certificado, llave y contraseña encriptados
3. **Autenticación**: JWT con tenantId embebido
4. **Aislamiento**: Cada tenant tiene su propio schema en PostgreSQL
- Timeout del polling (`SatSyncTimeoutError`).
- `SatTransientError` (ej. rechazo transitorio del SAT).
- Metadata aún no lista (`SatMetadataPendingError`).
## Servicios Systemd
### 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
```bash
# API Backend
systemctl status horux-api
# Web Frontend
systemctl status horux-web
pm2 status horux-api
pm2 logs horux-api --lines 50 --nostream
```
## Comandos Útiles
### Jobs recientes
```bash
# Ver logs de sincronización SAT
journalctl -u horux-api -f | grep "\[SAT\]"
# Estado de jobs
psql -U postgres -d horux360 -c "SELECT * FROM sat_sync_jobs ORDER BY created_at DESC LIMIT 5;"
# CFDIs sincronizados por tenant
psql -U postgres -d horux360 -c "SELECT COUNT(*) FROM tenant_xxx.cfdis WHERE source = 'sat';"
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;
"
```
## Changelog
### 2026-01-25
- Implementación inicial de sincronización SAT
- Integración con librería @nodecfdi/sat-ws-descarga-masiva
- Soporte para fechas personalizadas en sincronización
- Corrección de cast UUID en queries SQL
- Agregadas columnas faltantes a tabla cfdis
- UI para selección de periodo personalizado
- Cambio de servicio web a modo producción (next start)
## Estado Actual (2026-01-25)
### Completado
- [x] Servicio de encriptación de credenciales FIEL
- [x] Integración con @nodecfdi/sat-ws-descarga-masiva
- [x] Parser de XMLs de CFDI
- [x] UI para subir FIEL
- [x] UI para ver estado de sincronización
- [x] UI para seleccionar periodo personalizado
- [x] Cron job para sincronización diaria (3:00 AM)
- [x] Soporte para fechas personalizadas
- [x] Corrección de cast UUID en queries
- [x] Columnas adicionales en tabla cfdis de todos los tenants
### Pendiente por probar
El SAT bloqueó las solicitudes por exceso de pruebas. **Esperar 24 horas** y luego:
1. Ir a **Configuración > SAT**
2. Clic en **"Periodo personalizado"**
3. Seleccionar: **2025-01-01** a **2025-12-31**
4. Clic en **"Sincronizar periodo"**
### Tenant de prueba
- **RFC**: HTS240708LJA
- **Schema**: `tenant_cas2408138w2`
- **Nota**: Los CFDIs "recibidos" de este tenant están cancelados (SAT no permite descargarlos)
### Comandos para verificar después de 24h
### Jobs fallidos o atorados
```bash
# Ver estado del sync
PGPASSWORD=postgres psql -h localhost -U postgres -d horux360 -c \
"SELECT status, cfdis_found, cfdis_downloaded, cfdis_inserted FROM sat_sync_jobs ORDER BY created_at DESC LIMIT 1;"
# Ver logs en tiempo real
journalctl -u horux-api -f | grep "\[SAT\]"
# Contar CFDIs sincronizados
PGPASSWORD=postgres psql -h localhost -U postgres -d horux360 -c \
"SELECT COUNT(*) as total FROM tenant_cas2408138w2.cfdis WHERE source = 'sat';"
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;
"
```
### Problemas conocidos
### Contribuyentes de un tenant
1. **"Se han agotado las solicitudes de por vida"**: Límite de SAT alcanzado, esperar 24h
2. **"No se permite la descarga de xml que se encuentren cancelados"**: Normal para facturas canceladas
3. **"Información mayor a 6 años"**: SAT solo permite descargar últimos 6 años
```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;
"
```
## Próximos Pasos
## 12. Changelog reciente
- [ ] Probar sincronización completa después de 24h
- [ ] Verificar que los CFDIs se guarden correctamente
- [ ] Implementar reintentos automáticos para errores temporales
- [ ] Notificaciones por email al completar sincronización
- [ ] Dashboard con estadísticas de CFDIs por periodo
- [ ] Soporte para filtros adicionales (RFC emisor/receptor, tipo de comprobante)
### 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.