@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,413 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * Set Básico - Factura, Nota de Crédito, Nota de Débito
5
+ *
6
+ * Tipos DTE: 33, 56, 61
7
+ * Flujo:
8
+ * 1. Generar facturas (casos tipo 33)
9
+ * 2. Generar NC (referenciando facturas)
10
+ * 3. Generar ND (referenciando facturas)
11
+ *
12
+ * @module dte-sii/cert/SetBasico
13
+ */
14
+
15
+ const SetBase = require('./SetBase');
16
+ const { SET_LABELS, SETS_DTE } = require('./types');
17
+
18
+ class SetBasico extends SetBase {
19
+ constructor(deps) {
20
+ super(deps);
21
+
22
+ this.key = 'basico';
23
+ this.label = SET_LABELS.basico;
24
+ this.tiposDte = SETS_DTE.basico; // [33, 56, 61]
25
+
26
+ // Registro de referencias entre documentos
27
+ // { casoId: { tipoDte, folio, fecha, detalle, totales, items } }
28
+ this._docRefs = {};
29
+ }
30
+
31
+ /**
32
+ * @override
33
+ * Valida casos específicos del set básico
34
+ */
35
+ _validarCasos(casos) {
36
+ super._validarCasos(casos);
37
+
38
+ if (!casos.casosFactura?.length) {
39
+ throw new Error('SetBasico: casosFactura es requerido');
40
+ }
41
+ // NC y ND pueden estar vacíos en algunos escenarios
42
+ }
43
+
44
+ /**
45
+ * @override
46
+ * Calcula folios necesarios por tipo
47
+ */
48
+ _calcularCantidadFolios(casos, tipoDte) {
49
+ // Usar cafRequired si está definido
50
+ if (casos.cafRequired?.[tipoDte]) {
51
+ return casos.cafRequired[tipoDte];
52
+ }
53
+
54
+ // Si no, contar casos
55
+ switch (tipoDte) {
56
+ case 33: return casos.casosFactura?.length || 1;
57
+ case 61: return casos.casosNC?.length || 1;
58
+ case 56: return casos.casosND?.length || 1;
59
+ default: return 1;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * @override
65
+ * Genera DTEs del set básico
66
+ *
67
+ * @param {Object} casos - { casosFactura, casosNC, casosND, cafRequired }
68
+ * @param {Object} cafs - { 33: cafPath, 56: cafPath, 61: cafPath }
69
+ * @returns {Promise<DTE[]>}
70
+ */
71
+ async generarDtes(casos, cafs) {
72
+ const dtes = [];
73
+
74
+ // 1. Generar facturas primero (las NC/ND referencian facturas)
75
+ this.logger.log(' 📄 Generando facturas...');
76
+ for (const caso of casos.casosFactura || []) {
77
+ const dte = await this._generarFactura(caso, cafs[33]);
78
+ dtes.push(dte);
79
+ }
80
+
81
+ // 2. Generar notas de crédito
82
+ this.logger.log(' 📄 Generando notas de crédito...');
83
+ for (const caso of casos.casosNC || []) {
84
+ const dte = await this._generarNotaCredito(caso, cafs[61]);
85
+ dtes.push(dte);
86
+ }
87
+
88
+ // 3. Generar notas de débito
89
+ this.logger.log(' 📄 Generando notas de débito...');
90
+ for (const caso of casos.casosND || []) {
91
+ const dte = await this._generarNotaDebito(caso, cafs[56]);
92
+ dtes.push(dte);
93
+ }
94
+
95
+ return dtes;
96
+ }
97
+
98
+ /**
99
+ * Genera una factura (tipo 33)
100
+ * @private
101
+ */
102
+ async _generarFactura(caso, cafPath) {
103
+ const { DTE, CAF, buildDetalle, calcularTotalesDesdeItems, buildSetReferencia } = require('../index');
104
+ const fs = require('fs');
105
+
106
+ // Cargar CAF
107
+ const cafXml = fs.readFileSync(cafPath, 'utf8');
108
+ const caf = new CAF(cafXml);
109
+
110
+ // Reservar folio
111
+ const folio = this._reservarFolio(caf, cafXml);
112
+
113
+ // Construir detalle e items
114
+ const detalle = buildDetalle(caso.items);
115
+ const { totales } = calcularTotalesDesdeItems(caso.items, caso.descuentoGlobalPct);
116
+
117
+ // Descuento global si aplica
118
+ const dscRcgGlobal = caso.descuentoGlobalPct
119
+ ? [{
120
+ NroLinDR: 1,
121
+ TpoMov: 'D',
122
+ GlosaDR: 'DESCUENTO GLOBAL ITEMES AFECTOS',
123
+ TpoValor: '%',
124
+ ValorDR: caso.descuentoGlobalPct,
125
+ }]
126
+ : null;
127
+
128
+ // Referencia del set de pruebas
129
+ const fechaEmision = this._getFechaEmision();
130
+ const setReferencia = buildSetReferencia(caso.id, fechaEmision);
131
+
132
+ // Construir DTE
133
+ const dteDatos = {
134
+ Encabezado: {
135
+ IdDoc: {
136
+ TipoDTE: 33,
137
+ Folio: folio,
138
+ FchEmis: fechaEmision,
139
+ TpoTranVenta: 1,
140
+ },
141
+ Emisor: this._buildEmisor(),
142
+ Receptor: this._buildReceptor(),
143
+ Totales: totales,
144
+ },
145
+ Detalle: detalle,
146
+ Referencia: [setReferencia],
147
+ ...(dscRcgGlobal ? { DscRcgGlobal: dscRcgGlobal } : {}),
148
+ };
149
+
150
+ const dte = new DTE(dteDatos);
151
+ this._timbrarYFirmar(dte, caf);
152
+
153
+ // Guardar referencia para NC/ND
154
+ this._docRefs[caso.id] = {
155
+ tipoDte: 33,
156
+ folio,
157
+ fecha: fechaEmision,
158
+ detalle,
159
+ totales,
160
+ items: caso.items,
161
+ descuentoGlobalPct: caso.descuentoGlobalPct,
162
+ };
163
+
164
+ this.logger.log(` ✓ Factura caso ${caso.id}: folio ${folio}`);
165
+ return dte;
166
+ }
167
+
168
+ /**
169
+ * Genera una nota de crédito (tipo 61)
170
+ * @private
171
+ */
172
+ async _generarNotaCredito(caso, cafPath) {
173
+ const { DTE, CAF, buildDetalle, calcularTotalesDesdeItems, buildSetReferencia } = require('../index');
174
+ const fs = require('fs');
175
+
176
+ // Obtener documento referenciado
177
+ const base = this._docRefs[caso.referenciaCaso];
178
+ if (!base) {
179
+ throw new Error(`SetBasico: No se encontró referencia del caso ${caso.referenciaCaso}`);
180
+ }
181
+
182
+ // Cargar CAF
183
+ const cafXml = fs.readFileSync(cafPath, 'utf8');
184
+ const caf = new CAF(cafXml);
185
+
186
+ // Reservar folio
187
+ const folio = this._reservarFolio(caf, cafXml);
188
+
189
+ // Determinar items a usar
190
+ let items = caso.itemsFromCaso
191
+ ? (this._docRefs[caso.itemsFromCaso]?.items || [])
192
+ : (caso.items || []);
193
+
194
+ // Si codRef es 3 (DEVOLUCION/MODIFICACION), asegurar precios de la factura original
195
+ if (caso.codRef === 3 && items.length > 0 && base.items?.length > 0) {
196
+ const baseItemsMap = new Map(base.items.map(bi => [bi.nombre, bi]));
197
+ items = items.map(item => {
198
+ if ((!item.precio || item.precio === 0) && baseItemsMap.has(item.nombre)) {
199
+ const baseItem = baseItemsMap.get(item.nombre);
200
+ return { ...item, precio: baseItem.precio, descuentoPct: baseItem.descuentoPct };
201
+ }
202
+ return item;
203
+ });
204
+ }
205
+
206
+ const itemsFinal = items.length ? items : [{ nombre: 'SIN MONTO', cantidad: 1, precio: 0 }];
207
+ const detalle = buildDetalle(itemsFinal);
208
+ const { totales } = calcularTotalesDesdeItems(itemsFinal, null);
209
+
210
+ // Referencias
211
+ const fechaEmision = this._getFechaEmision();
212
+ const setReferencia = buildSetReferencia(caso.id, fechaEmision);
213
+ const docReferencia = {
214
+ NroLinRef: 2,
215
+ TpoDocRef: base.tipoDte,
216
+ FolioRef: base.folio,
217
+ FchRef: base.fecha,
218
+ CodRef: caso.codRef,
219
+ RazonRef: caso.razonRef,
220
+ };
221
+
222
+ // Construir DTE
223
+ const dteDatos = {
224
+ Encabezado: {
225
+ IdDoc: {
226
+ TipoDTE: 61,
227
+ Folio: folio,
228
+ FchEmis: fechaEmision,
229
+ },
230
+ Emisor: this._buildEmisor(),
231
+ Receptor: this._buildReceptor(caso.receptorOverride),
232
+ Totales: totales,
233
+ },
234
+ Detalle: detalle,
235
+ Referencia: [setReferencia, docReferencia],
236
+ };
237
+
238
+ const dte = new DTE(dteDatos);
239
+ this._timbrarYFirmar(dte, caf);
240
+
241
+ // Guardar referencia
242
+ this._docRefs[caso.id] = {
243
+ tipoDte: 61,
244
+ folio,
245
+ fecha: fechaEmision,
246
+ detalle,
247
+ totales,
248
+ items: itemsFinal,
249
+ };
250
+
251
+ this.logger.log(` ✓ NC caso ${caso.id}: folio ${folio} (ref: caso ${caso.referenciaCaso})`);
252
+ return dte;
253
+ }
254
+
255
+ /**
256
+ * Genera una nota de débito (tipo 56)
257
+ * @private
258
+ */
259
+ async _generarNotaDebito(caso, cafPath) {
260
+ const { DTE, CAF, buildDetalle, calcularTotalesDesdeItems, buildSetReferencia } = require('../index');
261
+ const fs = require('fs');
262
+
263
+ // Obtener documento referenciado
264
+ const base = this._docRefs[caso.referenciaCaso];
265
+ if (!base) {
266
+ throw new Error(`SetBasico: No se encontró referencia del caso ${caso.referenciaCaso}`);
267
+ }
268
+
269
+ // Cargar CAF
270
+ const cafXml = fs.readFileSync(cafPath, 'utf8');
271
+ const caf = new CAF(cafXml);
272
+
273
+ // Reservar folio
274
+ const folio = this._reservarFolio(caf, cafXml);
275
+
276
+ // Items
277
+ const itemsFinal = caso.items || [{ nombre: 'SIN MONTO', cantidad: 1, precio: 0 }];
278
+ const detalle = buildDetalle(itemsFinal);
279
+ const { totales } = calcularTotalesDesdeItems(itemsFinal, null);
280
+
281
+ // Referencias
282
+ const fechaEmision = this._getFechaEmision();
283
+ const setReferencia = buildSetReferencia(caso.id, fechaEmision);
284
+ const docReferencia = {
285
+ NroLinRef: 2,
286
+ TpoDocRef: base.tipoDte,
287
+ FolioRef: base.folio,
288
+ FchRef: base.fecha,
289
+ CodRef: caso.codRef,
290
+ RazonRef: caso.razonRef,
291
+ };
292
+
293
+ // Construir DTE
294
+ const dteDatos = {
295
+ Encabezado: {
296
+ IdDoc: {
297
+ TipoDTE: 56,
298
+ Folio: folio,
299
+ FchEmis: fechaEmision,
300
+ },
301
+ Emisor: this._buildEmisor(),
302
+ Receptor: this._buildReceptor(),
303
+ Totales: totales,
304
+ },
305
+ Detalle: detalle,
306
+ Referencia: [setReferencia, docReferencia],
307
+ };
308
+
309
+ const dte = new DTE(dteDatos);
310
+ this._timbrarYFirmar(dte, caf);
311
+
312
+ this.logger.log(` ✓ ND caso ${caso.id}: folio ${folio} (ref: caso ${caso.referenciaCaso})`);
313
+ return dte;
314
+ }
315
+
316
+ // ─────────────────────────────────────────────────────────────────
317
+ // Helpers internos
318
+ // ─────────────────────────────────────────────────────────────────
319
+
320
+ /**
321
+ * Reserva el siguiente folio disponible
322
+ * Compatible con folioHelper de cert-base
323
+ * @private
324
+ */
325
+ _reservarFolio(caf, cafXml) {
326
+ const cafFingerprint = this.folioHelper.createCafFingerprint(cafXml);
327
+ const folio = this.folioHelper.reserveNextFolio({
328
+ rutEmisor: this.config.emisor.rut,
329
+ tipoDte: caf.getTipoDTE(),
330
+ folioDesde: caf.getFolioDesde(),
331
+ folioHasta: caf.getFolioHasta(),
332
+ ambiente: this.config.ambiente || 'certificacion',
333
+ cafFingerprint,
334
+ });
335
+ return folio;
336
+ }
337
+
338
+ /**
339
+ * Obtiene la fecha de emisión en formato YYYY-MM-DD
340
+ * @private
341
+ */
342
+ _getFechaEmision() {
343
+ if (this._fechaEmision) return this._fechaEmision;
344
+ const now = new Date();
345
+ this._fechaEmision = now.toISOString().split('T')[0];
346
+ return this._fechaEmision;
347
+ }
348
+
349
+ /**
350
+ * Construye datos del emisor
351
+ * @private
352
+ */
353
+ _buildEmisor() {
354
+ const e = this.config.emisor;
355
+ return {
356
+ RUTEmisor: e.rut,
357
+ RznSoc: e.razon_social,
358
+ GiroEmis: e.giro,
359
+ Acteco: e.acteco,
360
+ DirOrigen: e.direccion,
361
+ CmnaOrigen: e.comuna,
362
+ CiudadOrigen: e.ciudad || e.comuna || 'Santiago',
363
+ };
364
+ }
365
+
366
+ /**
367
+ * Construye datos del receptor
368
+ * @private
369
+ */
370
+ _buildReceptor(override = null) {
371
+ const r = this.config.receptor;
372
+ return {
373
+ RUTRecep: r.rut,
374
+ RznSocRecep: r.razon_social,
375
+ GiroRecep: r.giro,
376
+ DirRecep: r.direccion,
377
+ CmnaRecep: r.comuna,
378
+ CiudadRecep: r.ciudad || r.comuna,
379
+ ...(override || {}),
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Timbra y firma el DTE
385
+ * @private
386
+ */
387
+ _timbrarYFirmar(dte, caf) {
388
+ const { Certificado } = require('../index');
389
+ const fs = require('fs');
390
+
391
+ // Cargar certificado
392
+ const pfxBuffer = fs.readFileSync(this.config.certificado.path);
393
+ const cert = new Certificado(pfxBuffer, this.config.certificado.password);
394
+
395
+ // Timestamp
396
+ const timestamp = new Date().toISOString().replace('Z', '');
397
+
398
+ // Generar, timbrar y firmar
399
+ dte.generarXML().timbrar(caf, timestamp);
400
+ dte.firmar(cert);
401
+ }
402
+
403
+ /**
404
+ * Obtiene las referencias de documentos generados
405
+ * Útil para debugging o para otros sets que necesiten referencias
406
+ * @returns {Object}
407
+ */
408
+ getDocRefs() {
409
+ return { ...this._docRefs };
410
+ }
411
+ }
412
+
413
+ module.exports = SetBasico;