# 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`

```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 |
| 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;
```

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

```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`.

## 9. 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.

## 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
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;
"
```

## 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

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.
