@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.
Files changed (60) hide show
  1. package/BoletaService.js +109 -0
  2. package/CAF.js +173 -0
  3. package/CafSolicitor.js +380 -0
  4. package/Certificado.js +123 -0
  5. package/ConsumoFolio.js +376 -0
  6. package/DTE.js +399 -0
  7. package/EnviadorSII.js +1304 -0
  8. package/Envio.js +196 -0
  9. package/FolioRegistry.js +553 -0
  10. package/FolioService.js +703 -0
  11. package/LICENSE +27 -0
  12. package/LibroBase.js +134 -0
  13. package/LibroCompraVenta.js +205 -0
  14. package/LibroGuia.js +225 -0
  15. package/README.md +239 -0
  16. package/Signer.js +94 -0
  17. package/SiiCertificacion.js +1189 -0
  18. package/SiiPortalAuth.js +460 -0
  19. package/SiiSession.js +499 -0
  20. package/cert/BoletaCert.js +731 -0
  21. package/cert/CertFolioHelper.js +185 -0
  22. package/cert/CertRunner.js +2658 -0
  23. package/cert/ConfigLoader.js +133 -0
  24. package/cert/IntercambioCert.js +429 -0
  25. package/cert/LibroCompras.js +359 -0
  26. package/cert/LibroGuias.js +171 -0
  27. package/cert/LibroVentas.js +153 -0
  28. package/cert/MuestrasImpresas.js +676 -0
  29. package/cert/SetBase.js +321 -0
  30. package/cert/SetBasico.js +413 -0
  31. package/cert/SetCompra.js +472 -0
  32. package/cert/SetExenta.js +490 -0
  33. package/cert/SetGuia.js +283 -0
  34. package/cert/SetParser.js +1184 -0
  35. package/cert/SetsProvider.js +499 -0
  36. package/cert/Simulacion.js +521 -0
  37. package/cert/comunaOficina.js +460 -0
  38. package/cert/index.js +124 -0
  39. package/cert/types.js +330 -0
  40. package/dte-sii.d.ts +458 -0
  41. package/index.js +428 -0
  42. package/package.json +48 -0
  43. package/utils/c14n.js +275 -0
  44. package/utils/calculo.js +396 -0
  45. package/utils/config.js +276 -0
  46. package/utils/constants.js +302 -0
  47. package/utils/emisor.js +174 -0
  48. package/utils/endpoints.js +225 -0
  49. package/utils/error.js +235 -0
  50. package/utils/index.js +339 -0
  51. package/utils/logger.js +239 -0
  52. package/utils/pfx.js +203 -0
  53. package/utils/receptor.js +218 -0
  54. package/utils/referencia.js +169 -0
  55. package/utils/resolucion.js +119 -0
  56. package/utils/rut.js +169 -0
  57. package/utils/sanitize.js +124 -0
  58. package/utils/tokenCache.js +214 -0
  59. package/utils/xml.js +358 -0
  60. package/utils.js +4 -0
