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>
This commit is contained in:
327
docs/plans/2026-01-25-sat-sync-design.md
Normal file
327
docs/plans/2026-01-25-sat-sync-design.md
Normal file
@@ -0,0 +1,327 @@
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user