@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,472 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * Set Factura de Compra
5
+ *
6
+ * Tipos DTE: 46 (Factura de Compra), 61 (NC), 56 (ND)
7
+ *
8
+ * Particularidades:
9
+ * - El emisor actúa como comprador (retiene IVA)
10
+ * - ImptoReten con TipoImp=15 (IVA Retenido Total)
11
+ * - MntTotal = MntNeto (IVA se cancela con retención)
12
+ * - NC/ND referencian la factura de compra original
13
+ *
14
+ * @module dte-sii/cert/SetCompra
15
+ */
16
+
17
+ const SetBase = require('./SetBase');
18
+ const { SET_LABELS, SETS_DTE } = require('./types');
19
+
20
+ class SetCompra extends SetBase {
21
+ constructor(deps) {
22
+ super(deps);
23
+
24
+ this.key = 'compra';
25
+ this.label = SET_LABELS.compra;
26
+ this.tiposDte = SETS_DTE.compra; // [46, 56, 61]
27
+
28
+ // Registro de referencias entre documentos
29
+ this._docRefs = {};
30
+ }
31
+
32
+ /**
33
+ * @override
34
+ * Valida casos específicos del set compra
35
+ */
36
+ _validarCasos(casos) {
37
+ super._validarCasos(casos);
38
+
39
+ if (!casos.casoFactura) {
40
+ throw new Error('SetCompra: casoFactura es requerido');
41
+ }
42
+ }
43
+
44
+ /**
45
+ * @override
46
+ * Calcula folios necesarios por tipo
47
+ */
48
+ _calcularCantidadFolios(casos, tipoDte) {
49
+ if (casos.cafRequired?.[tipoDte]) {
50
+ return casos.cafRequired[tipoDte];
51
+ }
52
+
53
+ // Set compra tiene 1 documento de cada tipo
54
+ return 1;
55
+ }
56
+
57
+ /**
58
+ * @override
59
+ * Genera DTEs del set factura compra
60
+ *
61
+ * @param {Object} casos - { casoFactura, casoNC, casoND, cafRequired }
62
+ * @param {Object} cafs - { 46: cafPath, 56: cafPath, 61: cafPath }
63
+ * @returns {Promise<DTE[]>}
64
+ */
65
+ async generarDtes(casos, cafs) {
66
+ const dtes = [];
67
+
68
+ // 1. Generar factura de compra
69
+ this.logger.log(' 📄 Generando factura de compra...');
70
+ const dteFactura = await this._generarFacturaCompra(casos.casoFactura, cafs[46]);
71
+ dtes.push(dteFactura);
72
+
73
+ // 2. Generar nota de crédito
74
+ if (casos.casoNC) {
75
+ this.logger.log(' 📄 Generando nota de crédito...');
76
+ const dteNc = await this._generarNotaCredito(casos.casoNC, cafs[61]);
77
+ dtes.push(dteNc);
78
+ }
79
+
80
+ // 3. Generar nota de débito
81
+ if (casos.casoND) {
82
+ this.logger.log(' 📄 Generando nota de débito...');
83
+ const dteNd = await this._generarNotaDebito(casos.casoND, cafs[56]);
84
+ dtes.push(dteNd);
85
+ }
86
+
87
+ return dtes;
88
+ }
89
+
90
+ // ═══════════════════════════════════════════════════════════════
91
+ // Generadores de DTEs
92
+ // ═══════════════════════════════════════════════════════════════
93
+
94
+ /**
95
+ * Genera factura de compra (tipo 46)
96
+ * @private
97
+ */
98
+ async _generarFacturaCompra(caso, cafPath) {
99
+ const { DTE, CAF, buildDetalleCompra, buildSetReferencia } = require('../index');
100
+ const fs = require('fs');
101
+
102
+ const cafXml = fs.readFileSync(cafPath, 'utf8');
103
+ const caf = new CAF(cafXml);
104
+ const folio = this._reservarFolio(caf, cafXml);
105
+
106
+ // Construir detalle con buildDetalleCompra (incluye CodImpAdic)
107
+ const items = this._normalizarItems(caso.items);
108
+ const detalle = buildDetalleCompra(items, { codImpAdic: 15 });
109
+ const totales = this._calcularTotalesFacturaCompra(detalle);
110
+
111
+ const fechaEmision = this._getFechaEmision();
112
+ const setReferencia = buildSetReferencia(caso.id, fechaEmision);
113
+
114
+ const dteDatos = {
115
+ Encabezado: {
116
+ IdDoc: {
117
+ TipoDTE: 46,
118
+ Folio: folio,
119
+ FchEmis: fechaEmision,
120
+ },
121
+ Emisor: this._buildEmisor(),
122
+ Receptor: this._buildEmisorAsReceptor(), // Emisor es también receptor en F. Compra
123
+ Totales: totales,
124
+ },
125
+ Detalle: detalle,
126
+ Referencia: [setReferencia],
127
+ };
128
+
129
+ const dte = new DTE(dteDatos);
130
+ this._timbrarYFirmar(dte, caf);
131
+
132
+ // Guardar referencia para NC/ND
133
+ this._docRefs[caso.id] = {
134
+ tipoDte: 46,
135
+ folio,
136
+ fecha: fechaEmision,
137
+ detalle,
138
+ totales,
139
+ items,
140
+ };
141
+
142
+ this.logger.log(` ✓ Factura Compra caso ${caso.id}: folio ${folio}`);
143
+ return dte;
144
+ }
145
+
146
+ /**
147
+ * Genera nota de crédito para factura de compra (tipo 61)
148
+ * @private
149
+ */
150
+ async _generarNotaCredito(caso, cafPath) {
151
+ const { DTE, CAF, buildDetalleCompra, buildSetReferencia } = require('../index');
152
+ const fs = require('fs');
153
+
154
+ const cafXml = fs.readFileSync(cafPath, 'utf8');
155
+ const caf = new CAF(cafXml);
156
+ const folio = this._reservarFolio(caf, cafXml);
157
+
158
+ // Obtener documento referenciado
159
+ const docRef = this._docRefs[caso.referenciaCaso];
160
+ if (!docRef) {
161
+ throw new Error(`SetCompra: Documento referencia ${caso.referenciaCaso} no encontrado para NC ${caso.id}`);
162
+ }
163
+
164
+ // Resolver items: NC puede tener cantidades parciales
165
+ const items = this._resolverItemsNC(caso, docRef);
166
+ const detalle = buildDetalleCompra(items, { codImpAdic: 15 });
167
+ const totales = this._calcularTotalesFacturaCompra(detalle);
168
+
169
+ const fechaEmision = this._getFechaEmision();
170
+ const setReferencia = buildSetReferencia(caso.id, fechaEmision);
171
+
172
+ // Referencia al documento original
173
+ const docReferencia = {
174
+ NroLinRef: 2,
175
+ TpoDocRef: docRef.tipoDte,
176
+ FolioRef: docRef.folio,
177
+ FchRef: docRef.fecha,
178
+ CodRef: caso.codRef,
179
+ RazonRef: caso.razonRef,
180
+ };
181
+
182
+ const dteDatos = {
183
+ Encabezado: {
184
+ IdDoc: {
185
+ TipoDTE: 61,
186
+ Folio: folio,
187
+ FchEmis: fechaEmision,
188
+ },
189
+ Emisor: this._buildEmisor(),
190
+ Receptor: this._buildEmisorAsReceptor(),
191
+ Totales: totales,
192
+ },
193
+ Detalle: detalle,
194
+ Referencia: [setReferencia, docReferencia],
195
+ };
196
+
197
+ const dte = new DTE(dteDatos);
198
+ this._timbrarYFirmar(dte, caf);
199
+
200
+ // Guardar referencia para ND
201
+ this._docRefs[caso.id] = {
202
+ tipoDte: 61,
203
+ folio,
204
+ fecha: fechaEmision,
205
+ detalle,
206
+ totales,
207
+ items,
208
+ };
209
+
210
+ this.logger.log(` ✓ NC Compra caso ${caso.id}: folio ${folio} (ref: ${caso.referenciaCaso})`);
211
+ return dte;
212
+ }
213
+
214
+ /**
215
+ * Genera nota de débito para factura de compra (tipo 56)
216
+ * @private
217
+ */
218
+ async _generarNotaDebito(caso, cafPath) {
219
+ const { DTE, CAF, buildDetalleCompra, buildSetReferencia } = require('../index');
220
+ const fs = require('fs');
221
+
222
+ const cafXml = fs.readFileSync(cafPath, 'utf8');
223
+ const caf = new CAF(cafXml);
224
+ const folio = this._reservarFolio(caf, cafXml);
225
+
226
+ // Obtener documento referenciado
227
+ const docRef = this._docRefs[caso.referenciaCaso];
228
+ if (!docRef) {
229
+ throw new Error(`SetCompra: Documento referencia ${caso.referenciaCaso} no encontrado para ND ${caso.id}`);
230
+ }
231
+
232
+ // ND generalmente tiene los mismos items que NC (anula NC)
233
+ const items = this._resolverItemsND(caso, docRef);
234
+ const detalle = buildDetalleCompra(items, { codImpAdic: 15 });
235
+ const totales = this._calcularTotalesFacturaCompra(detalle);
236
+
237
+ const fechaEmision = this._getFechaEmision();
238
+ const setReferencia = buildSetReferencia(caso.id, fechaEmision);
239
+
240
+ // Referencia al documento original
241
+ const docReferencia = {
242
+ NroLinRef: 2,
243
+ TpoDocRef: docRef.tipoDte,
244
+ FolioRef: docRef.folio,
245
+ FchRef: docRef.fecha,
246
+ CodRef: caso.codRef,
247
+ RazonRef: caso.razonRef,
248
+ };
249
+
250
+ const dteDatos = {
251
+ Encabezado: {
252
+ IdDoc: {
253
+ TipoDTE: 56,
254
+ Folio: folio,
255
+ FchEmis: fechaEmision,
256
+ },
257
+ Emisor: this._buildEmisor(),
258
+ Receptor: this._buildEmisorAsReceptor(),
259
+ Totales: totales,
260
+ },
261
+ Detalle: detalle,
262
+ Referencia: [setReferencia, docReferencia],
263
+ };
264
+
265
+ const dte = new DTE(dteDatos);
266
+ this._timbrarYFirmar(dte, caf);
267
+
268
+ this.logger.log(` ✓ ND Compra caso ${caso.id}: folio ${folio} (ref: ${caso.referenciaCaso})`);
269
+ return dte;
270
+ }
271
+
272
+ // ═══════════════════════════════════════════════════════════════
273
+ // Helpers de cálculo
274
+ // ═══════════════════════════════════════════════════════════════
275
+
276
+ /**
277
+ * Calcula totales para Factura de Compra con IVA Retenido Total
278
+ * @private
279
+ */
280
+ _calcularTotalesFacturaCompra(detalle) {
281
+ let mntBruto = 0;
282
+ let mntExento = 0;
283
+
284
+ for (const det of detalle) {
285
+ if (det.IndExe === 1) {
286
+ mntExento += det.MontoItem || 0;
287
+ } else {
288
+ mntBruto += det.MontoItem || 0;
289
+ }
290
+ }
291
+
292
+ const tasaIva = 19;
293
+ // Precios son netos en certificación
294
+ const mntNeto = mntBruto;
295
+ const iva = mntNeto > 0 ? Math.round(mntNeto * (tasaIva / 100)) : 0;
296
+
297
+ // MntTotal = MntNeto (IVA se cancela con retención)
298
+ const mntTotal = mntNeto + mntExento;
299
+
300
+ // Orden según schema: MntNeto, MntExe, TasaIVA, IVA, ImptoReten, MntTotal
301
+ const totales = {};
302
+ if (mntNeto > 0) totales.MntNeto = mntNeto;
303
+ if (mntExento > 0) totales.MntExe = mntExento;
304
+ if (mntNeto > 0) totales.TasaIVA = tasaIva;
305
+ if (mntNeto > 0) totales.IVA = iva;
306
+
307
+ // ImptoReten con TipoImp=15 = IVA Retenido Total
308
+ if (iva > 0) {
309
+ totales.ImptoReten = [{
310
+ TipoImp: 15,
311
+ TasaImp: tasaIva,
312
+ MontoImp: iva,
313
+ }];
314
+ }
315
+
316
+ totales.MntTotal = mntTotal;
317
+
318
+ return totales;
319
+ }
320
+
321
+ // ═══════════════════════════════════════════════════════════════
322
+ // Helpers de items
323
+ // ═══════════════════════════════════════════════════════════════
324
+
325
+ /**
326
+ * Normaliza items
327
+ * @private
328
+ */
329
+ _normalizarItems(items) {
330
+ return (items || []).map(item => ({
331
+ nombre: item.nombre,
332
+ cantidad: item.cantidad || 1,
333
+ precio: item.precio || 0,
334
+ unidad: item.unidad || 'UN',
335
+ }));
336
+ }
337
+
338
+ /**
339
+ * Resuelve items para NC
340
+ * Si NC tiene items con cantidad pero sin precio, usar precio del original
341
+ * @private
342
+ */
343
+ _resolverItemsNC(caso, docRef) {
344
+ const ncItems = caso.items || [];
345
+
346
+ return ncItems.map(ncItem => {
347
+ // Buscar item original por nombre para obtener precio
348
+ const originalItem = (docRef.items || []).find(
349
+ i => i.nombre === ncItem.nombre
350
+ );
351
+
352
+ return {
353
+ nombre: ncItem.nombre,
354
+ cantidad: ncItem.cantidad || 1,
355
+ precio: ncItem.precio || originalItem?.precio || 0,
356
+ unidad: ncItem.unidad || originalItem?.unidad || 'UN',
357
+ };
358
+ });
359
+ }
360
+
361
+ /**
362
+ * Resuelve items para ND
363
+ * @private
364
+ */
365
+ _resolverItemsND(caso, docRef) {
366
+ const ndItems = caso.items || [];
367
+
368
+ // Si ND tiene items propios
369
+ if (ndItems.length) {
370
+ return ndItems.map(ndItem => {
371
+ // Buscar item original por nombre para obtener precio
372
+ const originalItem = (docRef.items || []).find(
373
+ i => i.nombre === ndItem.nombre
374
+ );
375
+
376
+ return {
377
+ nombre: ndItem.nombre,
378
+ cantidad: ndItem.cantidad || 1,
379
+ precio: ndItem.precio || originalItem?.precio || 0,
380
+ unidad: ndItem.unidad || originalItem?.unidad || 'UN',
381
+ };
382
+ });
383
+ }
384
+
385
+ // Si no, usar items del documento referenciado
386
+ return docRef.items || [];
387
+ }
388
+
389
+ // ═══════════════════════════════════════════════════════════════
390
+ // Helpers comunes
391
+ // ═══════════════════════════════════════════════════════════════
392
+
393
+ /**
394
+ * Reserva el siguiente folio disponible
395
+ * @private
396
+ */
397
+ _reservarFolio(caf, cafXml) {
398
+ const cafFingerprint = this.folioHelper.createCafFingerprint(cafXml);
399
+ const folio = this.folioHelper.reserveNextFolio({
400
+ rutEmisor: this.config.emisor.rut,
401
+ tipoDte: caf.getTipoDTE(),
402
+ folioDesde: caf.getFolioDesde(),
403
+ folioHasta: caf.getFolioHasta(),
404
+ ambiente: this.config.ambiente || 'certificacion',
405
+ cafFingerprint,
406
+ });
407
+ return folio;
408
+ }
409
+
410
+ /**
411
+ * Obtiene la fecha de emisión
412
+ * @private
413
+ */
414
+ _getFechaEmision() {
415
+ if (this._fechaEmision) return this._fechaEmision;
416
+ const now = new Date();
417
+ this._fechaEmision = now.toISOString().split('T')[0];
418
+ return this._fechaEmision;
419
+ }
420
+
421
+ /**
422
+ * Construye datos del emisor
423
+ * @private
424
+ */
425
+ _buildEmisor() {
426
+ const e = this.config.emisor;
427
+ return {
428
+ RUTEmisor: e.rut,
429
+ RznSoc: e.razon_social,
430
+ GiroEmis: e.giro,
431
+ Acteco: e.acteco,
432
+ DirOrigen: e.direccion,
433
+ CmnaOrigen: e.comuna,
434
+ CiudadOrigen: e.ciudad || e.comuna || 'Santiago',
435
+ };
436
+ }
437
+
438
+ /**
439
+ * Construye datos del emisor como receptor (Factura de Compra)
440
+ * @private
441
+ */
442
+ _buildEmisorAsReceptor() {
443
+ const e = this.config.emisor;
444
+ return {
445
+ RUTRecep: e.rut,
446
+ RznSocRecep: e.razon_social,
447
+ GiroRecep: e.giro,
448
+ DirRecep: e.direccion,
449
+ CmnaRecep: e.comuna,
450
+ CiudadRecep: e.ciudad || e.comuna || 'Santiago',
451
+ };
452
+ }
453
+
454
+ /**
455
+ * Timbra y firma el DTE
456
+ * @private
457
+ */
458
+ _timbrarYFirmar(dte, caf) {
459
+ const { Certificado } = require('../index');
460
+ const fs = require('fs');
461
+
462
+ const pfxBuffer = fs.readFileSync(this.config.certificado.path);
463
+ const cert = new Certificado(pfxBuffer, this.config.certificado.password);
464
+
465
+ const timestamp = new Date().toISOString().replace('Z', '');
466
+
467
+ dte.generarXML().timbrar(caf, timestamp);
468
+ dte.firmar(cert);
469
+ }
470
+ }
471
+
472
+ module.exports = SetCompra;