@@ -0,0 +1,499 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * SetsProvider - Obtención y Parseo de Sets de Prueba del SII
5
+ *
6
+ * Responsabilidades:
7
+ * - Iniciar/reusar sesión SII
8
+ * - Obtener sets de prueba del portal
9
+ * - Parsear HTML a estructuras JSON
10
+ * - Detectar si se regeneró el set (requiere reiniciar proceso)
11
+ *
12
+ * @module dte-sii/cert/SetsProvider
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const SiiCertificacion = require('../SiiCertificacion');
18
+
19
+ // Parser de sets (ahora en el core)
20
+ const SetParser = require('./SetParser');
21
+
22
+ /**
23
+ * Resultado de obtener sets
24
+ * @typedef {Object} SetsResult
25
+ * @property {boolean} success
26
+ * @property {boolean} regenerated - Si el set fue regenerado (reiniciar proceso)
27
+ * @property {Object} estructuras - Casos parseados por set
28
+ * @property {Object} estadoSets - Estado de cada set en el SII
29
+ * @property {string|null} error
30
+ */
31
+
32
+ /**
33
+ * Sets opcionales por defecto para certificación
34
+ */
35
+ const DEFAULT_SETS_OPCIONALES = {
36
+ SET03: 'S', // Guía Despacho
37
+ SET06: 'S', // Factura Exenta
38
+ SET72: 'S', // Factura Compra
39
+ // SET11: 'S', // Exportación - NO INCLUIR por defecto
40
+ // SET84: 'S', // Liquidación - NO INCLUIR por defecto
41
+ };
42
+
43
+ class SetsProvider {
44
+ /**
45
+ * @param {Object} options
46
+ * @param {string} options.pfxPath - Ruta al certificado .pfx
47
+ * @param {string} options.pfxPassword - Contraseña del certificado
48
+ * @param {string} options.rutEmpresa - RUT sin DV (ej: "76123456")
49
+ * @param {string} options.dvEmpresa - Dígito verificador
50
+ * @param {string} [options.sessionPath] - Ruta para guardar/cargar sesión
51
+ * @param {string} [options.debugDir] - Directorio para debug
52
+ * @param {Object} [options.logger] - Logger opcional
53
+ */
54
+ constructor(options) {
55
+ this._validateOptions(options);
56
+
57
+ this.pfxPath = options.pfxPath;
58
+ this.pfxPassword = options.pfxPassword;
59
+ this.rutEmpresa = options.rutEmpresa;
60
+ this.dvEmpresa = options.dvEmpresa;
61
+ this.sessionPath = options.sessionPath || null;
62
+ this.debugDir = options.debugDir || null;
63
+ this.logger = options.logger || console;
64
+
65
+ // Instancia de SiiCertificacion (lazy init)
66
+ this._siiCert = null;
67
+
68
+ // Cache de estructuras
69
+ this._estructurasCache = null;
70
+ this._lastFetchTime = null;
71
+ this._lastNumeroAtencion = null; // Para detectar regeneración
72
+ }
73
+
74
+ /**
75
+ * Valida opciones requeridas
76
+ * @private
77
+ */
78
+ _validateOptions(options) {
79
+ const required = ['pfxPath', 'pfxPassword', 'rutEmpresa', 'dvEmpresa'];
80
+ for (const key of required) {
81
+ if (!options[key]) {
82
+ throw new Error(`SetsProvider: '${key}' es requerido`);
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Obtiene la instancia de SiiCertificacion (lazy init)
89
+ * @private
90
+ */
91
+ _getSiiCert() {
92
+ if (!this._siiCert) {
93
+ this._siiCert = new SiiCertificacion({
94
+ pfxPath: this.pfxPath,
95
+ pfxPassword: this.pfxPassword,
96
+ rutEmpresa: this.rutEmpresa,
97
+ dvEmpresa: this.dvEmpresa,
98
+ });
99
+ }
100
+ return this._siiCert;
101
+ }
102
+
103
+ /**
104
+ * Inicializa o reutiliza la sesión SII
105
+ * @returns {Promise<boolean>}
106
+ */
107
+ async initSession() {
108
+ this.logger.log('[SetsProvider] 🔐 Inicializando sesión SII...');
109
+
110
+ const siiCert = this._getSiiCert();
111
+
112
+ // Intentar cargar sesión existente
113
+ if (this.sessionPath && SiiCertificacion.isSessionValid(this.sessionPath)) {
114
+ this.logger.log('[SetsProvider] ✓ Sesión existente válida');
115
+ const loaded = siiCert.loadSession(this.sessionPath);
116
+ if (loaded) {
117
+ return true;
118
+ }
119
+ }
120
+
121
+ // Crear nueva sesión
122
+ this.logger.log('[SetsProvider] Estableciendo nueva sesión...');
123
+ try {
124
+ // verAvance() fuerza el login
125
+ await siiCert.verAvance();
126
+
127
+ // Guardar sesión para reutilizar
128
+ if (this.sessionPath) {
129
+ this._ensureDir(path.dirname(this.sessionPath));
130
+ siiCert.saveSession(this.sessionPath);
131
+ this.logger.log('[SetsProvider] ✓ Sesión guardada');
132
+ }
133
+
134
+ return true;
135
+ } catch (error) {
136
+ this.logger.error(`[SetsProvider] ❌ Error: ${error.message}`);
137
+ return false;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Obtiene los sets de prueba del SII
143
+ * @param {Object} [options]
144
+ * @param {boolean} [options.descargar=true] - Descargar contenido del set
145
+ * @param {boolean} [options.forceRefresh=false] - Forzar nueva descarga
146
+ * @param {Object} [options.setsOpcionales] - Sets opcionales a incluir
147
+ * @returns {Promise<SetsResult>}
148
+ */
149
+ async obtenerSets(options = {}) {
150
+ const {
151
+ descargar = true,
152
+ forceRefresh = false,
153
+ setsOpcionales = DEFAULT_SETS_OPCIONALES,
154
+ } = options;
155
+
156
+ this.logger.log('\n[SetsProvider] 📦 Obteniendo sets de prueba...');
157
+
158
+ // Si hay cache y no se fuerza refresh, retornar cache
159
+ if (!forceRefresh && this._estructurasCache) {
160
+ this.logger.log('[SetsProvider] ✓ Usando cache de estructuras');
161
+ return {
162
+ success: true,
163
+ regenerated: false,
164
+ estructuras: this._estructurasCache,
165
+ fromCache: true,
166
+ };
167
+ }
168
+
169
+ try {
170
+ // Asegurar sesión activa
171
+ const sessionOk = await this.initSession();
172
+ if (!sessionOk) {
173
+ return {
174
+ success: false,
175
+ error: 'No se pudo establecer sesión con el SII',
176
+ };
177
+ }
178
+
179
+ const siiCert = this._getSiiCert();
180
+
181
+ // Obtener sets del SII
182
+ const result = await siiCert.generarSetPruebas({
183
+ descargar,
184
+ setsOpcionales,
185
+ });
186
+
187
+ if (!result.success) {
188
+ return {
189
+ success: false,
190
+ error: result.error || 'Error al obtener sets del SII',
191
+ };
192
+ }
193
+
194
+ // Si después del self-reauth interno de generarSetPruebas SIGUE mostrando
195
+ // "no inscrito", la empresa genuinamente no está en el programa de postulación
196
+ if (result.noInscrito) {
197
+ return {
198
+ success: false,
199
+ error: 'El contribuyente no está inscrito en Postulación en el portal SII. ' +
200
+ 'Accede a https://maullin.sii.cl y verifica el estado de certificación.',
201
+ };
202
+ }
203
+
204
+ // Guardar sesión después de operación exitosa
205
+ this.saveSession();
206
+
207
+ // Guardar HTML para debug
208
+ if (this.debugDir && result.rawHtml) {
209
+ this._saveDebug('sets-raw.html', result.rawHtml);
210
+ }
211
+
212
+ // Detectar si el set fue regenerado
213
+ const regenerated = this._detectRegeneration(result);
214
+ if (regenerated) {
215
+ this.logger.log('[SetsProvider] ⚠️ Set regenerado - reiniciar proceso');
216
+ // Invalidar cualquier cache anterior
217
+ this.invalidateCache();
218
+ }
219
+
220
+ // Parsear estructuras si se descargó
221
+ let estructuras = null;
222
+ let datosExtraidos = null;
223
+
224
+ if (descargar && result.setDescargado) {
225
+ // Verificar sesión válida
226
+ const htmlLower = String(result.setDescargado.rawHtml || '').toLowerCase();
227
+ if (htmlLower.includes('no se encuentra autenticado') ||
228
+ (htmlLower.includes('postulacion factura electronica') && !htmlLower.includes('caso'))) {
229
+ // Distinguir: ¿es "no inscrito" o expiró la sesión?
230
+ if (htmlLower.includes('no inscrito') || (htmlLower.includes('no est') && htmlLower.includes('inscrito'))) {
231
+ this.logger.log('[SetsProvider] ℹ️ pe_generar2: empresa no inscrita en Postulación (fase avanzada).');
232
+ return { success: false, error: 'El contribuyente no está inscrito en Postulación en el portal SII.' };
233
+ }
234
+ // Sesión expiró en el portal aunque el archivo no lo sabía → forzar re-autenticación
235
+ // Cookies vencidas server-side: limpiar sesión guardada y pedir a
236
+ // generarSetPruebas que re-autentique por sí mismo via pe_generar
237
+ this.logger.log('[SetsProvider] ⚠️ Sesión rechazada por portal, limpiando y reintentando...');
238
+ if (this.sessionPath) {
239
+ const SiiSession = require('../SiiSession');
240
+ SiiSession.clearSession(this.sessionPath);
241
+ }
242
+ this._siiCert = null;
243
+
244
+ const siiCertFresh = this._getSiiCert();
245
+ // sesión vacía → ensureSession dentro de generarSetPruebas hará el login redirect
246
+ const retryResult = await siiCertFresh.generarSetPruebas({ descargar, setsOpcionales });
247
+ if (!retryResult.success) {
248
+ return {
249
+ success: false,
250
+ error: retryResult.error || 'Error al obtener sets tras re-autenticación',
251
+ };
252
+ }
253
+ if (retryResult.noInscrito) {
254
+ return {
255
+ success: false,
256
+ error: 'El contribuyente no está inscrito en Postulación en el portal SII.',
257
+ };
258
+ }
259
+ // Guardar nueva sesión
260
+ if (this.sessionPath) {
261
+ this._ensureDir(path.dirname(this.sessionPath));
262
+ siiCertFresh.saveSession(this.sessionPath);
263
+ }
264
+ // Reemplazar result con el reintento exitoso para continuar el flujo normal
265
+ Object.assign(result, retryResult);
266
+ }
267
+
268
+ // Parsear usando flujoCert si está disponible
269
+ const parseResult = this._parseSetDescargado(result.setDescargado.rawHtml);
270
+ estructuras = parseResult.estructuras;
271
+ datosExtraidos = parseResult.datosExtraidos;
272
+
273
+ if (estructuras) {
274
+ this._estructurasCache = estructuras;
275
+ this._lastFetchTime = Date.now();
276
+
277
+ // Guardar estructuras para debug
278
+ if (this.debugDir) {
279
+ this._saveDebug('estructuras.json', JSON.stringify(estructuras, null, 2));
280
+ if (datosExtraidos) {
281
+ this._saveDebug('datos-extraidos.json', JSON.stringify(datosExtraidos, null, 2));
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ this.logger.log('[SetsProvider] ✓ Sets obtenidos correctamente');
288
+
289
+ return {
290
+ success: true,
291
+ regenerated,
292
+ estructuras,
293
+ datosExtraidos,
294
+ estadoSets: result.estadoSets,
295
+ setsOpcionales: result.setsOpcionales,
296
+ };
297
+
298
+ } catch (error) {
299
+ this.logger.error(`[SetsProvider] ❌ Error: ${error.message}`);
300
+ return {
301
+ success: false,
302
+ error: error.message,
303
+ };
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Consulta el estado de avance en el SII
309
+ * @returns {Promise<Object>}
310
+ */
311
+ async consultarAvance() {
312
+ const siiCert = this._getSiiCert();
313
+ return siiCert.verAvance();
314
+ }
315
+
316
+ /**
317
+ * Detecta si el set fue regenerado (folios nuevos)
318
+ * @private
319
+ */
320
+ _detectRegeneration(result) {
321
+ // Si el HTML contiene indicadores de regeneración
322
+ const html = result.rawHtml || '';
323
+ if (html.includes('Set generado correctamente') ||
324
+ html.includes('nuevos folios asignados')) {
325
+ return true;
326
+ }
327
+
328
+ // Comparar número de atención con el anterior si existe
329
+ if (result.setDescargado?.rawHtml && this._lastNumeroAtencion) {
330
+ const match = result.setDescargado.rawHtml.match(/NUMERO DE ATENCI[^:]*:\s*(\d+)/i);
331
+ if (match && match[1] !== this._lastNumeroAtencion) {
332
+ this.logger.log(`[SetsProvider] Número de atención cambió: ${this._lastNumeroAtencion} → ${match[1]}`);
333
+ return true;
334
+ }
335
+ }
336
+
337
+ return false;
338
+ }
339
+
340
+ /**
341
+ * Parsea el set descargado usando flujoCert
342
+ * @private
343
+ * @param {string} rawHtml - HTML del set descargado
344
+ * @returns {{ estructuras: Object, datosExtraidos: Object }}
345
+ */
346
+ _parseSetDescargado(rawHtml) {
347
+ // Limpiar HTML y extraer texto
348
+ const textoLimpio = this._limpiarHtml(rawHtml);
349
+
350
+ // Guardar texto limpio para debug
351
+ if (this.debugDir) {
352
+ this._saveDebug('set-texto.txt', textoLimpio);
353
+ }
354
+
355
+ // Usar SetParser del core
356
+ this.logger.log('[SetsProvider] Usando SetParser del core...');
357
+
358
+ const datosExtraidos = SetParser.extraerCasosDelSet(textoLimpio);
359
+
360
+ if (datosExtraidos.sets.length === 0) {
361
+ this.logger.log('[SetsProvider] ⚠️ No se encontraron sets en el contenido');
362
+ return { estructuras: null, datosExtraidos: null };
363
+ }
364
+
365
+ this.logger.log(`[SetsProvider] 📦 Sets encontrados: ${datosExtraidos.sets.length}`);
366
+ this.logger.log(`[SetsProvider] 📄 Total casos: ${datosExtraidos.totalCasos}`);
367
+
368
+ // Guardar número de atención para detectar regeneración
369
+ if (datosExtraidos.sets[0]?.numeroAtencion) {
370
+ this._lastNumeroAtencion = datosExtraidos.sets[0].numeroAtencion;
371
+ }
372
+
373
+ // Generar estructuras para scripts
374
+ const estructuras = SetParser.generarEstructurasParaScripts(datosExtraidos);
375
+
376
+ return { estructuras, datosExtraidos };
377
+ }
378
+
379
+ /**
380
+ * Limpia HTML y extrae texto plano
381
+ * @private
382
+ */
383
+ _limpiarHtml(html) {
384
+ return html
385
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
386
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
387
+ .replace(/<[^>]+>/g, '\n')
388
+ .replace(/&nbsp;/g, ' ')
389
+ .replace(/&aacute;/g, 'á')
390
+ .replace(/&eacute;/g, 'é')
391
+ .replace(/&iacute;/g, 'í')
392
+ .replace(/&oacute;/g, 'ó')
393
+ .replace(/&uacute;/g, 'ú')
394
+ .replace(/&ntilde;/g, 'ñ')
395
+ .replace(/&Aacute;/g, 'Á')
396
+ .replace(/&Eacute;/g, 'É')
397
+ .replace(/&Iacute;/g, 'Í')
398
+ .replace(/&Oacute;/g, 'Ó')
399
+ .replace(/&Uacute;/g, 'Ú')
400
+ .replace(/&Ntilde;/g, 'Ñ')
401
+ .replace(/\n\s*\n/g, '\n')
402
+ .trim();
403
+ }
404
+
405
+ /**
406
+ * Parsea las estructuras del set descargado (método legacy)
407
+ * @private
408
+ * @deprecated Usar _parseSetDescargado en su lugar
409
+ */
410
+ _parseEstructuras(setDescargado) {
411
+ if (setDescargado.rawHtml) {
412
+ const result = this._parseSetDescargado(setDescargado.rawHtml);
413
+ return result.estructuras;
414
+ }
415
+ return null;
416
+ }
417
+
418
+ /**
419
+ * Organiza los casos por tipo de set
420
+ * @private
421
+ */
422
+ _organizarCasosPorSet(casos) {
423
+ const estructuras = {
424
+ setBasico: { casos: [], cafRequired: {} },
425
+ setExenta: { casos: [], cafRequired: {} },
426
+ setGuia: { casos: [], cafRequired: {} },
427
+ setCompra: { casos: [], cafRequired: {} },
428
+ };
429
+
430
+ for (const caso of casos) {
431
+ const tipoDte = caso.tipoDte || caso.tipo;
432
+
433
+ // Clasificar por tipo de DTE
434
+ if ([33, 56, 61].includes(tipoDte)) {
435
+ estructuras.setBasico.casos.push(caso);
436
+ this._incrementCafRequired(estructuras.setBasico.cafRequired, tipoDte);
437
+ } else if (tipoDte === 34) {
438
+ estructuras.setExenta.casos.push(caso);
439
+ this._incrementCafRequired(estructuras.setExenta.cafRequired, tipoDte);
440
+ } else if (tipoDte === 52) {
441
+ estructuras.setGuia.casos.push(caso);
442
+ this._incrementCafRequired(estructuras.setGuia.cafRequired, tipoDte);
443
+ } else if (tipoDte === 46) {
444
+ estructuras.setCompra.casos.push(caso);
445
+ this._incrementCafRequired(estructuras.setCompra.cafRequired, tipoDte);
446
+ }
447
+ }
448
+
449
+ return estructuras;
450
+ }
451
+
452
+ /**
453
+ * Incrementa contador de CAF requerido
454
+ * @private
455
+ */
456
+ _incrementCafRequired(cafRequired, tipoDte) {
457
+ cafRequired[tipoDte] = (cafRequired[tipoDte] || 0) + 1;
458
+ }
459
+
460
+ /**
461
+ * Guarda la sesión actual
462
+ */
463
+ saveSession() {
464
+ if (this.sessionPath && this._siiCert) {
465
+ this._ensureDir(path.dirname(this.sessionPath));
466
+ this._siiCert.saveSession(this.sessionPath);
467
+ }
468
+ }
469
+
470
+ /**
471
+ * Invalida el cache de estructuras
472
+ */
473
+ invalidateCache() {
474
+ this._estructurasCache = null;
475
+ this._lastFetchTime = null;
476
+ }
477
+
478
+ /**
479
+ * Crea directorio si no existe
480
+ * @private
481
+ */
482
+ _ensureDir(dirPath) {
483
+ if (!fs.existsSync(dirPath)) {
484
+ fs.mkdirSync(dirPath, { recursive: true });
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Guarda archivo de debug
490
+ * @private
491
+ */
492
+ _saveDebug(filename, content) {
493
+ if (!this.debugDir) return;
494
+ this._ensureDir(this.debugDir);
495
+ fs.writeFileSync(path.join(this.debugDir, filename), content);
496
+ }
497
+ }
498
+
499
+ module.exports = SetsProvider;