diff --git a/apps/api/src/services/sat/sat.service.ts b/apps/api/src/services/sat/sat.service.ts index c6d047b..1de7f59 100644 --- a/apps/api/src/services/sat/sat.service.ts +++ b/apps/api/src/services/sat/sat.service.ts @@ -96,7 +96,7 @@ async function saveCfdis( estado = $22, xml_original = $23, last_sat_sync = NOW(), - sat_sync_job_id = $24, + sat_sync_job_id = $24::uuid, updated_at = NOW() WHERE uuid_fiscal = $1`, cfdi.uuidFiscal, @@ -137,7 +137,7 @@ async function saveCfdis( ) VALUES ( gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, - $23, 'sat', $24, NOW(), NOW() + $23, 'sat', $24::uuid, NOW(), NOW() )`, cfdi.uuidFiscal, cfdi.tipo, @@ -278,11 +278,18 @@ async function processDateRange( } /** - * Ejecuta sincronización inicial (últimos 10 años) + * Ejecuta sincronización inicial o por rango personalizado */ -async function processInitialSync(ctx: SyncContext, jobId: string): Promise { +async function processInitialSync( + ctx: SyncContext, + jobId: string, + customDateFrom?: Date, + customDateTo?: Date +): Promise { const ahora = new Date(); - const inicioHistorico = new Date(ahora.getFullYear() - YEARS_TO_SYNC, ahora.getMonth(), 1); + // Usar fechas personalizadas si se proporcionan, sino calcular desde YEARS_TO_SYNC + const inicioHistorico = customDateFrom || new Date(ahora.getFullYear() - YEARS_TO_SYNC, ahora.getMonth(), 1); + const fechaFin = customDateTo || ahora; let totalFound = 0; let totalDownloaded = 0; @@ -292,9 +299,9 @@ async function processInitialSync(ctx: SyncContext, jobId: string): Promise ahora ? ahora : monthEnd; + const rangeEnd = monthEnd > fechaFin ? fechaFin : monthEnd; // Procesar emitidos try { @@ -446,7 +453,7 @@ export async function startSync( (async () => { try { if (type === 'initial') { - await processInitialSync(ctx, job.id); + await processInitialSync(ctx, job.id, dateFrom, dateTo); } else { await processDailySync(ctx, job.id); } diff --git a/apps/web/components/sat/SyncStatus.tsx b/apps/web/components/sat/SyncStatus.tsx index c2c4cf4..7f2bc97 100644 --- a/apps/web/components/sat/SyncStatus.tsx +++ b/apps/web/components/sat/SyncStatus.tsx @@ -3,6 +3,8 @@ import { useEffect, useState } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; import { getSyncStatus, startSync } from '@/lib/api/sat'; import type { SatSyncStatusResponse } from '@horux/shared'; @@ -30,6 +32,9 @@ export function SyncStatus({ fielConfigured, onSyncStarted }: SyncStatusProps) { const [loading, setLoading] = useState(true); const [startingSync, setStartingSync] = useState(false); const [error, setError] = useState(''); + const [showCustomDate, setShowCustomDate] = useState(false); + const [dateFrom, setDateFrom] = useState(''); + const [dateTo, setDateTo] = useState(''); const fetchStatus = async () => { try { @@ -53,12 +58,21 @@ export function SyncStatus({ fielConfigured, onSyncStarted }: SyncStatusProps) { } }, [fielConfigured]); - const handleStartSync = async (type: 'initial' | 'daily') => { + const handleStartSync = async (type: 'initial' | 'daily', customDates?: boolean) => { setStartingSync(true); setError(''); try { - await startSync({ type }); + const params: { type: 'initial' | 'daily'; dateFrom?: string; dateTo?: string } = { type }; + + if (customDates && dateFrom && dateTo) { + // Convertir a formato completo con hora + params.dateFrom = `${dateFrom}T00:00:00`; + params.dateTo = `${dateTo}T23:59:59`; + } + + await startSync(params); await fetchStatus(); + setShowCustomDate(false); onSyncStarted?.(); } catch (err: any) { setError(err.response?.data?.error || 'Error al iniciar sincronizacion'); @@ -162,6 +176,49 @@ export function SyncStatus({ fielConfigured, onSyncStarted }: SyncStatusProps) {

{error}

)} + {/* Formulario de fechas personalizadas */} + {showCustomDate && ( +
+
+
+ + setDateFrom(e.target.value)} + max={dateTo || undefined} + /> +
+
+ + setDateTo(e.target.value)} + min={dateFrom || undefined} + /> +
+
+
+ + +
+
+ )} +
+ - {!status?.lastCompletedJob && ( - - )}
+ + {!status?.lastCompletedJob && ( + + )} ); diff --git a/docs/SAT-SYNC-IMPLEMENTATION.md b/docs/SAT-SYNC-IMPLEMENTATION.md new file mode 100644 index 0000000..6703711 --- /dev/null +++ b/docs/SAT-SYNC-IMPLEMENTATION.md @@ -0,0 +1,245 @@ +# Implementación de Sincronización SAT + +## Resumen + +Sistema de sincronización automática de CFDIs con el SAT (Servicio de Administración Tributaria de México) para Horux360. + +## Componentes Implementados + +### 1. Backend (API) + +#### Servicios + +| 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 | + +#### Controladores + +| Archivo | Descripción | +|---------|-------------| +| `src/controllers/fiel.controller.ts` | Endpoints para gestión de FIEL | +| `src/controllers/sat.controller.ts` | Endpoints para sincronización SAT | + +#### Job Programado + +| Archivo | Descripción | +|---------|-------------| +| `src/jobs/sat-sync.job.ts` | Cron job para sincronización diaria (3:00 AM) | + +### 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) + +```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), + 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 +); + +-- 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() +); +``` + +#### 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 | + +### Sincronización SAT + +| 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 | + +### Parámetros de sincronización + +```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" +} +``` + +## Configuración + +### Variables de entorno + +```env +# Clave para encriptar credenciales FIEL (32 bytes hex) +FIEL_ENCRYPTION_KEY=tu_clave_de_32_bytes_en_hexadecimal + +# Zona horaria para el cron +TZ=America/Mexico_City +``` + +### Límites del SAT + +- **Antigüedad máxima**: 6 años +- **Solicitudes por día**: Limitadas (se reinicia cada 24h) +- **Tamaño de paquete**: Variable + +## Errores Comunes del SAT + +| 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 | + +## Seguridad + +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 + +## Servicios Systemd + +```bash +# API Backend +systemctl status horux-api + +# Web Frontend +systemctl status horux-web +``` + +## Comandos Útiles + +```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';" +``` + +## 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) + +## Próximos Pasos + +- [ ] 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)