@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
|
@@ -0,0 +1,731 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* BoletaCert.js
|
|
5
|
+
*
|
|
6
|
+
* Módulo para certificación de Boletas Electrónicas (tipo 39 y 41).
|
|
7
|
+
*
|
|
8
|
+
* Proceso de certificación:
|
|
9
|
+
* 1. Usuario obtiene Set de Pruebas manualmente desde SII (requiere clave tributaria)
|
|
10
|
+
* URL: https://www4.sii.cl/certBolElectDteInternet/?SET=1
|
|
11
|
+
* 2. Se genera EnvioBOLETA con todas las boletas del set
|
|
12
|
+
* 3. Se genera RCOF (ConsumoFolio) para reportar los folios usados
|
|
13
|
+
* 4. Se envían ambos documentos al SII
|
|
14
|
+
* 5. SII valida y aprueba
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
|
|
20
|
+
class BoletaCert {
|
|
21
|
+
/**
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {Object} options.certificado - Instancia de Certificado
|
|
24
|
+
* @param {Object} options.emisor - Datos del emisor { rut, razon_social, giro, direccion, comuna, ciudad, fch_resol, nro_resol }
|
|
25
|
+
* @param {string} options.ambiente - 'certificacion' | 'produccion'
|
|
26
|
+
* @param {Object} options.resolucion - { fecha, numero }
|
|
27
|
+
* @param {string} options.debugDir - Directorio para debug
|
|
28
|
+
*/
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.certificado = options.certificado;
|
|
31
|
+
this.emisor = options.emisor;
|
|
32
|
+
this.ambiente = options.ambiente || 'certificacion';
|
|
33
|
+
this.resolucion = options.resolucion;
|
|
34
|
+
this.debugDir = options.debugDir;
|
|
35
|
+
|
|
36
|
+
// Librerías (se cargan bajo demanda)
|
|
37
|
+
this._dteLib = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_lib() {
|
|
41
|
+
if (!this._dteLib) {
|
|
42
|
+
this._dteLib = require('../index');
|
|
43
|
+
}
|
|
44
|
+
return this._dteLib;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Parsea el set de pruebas desde archivo de texto
|
|
49
|
+
* @param {string} filePath - Ruta al archivo del set
|
|
50
|
+
* @returns {Object[]} Array de casos parseados
|
|
51
|
+
*/
|
|
52
|
+
parseSetPruebas(filePath) {
|
|
53
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
54
|
+
const lines = content.split('\n').map(l => l.trim());
|
|
55
|
+
|
|
56
|
+
const casos = [];
|
|
57
|
+
let currentCaso = null;
|
|
58
|
+
let inItems = false;
|
|
59
|
+
|
|
60
|
+
for (const line of lines) {
|
|
61
|
+
// Detectar inicio de caso
|
|
62
|
+
const casoMatch = line.match(/^CASO-?(\d+)/i);
|
|
63
|
+
if (casoMatch) {
|
|
64
|
+
if (currentCaso) {
|
|
65
|
+
casos.push(currentCaso);
|
|
66
|
+
}
|
|
67
|
+
currentCaso = {
|
|
68
|
+
numero: parseInt(casoMatch[1], 10),
|
|
69
|
+
items: [],
|
|
70
|
+
observaciones: []
|
|
71
|
+
};
|
|
72
|
+
inItems = false;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Detectar línea de headers de items
|
|
77
|
+
if (line.includes('Item') && line.includes('Cantidad') && line.includes('Precio')) {
|
|
78
|
+
inItems = true;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Detectar observación
|
|
83
|
+
if (line.startsWith('OBSERVACION')) {
|
|
84
|
+
const obsMatch = line.match(/OBSERVACION[ES]*:\s*"?([^"]+)"?/i);
|
|
85
|
+
if (obsMatch && currentCaso) {
|
|
86
|
+
currentCaso.observaciones.push(obsMatch[1].trim());
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Parsear item (línea con tabs separando: nombre, cantidad, precio)
|
|
92
|
+
if (inItems && currentCaso && line.length > 0 && !line.startsWith('=')) {
|
|
93
|
+
// Separar por tabs o múltiples espacios
|
|
94
|
+
const parts = line.split(/\t+|\s{2,}/).filter(p => p.trim().length > 0);
|
|
95
|
+
if (parts.length >= 3) {
|
|
96
|
+
const nombre = parts[0].trim();
|
|
97
|
+
const cantidad = parseFloat(parts[1].replace(',', '.'));
|
|
98
|
+
const precio = parseInt(parts[2].replace(/\./g, '').replace(',', ''), 10);
|
|
99
|
+
|
|
100
|
+
if (!isNaN(cantidad) && !isNaN(precio)) {
|
|
101
|
+
// Detectar si es exento por el nombre del item (ej: "item exento 2")
|
|
102
|
+
const esExento = nombre.toLowerCase().includes('exento');
|
|
103
|
+
|
|
104
|
+
currentCaso.items.push({
|
|
105
|
+
nombre,
|
|
106
|
+
cantidad,
|
|
107
|
+
precio,
|
|
108
|
+
exento: esExento
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Agregar último caso
|
|
116
|
+
if (currentCaso) {
|
|
117
|
+
casos.push(currentCaso);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return casos;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Genera boletas a partir de los casos del set
|
|
125
|
+
* @param {Object[]} casos - Casos parseados del set
|
|
126
|
+
* @param {Object} cafBoleta - CAF para boletas tipo 39
|
|
127
|
+
* @param {number} folioInicial - Folio inicial
|
|
128
|
+
* @returns {Object} { boletas: DTE[], folioUsados }
|
|
129
|
+
*/
|
|
130
|
+
async generarBoletasSet(casos, cafBoleta, folioInicial) {
|
|
131
|
+
const { DTE, CAF } = this._lib();
|
|
132
|
+
|
|
133
|
+
const boletas = [];
|
|
134
|
+
let folioActual = folioInicial;
|
|
135
|
+
|
|
136
|
+
const fechaHoy = new Date().toISOString().split('T')[0];
|
|
137
|
+
|
|
138
|
+
for (const caso of casos) {
|
|
139
|
+
// Preparar items
|
|
140
|
+
const items = caso.items.map(item => ({
|
|
141
|
+
NmbItem: item.nombre,
|
|
142
|
+
QtyItem: item.cantidad,
|
|
143
|
+
PrcItem: item.precio,
|
|
144
|
+
...(item.unidad ? { UnmdItem: item.unidad } : {}),
|
|
145
|
+
...(item.exento ? { IndExe: 1 } : {}),
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
// Verificar si el caso tiene observación sobre unidad de medida
|
|
149
|
+
const obsUnidad = caso.observaciones.find(obs =>
|
|
150
|
+
obs.toLowerCase().includes('unidad de medida')
|
|
151
|
+
);
|
|
152
|
+
if (obsUnidad) {
|
|
153
|
+
// Ejemplo: "Se debe informar en el XML Unidad de medida en Kg."
|
|
154
|
+
const unidadMatch = obsUnidad.match(/en\s+(\w+)\.?$/i);
|
|
155
|
+
if (unidadMatch && items.length > 0) {
|
|
156
|
+
items[0].UnmdItem = unidadMatch[1];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Crear DTE
|
|
161
|
+
const dteDatos = {
|
|
162
|
+
tipo: 39,
|
|
163
|
+
folio: folioActual,
|
|
164
|
+
fechaEmision: fechaHoy,
|
|
165
|
+
emisor: {
|
|
166
|
+
RUTEmisor: this.emisor.rut,
|
|
167
|
+
RznSocEmisor: this.emisor.razon_social,
|
|
168
|
+
GiroEmisor: this.emisor.giro,
|
|
169
|
+
DirOrigen: this.emisor.direccion,
|
|
170
|
+
CmnaOrigen: this.emisor.comuna,
|
|
171
|
+
CiudadOrigen: this.emisor.ciudad,
|
|
172
|
+
},
|
|
173
|
+
receptor: {
|
|
174
|
+
RUTRecep: '66666666-6', // Consumidor final
|
|
175
|
+
RznSocRecep: 'Consumidor Final',
|
|
176
|
+
DirRecep: 'Sin Direccion',
|
|
177
|
+
CmnaRecep: 'Santiago',
|
|
178
|
+
},
|
|
179
|
+
items,
|
|
180
|
+
referencia: {
|
|
181
|
+
codigo: 'SET',
|
|
182
|
+
razon: `CASO-${caso.numero}`,
|
|
183
|
+
},
|
|
184
|
+
certificado: this.certificado,
|
|
185
|
+
caf: cafBoleta,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const dte = new DTE(dteDatos);
|
|
189
|
+
boletas.push({
|
|
190
|
+
dte,
|
|
191
|
+
caso: caso.numero,
|
|
192
|
+
folio: folioActual,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
folioActual++;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
boletas,
|
|
200
|
+
foliosUsados: boletas.length,
|
|
201
|
+
folioInicial,
|
|
202
|
+
folioFinal: folioActual - 1,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Genera el EnvioBOLETA con todas las boletas del set
|
|
208
|
+
* @param {Object[]} boletas - Array de { dte, caso, folio }
|
|
209
|
+
* @returns {EnvioBOLETA}
|
|
210
|
+
*/
|
|
211
|
+
generarEnvioBoleta(boletas) {
|
|
212
|
+
const { EnvioBOLETA } = this._lib();
|
|
213
|
+
|
|
214
|
+
const envioBoleta = new EnvioBOLETA({
|
|
215
|
+
rutEmisor: this.emisor.rut,
|
|
216
|
+
rutEnvia: this.certificado.rut,
|
|
217
|
+
fchResol: this.resolucion.fecha,
|
|
218
|
+
nroResol: this.resolucion.numero,
|
|
219
|
+
certificado: this.certificado,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Agregar cada boleta
|
|
223
|
+
for (const { dte } of boletas) {
|
|
224
|
+
envioBoleta.agregar(dte);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Carátula para certificación
|
|
228
|
+
envioBoleta.setCaratula({
|
|
229
|
+
RutEmisor: this.emisor.rut,
|
|
230
|
+
RutEnvia: this.certificado.rut,
|
|
231
|
+
RutReceptor: '60803000-K', // SII en certificación
|
|
232
|
+
FchResol: this.resolucion.fecha,
|
|
233
|
+
NroResol: this.resolucion.numero,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
envioBoleta.generar();
|
|
237
|
+
|
|
238
|
+
return envioBoleta;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Genera el RCOF (ConsumoFolio) para las boletas
|
|
243
|
+
* @param {EnvioBOLETA} envioBoleta - EnvioBOLETA generado
|
|
244
|
+
* @param {Object} [options] - Opciones opcionales
|
|
245
|
+
* @param {number} [options.secEnvio] - Número de secuencia del envío (default: 1)
|
|
246
|
+
* @returns {ConsumoFolio}
|
|
247
|
+
*/
|
|
248
|
+
generarConsumoFolio(envioBoleta, options = {}) {
|
|
249
|
+
const { ConsumoFolio } = this._lib();
|
|
250
|
+
|
|
251
|
+
const consumoFolio = new ConsumoFolio(this.certificado);
|
|
252
|
+
|
|
253
|
+
// Agregar documentos desde el EnvioBOLETA
|
|
254
|
+
// Necesitamos extraer la info de cada boleta
|
|
255
|
+
const boletas = envioBoleta.dtes || [];
|
|
256
|
+
for (const dte of boletas) {
|
|
257
|
+
const tipo = dte.datos.Encabezado.IdDoc.TipoDTE;
|
|
258
|
+
const folio = dte.datos.Encabezado.IdDoc.Folio;
|
|
259
|
+
const fechaEmision = dte.datos.Encabezado.IdDoc.FchEmis;
|
|
260
|
+
const totales = dte.datos.Encabezado.Totales;
|
|
261
|
+
|
|
262
|
+
// Estructura que espera ConsumoFolio: Totales como hijo de Encabezado
|
|
263
|
+
consumoFolio.agregar(tipo, {
|
|
264
|
+
Encabezado: {
|
|
265
|
+
IdDoc: {
|
|
266
|
+
TipoDTE: tipo,
|
|
267
|
+
Folio: folio,
|
|
268
|
+
FchEmis: fechaEmision,
|
|
269
|
+
},
|
|
270
|
+
Totales: {
|
|
271
|
+
MntNeto: totales.MntNeto || 0,
|
|
272
|
+
MntExe: totales.MntExe || 0,
|
|
273
|
+
IVA: totales.IVA || 0,
|
|
274
|
+
MntTotal: totales.MntTotal || 0,
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Establecer carátula
|
|
281
|
+
consumoFolio.setCaratula({
|
|
282
|
+
RutEmisor: this.emisor.rut,
|
|
283
|
+
RutEnvia: this.certificado.rut,
|
|
284
|
+
FchResol: this.resolucion.fecha,
|
|
285
|
+
NroResol: this.resolucion.numero,
|
|
286
|
+
SecEnvio: options?.secEnvio || 1,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return consumoFolio;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Reenvía solo el RCOF para un EnvioBOLETA ya enviado
|
|
294
|
+
* Útil cuando el RCOF anterior fue rechazado por secuencia o tenía datos incorrectos
|
|
295
|
+
* @param {Object} options
|
|
296
|
+
* @param {string} options.envioBOLETAPath - Ruta al XML del EnvioBOLETA ya enviado
|
|
297
|
+
* @param {number} options.secEnvio - Número de secuencia para el RCOF
|
|
298
|
+
* @returns {Promise<Object>} Resultado con trackId
|
|
299
|
+
*/
|
|
300
|
+
async reenviarRCOF(options) {
|
|
301
|
+
const { EnviadorSII, DOMParser } = this._lib();
|
|
302
|
+
const { DOMParser: XMLParser } = require('@xmldom/xmldom');
|
|
303
|
+
|
|
304
|
+
console.log('\n' + '═'.repeat(60));
|
|
305
|
+
console.log('📊 REENVÍO RCOF (ConsumoFolio)');
|
|
306
|
+
console.log('═'.repeat(60));
|
|
307
|
+
|
|
308
|
+
// 1. Parsear el EnvioBOLETA existente para extraer info
|
|
309
|
+
console.log('\n📄 Leyendo EnvioBOLETA...');
|
|
310
|
+
const envioBOLETAXml = fs.readFileSync(options.envioBOLETAPath, 'utf-8');
|
|
311
|
+
const parser = new XMLParser();
|
|
312
|
+
const doc = parser.parseFromString(envioBOLETAXml, 'text/xml');
|
|
313
|
+
|
|
314
|
+
// Extraer info de cada DTE
|
|
315
|
+
const dtes = doc.getElementsByTagName('Documento');
|
|
316
|
+
const boletas = [];
|
|
317
|
+
|
|
318
|
+
for (let i = 0; i < dtes.length; i++) {
|
|
319
|
+
const dte = dtes[i];
|
|
320
|
+
const tipo = parseInt(dte.getElementsByTagName('TipoDTE')[0]?.textContent || '39', 10);
|
|
321
|
+
const folio = parseInt(dte.getElementsByTagName('Folio')[0]?.textContent || '0', 10);
|
|
322
|
+
const fchEmis = dte.getElementsByTagName('FchEmis')[0]?.textContent || '';
|
|
323
|
+
|
|
324
|
+
const totalesEl = dte.getElementsByTagName('Totales')[0];
|
|
325
|
+
const mntNeto = parseInt(totalesEl?.getElementsByTagName('MntNeto')[0]?.textContent || '0', 10);
|
|
326
|
+
const mntExe = parseInt(totalesEl?.getElementsByTagName('MntExe')[0]?.textContent || '0', 10);
|
|
327
|
+
const iva = parseInt(totalesEl?.getElementsByTagName('IVA')[0]?.textContent || '0', 10);
|
|
328
|
+
const mntTotal = parseInt(totalesEl?.getElementsByTagName('MntTotal')[0]?.textContent || '0', 10);
|
|
329
|
+
|
|
330
|
+
boletas.push({
|
|
331
|
+
Encabezado: {
|
|
332
|
+
IdDoc: { TipoDTE: tipo, Folio: folio, FchEmis: fchEmis },
|
|
333
|
+
Totales: { MntNeto: mntNeto, MntExe: mntExe, IVA: iva, MntTotal: mntTotal }
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
console.log(` - Folio ${folio}: Neto=${mntNeto}, Exento=${mntExe}, IVA=${iva}, Total=${mntTotal}`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 2. Crear RCOF
|
|
340
|
+
console.log(`\n📊 Generando RCOF con SecEnvio=${options.secEnvio}...`);
|
|
341
|
+
const { ConsumoFolio } = this._lib();
|
|
342
|
+
const consumoFolio = new ConsumoFolio(this.certificado);
|
|
343
|
+
|
|
344
|
+
for (const b of boletas) {
|
|
345
|
+
consumoFolio.agregar(b.Encabezado.IdDoc.TipoDTE, b);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
consumoFolio.setCaratula({
|
|
349
|
+
RutEmisor: this.emisor.rut,
|
|
350
|
+
RutEnvia: this.certificado.rut,
|
|
351
|
+
FchResol: this.resolucion.fecha,
|
|
352
|
+
NroResol: this.resolucion.numero,
|
|
353
|
+
SecEnvio: options.secEnvio,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
consumoFolio.generar();
|
|
357
|
+
console.log(` ✓ XML generado: ${consumoFolio.xml.length} bytes`);
|
|
358
|
+
|
|
359
|
+
// Guardar debug
|
|
360
|
+
if (this.debugDir) {
|
|
361
|
+
const debugPath = path.join(this.debugDir, 'boleta-cert');
|
|
362
|
+
fs.mkdirSync(debugPath, { recursive: true });
|
|
363
|
+
fs.writeFileSync(path.join(debugPath, `ConsumoFolio-sec${options.secEnvio}.xml`), consumoFolio.xml, 'utf-8');
|
|
364
|
+
console.log(` 📄 Guardado en: ${path.join(debugPath, `ConsumoFolio-sec${options.secEnvio}.xml`)}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 3. Enviar RCOF
|
|
368
|
+
console.log('\n📤 Enviando RCOF al SII...');
|
|
369
|
+
const enviador = new EnviadorSII(this.certificado, this.ambiente);
|
|
370
|
+
const resultadoRCOF = await enviador.enviarConsumoFolios(consumoFolio);
|
|
371
|
+
|
|
372
|
+
if (!resultadoRCOF.ok) {
|
|
373
|
+
console.log(` ❌ Error: ${resultadoRCOF.error}`);
|
|
374
|
+
return { success: false, error: resultadoRCOF.error };
|
|
375
|
+
}
|
|
376
|
+
console.log(` ✅ Enviado - TrackId: ${resultadoRCOF.trackId}`);
|
|
377
|
+
|
|
378
|
+
return { success: true, trackIdRCOF: resultadoRCOF.trackId };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Ejecuta el proceso completo de certificación de boletas
|
|
383
|
+
* @param {Object} options
|
|
384
|
+
* @param {string} options.setPath - Ruta al archivo del set de pruebas
|
|
385
|
+
* @param {Object} options.cafBoleta - CAF para boletas tipo 39
|
|
386
|
+
* @param {number} options.folioInicial - Folio inicial a usar
|
|
387
|
+
* @returns {Promise<Object>} Resultado con trackIds
|
|
388
|
+
*/
|
|
389
|
+
async ejecutarCertificacion(options) {
|
|
390
|
+
const { EnviadorSII } = this._lib();
|
|
391
|
+
|
|
392
|
+
console.log('\n' + '═'.repeat(60));
|
|
393
|
+
console.log('🎫 CERTIFICACIÓN BOLETAS ELECTRÓNICAS');
|
|
394
|
+
console.log('═'.repeat(60));
|
|
395
|
+
|
|
396
|
+
// 1. Parsear set de pruebas
|
|
397
|
+
console.log('\n📋 Parseando set de pruebas...');
|
|
398
|
+
const casos = this.parseSetPruebas(options.setPath);
|
|
399
|
+
console.log(` ✓ ${casos.length} casos encontrados`);
|
|
400
|
+
|
|
401
|
+
// 2. Generar boletas
|
|
402
|
+
console.log('\n📝 Generando boletas...');
|
|
403
|
+
const { boletas, foliosUsados, folioInicial, folioFinal } = await this.generarBoletasSet(
|
|
404
|
+
casos,
|
|
405
|
+
options.cafBoleta,
|
|
406
|
+
options.folioInicial
|
|
407
|
+
);
|
|
408
|
+
console.log(` ✓ ${boletas.length} boletas generadas (folios ${folioInicial}-${folioFinal})`);
|
|
409
|
+
|
|
410
|
+
for (const b of boletas) {
|
|
411
|
+
const monto = b.dte.montoTotal || 0;
|
|
412
|
+
console.log(` - CASO-${b.caso}: Folio ${b.folio} - $${monto.toLocaleString('es-CL')}`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// 3. Generar EnvioBOLETA
|
|
416
|
+
console.log('\n📦 Generando EnvioBOLETA...');
|
|
417
|
+
const envioBoleta = this.generarEnvioBoleta(boletas);
|
|
418
|
+
console.log(` ✓ XML generado: ${envioBoleta.xml.length} bytes`);
|
|
419
|
+
|
|
420
|
+
// Guardar debug
|
|
421
|
+
if (this.debugDir) {
|
|
422
|
+
const debugPath = path.join(this.debugDir, 'boleta-cert');
|
|
423
|
+
fs.mkdirSync(debugPath, { recursive: true });
|
|
424
|
+
fs.writeFileSync(path.join(debugPath, 'EnvioBOLETA.xml'), envioBoleta.xml, 'utf-8');
|
|
425
|
+
console.log(` 📄 Guardado en: ${path.join(debugPath, 'EnvioBOLETA.xml')}`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 4. Enviar EnvioBOLETA
|
|
429
|
+
console.log('\n📤 Enviando EnvioBOLETA al SII...');
|
|
430
|
+
const enviador = new EnviadorSII(this.certificado, this.ambiente);
|
|
431
|
+
const resultadoBoleta = await enviador.enviarBoletaSoap(envioBoleta);
|
|
432
|
+
|
|
433
|
+
if (!resultadoBoleta.ok) {
|
|
434
|
+
console.log(` ❌ Error: ${resultadoBoleta.error}`);
|
|
435
|
+
return { success: false, error: resultadoBoleta.error, fase: 'EnvioBOLETA' };
|
|
436
|
+
}
|
|
437
|
+
console.log(` ✅ Enviado - TrackId: ${resultadoBoleta.trackId}`);
|
|
438
|
+
|
|
439
|
+
// 5. Generar RCOF
|
|
440
|
+
console.log('\n📊 Generando RCOF (ConsumoFolio)...');
|
|
441
|
+
const consumoFolio = this.generarConsumoFolio(envioBoleta);
|
|
442
|
+
consumoFolio.generar(); // generar() ya incluye firmar() internamente
|
|
443
|
+
console.log(` ✓ XML generado: ${consumoFolio.xml.length} bytes`);
|
|
444
|
+
|
|
445
|
+
// Guardar debug
|
|
446
|
+
if (this.debugDir) {
|
|
447
|
+
const debugPath = path.join(this.debugDir, 'boleta-cert');
|
|
448
|
+
fs.writeFileSync(path.join(debugPath, 'ConsumoFolio.xml'), consumoFolio.xml, 'utf-8');
|
|
449
|
+
console.log(` 📄 Guardado en: ${path.join(debugPath, 'ConsumoFolio.xml')}`);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// 6. Enviar RCOF
|
|
453
|
+
console.log('\n📤 Enviando RCOF al SII...');
|
|
454
|
+
const resultadoRCOF = await enviador.enviarConsumoFolios(consumoFolio);
|
|
455
|
+
|
|
456
|
+
if (!resultadoRCOF.ok) {
|
|
457
|
+
console.log(` ❌ Error: ${resultadoRCOF.error}`);
|
|
458
|
+
return {
|
|
459
|
+
success: false,
|
|
460
|
+
error: resultadoRCOF.error,
|
|
461
|
+
fase: 'RCOF',
|
|
462
|
+
trackIdBoleta: resultadoBoleta.trackId
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
console.log(` ✅ Enviado - TrackId: ${resultadoRCOF.trackId}`);
|
|
466
|
+
|
|
467
|
+
// Resumen
|
|
468
|
+
console.log('\n' + '═'.repeat(60));
|
|
469
|
+
console.log('✅ CERTIFICACIÓN BOLETAS COMPLETADA');
|
|
470
|
+
console.log('═'.repeat(60));
|
|
471
|
+
console.log(` 📄 EnvioBOLETA: ${resultadoBoleta.trackId}`);
|
|
472
|
+
console.log(` 📊 RCOF: ${resultadoRCOF.trackId}`);
|
|
473
|
+
console.log(` 📦 Boletas: ${boletas.length}`);
|
|
474
|
+
console.log(` 📑 Folios: ${folioInicial} - ${folioFinal}`);
|
|
475
|
+
|
|
476
|
+
return {
|
|
477
|
+
success: true,
|
|
478
|
+
trackIdBoleta: resultadoBoleta.trackId,
|
|
479
|
+
trackIdRCOF: resultadoRCOF.trackId,
|
|
480
|
+
boletas: boletas.length,
|
|
481
|
+
folioInicial,
|
|
482
|
+
folioFinal,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Declara avance de certificación de boletas validando el TrackId
|
|
488
|
+
* Usa el endpoint DTEauth?3 que requiere autenticación con certificado
|
|
489
|
+
* @param {string} trackId - TrackId del envío a validar
|
|
490
|
+
* @returns {Promise<Object>} Resultado de la validación
|
|
491
|
+
*/
|
|
492
|
+
async declararAvance(trackId) {
|
|
493
|
+
const https = require('https');
|
|
494
|
+
const forge = require('node-forge');
|
|
495
|
+
|
|
496
|
+
console.log('\n' + '═'.repeat(60));
|
|
497
|
+
console.log('📋 DECLARAR AVANCE BOLETAS');
|
|
498
|
+
console.log('═'.repeat(60));
|
|
499
|
+
console.log(` TrackId: ${trackId}`);
|
|
500
|
+
|
|
501
|
+
const host = this.ambiente === 'produccion' ? 'palena.sii.cl' : 'maullin.sii.cl';
|
|
502
|
+
const endpoint = '/cgi_dte/UPL/DTEauth?3';
|
|
503
|
+
const url = `https://${host}${endpoint}`;
|
|
504
|
+
|
|
505
|
+
// Preparar datos del formulario
|
|
506
|
+
const { splitRut } = this._lib();
|
|
507
|
+
const { numero: rutNum, dv } = splitRut(this.emisor.rut);
|
|
508
|
+
|
|
509
|
+
const formData = {
|
|
510
|
+
RUT: rutNum,
|
|
511
|
+
DV: dv,
|
|
512
|
+
ESSION_ID: trackId,
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
// Codificar como form-urlencoded
|
|
516
|
+
const formEncode = (obj) => Object.entries(obj)
|
|
517
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
518
|
+
.join('&');
|
|
519
|
+
const body = formEncode(formData);
|
|
520
|
+
|
|
521
|
+
// Cargar certificado para TLS mutuo
|
|
522
|
+
const pfx = this.certificado.pfxBuffer;
|
|
523
|
+
const password = this.certificado.password;
|
|
524
|
+
const p12Asn1 = forge.asn1.fromDer(pfx.toString('binary'));
|
|
525
|
+
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);
|
|
526
|
+
const keyObj = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag })[forge.pki.oids.pkcs8ShroudedKeyBag][0];
|
|
527
|
+
const certObj = p12.getBags({ bagType: forge.pki.oids.certBag })[forge.pki.oids.certBag][0];
|
|
528
|
+
|
|
529
|
+
const tlsOptions = {
|
|
530
|
+
key: forge.pki.privateKeyToPem(keyObj.key),
|
|
531
|
+
cert: forge.pki.certificateToPem(certObj.cert),
|
|
532
|
+
rejectUnauthorized: false,
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// Hacer request
|
|
536
|
+
const requestWithCert = (options, payload) => new Promise((resolve, reject) => {
|
|
537
|
+
const req = https.request({ ...options, ...tlsOptions }, (res) => {
|
|
538
|
+
let data = '';
|
|
539
|
+
res.on('data', (chunk) => (data += chunk));
|
|
540
|
+
res.on('end', () => resolve({
|
|
541
|
+
status: res.statusCode,
|
|
542
|
+
text: data,
|
|
543
|
+
headers: res.headers,
|
|
544
|
+
}));
|
|
545
|
+
});
|
|
546
|
+
req.on('error', reject);
|
|
547
|
+
if (payload) req.write(payload);
|
|
548
|
+
req.end();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
console.log(` 🔗 URL: ${url}`);
|
|
552
|
+
|
|
553
|
+
try {
|
|
554
|
+
const response = await requestWithCert({
|
|
555
|
+
method: 'POST',
|
|
556
|
+
hostname: host,
|
|
557
|
+
path: endpoint,
|
|
558
|
+
headers: {
|
|
559
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
560
|
+
'Content-Length': Buffer.byteLength(body),
|
|
561
|
+
},
|
|
562
|
+
}, body);
|
|
563
|
+
|
|
564
|
+
console.log(` 📥 Status: ${response.status}`);
|
|
565
|
+
|
|
566
|
+
// Guardar respuesta para debug
|
|
567
|
+
if (this.debugDir) {
|
|
568
|
+
const debugPath = path.join(this.debugDir, 'boleta-cert');
|
|
569
|
+
fs.mkdirSync(debugPath, { recursive: true });
|
|
570
|
+
fs.writeFileSync(
|
|
571
|
+
path.join(debugPath, `validacion-${trackId}.html`),
|
|
572
|
+
response.text,
|
|
573
|
+
'utf-8'
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Parsear respuesta
|
|
578
|
+
const html = response.text;
|
|
579
|
+
|
|
580
|
+
// Detectar estados
|
|
581
|
+
const esAprobado = /REVISADO CONFORME|APROBADO|OK/i.test(html);
|
|
582
|
+
const esRechazado = /RECHAZADO|ERROR|REPARO/i.test(html);
|
|
583
|
+
const enRevision = /EN REVISION|PROCESANDO/i.test(html);
|
|
584
|
+
|
|
585
|
+
if (esAprobado) {
|
|
586
|
+
console.log(' ✅ BOLETAS APROBADAS');
|
|
587
|
+
return { success: true, estado: 'APROBADO', html };
|
|
588
|
+
} else if (esRechazado) {
|
|
589
|
+
console.log(' ❌ BOLETAS RECHAZADAS');
|
|
590
|
+
// Extraer mensaje de error si existe
|
|
591
|
+
const errorMatch = html.match(/<font[^>]*color[^>]*red[^>]*>([^<]+)</i);
|
|
592
|
+
const errorMsg = errorMatch ? errorMatch[1].trim() : 'Error desconocido';
|
|
593
|
+
return { success: false, estado: 'RECHAZADO', error: errorMsg, html };
|
|
594
|
+
} else if (enRevision) {
|
|
595
|
+
console.log(' 🔄 EN REVISIÓN');
|
|
596
|
+
return { success: true, estado: 'EN_REVISION', html };
|
|
597
|
+
} else {
|
|
598
|
+
console.log(' 📋 Respuesta recibida (verificar manualmente)');
|
|
599
|
+
return { success: true, estado: 'DESCONOCIDO', html };
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
} catch (error) {
|
|
603
|
+
console.error(` ❌ Error: ${error.message}`);
|
|
604
|
+
return { success: false, error: error.message };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Valida el estado del set de pruebas de boletas consultando al SII
|
|
610
|
+
* @param {string} trackId - TrackId del EnvioBOLETA
|
|
611
|
+
* @returns {Promise<Object>} Estado del set (SOK, SRH, etc.)
|
|
612
|
+
*/
|
|
613
|
+
async validarSetBoletas(trackId) {
|
|
614
|
+
const https = require('https');
|
|
615
|
+
const forge = require('node-forge');
|
|
616
|
+
|
|
617
|
+
console.log(`\n Consultando estado del set...`);
|
|
618
|
+
console.log(` TrackId: ${trackId}`);
|
|
619
|
+
|
|
620
|
+
const host = this.ambiente === 'produccion' ? 'www4.sii.cl' : 'www4.sii.cl';
|
|
621
|
+
|
|
622
|
+
// Endpoint para consultar estado del set de boletas
|
|
623
|
+
// El portal usa certBolElectDteInternet con parámetro SET=2 para revisar
|
|
624
|
+
const endpoint = `/certBolElectDteInternet/ConsSetDte.do`;
|
|
625
|
+
|
|
626
|
+
// Preparar datos del formulario
|
|
627
|
+
const { splitRut } = this._lib();
|
|
628
|
+
const { numero: rutNum, dv } = splitRut(this.emisor.rut);
|
|
629
|
+
|
|
630
|
+
const formData = {
|
|
631
|
+
RUT_EMPRESA: rutNum,
|
|
632
|
+
DV_EMPRESA: dv,
|
|
633
|
+
TRACK_ID: trackId,
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// Codificar como form-urlencoded
|
|
637
|
+
const formEncode = (obj) => Object.entries(obj)
|
|
638
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
639
|
+
.join('&');
|
|
640
|
+
const body = formEncode(formData);
|
|
641
|
+
|
|
642
|
+
// Cargar certificado para TLS mutuo
|
|
643
|
+
const pfx = this.certificado.pfxBuffer;
|
|
644
|
+
const password = this.certificado.password;
|
|
645
|
+
const p12Asn1 = forge.asn1.fromDer(pfx.toString('binary'));
|
|
646
|
+
const p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, password);
|
|
647
|
+
const keyObj = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag })[forge.pki.oids.pkcs8ShroudedKeyBag][0];
|
|
648
|
+
const certObj = p12.getBags({ bagType: forge.pki.oids.certBag })[forge.pki.oids.certBag][0];
|
|
649
|
+
|
|
650
|
+
const tlsOptions = {
|
|
651
|
+
key: forge.pki.privateKeyToPem(keyObj.key),
|
|
652
|
+
cert: forge.pki.certificateToPem(certObj.cert),
|
|
653
|
+
rejectUnauthorized: false,
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
// Hacer request
|
|
657
|
+
const requestWithCert = (options, payload) => new Promise((resolve, reject) => {
|
|
658
|
+
const req = https.request({ ...options, ...tlsOptions }, (res) => {
|
|
659
|
+
let data = '';
|
|
660
|
+
res.on('data', (chunk) => (data += chunk));
|
|
661
|
+
res.on('end', () => resolve({
|
|
662
|
+
status: res.statusCode,
|
|
663
|
+
text: data,
|
|
664
|
+
headers: res.headers,
|
|
665
|
+
}));
|
|
666
|
+
});
|
|
667
|
+
req.on('error', reject);
|
|
668
|
+
if (payload) req.write(payload);
|
|
669
|
+
req.end();
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
try {
|
|
673
|
+
const response = await requestWithCert({
|
|
674
|
+
method: 'POST',
|
|
675
|
+
hostname: host,
|
|
676
|
+
path: endpoint,
|
|
677
|
+
headers: {
|
|
678
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
679
|
+
'Content-Length': Buffer.byteLength(body),
|
|
680
|
+
},
|
|
681
|
+
}, body);
|
|
682
|
+
|
|
683
|
+
// Guardar respuesta para debug
|
|
684
|
+
if (this.debugDir) {
|
|
685
|
+
const debugPath = path.join(this.debugDir, 'boleta-cert');
|
|
686
|
+
fs.mkdirSync(debugPath, { recursive: true });
|
|
687
|
+
fs.writeFileSync(
|
|
688
|
+
path.join(debugPath, `set-estado-${trackId}.html`),
|
|
689
|
+
response.text,
|
|
690
|
+
'utf-8'
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const html = response.text;
|
|
695
|
+
|
|
696
|
+
// Buscar estado del set en la respuesta
|
|
697
|
+
const sokMatch = /SOK|SET DE PRUEBA CORRECTO/i.test(html);
|
|
698
|
+
const srhMatch = /SRH|SET DE PRUEBA RECHAZADO/i.test(html);
|
|
699
|
+
const pendienteMatch = /PENDIENTE|EN PROCESO|PROCESANDO/i.test(html);
|
|
700
|
+
|
|
701
|
+
// Extraer detalle de reparos si existe
|
|
702
|
+
const reparosMatch = html.match(/Detalle de Reparos[\s\S]*?(<table[\s\S]*?<\/table>|CASO-\d+[\s\S]*?(?=\n\n|\<))/i);
|
|
703
|
+
const detalle = reparosMatch ? reparosMatch[0].replace(/<[^>]+>/g, ' ').trim().substring(0, 500) : null;
|
|
704
|
+
|
|
705
|
+
if (sokMatch) {
|
|
706
|
+
return { success: true, estado: 'SOK', detalle: 'Set de prueba correcto' };
|
|
707
|
+
} else if (srhMatch) {
|
|
708
|
+
return { success: false, estado: 'SRH', detalle: detalle || 'Set rechazado - ver correo SII' };
|
|
709
|
+
} else if (pendienteMatch) {
|
|
710
|
+
return { success: true, estado: 'PENDIENTE', detalle: 'Aún en proceso de validación' };
|
|
711
|
+
} else {
|
|
712
|
+
// Si no podemos determinar el estado, intentar consultar por correo
|
|
713
|
+
return {
|
|
714
|
+
success: true,
|
|
715
|
+
estado: 'DESCONOCIDO',
|
|
716
|
+
detalle: 'Estado no determinado - verificar correo SII o portal manualmente'
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
} catch (error) {
|
|
721
|
+
console.error(` ⚠️ Error consultando estado: ${error.message}`);
|
|
722
|
+
return {
|
|
723
|
+
success: true,
|
|
724
|
+
estado: 'DESCONOCIDO',
|
|
725
|
+
detalle: `No se pudo consultar automáticamente: ${error.message}`
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
module.exports = BoletaCert;
|