@devlas/dte-sii 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BoletaService.js +109 -0
- package/CAF.js +173 -0
- package/CafSolicitor.js +380 -0
- package/Certificado.js +123 -0
- package/ConsumoFolio.js +376 -0
- package/DTE.js +399 -0
- package/EnviadorSII.js +1304 -0
- package/Envio.js +196 -0
- package/FolioRegistry.js +553 -0
- package/FolioService.js +703 -0
- package/LICENSE +27 -0
- package/LibroBase.js +134 -0
- package/LibroCompraVenta.js +205 -0
- package/LibroGuia.js +225 -0
- package/README.md +239 -0
- package/Signer.js +94 -0
- package/SiiCertificacion.js +1189 -0
- package/SiiPortalAuth.js +460 -0
- package/SiiSession.js +499 -0
- package/cert/BoletaCert.js +731 -0
- package/cert/CertFolioHelper.js +185 -0
- package/cert/CertRunner.js +2658 -0
- package/cert/ConfigLoader.js +133 -0
- package/cert/IntercambioCert.js +429 -0
- package/cert/LibroCompras.js +359 -0
- package/cert/LibroGuias.js +171 -0
- package/cert/LibroVentas.js +153 -0
- package/cert/MuestrasImpresas.js +676 -0
- package/cert/SetBase.js +321 -0
- package/cert/SetBasico.js +413 -0
- package/cert/SetCompra.js +472 -0
- package/cert/SetExenta.js +490 -0
- package/cert/SetGuia.js +283 -0
- package/cert/SetParser.js +1184 -0
- package/cert/SetsProvider.js +499 -0
- package/cert/Simulacion.js +521 -0
- package/cert/comunaOficina.js +460 -0
- package/cert/index.js +124 -0
- package/cert/types.js +330 -0
- package/dte-sii.d.ts +458 -0
- package/index.js +428 -0
- package/package.json +48 -0
- package/utils/c14n.js +275 -0
- package/utils/calculo.js +396 -0
- package/utils/config.js +276 -0
- package/utils/constants.js +302 -0
- package/utils/emisor.js +174 -0
- package/utils/endpoints.js +225 -0
- package/utils/error.js +235 -0
- package/utils/index.js +339 -0
- package/utils/logger.js +239 -0
- package/utils/pfx.js +203 -0
- package/utils/receptor.js +218 -0
- package/utils/referencia.js +169 -0
- package/utils/resolucion.js +119 -0
- package/utils/rut.js +169 -0
- package/utils/sanitize.js +124 -0
- package/utils/tokenCache.js +214 -0
- package/utils/xml.js +358 -0
- package/utils.js +4 -0
package/BoletaService.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* BoletaService
|
|
5
|
+
*
|
|
6
|
+
* Servicio simplificado para crear boletas electrónicas
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const Certificado = require('./Certificado');
|
|
10
|
+
const CAF = require('./CAF');
|
|
11
|
+
const DTE = require('./DTE');
|
|
12
|
+
const {
|
|
13
|
+
sanitizeSiiText,
|
|
14
|
+
buildDetalle,
|
|
15
|
+
calcularTotalesDesdeItems,
|
|
16
|
+
buildReceptorConsumidorFinal,
|
|
17
|
+
dteError,
|
|
18
|
+
ERROR_CODES,
|
|
19
|
+
createScopedLogger,
|
|
20
|
+
TIPOS_DTE,
|
|
21
|
+
esBoleta,
|
|
22
|
+
} = require('./utils');
|
|
23
|
+
|
|
24
|
+
const log = createScopedLogger('BoletaService');
|
|
25
|
+
|
|
26
|
+
class BoletaService {
|
|
27
|
+
constructor(config) {
|
|
28
|
+
this.config = config;
|
|
29
|
+
this.certificado = null;
|
|
30
|
+
this.caf = null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cargar certificado desde buffer PFX
|
|
35
|
+
*/
|
|
36
|
+
cargarCertificado(pfxBuffer, password) {
|
|
37
|
+
this.certificado = new Certificado(pfxBuffer, password);
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Cargar CAF desde string XML
|
|
43
|
+
*/
|
|
44
|
+
cargarCAF(xmlContent) {
|
|
45
|
+
this.caf = new CAF(xmlContent);
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Crear y firmar una boleta
|
|
51
|
+
* @param {Array} items - Items de la boleta [{NmbItem, QtyItem, PrcItem}]
|
|
52
|
+
* @param {number} folio - Número de folio
|
|
53
|
+
* @param {Object} receptor - Datos del receptor (opcional)
|
|
54
|
+
* @returns {Object} - Resultado con XML firmado
|
|
55
|
+
*/
|
|
56
|
+
crearBoleta(items, folio, receptor = null) {
|
|
57
|
+
if (!this.certificado) {
|
|
58
|
+
throw dteError('Certificado no cargado', ERROR_CODES.CERT_INVALID);
|
|
59
|
+
}
|
|
60
|
+
if (!this.caf) {
|
|
61
|
+
throw dteError('CAF no cargado', ERROR_CODES.CAF_INVALID);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Usar utilidades centralizadas para construir detalle y calcular totales
|
|
65
|
+
const detalle = buildDetalle(items);
|
|
66
|
+
const totales = calcularTotalesDesdeItems(items);
|
|
67
|
+
|
|
68
|
+
const datos = {
|
|
69
|
+
Encabezado: {
|
|
70
|
+
IdDoc: {
|
|
71
|
+
TipoDTE: TIPOS_DTE.BOLETA,
|
|
72
|
+
Folio: folio,
|
|
73
|
+
FchEmis: new Date().toISOString().split('T')[0],
|
|
74
|
+
IndServicio: 3,
|
|
75
|
+
},
|
|
76
|
+
Emisor: {
|
|
77
|
+
RUTEmisor: this.config.rutEmisor,
|
|
78
|
+
RznSocEmisor: this.config.razonSocial,
|
|
79
|
+
GiroEmisor: this.config.giro,
|
|
80
|
+
DirOrigen: this.config.direccion,
|
|
81
|
+
CmnaOrigen: this.config.comuna,
|
|
82
|
+
},
|
|
83
|
+
Receptor: receptor || buildReceptorConsumidorFinal(),
|
|
84
|
+
Totales: {
|
|
85
|
+
MntTotal: totales.MntTotal,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
Detalle: detalle,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const dte = new DTE(datos);
|
|
92
|
+
dte.generarXML();
|
|
93
|
+
dte.timbrar(this.caf);
|
|
94
|
+
dte.firmar(this.certificado);
|
|
95
|
+
|
|
96
|
+
log.log(`✅ Boleta creada: Folio ${folio}, Monto $${totales.MntTotal}`);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
ok: true,
|
|
100
|
+
folio,
|
|
101
|
+
tipo_dte: TIPOS_DTE.BOLETA,
|
|
102
|
+
fecha: datos.Encabezado.IdDoc.FchEmis,
|
|
103
|
+
monto_total: totales.MntTotal,
|
|
104
|
+
xml: dte.getXML(),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = BoletaService;
|
package/CAF.js
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* CAF (Código de Autorización de Folios)
|
|
5
|
+
*
|
|
6
|
+
* Maneja los archivos CAF del SII para timbraje de documentos
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const forge = require('node-forge');
|
|
10
|
+
const {
|
|
11
|
+
cafError,
|
|
12
|
+
ERROR_CODES,
|
|
13
|
+
createScopedLogger,
|
|
14
|
+
parseXml,
|
|
15
|
+
buildXml,
|
|
16
|
+
IDK_CERTIFICACION,
|
|
17
|
+
getNombreDte,
|
|
18
|
+
} = require('./utils');
|
|
19
|
+
|
|
20
|
+
const log = createScopedLogger('CAF');
|
|
21
|
+
|
|
22
|
+
class CAF {
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} xmlContent - Contenido XML del archivo CAF
|
|
25
|
+
* @throws {DteSiiError} Si el CAF es inválido
|
|
26
|
+
*/
|
|
27
|
+
constructor(xmlContent) {
|
|
28
|
+
if (!xmlContent || typeof xmlContent !== 'string') {
|
|
29
|
+
throw cafError('CAF XML content es requerido', ERROR_CODES.CAF_INVALID, { xmlContent });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Usar parser centralizado
|
|
33
|
+
try {
|
|
34
|
+
this.data = parseXml(xmlContent);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
throw cafError(`Error parseando CAF XML: ${err.message}`, ERROR_CODES.CAF_INVALID, { originalError: err });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Validar estructura
|
|
40
|
+
if (!this.data.AUTORIZACION) {
|
|
41
|
+
throw cafError('CAF inválido: falta elemento AUTORIZACION', ERROR_CODES.CAF_INVALID);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.autorizacion = this.data.AUTORIZACION;
|
|
45
|
+
this.caf = this.autorizacion.CAF;
|
|
46
|
+
|
|
47
|
+
if (!this.caf) {
|
|
48
|
+
throw cafError('CAF inválido: falta elemento CAF', ERROR_CODES.CAF_INVALID);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.da = this.caf.DA;
|
|
52
|
+
|
|
53
|
+
if (!this.da) {
|
|
54
|
+
throw cafError('CAF inválido: falta elemento DA (Datos de Autorización)', ERROR_CODES.CAF_INVALID);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Propiedades principales
|
|
58
|
+
this.tipo = parseInt(this.da.TD, 10);
|
|
59
|
+
this.folioDesde = parseInt(this.da.RNG?.D, 10);
|
|
60
|
+
this.folioHasta = parseInt(this.da.RNG?.H, 10);
|
|
61
|
+
this.rutEmisor = this.da.RE;
|
|
62
|
+
|
|
63
|
+
if (isNaN(this.folioDesde) || isNaN(this.folioHasta)) {
|
|
64
|
+
throw cafError('CAF inválido: rango de folios no válido', ERROR_CODES.CAF_INVALID, {
|
|
65
|
+
folioDesde: this.da.RNG?.D,
|
|
66
|
+
folioHasta: this.da.RNG?.H,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Clave privada del CAF
|
|
71
|
+
if (!this.autorizacion.RSASK) {
|
|
72
|
+
throw cafError('CAF inválido: falta clave privada RSASK', ERROR_CODES.CAF_INVALID);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
this.privateKeyPem = this.autorizacion.RSASK.trim();
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
this.privateKey = forge.pki.privateKeyFromPem(this.privateKeyPem);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
throw cafError(`Error cargando clave privada del CAF: ${err.message}`, ERROR_CODES.CAF_INVALID, { originalError: err });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// CAF XML original (el SII exige formato exacto)
|
|
84
|
+
this._originalCafXml = this._extractCafXml(xmlContent);
|
|
85
|
+
|
|
86
|
+
log.debug(`CAF cargado: Tipo ${this.tipo}, Folios ${this.folioDesde}-${this.folioHasta}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Extraer el CAF XML exacto del archivo original
|
|
91
|
+
*/
|
|
92
|
+
_extractCafXml(xmlContent) {
|
|
93
|
+
const cafMatch = xmlContent.match(/<CAF[^>]*>[\s\S]*?<\/CAF>/);
|
|
94
|
+
if (cafMatch) {
|
|
95
|
+
// Normalizar: quitar espacios entre tags
|
|
96
|
+
return cafMatch[0].replace(/>\s+</g, '><');
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Getters
|
|
102
|
+
getRutEmisor() { return this.da.RE; }
|
|
103
|
+
getTipoDTE() { return parseInt(this.da.TD, 10); }
|
|
104
|
+
getFolioDesde() { return parseInt(this.da.RNG.D, 10); }
|
|
105
|
+
getFolioHasta() { return parseInt(this.da.RNG.H, 10); }
|
|
106
|
+
getIDK() { return parseInt(this.da.IDK, 10); }
|
|
107
|
+
esCertificacion() { return this.getIDK() === IDK_CERTIFICACION; }
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Obtener nombre del tipo de DTE
|
|
111
|
+
* @param {boolean} [corto=false] - Si usar nombre corto
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
getNombreTipoDTE(corto = false) {
|
|
115
|
+
return getNombreDte(this.tipo, corto);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Obtener CAF XML para insertar en TED
|
|
120
|
+
*/
|
|
121
|
+
getCafXml() {
|
|
122
|
+
if (this._originalCafXml) {
|
|
123
|
+
return this._originalCafXml;
|
|
124
|
+
}
|
|
125
|
+
// Fallback: reconstruir usando builder centralizado
|
|
126
|
+
return buildXml({ CAF: this.caf });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Firmar datos con la clave privada del CAF (para TED)
|
|
131
|
+
* IMPORTANTE: Usamos 'latin1' porque así interpreta forge los bytes
|
|
132
|
+
* y debe coincidir con lo que se pasa al PDF417
|
|
133
|
+
*/
|
|
134
|
+
sign(data) {
|
|
135
|
+
const md = forge.md.sha1.create();
|
|
136
|
+
md.update(data, 'latin1');
|
|
137
|
+
const signature = this.privateKey.sign(md);
|
|
138
|
+
return forge.util.encode64(signature);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Verificar si un folio está dentro del rango autorizado
|
|
143
|
+
* @param {number} folio - Número de folio
|
|
144
|
+
* @returns {boolean} True si es válido
|
|
145
|
+
*/
|
|
146
|
+
isFolioValido(folio) {
|
|
147
|
+
return folio >= this.folioDesde && folio <= this.folioHasta;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Validar folio y lanzar error si no es válido
|
|
152
|
+
* @param {number} folio - Número de folio
|
|
153
|
+
* @throws {DteSiiError} Si el folio está fuera de rango
|
|
154
|
+
*/
|
|
155
|
+
validarFolio(folio) {
|
|
156
|
+
if (!this.isFolioValido(folio)) {
|
|
157
|
+
throw cafError(
|
|
158
|
+
`Folio ${folio} fuera de rango autorizado (${this.folioDesde}-${this.folioHasta})`,
|
|
159
|
+
ERROR_CODES.FOLIO_OUT_OF_RANGE,
|
|
160
|
+
{ folio, folioDesde: this.folioDesde, folioHasta: this.folioHasta, tipo: this.tipo }
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Obtener folios disponibles
|
|
167
|
+
*/
|
|
168
|
+
getFoliosDisponibles() {
|
|
169
|
+
return this.folioHasta - this.folioDesde + 1;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = CAF;
|
package/CafSolicitor.js
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* CafSolicitor.js - Solicitador de CAFs al SII
|
|
5
|
+
*
|
|
6
|
+
* Módulo interno del core para solicitar Códigos de Autorización de Folios (CAF)
|
|
7
|
+
* directamente al SII. Usa SiiSession para evitar duplicación de código.
|
|
8
|
+
*
|
|
9
|
+
* Migrado desde: scripts/cert/test-caf-solicitar.js
|
|
10
|
+
* Refactorizado: Usa SiiSession para HTTP y utilidades
|
|
11
|
+
*
|
|
12
|
+
* @module CafSolicitor
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const SiiSession = require('./SiiSession');
|
|
18
|
+
const { splitRut } = require('./utils/rut');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Clase para solicitar CAFs al SII
|
|
22
|
+
*/
|
|
23
|
+
class CafSolicitor {
|
|
24
|
+
/**
|
|
25
|
+
* @param {Object} options - Opciones de configuración
|
|
26
|
+
* @param {string} options.ambiente - 'certificacion' o 'produccion'
|
|
27
|
+
* @param {string} options.rutEmisor - RUT del emisor (ej: 76192083-9)
|
|
28
|
+
* @param {string} options.pfxPath - Ruta absoluta al certificado PFX
|
|
29
|
+
* @param {string} options.pfxPassword - Contraseña del certificado
|
|
30
|
+
* @param {string} [options.baseDir] - Directorio base para guardar archivos
|
|
31
|
+
* @param {string} [options.sessionPath] - Ruta al archivo de sesión compartida
|
|
32
|
+
* @param {string} [options.runStamp] - Timestamp de la ejecución
|
|
33
|
+
*/
|
|
34
|
+
constructor(options = {}) {
|
|
35
|
+
if (!options.ambiente) {
|
|
36
|
+
throw new Error('CafSolicitor: options.ambiente es obligatorio');
|
|
37
|
+
}
|
|
38
|
+
if (!options.rutEmisor) {
|
|
39
|
+
throw new Error('CafSolicitor: options.rutEmisor es obligatorio');
|
|
40
|
+
}
|
|
41
|
+
if (!options.pfxPath) {
|
|
42
|
+
throw new Error('CafSolicitor: options.pfxPath es obligatorio');
|
|
43
|
+
}
|
|
44
|
+
if (!options.pfxPassword) {
|
|
45
|
+
throw new Error('CafSolicitor: options.pfxPassword es obligatorio');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.ambiente = options.ambiente.toLowerCase();
|
|
49
|
+
this.rutEmisor = options.rutEmisor;
|
|
50
|
+
this.baseDir = options.baseDir || path.resolve(__dirname, '..', '..');
|
|
51
|
+
this.sessionPath = options.sessionPath || null;
|
|
52
|
+
this.runStamp = options.runStamp || new Date().toISOString().replace(/[:.]/g, '-');
|
|
53
|
+
|
|
54
|
+
// Crear SiiSession para manejar HTTP y cookies
|
|
55
|
+
this.session = new SiiSession({
|
|
56
|
+
ambiente: this.ambiente,
|
|
57
|
+
pfxPath: options.pfxPath,
|
|
58
|
+
pfxPassword: options.pfxPassword,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Cargar sesión compartida si existe
|
|
62
|
+
if (this.sessionPath) {
|
|
63
|
+
const loaded = this.session.loadSession(this.sessionPath);
|
|
64
|
+
if (loaded) {
|
|
65
|
+
console.log('[CafSolicitor] ✓ Usando sesión compartida');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Crea directorio para debug de esta solicitud
|
|
72
|
+
* @private
|
|
73
|
+
*/
|
|
74
|
+
_getDebugDir(tipoDte) {
|
|
75
|
+
const rutClean = String(this.rutEmisor).replace(/\./g, '').toUpperCase();
|
|
76
|
+
const runDir = path.join(this.baseDir, 'debug', 'auto-caf', rutClean, this.runStamp, String(tipoDte));
|
|
77
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
78
|
+
return runDir;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Guarda respuesta de debug
|
|
83
|
+
* @private
|
|
84
|
+
*/
|
|
85
|
+
_saveDebug(debugDir, filename, content) {
|
|
86
|
+
const filePath = path.join(debugDir, filename);
|
|
87
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
88
|
+
console.log(`${filename}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Extrae información del CAF desde XML
|
|
93
|
+
* @private
|
|
94
|
+
*/
|
|
95
|
+
_extractCafInfo(xml, tipoDte) {
|
|
96
|
+
const tdMatch = xml.match(/<TD>(\d+)<\/TD>/i);
|
|
97
|
+
const dMatch = xml.match(/<D>(\d+)<\/D>/i);
|
|
98
|
+
const hMatch = xml.match(/<H>(\d+)<\/H>/i);
|
|
99
|
+
const faMatch = xml.match(/<FA>(\d{4}-\d{2}-\d{2})<\/FA>/i);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
tipoDte: tdMatch ? tdMatch[1] : tipoDte,
|
|
103
|
+
folioDesde: dMatch ? dMatch[1] : 'unknown',
|
|
104
|
+
folioHasta: hMatch ? hMatch[1] : 'unknown',
|
|
105
|
+
fechaAutorizacion: faMatch ? faMatch[1] : new Date().toISOString().slice(0, 10),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Guarda el CAF en ubicación organizada
|
|
111
|
+
* @private
|
|
112
|
+
*/
|
|
113
|
+
_saveCafOrganized(xml, tipoDte) {
|
|
114
|
+
const info = this._extractCafInfo(xml, tipoDte);
|
|
115
|
+
const rutClean = this.rutEmisor.replace(/\./g, '').toUpperCase();
|
|
116
|
+
|
|
117
|
+
const cafDir = path.join(
|
|
118
|
+
this.baseDir, 'debug', 'caf', this.ambiente,
|
|
119
|
+
rutClean, String(info.tipoDte), this.runStamp
|
|
120
|
+
);
|
|
121
|
+
fs.mkdirSync(cafDir, { recursive: true });
|
|
122
|
+
|
|
123
|
+
const cafFileName = `caf-${info.tipoDte}-${info.folioDesde}-${info.folioHasta}.xml`;
|
|
124
|
+
const cafPath = path.join(cafDir, cafFileName);
|
|
125
|
+
fs.writeFileSync(cafPath, xml, 'utf-8');
|
|
126
|
+
|
|
127
|
+
console.log(`✅ CAF guardado: ${cafFileName}`);
|
|
128
|
+
console.log(` Ruta: ${cafPath}`);
|
|
129
|
+
|
|
130
|
+
return cafPath;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Detecta si la respuesta del SII requiere autenticación con certificado.
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
_requiresAuthentication(responseBody = '') {
|
|
138
|
+
if (!responseBody || typeof responseBody !== 'string') return false;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
responseBody.includes('Autenticaci') ||
|
|
142
|
+
responseBody.includes('autInicioDTE.cgi') ||
|
|
143
|
+
responseBody.includes('cgi_AUT2000') ||
|
|
144
|
+
responseBody.includes('302 Found')
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Solicita un CAF al SII
|
|
150
|
+
* @param {Object} params - Parámetros
|
|
151
|
+
* @param {number} params.tipoDte - Tipo de DTE (33, 34, 39, 56, 61, etc.)
|
|
152
|
+
* @param {number} [params.cantidad=1] - Cantidad de folios a solicitar
|
|
153
|
+
* @returns {Promise<Object>} - { success, cafPath, xml, error }
|
|
154
|
+
*/
|
|
155
|
+
async solicitar({ tipoDte, cantidad = 1 }) {
|
|
156
|
+
const { numero: rut, dv } = splitRut(this.rutEmisor);
|
|
157
|
+
const debugDir = this._getDebugDir(tipoDte);
|
|
158
|
+
|
|
159
|
+
console.log('─'.repeat(60));
|
|
160
|
+
console.log(`[CafSolicitor] Solicitando CAF tipo ${tipoDte} x${cantidad}`);
|
|
161
|
+
console.log(` RUT: ${this.rutEmisor} | Ambiente: ${this.ambiente}`);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
// Paso 1: POST inicial a of_solicita_folios
|
|
165
|
+
const fields = {
|
|
166
|
+
RUT_EMP: rut,
|
|
167
|
+
DV_EMP: dv,
|
|
168
|
+
COD_DOCTO: tipoDte,
|
|
169
|
+
CANTIDAD: cantidad,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
let response = await this.session.submitForm('/cvc_cgi/dte/of_solicita_folios', fields);
|
|
173
|
+
|
|
174
|
+
// Manejar autenticación si es necesaria (incluye 302 a autInicioDTE)
|
|
175
|
+
if (this._requiresAuthentication(response.body)) {
|
|
176
|
+
const authResult = await this.session.ensureSession('/cvc_cgi/dte/of_solicita_folios');
|
|
177
|
+
if (authResult.body) {
|
|
178
|
+
// Reintentar después de autenticación
|
|
179
|
+
response = await this.session.submitForm('/cvc_cgi/dte/of_solicita_folios', fields);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Guardar sesión para reutilización
|
|
183
|
+
if (this.sessionPath) {
|
|
184
|
+
this.session.saveSession(this.sessionPath);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Procesar flujo multi-paso del SII
|
|
189
|
+
response = await this._processMultiStepFlow(response, rut, dv, tipoDte, cantidad, debugDir);
|
|
190
|
+
|
|
191
|
+
// Guardar respuesta final
|
|
192
|
+
this._saveDebug(debugDir, `caf-final-${this.runStamp}.html`, response.body || '');
|
|
193
|
+
|
|
194
|
+
// Verificar si obtuvimos el CAF
|
|
195
|
+
if (response.body && response.body.includes('<AUTORIZACION')) {
|
|
196
|
+
const cafPath = this._saveCafOrganized(response.body, tipoDte);
|
|
197
|
+
return { success: true, cafPath, xml: response.body };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (response.body && response.body.includes('Autenticaci')) {
|
|
201
|
+
return { success: false, error: 'El SII devolvió página de autenticación' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { success: false, error: 'No se obtuvo CAF en la respuesta' };
|
|
205
|
+
|
|
206
|
+
} catch (err) {
|
|
207
|
+
console.error(`[CafSolicitor] Error: ${err.message}`);
|
|
208
|
+
return { success: false, error: err.message };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Procesa el flujo multi-paso del SII para obtener CAF
|
|
214
|
+
* @private
|
|
215
|
+
*/
|
|
216
|
+
async _processMultiStepFlow(response, rut, dv, tipoDte, cantidad, debugDir) {
|
|
217
|
+
let currentHtml = response.body || '';
|
|
218
|
+
|
|
219
|
+
// Paso 2: of_solicita_folios_dcto
|
|
220
|
+
if (currentHtml.includes('of_solicita_folios_dcto')) {
|
|
221
|
+
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_solicita_folios_dcto';
|
|
222
|
+
const hiddenInputs = SiiSession.extractInputValues(currentHtml);
|
|
223
|
+
|
|
224
|
+
const step2Fields = {
|
|
225
|
+
...hiddenInputs,
|
|
226
|
+
RUT_EMP: rut,
|
|
227
|
+
DV_EMP: dv,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
response = await this.session.submitForm(formAction, step2Fields);
|
|
231
|
+
currentHtml = response.body || '';
|
|
232
|
+
this._saveDebug(debugDir, `step2-${this.runStamp}.html`, currentHtml);
|
|
233
|
+
|
|
234
|
+
// Selección de tipo de documento
|
|
235
|
+
if (currentHtml.includes('COD_DOCTO')) {
|
|
236
|
+
const selectInputs = SiiSession.extractInputValues(currentHtml);
|
|
237
|
+
const selectFields = {
|
|
238
|
+
...selectInputs,
|
|
239
|
+
RUT_EMP: rut,
|
|
240
|
+
DV_EMP: dv,
|
|
241
|
+
COD_DOCTO: tipoDte,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
response = await this.session.submitForm('/cvc_cgi/dte/of_solicita_folios_dcto', selectFields);
|
|
245
|
+
currentHtml = response.body || '';
|
|
246
|
+
this._saveDebug(debugDir, `select-${this.runStamp}.html`, currentHtml);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Paso 3: Solicitar numeración
|
|
250
|
+
response = await this._processStep3(response, rut, dv, tipoDte, cantidad, debugDir);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return response;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Procesa paso 3 y siguientes
|
|
258
|
+
* @private
|
|
259
|
+
*/
|
|
260
|
+
async _processStep3(response, rut, dv, tipoDte, cantidad, debugDir) {
|
|
261
|
+
let currentHtml = response.body || '';
|
|
262
|
+
|
|
263
|
+
const formAction3 = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_confirma_folio';
|
|
264
|
+
const inputs3 = SiiSession.extractInputValues(currentHtml);
|
|
265
|
+
|
|
266
|
+
const step3Fields = {
|
|
267
|
+
...inputs3,
|
|
268
|
+
RUT_EMP: rut,
|
|
269
|
+
DV_EMP: dv,
|
|
270
|
+
COD_DOCTO: tipoDte,
|
|
271
|
+
CANT_DOCTOS: cantidad,
|
|
272
|
+
ACEPTAR: 'Solicitar Numeración',
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
response = await this.session.submitForm(formAction3, step3Fields);
|
|
276
|
+
currentHtml = response.body || '';
|
|
277
|
+
this._saveDebug(debugDir, `step3-${this.runStamp}.html`, currentHtml);
|
|
278
|
+
|
|
279
|
+
// Confirmar folio inicial
|
|
280
|
+
if (currentHtml.includes('of_confirma_folio')) {
|
|
281
|
+
response = await this._processConfirmFolio(response, debugDir);
|
|
282
|
+
} else if (currentHtml.includes('of_genera_folio')) {
|
|
283
|
+
response = await this._processGeneraFolio(response, debugDir);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return response;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Procesa confirmación de folio
|
|
291
|
+
* @private
|
|
292
|
+
*/
|
|
293
|
+
async _processConfirmFolio(response, debugDir) {
|
|
294
|
+
let currentHtml = response.body || '';
|
|
295
|
+
|
|
296
|
+
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_confirma_folio';
|
|
297
|
+
const inputs = SiiSession.extractInputValues(currentHtml);
|
|
298
|
+
|
|
299
|
+
const fields = {
|
|
300
|
+
...inputs,
|
|
301
|
+
FOLIO_INICIAL: inputs.FOLIO_INICIAL || '1',
|
|
302
|
+
ACEPTAR: 'Confirmar Folio Inicial',
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
response = await this.session.submitForm(formAction, fields);
|
|
306
|
+
currentHtml = response.body || '';
|
|
307
|
+
this._saveDebug(debugDir, `confirm-${this.runStamp}.html`, currentHtml);
|
|
308
|
+
|
|
309
|
+
if (currentHtml.includes('of_genera_folio')) {
|
|
310
|
+
response = await this._processGeneraFolio(response, debugDir);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return response;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Procesa generación de folio
|
|
318
|
+
* @private
|
|
319
|
+
*/
|
|
320
|
+
async _processGeneraFolio(response, debugDir) {
|
|
321
|
+
let currentHtml = response.body || '';
|
|
322
|
+
|
|
323
|
+
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_genera_folio';
|
|
324
|
+
const inputs = SiiSession.extractInputValues(currentHtml);
|
|
325
|
+
|
|
326
|
+
const fields = {
|
|
327
|
+
...inputs,
|
|
328
|
+
ACEPTAR: 'Obtener Folios',
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
response = await this.session.submitForm(formAction, fields);
|
|
332
|
+
currentHtml = response.body || '';
|
|
333
|
+
this._saveDebug(debugDir, `genera-${this.runStamp}.html`, currentHtml);
|
|
334
|
+
|
|
335
|
+
// Paso final: of_genera_archivo
|
|
336
|
+
if (!currentHtml.includes('<AUTORIZACION') && currentHtml.includes('of_genera_archivo')) {
|
|
337
|
+
response = await this._processGeneraArchivo(response, debugDir);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return response;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Procesa generación de archivo CAF
|
|
345
|
+
* @private
|
|
346
|
+
*/
|
|
347
|
+
async _processGeneraArchivo(response, debugDir) {
|
|
348
|
+
let currentHtml = response.body || '';
|
|
349
|
+
|
|
350
|
+
const formAction = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_genera_archivo';
|
|
351
|
+
const inputs = SiiSession.extractInputValues(currentHtml);
|
|
352
|
+
|
|
353
|
+
const fields = {
|
|
354
|
+
...inputs,
|
|
355
|
+
ACEPTAR: 'AQUI',
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
response = await this.session.submitForm(formAction, fields);
|
|
359
|
+
currentHtml = response.body || '';
|
|
360
|
+
this._saveDebug(debugDir, `archivo-${this.runStamp}.xml`, currentHtml);
|
|
361
|
+
|
|
362
|
+
// A veces hay un paso extra
|
|
363
|
+
if (!currentHtml.includes('<AUTORIZACION') && currentHtml.includes('of_genera_archivo')) {
|
|
364
|
+
const formAction2 = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_genera_archivo';
|
|
365
|
+
const inputs2 = SiiSession.extractInputValues(currentHtml);
|
|
366
|
+
|
|
367
|
+
const fields2 = {
|
|
368
|
+
...inputs2,
|
|
369
|
+
ACEPTAR: 'AQUI',
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
response = await this.session.submitForm(formAction2, fields2);
|
|
373
|
+
this._saveDebug(debugDir, `archivo2-${this.runStamp}.xml`, response.body || '');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return response;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
module.exports = CafSolicitor;
|