Files
Horux360/docs/plans/2026-01-25-sat-sync-design.md
Consultoria AS b49902bcff docs: add SAT sync feature design
Design document for automatic CFDI synchronization with SAT:
- FIEL (e.firma) authentication
- Download emitted and received CFDIs
- Daily automated sync at 3:00 AM
- Initial extraction of last 10 years
- Encrypted credential storage (AES-256-GCM)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 00:35:12 +00:00

328 lines
10 KiB
Markdown

# Diseño: Sincronización con SAT
## Resumen
Implementar sincronización automática de CFDIs desde el portal del SAT usando la e.firma (FIEL).
## Requisitos
| Aspecto | Decisión |
|---------|----------|
| Autenticación | FIEL (archivos .cer y .key + contraseña) |
| Tipos de CFDI | Emitidos y recibidos |
| Ejecución | Programada diaria a las 3:00 AM |
| Almacenamiento credenciales | Encriptadas en PostgreSQL (AES-256-GCM) |
| Primera extracción | Últimos 10 años |
| Extracciones posteriores | Solo mes actual |
| Duplicados | Actualizar con versión del SAT |
---
## Arquitectura General
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────┐
│ Frontend │────▶│ API Horux │────▶│ SAT WSDL │
│ (Configuración)│ │ (sat.service) │ │ Web Service│
└─────────────────┘ └──────────────────┘ └─────────────┘
┌──────────────┐
│ PostgreSQL │
│ - fiel_credentials
│ - sat_sync_jobs
│ - cfdis
└──────────────┘
```
---
## Integración con Web Services del SAT
### Flujo de Descarga
```
1. AUTENTICACIÓN (Token válido por 5 minutos)
- Crear timestamp (Created + Expires)
- Generar digest SHA-1 del timestamp
- Firmar digest con llave privada (.key) usando RSA-SHA1
- Enviar SOAP con certificado (.cer) + firma
- Recibir token SAML para usar en siguientes llamadas
2. SOLICITUD DE DESCARGA
Parámetros:
- RfcSolicitante: RFC de la empresa
- FechaInicio: YYYY-MM-DDTHH:MM:SS
- FechaFin: YYYY-MM-DDTHH:MM:SS
- TipoSolicitud: "CFDI" o "Metadata"
- TipoComprobante: "I"(ingreso), "E"(egreso), "T", "N", "P"
- RfcEmisor / RfcReceptor: Filtrar por contraparte (opcional)
Respuesta:
- IdSolicitud: UUID para tracking
- CodEstatus: 5000 = Aceptada
3. VERIFICACIÓN (Polling cada 30-60 segundos)
Estados posibles:
- 1: Aceptada (en proceso)
- 2: En proceso
- 3: Terminada (lista para descargar)
- 4: Error
- 5: Rechazada
- 6: Vencida
Respuesta exitosa incluye:
- IdsPaquetes: Array de IDs de paquetes ZIP a descargar
- NumeroCFDIs: Total de comprobantes encontrados
4. DESCARGA DE PAQUETES
- Por cada IdPaquete, solicitar descarga
- Respuesta: Paquete en Base64 (archivo ZIP)
- Decodificar y extraer XMLs
- Cada ZIP puede contener hasta 200,000 CFDIs
5. PROCESAMIENTO DE XMLs
Por cada XML:
- Parsear con @nodecfdi/cfdi-core
- Extraer: UUID, emisor, receptor, total, impuestos, fecha
- Buscar en BD por UUID
- Si existe → UPDATE
- Si no existe → INSERT
- Guardar XML original
```
### Endpoints del SAT
| Servicio | URL |
|----------|-----|
| Autenticación | `https://cfdidescargamasivasolicitud.clouda.sat.gob.mx/Autenticacion/Autenticacion.svc` |
| Solicitud | `https://cfdidescargamasivasolicitud.clouda.sat.gob.mx/SolicitaDescargaService.svc` |
| Verificación | `https://cfdidescargamasivasolicitud.clouda.sat.gob.mx/VerificaSolicitudDescargaService.svc` |
| Descarga | `https://cfdidescargamasiva.clouda.sat.gob.mx/DescargaMasivaService.svc` |
### Estructura SOAP para Autenticación
```xml
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<s:Header>
<o:Security xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
<u:Timestamp u:Id="_0">
<u:Created>2026-01-25T00:00:00.000Z</u:Created>
<u:Expires>2026-01-25T00:05:00.000Z</u:Expires>
</u:Timestamp>
<o:BinarySecurityToken
ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"
EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"
u:Id="uuid-cert">
<!-- Certificado .cer en Base64 -->
</o:BinarySecurityToken>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<Reference URI="#_0">
<Transforms>
<Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<DigestValue><!-- SHA1 del Timestamp --></DigestValue>
</Reference>
</SignedInfo>
<SignatureValue><!-- Firma RSA-SHA1 --></SignatureValue>
<KeyInfo>
<o:SecurityTokenReference>
<o:Reference URI="#uuid-cert"/>
</o:SecurityTokenReference>
</KeyInfo>
</Signature>
</o:Security>
</s:Header>
<s:Body/>
</s:Envelope>
```
### Dependencias Node.js
```json
{
"@nodecfdi/credentials": "^2.0",
"@nodecfdi/cfdi-core": "^0.5",
"node-forge": "^1.3",
"fast-xml-parser": "^4.0",
"adm-zip": "^0.5",
"node-cron": "^3.0"
}
```
### Códigos de Error del SAT
| Código | Significado | Acción |
|--------|-------------|--------|
| 5000 | Solicitud recibida | Continuar con verificación |
| 5002 | Se agotó límite de solicitudes | Esperar 24 horas |
| 5004 | No se encontraron CFDIs | Registrar, no es error |
| 5005 | Solicitud duplicada | Usar IdSolicitud existente |
| 404 | Paquete no encontrado | Reintentar en 1 minuto |
| 500 | Error interno SAT | Reintentar con backoff |
### Estrategia de Extracción Inicial (10 años)
- Dividir en solicitudes mensuales (~121 solicitudes)
- Procesar 3-4 meses por día para no saturar
- Guardar progreso en sat_sync_jobs
- Si falla, continuar desde último mes exitoso
### Tiempos Estimados
| Operación | Tiempo |
|-----------|--------|
| Autenticación | 1-2 segundos |
| Solicitud aceptada | 1-2 segundos |
| Verificación (paquete listo) | 1-30 minutos |
| Descarga 10,000 CFDIs | 30-60 segundos |
| Procesamiento 10,000 XMLs | 2-5 minutos |
---
## Modelo de Datos
### Nuevas Tablas (schema public)
```sql
-- Credenciales FIEL por tenant (encriptadas)
CREATE TABLE fiel_credentials (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
rfc VARCHAR(13) NOT NULL,
cer_data BYTEA NOT NULL,
key_data BYTEA NOT NULL,
key_password_encrypted BYTEA NOT NULL,
serial_number VARCHAR(50),
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(),
UNIQUE(tenant_id)
);
-- Jobs de sincronización
CREATE TABLE sat_sync_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
type VARCHAR(20) NOT NULL,
status VARCHAR(20) NOT NULL,
date_from DATE NOT NULL,
date_to DATE NOT NULL,
cfdi_type VARCHAR(10),
sat_request_id VARCHAR(50),
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,
error_message TEXT,
started_at TIMESTAMP,
completed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
retry_count INTEGER DEFAULT 0,
next_retry_at TIMESTAMP
);
CREATE INDEX idx_sat_sync_jobs_tenant ON sat_sync_jobs(tenant_id);
CREATE INDEX idx_sat_sync_jobs_status ON sat_sync_jobs(status);
```
### Modificaciones a tabla cfdis
```sql
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS source VARCHAR(20) DEFAULT 'manual';
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS sat_sync_job_id UUID;
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS xml_original TEXT;
ALTER TABLE cfdis ADD COLUMN IF NOT EXISTS last_sat_sync TIMESTAMP;
```
---
## Estructura de Archivos
```
apps/api/src/
├── services/
│ ├── sat/
│ │ ├── sat.service.ts
│ │ ├── sat-auth.service.ts
│ │ ├── sat-download.service.ts
│ │ ├── sat-parser.service.ts
│ │ └── sat-crypto.service.ts
│ └── fiel.service.ts
├── controllers/
│ ├── sat.controller.ts
│ └── fiel.controller.ts
├── routes/
│ ├── sat.routes.ts
│ └── fiel.routes.ts
└── jobs/
└── sat-sync.job.ts
```
---
## API Endpoints
```
POST /api/fiel/upload # Subir .cer, .key y contraseña
GET /api/fiel/status # Estado de FIEL configurada
DELETE /api/fiel # Eliminar credenciales
POST /api/sat/sync # Sincronización manual
GET /api/sat/sync/status # Estado actual
GET /api/sat/sync/history # Historial
GET /api/sat/sync/:id # Detalle de job
POST /api/sat/sync/:id/retry # Reintentar job fallido
```
---
## Interfaz de Usuario
### Sección en Configuración
- Estado de FIEL (configurada/no configurada, vigencia)
- Botones: Actualizar FIEL, Eliminar
- Sincronización automática (frecuencia, última sync, total CFDIs)
- Botón: Sincronizar Ahora
- Historial de sincronizaciones (tabla)
### Modal de Carga FIEL
- Input para archivo .cer
- Input para archivo .key
- Input para contraseña
- Mensaje de seguridad
- Botones: Cancelar, Guardar y Validar
---
## Notificaciones
| Evento | Mensaje |
|--------|---------|
| Sync completada | "Se descargaron X CFDIs del SAT" |
| Sync fallida | "Error al sincronizar: [mensaje]" |
| FIEL por vencer (30 días) | "Tu e.firma vence el DD/MMM/YYYY" |
| FIEL vencida | "Tu e.firma ha vencido" |
---
## Seguridad
- Solo rol `admin` puede gestionar FIEL
- Credenciales nunca se devuelven en API
- Logs de auditoría para accesos
- Rate limiting en endpoints de sincronización
- Encriptación AES-256-GCM para credenciales