Compare commits
5 Commits
4fd6f01303
...
a64aa11548
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a64aa11548 | ||
|
|
787aac9a4c | ||
|
|
3763014eca | ||
|
|
b49902bcff | ||
|
|
519de61c6f |
@@ -15,6 +15,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@horux/shared": "workspace:*",
|
||||
"@nodecfdi/credentials": "^3.2.0",
|
||||
"@prisma/client": "^5.22.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"cors": "^2.8.5",
|
||||
@@ -23,6 +24,7 @@
|
||||
"express": "^4.21.0",
|
||||
"helmet": "^8.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"node-forge": "^1.3.3",
|
||||
"zod": "^3.23.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -31,6 +33,7 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"prisma": "^5.22.0",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.3.0"
|
||||
|
||||
@@ -20,6 +20,8 @@ model Tenant {
|
||||
expiresAt DateTime? @map("expires_at")
|
||||
|
||||
users User[]
|
||||
fielCredential FielCredential?
|
||||
satSyncJobs SatSyncJob[]
|
||||
|
||||
@@map("tenants")
|
||||
}
|
||||
@@ -62,3 +64,75 @@ enum Role {
|
||||
contador
|
||||
visor
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SAT Sync Models
|
||||
// ============================================
|
||||
|
||||
model FielCredential {
|
||||
id String @id @default(uuid())
|
||||
tenantId String @unique @map("tenant_id")
|
||||
rfc String @db.VarChar(13)
|
||||
cerData Bytes @map("cer_data")
|
||||
keyData Bytes @map("key_data")
|
||||
keyPasswordEncrypted Bytes @map("key_password_encrypted")
|
||||
encryptionIv Bytes @map("encryption_iv")
|
||||
encryptionTag Bytes @map("encryption_tag")
|
||||
serialNumber String? @map("serial_number") @db.VarChar(50)
|
||||
validFrom DateTime @map("valid_from")
|
||||
validUntil DateTime @map("valid_until")
|
||||
isActive Boolean @default(true) @map("is_active")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("fiel_credentials")
|
||||
}
|
||||
|
||||
model SatSyncJob {
|
||||
id String @id @default(uuid())
|
||||
tenantId String @map("tenant_id")
|
||||
type SatSyncType
|
||||
status SatSyncStatus @default(pending)
|
||||
dateFrom DateTime @map("date_from") @db.Date
|
||||
dateTo DateTime @map("date_to") @db.Date
|
||||
cfdiType CfdiSyncType? @map("cfdi_type")
|
||||
satRequestId String? @map("sat_request_id") @db.VarChar(50)
|
||||
satPackageIds String[] @map("sat_package_ids")
|
||||
cfdisFound Int @default(0) @map("cfdis_found")
|
||||
cfdisDownloaded Int @default(0) @map("cfdis_downloaded")
|
||||
cfdisInserted Int @default(0) @map("cfdis_inserted")
|
||||
cfdisUpdated Int @default(0) @map("cfdis_updated")
|
||||
progressPercent Int @default(0) @map("progress_percent")
|
||||
errorMessage String? @map("error_message")
|
||||
startedAt DateTime? @map("started_at")
|
||||
completedAt DateTime? @map("completed_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
retryCount Int @default(0) @map("retry_count")
|
||||
nextRetryAt DateTime? @map("next_retry_at")
|
||||
|
||||
tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([tenantId])
|
||||
@@index([status])
|
||||
@@index([status, nextRetryAt])
|
||||
@@map("sat_sync_jobs")
|
||||
}
|
||||
|
||||
enum SatSyncType {
|
||||
initial
|
||||
daily
|
||||
}
|
||||
|
||||
enum SatSyncStatus {
|
||||
pending
|
||||
running
|
||||
completed
|
||||
failed
|
||||
}
|
||||
|
||||
enum CfdiSyncType {
|
||||
emitidos
|
||||
recibidos
|
||||
}
|
||||
|
||||
232
apps/api/src/services/fiel.service.ts
Normal file
232
apps/api/src/services/fiel.service.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { Credential } from '@nodecfdi/credentials/node';
|
||||
import { prisma } from '../config/database.js';
|
||||
import { encrypt, decrypt } from './sat/sat-crypto.service.js';
|
||||
import type { FielStatus } from '@horux/shared';
|
||||
|
||||
/**
|
||||
* Sube y valida credenciales FIEL
|
||||
*/
|
||||
export async function uploadFiel(
|
||||
tenantId: string,
|
||||
cerBase64: string,
|
||||
keyBase64: string,
|
||||
password: string
|
||||
): Promise<{ success: boolean; message: string; status?: FielStatus }> {
|
||||
try {
|
||||
// Decodificar archivos de Base64
|
||||
const cerData = Buffer.from(cerBase64, 'base64');
|
||||
const keyData = Buffer.from(keyBase64, 'base64');
|
||||
|
||||
// Validar que los archivos sean válidos y coincidan
|
||||
let credential: Credential;
|
||||
try {
|
||||
credential = Credential.create(
|
||||
cerData.toString('binary'),
|
||||
keyData.toString('binary'),
|
||||
password
|
||||
);
|
||||
} catch (error: any) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Los archivos de la FIEL no son válidos o la contraseña es incorrecta',
|
||||
};
|
||||
}
|
||||
|
||||
// Verificar que sea una FIEL (no CSD)
|
||||
if (!credential.isFiel()) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'El certificado proporcionado no es una FIEL (e.firma). Parece ser un CSD.',
|
||||
};
|
||||
}
|
||||
|
||||
// Obtener información del certificado
|
||||
const certificate = credential.certificate();
|
||||
const rfc = certificate.rfc();
|
||||
const serialNumber = certificate.serialNumber().bytes();
|
||||
const validFrom = certificate.validFromDateTime();
|
||||
const validUntil = certificate.validToDateTime();
|
||||
|
||||
// Verificar que no esté vencida
|
||||
if (new Date() > validUntil) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'La FIEL está vencida desde ' + validUntil.toLocaleDateString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Encriptar credenciales
|
||||
const { encrypted: encryptedCer, iv, tag } = encrypt(cerData);
|
||||
const { encrypted: encryptedKey } = encrypt(keyData);
|
||||
const { encrypted: encryptedPassword } = encrypt(Buffer.from(password, 'utf-8'));
|
||||
|
||||
// Guardar o actualizar en BD
|
||||
await prisma.fielCredential.upsert({
|
||||
where: { tenantId },
|
||||
create: {
|
||||
tenantId,
|
||||
rfc,
|
||||
cerData: encryptedCer,
|
||||
keyData: encryptedKey,
|
||||
keyPasswordEncrypted: encryptedPassword,
|
||||
encryptionIv: iv,
|
||||
encryptionTag: tag,
|
||||
serialNumber,
|
||||
validFrom,
|
||||
validUntil,
|
||||
isActive: true,
|
||||
},
|
||||
update: {
|
||||
rfc,
|
||||
cerData: encryptedCer,
|
||||
keyData: encryptedKey,
|
||||
keyPasswordEncrypted: encryptedPassword,
|
||||
encryptionIv: iv,
|
||||
encryptionTag: tag,
|
||||
serialNumber,
|
||||
validFrom,
|
||||
validUntil,
|
||||
isActive: true,
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
const daysUntilExpiration = Math.ceil(
|
||||
(validUntil.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'FIEL configurada correctamente',
|
||||
status: {
|
||||
configured: true,
|
||||
rfc,
|
||||
serialNumber,
|
||||
validFrom: validFrom.toISOString(),
|
||||
validUntil: validUntil.toISOString(),
|
||||
isExpired: false,
|
||||
daysUntilExpiration,
|
||||
},
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('[FIEL Upload Error]', error);
|
||||
return {
|
||||
success: false,
|
||||
message: error.message || 'Error al procesar la FIEL',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene el estado de la FIEL de un tenant
|
||||
*/
|
||||
export async function getFielStatus(tenantId: string): Promise<FielStatus> {
|
||||
const fiel = await prisma.fielCredential.findUnique({
|
||||
where: { tenantId },
|
||||
select: {
|
||||
rfc: true,
|
||||
serialNumber: true,
|
||||
validFrom: true,
|
||||
validUntil: true,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!fiel || !fiel.isActive) {
|
||||
return { configured: false };
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const isExpired = now > fiel.validUntil;
|
||||
const daysUntilExpiration = Math.ceil(
|
||||
(fiel.validUntil.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
return {
|
||||
configured: true,
|
||||
rfc: fiel.rfc,
|
||||
serialNumber: fiel.serialNumber || undefined,
|
||||
validFrom: fiel.validFrom.toISOString(),
|
||||
validUntil: fiel.validUntil.toISOString(),
|
||||
isExpired,
|
||||
daysUntilExpiration: isExpired ? 0 : daysUntilExpiration,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Elimina la FIEL de un tenant
|
||||
*/
|
||||
export async function deleteFiel(tenantId: string): Promise<boolean> {
|
||||
try {
|
||||
await prisma.fielCredential.delete({
|
||||
where: { tenantId },
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtiene las credenciales desencriptadas para usar en sincronización
|
||||
* Solo debe usarse internamente por el servicio de SAT
|
||||
*/
|
||||
export async function getDecryptedFiel(tenantId: string): Promise<{
|
||||
credential: Credential;
|
||||
rfc: string;
|
||||
} | null> {
|
||||
const fiel = await prisma.fielCredential.findUnique({
|
||||
where: { tenantId },
|
||||
});
|
||||
|
||||
if (!fiel || !fiel.isActive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verificar que no esté vencida
|
||||
if (new Date() > fiel.validUntil) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Desencriptar
|
||||
const cerData = decrypt(
|
||||
Buffer.from(fiel.cerData),
|
||||
Buffer.from(fiel.encryptionIv),
|
||||
Buffer.from(fiel.encryptionTag)
|
||||
);
|
||||
const keyData = decrypt(
|
||||
Buffer.from(fiel.keyData),
|
||||
Buffer.from(fiel.encryptionIv),
|
||||
Buffer.from(fiel.encryptionTag)
|
||||
);
|
||||
const password = decrypt(
|
||||
Buffer.from(fiel.keyPasswordEncrypted),
|
||||
Buffer.from(fiel.encryptionIv),
|
||||
Buffer.from(fiel.encryptionTag)
|
||||
).toString('utf-8');
|
||||
|
||||
// Crear credencial
|
||||
const credential = Credential.create(
|
||||
cerData.toString('binary'),
|
||||
keyData.toString('binary'),
|
||||
password
|
||||
);
|
||||
|
||||
return {
|
||||
credential,
|
||||
rfc: fiel.rfc,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[FIEL Decrypt Error]', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifica si un tenant tiene FIEL configurada y válida
|
||||
*/
|
||||
export async function hasFielConfigured(tenantId: string): Promise<boolean> {
|
||||
const status = await getFielStatus(tenantId);
|
||||
return status.configured && !status.isExpired;
|
||||
}
|
||||
122
apps/api/src/services/sat/sat-crypto.service.ts
Normal file
122
apps/api/src/services/sat/sat-crypto.service.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { createCipheriv, createDecipheriv, randomBytes, createHash } from 'crypto';
|
||||
import { env } from '../../config/env.js';
|
||||
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const IV_LENGTH = 16;
|
||||
const TAG_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* Deriva una clave de 256 bits del JWT_SECRET
|
||||
*/
|
||||
function deriveKey(): Buffer {
|
||||
return createHash('sha256').update(env.JWT_SECRET).digest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encripta datos usando AES-256-GCM
|
||||
*/
|
||||
export function encrypt(data: Buffer): { encrypted: Buffer; iv: Buffer; tag: Buffer } {
|
||||
const iv = randomBytes(IV_LENGTH);
|
||||
const key = deriveKey();
|
||||
const cipher = createCipheriv(ALGORITHM, key, iv);
|
||||
|
||||
const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return { encrypted, iv, tag };
|
||||
}
|
||||
|
||||
/**
|
||||
* Desencripta datos usando AES-256-GCM
|
||||
*/
|
||||
export function decrypt(encrypted: Buffer, iv: Buffer, tag: Buffer): Buffer {
|
||||
const key = deriveKey();
|
||||
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
|
||||
return Buffer.concat([decipher.update(encrypted), decipher.final()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encripta un string y retorna los componentes
|
||||
*/
|
||||
export function encryptString(text: string): { encrypted: Buffer; iv: Buffer; tag: Buffer } {
|
||||
return encrypt(Buffer.from(text, 'utf-8'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Desencripta a string
|
||||
*/
|
||||
export function decryptToString(encrypted: Buffer, iv: Buffer, tag: Buffer): string {
|
||||
return decrypt(encrypted, iv, tag).toString('utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Encripta credenciales FIEL (cer, key, password)
|
||||
*/
|
||||
export function encryptFielCredentials(
|
||||
cerData: Buffer,
|
||||
keyData: Buffer,
|
||||
password: string
|
||||
): {
|
||||
encryptedCer: Buffer;
|
||||
encryptedKey: Buffer;
|
||||
encryptedPassword: Buffer;
|
||||
iv: Buffer;
|
||||
tag: Buffer;
|
||||
} {
|
||||
// Usamos el mismo IV y tag para simplificar, concatenando los datos
|
||||
const combined = Buffer.concat([
|
||||
Buffer.from(cerData.length.toString().padStart(10, '0')),
|
||||
cerData,
|
||||
Buffer.from(keyData.length.toString().padStart(10, '0')),
|
||||
keyData,
|
||||
Buffer.from(password, 'utf-8'),
|
||||
]);
|
||||
|
||||
const { encrypted, iv, tag } = encrypt(combined);
|
||||
|
||||
// Extraemos las partes encriptadas
|
||||
const cerLength = cerData.length;
|
||||
const keyLength = keyData.length;
|
||||
const passwordLength = Buffer.from(password, 'utf-8').length;
|
||||
|
||||
return {
|
||||
encryptedCer: encrypted.subarray(0, 10 + cerLength),
|
||||
encryptedKey: encrypted.subarray(10 + cerLength, 20 + cerLength + keyLength),
|
||||
encryptedPassword: encrypted.subarray(20 + cerLength + keyLength),
|
||||
iv,
|
||||
tag,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Desencripta credenciales FIEL
|
||||
*/
|
||||
export function decryptFielCredentials(
|
||||
encryptedCer: Buffer,
|
||||
encryptedKey: Buffer,
|
||||
encryptedPassword: Buffer,
|
||||
iv: Buffer,
|
||||
tag: Buffer
|
||||
): {
|
||||
cerData: Buffer;
|
||||
keyData: Buffer;
|
||||
password: string;
|
||||
} {
|
||||
const combined = Buffer.concat([encryptedCer, encryptedKey, encryptedPassword]);
|
||||
const decrypted = decrypt(combined, iv, tag);
|
||||
|
||||
// Parseamos las partes
|
||||
const cerLengthStr = decrypted.subarray(0, 10).toString();
|
||||
const cerLength = parseInt(cerLengthStr, 10);
|
||||
const cerData = decrypted.subarray(10, 10 + cerLength);
|
||||
|
||||
const keyLengthStr = decrypted.subarray(10 + cerLength, 20 + cerLength).toString();
|
||||
const keyLength = parseInt(keyLengthStr, 10);
|
||||
const keyData = decrypted.subarray(20 + cerLength, 20 + cerLength + keyLength);
|
||||
|
||||
const password = decrypted.subarray(20 + cerLength + keyLength).toString('utf-8');
|
||||
|
||||
return { cerData, keyData, password };
|
||||
}
|
||||
17
deploy/systemd/horux-api.service
Normal file
17
deploy/systemd/horux-api.service
Normal file
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Horux360 API Server
|
||||
After=network.target postgresql.service
|
||||
Wants=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/root/Horux/apps/api
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PATH=/root/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin
|
||||
ExecStart=/root/.local/share/pnpm/pnpm dev
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
17
deploy/systemd/horux-web.service
Normal file
17
deploy/systemd/horux-web.service
Normal file
@@ -0,0 +1,17 @@
|
||||
[Unit]
|
||||
Description=Horux360 Web Frontend
|
||||
After=network.target horux-api.service
|
||||
Wants=horux-api.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/root/Horux/apps/web
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PATH=/root/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin
|
||||
ExecStart=/root/.local/share/pnpm/pnpm dev
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
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
|
||||
|
||||
228
docs/plans/2026-01-25-sat-sync-implementation.md
Normal file
228
docs/plans/2026-01-25-sat-sync-implementation.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# Plan de Implementación: Sincronización SAT
|
||||
|
||||
## Fase 1: Base de Datos y Modelos
|
||||
|
||||
### 1.1 Migraciones Prisma
|
||||
- [ ] Agregar modelo `FielCredential` en schema.prisma
|
||||
- [ ] Agregar modelo `SatSyncJob` en schema.prisma
|
||||
- [ ] Agregar campos a modelo `Cfdi`: source, sat_sync_job_id, xml_original, last_sat_sync
|
||||
- [ ] Ejecutar migración
|
||||
|
||||
### 1.2 Tipos TypeScript
|
||||
- [ ] Crear `packages/shared/src/types/sat.ts` con interfaces
|
||||
- [ ] Exportar tipos en index.ts
|
||||
|
||||
## Fase 2: Servicios de Criptografía y FIEL
|
||||
|
||||
### 2.1 Servicio de Criptografía
|
||||
- [ ] Crear `apps/api/src/services/sat/sat-crypto.service.ts`
|
||||
- [ ] Implementar encrypt() con AES-256-GCM
|
||||
- [ ] Implementar decrypt()
|
||||
- [ ] Tests unitarios
|
||||
|
||||
### 2.2 Servicio de FIEL
|
||||
- [ ] Crear `apps/api/src/services/fiel.service.ts`
|
||||
- [ ] uploadFiel() - validar y guardar credenciales encriptadas
|
||||
- [ ] getFielStatus() - obtener estado sin exponer datos sensibles
|
||||
- [ ] deleteFiel() - eliminar credenciales
|
||||
- [ ] validateFiel() - verificar que .cer y .key coincidan
|
||||
- [ ] isExpired() - verificar vigencia
|
||||
|
||||
### 2.3 Dependencias
|
||||
- [ ] Instalar @nodecfdi/credentials
|
||||
- [ ] Instalar node-forge
|
||||
|
||||
## Fase 3: Servicios de Comunicación SAT
|
||||
|
||||
### 3.1 Servicio de Autenticación SAT
|
||||
- [ ] Crear `apps/api/src/services/sat/sat-auth.service.ts`
|
||||
- [ ] buildAuthSoapEnvelope() - construir XML de autenticación
|
||||
- [ ] signWithFiel() - firmar con llave privada
|
||||
- [ ] getToken() - obtener token SAML del SAT
|
||||
- [ ] Manejo de errores y reintentos
|
||||
|
||||
### 3.2 Servicio de Descarga SAT
|
||||
- [ ] Crear `apps/api/src/services/sat/sat-download.service.ts`
|
||||
- [ ] requestDownload() - solicitar descarga de CFDIs
|
||||
- [ ] verifyRequest() - verificar estado de solicitud
|
||||
- [ ] downloadPackage() - descargar paquete ZIP
|
||||
- [ ] Polling con backoff exponencial
|
||||
|
||||
### 3.3 Dependencias
|
||||
- [ ] Instalar fast-xml-parser
|
||||
- [ ] Instalar adm-zip
|
||||
|
||||
## Fase 4: Procesamiento de CFDIs
|
||||
|
||||
### 4.1 Servicio de Parser
|
||||
- [ ] Crear `apps/api/src/services/sat/sat-parser.service.ts`
|
||||
- [ ] extractZip() - extraer XMLs del ZIP
|
||||
- [ ] parseXml() - parsear XML a objeto
|
||||
- [ ] mapToDbModel() - mapear a modelo de BD
|
||||
|
||||
### 4.2 Dependencias
|
||||
- [ ] Instalar @nodecfdi/cfdi-core
|
||||
|
||||
## Fase 5: Orquestador Principal
|
||||
|
||||
### 5.1 Servicio Principal SAT
|
||||
- [ ] Crear `apps/api/src/services/sat/sat.service.ts`
|
||||
- [ ] startSync() - iniciar sincronización
|
||||
- [ ] processInitialSync() - extracción de 10 años
|
||||
- [ ] processDailySync() - extracción mensual
|
||||
- [ ] saveProgress() - guardar progreso en sat_sync_jobs
|
||||
- [ ] handleError() - manejo de errores y reintentos
|
||||
|
||||
## Fase 6: Job Programado
|
||||
|
||||
### 6.1 Cron Job
|
||||
- [ ] Crear `apps/api/src/jobs/sat-sync.job.ts`
|
||||
- [ ] Configurar ejecución a las 3:00 AM
|
||||
- [ ] Obtener tenants con FIEL activa
|
||||
- [ ] Ejecutar sync para cada tenant
|
||||
- [ ] Logging y monitoreo
|
||||
|
||||
### 6.2 Dependencias
|
||||
- [ ] Instalar node-cron
|
||||
|
||||
## Fase 7: API Endpoints
|
||||
|
||||
### 7.1 Controlador FIEL
|
||||
- [ ] Crear `apps/api/src/controllers/fiel.controller.ts`
|
||||
- [ ] POST /upload - subir credenciales
|
||||
- [ ] GET /status - obtener estado
|
||||
- [ ] DELETE / - eliminar credenciales
|
||||
|
||||
### 7.2 Controlador SAT
|
||||
- [ ] Crear `apps/api/src/controllers/sat.controller.ts`
|
||||
- [ ] POST /sync - iniciar sincronización manual
|
||||
- [ ] GET /sync/status - estado actual
|
||||
- [ ] GET /sync/history - historial
|
||||
- [ ] GET /sync/:id - detalle de job
|
||||
- [ ] POST /sync/:id/retry - reintentar
|
||||
|
||||
### 7.3 Rutas
|
||||
- [ ] Crear `apps/api/src/routes/fiel.routes.ts`
|
||||
- [ ] Crear `apps/api/src/routes/sat.routes.ts`
|
||||
- [ ] Registrar en app.ts
|
||||
|
||||
## Fase 8: Frontend
|
||||
|
||||
### 8.1 Componentes
|
||||
- [ ] Crear `apps/web/components/sat/FielUploadModal.tsx`
|
||||
- [ ] Crear `apps/web/components/sat/SyncStatus.tsx`
|
||||
- [ ] Crear `apps/web/components/sat/SyncHistory.tsx`
|
||||
|
||||
### 8.2 Página de Configuración
|
||||
- [ ] Crear `apps/web/app/(dashboard)/configuracion/sat/page.tsx`
|
||||
- [ ] Integrar componentes
|
||||
- [ ] Conectar con API
|
||||
|
||||
### 8.3 API Client
|
||||
- [ ] Agregar métodos en `apps/web/lib/api.ts`
|
||||
- [ ] uploadFiel()
|
||||
- [ ] getFielStatus()
|
||||
- [ ] deleteFiel()
|
||||
- [ ] startSync()
|
||||
- [ ] getSyncStatus()
|
||||
- [ ] getSyncHistory()
|
||||
|
||||
## Fase 9: Testing y Validación
|
||||
|
||||
### 9.1 Tests
|
||||
- [ ] Tests unitarios para servicios de criptografía
|
||||
- [ ] Tests unitarios para parser de XML
|
||||
- [ ] Tests de integración para flujo completo
|
||||
- [ ] Test con FIEL de prueba del SAT
|
||||
|
||||
### 9.2 Validación
|
||||
- [ ] Probar carga de FIEL
|
||||
- [ ] Probar sincronización manual
|
||||
- [ ] Probar job programado
|
||||
- [ ] Verificar CFDIs descargados
|
||||
|
||||
## Orden de Implementación
|
||||
|
||||
```
|
||||
Fase 1 (BD)
|
||||
│
|
||||
▼
|
||||
Fase 2 (Crypto + FIEL)
|
||||
│
|
||||
▼
|
||||
Fase 3 (Auth + Download SAT)
|
||||
│
|
||||
▼
|
||||
Fase 4 (Parser)
|
||||
│
|
||||
▼
|
||||
Fase 5 (Orquestador)
|
||||
│
|
||||
▼
|
||||
Fase 6 (Cron Job)
|
||||
│
|
||||
▼
|
||||
Fase 7 (API)
|
||||
│
|
||||
▼
|
||||
Fase 8 (Frontend)
|
||||
│
|
||||
▼
|
||||
Fase 9 (Testing)
|
||||
```
|
||||
|
||||
## Archivos a Crear/Modificar
|
||||
|
||||
### Nuevos Archivos (16)
|
||||
```
|
||||
apps/api/src/services/sat/sat-crypto.service.ts
|
||||
apps/api/src/services/sat/sat-auth.service.ts
|
||||
apps/api/src/services/sat/sat-download.service.ts
|
||||
apps/api/src/services/sat/sat-parser.service.ts
|
||||
apps/api/src/services/sat/sat.service.ts
|
||||
apps/api/src/services/fiel.service.ts
|
||||
apps/api/src/controllers/fiel.controller.ts
|
||||
apps/api/src/controllers/sat.controller.ts
|
||||
apps/api/src/routes/fiel.routes.ts
|
||||
apps/api/src/routes/sat.routes.ts
|
||||
apps/api/src/jobs/sat-sync.job.ts
|
||||
packages/shared/src/types/sat.ts
|
||||
apps/web/components/sat/FielUploadModal.tsx
|
||||
apps/web/components/sat/SyncStatus.tsx
|
||||
apps/web/components/sat/SyncHistory.tsx
|
||||
apps/web/app/(dashboard)/configuracion/sat/page.tsx
|
||||
```
|
||||
|
||||
### Archivos a Modificar (5)
|
||||
```
|
||||
apps/api/prisma/schema.prisma
|
||||
apps/api/src/app.ts
|
||||
apps/api/src/index.ts
|
||||
packages/shared/src/index.ts
|
||||
apps/web/lib/api.ts
|
||||
```
|
||||
|
||||
## Dependencias a Instalar
|
||||
|
||||
```bash
|
||||
# En apps/api
|
||||
pnpm add @nodecfdi/credentials @nodecfdi/cfdi-core node-forge fast-xml-parser adm-zip node-cron
|
||||
|
||||
# Tipos
|
||||
pnpm add -D @types/node-forge @types/node-cron
|
||||
```
|
||||
|
||||
## Estimación por Fase
|
||||
|
||||
| Fase | Descripción | Complejidad |
|
||||
|------|-------------|-------------|
|
||||
| 1 | Base de datos | Baja |
|
||||
| 2 | Crypto + FIEL | Media |
|
||||
| 3 | Comunicación SAT | Alta |
|
||||
| 4 | Parser | Media |
|
||||
| 5 | Orquestador | Alta |
|
||||
| 6 | Cron Job | Baja |
|
||||
| 7 | API | Media |
|
||||
| 8 | Frontend | Media |
|
||||
| 9 | Testing | Media |
|
||||
|
||||
@@ -8,6 +8,7 @@ export * from './types/impuestos';
|
||||
export * from './types/alertas';
|
||||
export * from './types/reportes';
|
||||
export * from './types/calendario';
|
||||
export * from './types/sat';
|
||||
|
||||
// Constants
|
||||
export * from './constants/plans';
|
||||
|
||||
132
packages/shared/src/types/sat.ts
Normal file
132
packages/shared/src/types/sat.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
// ============================================
|
||||
// FIEL (e.firma) Types
|
||||
// ============================================
|
||||
|
||||
export interface FielUploadRequest {
|
||||
cerFile: string; // Base64
|
||||
keyFile: string; // Base64
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface FielStatus {
|
||||
configured: boolean;
|
||||
rfc?: string;
|
||||
serialNumber?: string;
|
||||
validFrom?: string;
|
||||
validUntil?: string;
|
||||
isExpired?: boolean;
|
||||
daysUntilExpiration?: number;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SAT Sync Types
|
||||
// ============================================
|
||||
|
||||
export type SatSyncType = 'initial' | 'daily';
|
||||
export type SatSyncStatus = 'pending' | 'running' | 'completed' | 'failed';
|
||||
export type CfdiSyncType = 'emitidos' | 'recibidos';
|
||||
|
||||
export interface SatSyncJob {
|
||||
id: string;
|
||||
tenantId: string;
|
||||
type: SatSyncType;
|
||||
status: SatSyncStatus;
|
||||
dateFrom: string;
|
||||
dateTo: string;
|
||||
cfdiType?: CfdiSyncType;
|
||||
satRequestId?: string;
|
||||
satPackageIds: string[];
|
||||
cfdisFound: number;
|
||||
cfdisDownloaded: number;
|
||||
cfdisInserted: number;
|
||||
cfdisUpdated: number;
|
||||
progressPercent: number;
|
||||
errorMessage?: string;
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
createdAt: string;
|
||||
retryCount: number;
|
||||
}
|
||||
|
||||
export interface SatSyncStatusResponse {
|
||||
hasActiveSync: boolean;
|
||||
currentJob?: SatSyncJob;
|
||||
lastCompletedJob?: SatSyncJob;
|
||||
totalCfdisSynced: number;
|
||||
}
|
||||
|
||||
export interface SatSyncHistoryResponse {
|
||||
jobs: SatSyncJob[];
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface StartSyncRequest {
|
||||
type?: SatSyncType;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
}
|
||||
|
||||
export interface StartSyncResponse {
|
||||
jobId: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SAT Web Service Types
|
||||
// ============================================
|
||||
|
||||
export interface SatAuthResponse {
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}
|
||||
|
||||
export interface SatDownloadRequest {
|
||||
rfcSolicitante: string;
|
||||
fechaInicio: Date;
|
||||
fechaFin: Date;
|
||||
tipoSolicitud: 'CFDI' | 'Metadata';
|
||||
tipoComprobante?: 'I' | 'E' | 'T' | 'N' | 'P';
|
||||
rfcEmisor?: string;
|
||||
rfcReceptor?: string;
|
||||
}
|
||||
|
||||
export interface SatDownloadRequestResponse {
|
||||
idSolicitud: string;
|
||||
codEstatus: string;
|
||||
mensaje: string;
|
||||
}
|
||||
|
||||
export interface SatVerifyResponse {
|
||||
codEstatus: string;
|
||||
estadoSolicitud: number; // 1=Aceptada, 2=EnProceso, 3=Terminada, 4=Error, 5=Rechazada, 6=Vencida
|
||||
codigoEstadoSolicitud: string;
|
||||
numeroCfdis: number;
|
||||
mensaje: string;
|
||||
paquetes: string[];
|
||||
}
|
||||
|
||||
export interface SatPackageResponse {
|
||||
paquete: string; // Base64 ZIP
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SAT Error Codes
|
||||
// ============================================
|
||||
|
||||
export const SAT_STATUS_CODES: Record<string, string> = {
|
||||
'5000': 'Solicitud recibida con éxito',
|
||||
'5002': 'Se agotó el límite de solicitudes',
|
||||
'5004': 'No se encontraron CFDIs',
|
||||
'5005': 'Solicitud duplicada',
|
||||
};
|
||||
|
||||
export const SAT_REQUEST_STATUS: Record<number, string> = {
|
||||
1: 'Aceptada',
|
||||
2: 'En proceso',
|
||||
3: 'Terminada',
|
||||
4: 'Error',
|
||||
5: 'Rechazada',
|
||||
6: 'Vencida',
|
||||
};
|
||||
62
pnpm-lock.yaml
generated
62
pnpm-lock.yaml
generated
@@ -20,6 +20,9 @@ importers:
|
||||
'@horux/shared':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/shared
|
||||
'@nodecfdi/credentials':
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0(luxon@3.7.2)
|
||||
'@prisma/client':
|
||||
specifier: ^5.22.0
|
||||
version: 5.22.0(prisma@5.22.0)
|
||||
@@ -44,6 +47,9 @@ importers:
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.3
|
||||
node-forge:
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3
|
||||
zod:
|
||||
specifier: ^3.23.0
|
||||
version: 3.25.76
|
||||
@@ -63,6 +69,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.7
|
||||
'@types/node-forge':
|
||||
specifier: ^1.3.14
|
||||
version: 1.3.14
|
||||
prisma:
|
||||
specifier: ^5.22.0
|
||||
version: 5.22.0
|
||||
@@ -439,6 +448,20 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@nodecfdi/base-converter@1.0.7':
|
||||
resolution: {integrity: sha512-YoWtdhCPB86W+2TpXrZ1yXzehNC2sEFCB0vw4XtnHKdtw6pKxKyDT2qQf4TqICROp0IZNNKunFDw3EhcoR41Tw==}
|
||||
engines: {node: '>=18 <=22 || ^16'}
|
||||
|
||||
'@nodecfdi/credentials@3.2.0':
|
||||
resolution: {integrity: sha512-knZE8kIrIib27M/tcUQRgvnObMd7oR9EKZTSdBSHXW/5Pw6UB23v0ruUAJSFY0789J3OLfKaIVRXBG2I+q9ZTA==}
|
||||
engines: {node: '>=18 <=22 || ^16'}
|
||||
peerDependencies:
|
||||
'@types/luxon': 3.4.2
|
||||
luxon: ^3.5.0
|
||||
peerDependenciesMeta:
|
||||
'@types/luxon':
|
||||
optional: true
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -989,6 +1012,9 @@ packages:
|
||||
'@types/ms@2.1.0':
|
||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||
|
||||
'@types/node-forge@1.3.14':
|
||||
resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
|
||||
|
||||
'@types/node@14.18.63':
|
||||
resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==}
|
||||
|
||||
@@ -1018,6 +1044,10 @@ packages:
|
||||
'@types/serve-static@2.2.0':
|
||||
resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==}
|
||||
|
||||
'@vilic/node-forge@1.3.2-5':
|
||||
resolution: {integrity: sha512-8GVr3S/nmLKL7QI7RYhVIcz3PuT/fxfkQLuh/F1CaT+/3QgI14RqiJkcKIni7h9u4ySbQGiGvm4XbNxRBJin4g==}
|
||||
engines: {node: '>= 6.13.0'}
|
||||
|
||||
accepts@1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -1663,6 +1693,10 @@ packages:
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
|
||||
|
||||
luxon@3.7.2:
|
||||
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
math-intrinsics@1.1.0:
|
||||
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -1749,6 +1783,10 @@ packages:
|
||||
sass:
|
||||
optional: true
|
||||
|
||||
node-forge@1.3.3:
|
||||
resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==}
|
||||
engines: {node: '>= 6.13.0'}
|
||||
|
||||
node-releases@2.0.27:
|
||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||
|
||||
@@ -2140,6 +2178,9 @@ packages:
|
||||
ts-interface-checker@0.1.13:
|
||||
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
|
||||
|
||||
ts-mixer@6.0.4:
|
||||
resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
@@ -2444,6 +2485,15 @@ snapshots:
|
||||
'@next/swc-win32-x64-msvc@14.2.33':
|
||||
optional: true
|
||||
|
||||
'@nodecfdi/base-converter@1.0.7': {}
|
||||
|
||||
'@nodecfdi/credentials@3.2.0(luxon@3.7.2)':
|
||||
dependencies:
|
||||
'@nodecfdi/base-converter': 1.0.7
|
||||
'@vilic/node-forge': 1.3.2-5
|
||||
luxon: 3.7.2
|
||||
ts-mixer: 6.0.4
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
dependencies:
|
||||
'@nodelib/fs.stat': 2.0.5
|
||||
@@ -2988,6 +3038,10 @@ snapshots:
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
'@types/node-forge@1.3.14':
|
||||
dependencies:
|
||||
'@types/node': 22.19.7
|
||||
|
||||
'@types/node@14.18.63': {}
|
||||
|
||||
'@types/node@22.19.7':
|
||||
@@ -3018,6 +3072,8 @@ snapshots:
|
||||
'@types/http-errors': 2.0.5
|
||||
'@types/node': 22.19.7
|
||||
|
||||
'@vilic/node-forge@1.3.2-5': {}
|
||||
|
||||
accepts@1.3.8:
|
||||
dependencies:
|
||||
mime-types: 2.1.35
|
||||
@@ -3717,6 +3773,8 @@ snapshots:
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
|
||||
luxon@3.7.2: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
|
||||
media-typer@0.3.0: {}
|
||||
@@ -3793,6 +3851,8 @@ snapshots:
|
||||
- '@babel/core'
|
||||
- babel-plugin-macros
|
||||
|
||||
node-forge@1.3.3: {}
|
||||
|
||||
node-releases@2.0.27: {}
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
@@ -4207,6 +4267,8 @@ snapshots:
|
||||
|
||||
ts-interface-checker@0.1.13: {}
|
||||
|
||||
ts-mixer@6.0.4: {}
|
||||
|
||||
tslib@2.8.1: {}
|
||||
|
||||
tsx@4.21.0:
|
||||
|
||||
Reference in New Issue
Block a user