@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
package/DTE.js ADDED
@@ -0,0 +1,399 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * Documento Tributario Electrónico
5
+ *
6
+ * Genera, timbra y firma documentos tributarios electrónicos
7
+ */
8
+
9
+ const crypto = require('crypto');
10
+ const { XMLBuilder } = require('fast-xml-parser');
11
+ const { DOMParser } = require('@xmldom/xmldom');
12
+ const {
13
+ sanitizeSiiText,
14
+ formatBase64InXml,
15
+ normalizeEmisor,
16
+ normalizeReceptor,
17
+ calcularTotalesDesdeDetalle,
18
+ TASA_IVA_DEFAULT,
19
+ TIPOS_BOLETA,
20
+ TASA_IVA,
21
+ } = require('./utils');
22
+ const { serializeNode, fixEntities, escapeAttr, escapeText, buildSignedInfo, buildSignature } = require('./utils/c14n');
23
+
24
+ // ============================================
25
+ // CONSTANTES
26
+ // ============================================
27
+
28
+ // Según schema SII EnvioDTE_v10.xsd, el orden de campos en IdDoc es:
29
+ // TipoDTE, Folio, FchEmis, IndNoRebaja, TipoDespacho, IndTraslado, TpoImpresion,
30
+ // IndServicio, MntBruto, TpoTranCompra, TpoTranVenta, FmaPago, FmaPagExp, ...
31
+ const CAMPOS_IDDOC_OPCIONALES = [
32
+ 'IndNoRebaja', 'TipoDespacho', 'IndTraslado', 'TpoImpresion', 'IndServicio',
33
+ 'MntBruto', 'TpoTranCompra', 'TpoTranVenta', 'FmaPago', 'FmaPagExp',
34
+ 'FchCancel', 'MntCancel', 'SaldoInsol', 'MntPagos', 'FchVenc',
35
+ 'PeriodoDesde', 'PeriodoHasta', 'MedioPago', 'TpoCtaPago', 'NumCtaPago',
36
+ 'BcoPago', 'TermPagoCdg', 'TermPagoGlosa', 'TermPagoDias', 'FchVencPago',
37
+ 'IndMntNeto',
38
+ ];
39
+
40
+ // ============================================
41
+ // CLASE DTE
42
+ // ============================================
43
+
44
+ class DTE {
45
+ /**
46
+ * @param {Object} datos - Datos del DTE (simplificado o estructurado)
47
+ */
48
+ constructor(datos) {
49
+ if (this._esFormatoSimplificado(datos)) {
50
+ this.datos = this._convertirDatosSimplificados(datos);
51
+ this._certificado = datos.certificado;
52
+ this._caf = datos.caf;
53
+ } else {
54
+ this.datos = datos;
55
+ }
56
+
57
+ this.montoTotal = this.datos.Encabezado?.Totales?.MntTotal || 0;
58
+ this.fechaEmision = this.datos.Encabezado?.IdDoc?.FchEmis;
59
+ this.xml = null;
60
+ this.tedXml = null;
61
+ this.tmstFirma = null;
62
+ }
63
+
64
+ _esFormatoSimplificado(datos) {
65
+ return datos.tipo !== undefined && datos.folio !== undefined;
66
+ }
67
+
68
+ // ============================================
69
+ // CONVERSIÓN DE DATOS
70
+ // ============================================
71
+
72
+ _convertirDatosSimplificados(d) {
73
+ const esExenta = d.tipo === 41;
74
+ const { detalle, mntBruto, mntExento } = this._procesarItems(d.items, esExenta);
75
+ const totales = this._calcularTotales(mntBruto, mntExento, esExenta);
76
+
77
+ const resultado = {
78
+ Encabezado: {
79
+ IdDoc: {
80
+ TipoDTE: d.tipo,
81
+ Folio: d.folio,
82
+ FchEmis: d.fechaEmision,
83
+ IndServicio: d.indServicio || 3,
84
+ },
85
+ Emisor: d.emisor,
86
+ Receptor: {
87
+ RUTRecep: d.receptor?.RUTRecep ?? '66666666-6',
88
+ RznSocRecep: sanitizeSiiText(d.receptor?.RznSocRecep ?? 'Consumidor Final'),
89
+ DirRecep: sanitizeSiiText(d.receptor?.DirRecep ?? 'Sin Direccion'),
90
+ CmnaRecep: d.receptor?.CmnaRecep ?? 'Santiago',
91
+ },
92
+ Totales: totales,
93
+ },
94
+ Detalle: detalle,
95
+ };
96
+
97
+ if (d.referencia) {
98
+ resultado.Referencia = {
99
+ NroLinRef: d.referencia.NroLinRef || 1,
100
+ CodRef: d.referencia.CodRef || d.referencia.codigo,
101
+ RazonRef: d.referencia.RazonRef || d.referencia.razon,
102
+ };
103
+ }
104
+
105
+ return resultado;
106
+ }
107
+
108
+ _procesarItems(items, esExenta) {
109
+ let mntBruto = 0;
110
+ let mntExento = 0;
111
+
112
+ const detalle = items.map((item, idx) => {
113
+ const qty = item.QtyItem || 1;
114
+ const prc = item.PrcItem;
115
+ const montoItem = Math.round(qty * prc);
116
+
117
+ if (esExenta || item.IndExe === 1) {
118
+ mntExento += montoItem;
119
+ } else {
120
+ mntBruto += montoItem;
121
+ }
122
+
123
+ const det = {
124
+ NroLinDet: idx + 1,
125
+ ...(esExenta || item.IndExe ? { IndExe: 1 } : {}),
126
+ NmbItem: sanitizeSiiText(item.NmbItem),
127
+ QtyItem: qty,
128
+ ...(item.UnmdItem ? { UnmdItem: item.UnmdItem } : {}),
129
+ PrcItem: prc,
130
+ MontoItem: montoItem,
131
+ };
132
+
133
+ return det;
134
+ });
135
+
136
+ return { detalle, mntBruto, mntExento };
137
+ }
138
+
139
+ _calcularTotales(mntBruto, mntExento, esExenta) {
140
+ const mntNeto = esExenta ? 0 : Math.round(mntBruto / (1 + (TASA_IVA / 100)));
141
+ const iva = esExenta ? 0 : (mntBruto - mntNeto);
142
+ const mntTotal = mntNeto + iva + mntExento;
143
+
144
+ const totales = {};
145
+ if (mntNeto > 0) totales.MntNeto = mntNeto;
146
+ if (mntExento > 0) totales.MntExe = mntExento;
147
+ if (mntNeto > 0) totales.IVA = iva;
148
+ totales.MntTotal = mntTotal;
149
+
150
+ return totales;
151
+ }
152
+
153
+ // ============================================
154
+ // GENERACIÓN XML
155
+ // ============================================
156
+
157
+ generarXML() {
158
+ const enc = this.datos.Encabezado;
159
+ const det = this.datos.Detalle;
160
+ const tipoDte = Number(enc.IdDoc.TipoDTE);
161
+ const esBoleta = TIPOS_BOLETA.includes(tipoDte);
162
+
163
+ this.id = `DTE_T${tipoDte}F${enc.IdDoc.Folio}`;
164
+
165
+ const idDoc = this._buildIdDoc(enc.IdDoc, esBoleta);
166
+ const emisor = this._buildEmisor(enc.Emisor, esBoleta);
167
+ const receptor = this._buildReceptor(enc.Receptor, esBoleta);
168
+ const detalle = this._buildDetalle(det);
169
+
170
+ this.documento = {
171
+ Documento: {
172
+ '@_ID': this.id,
173
+ Encabezado: { IdDoc: idDoc, Emisor: emisor, Receptor: receptor, Totales: enc.Totales },
174
+ Detalle: detalle,
175
+ ...(this.datos.DscRcgGlobal ? { DscRcgGlobal: this.datos.DscRcgGlobal } : {}),
176
+ ...(this.datos.Referencia ? { Referencia: this.datos.Referencia } : {}),
177
+ TED: null,
178
+ }
179
+ };
180
+
181
+ return this;
182
+ }
183
+
184
+ _buildIdDoc(idDoc, esBoleta) {
185
+ const result = {
186
+ TipoDTE: idDoc.TipoDTE,
187
+ Folio: idDoc.Folio,
188
+ FchEmis: idDoc.FchEmis,
189
+ };
190
+
191
+ CAMPOS_IDDOC_OPCIONALES.forEach(campo => {
192
+ if (idDoc[campo] !== undefined && idDoc[campo] !== null) {
193
+ result[campo] = idDoc[campo];
194
+ }
195
+ });
196
+
197
+ if (esBoleta && !result.IndServicio) {
198
+ result.IndServicio = 3;
199
+ }
200
+
201
+ return result;
202
+ }
203
+
204
+ _buildEmisor(emisor, esBoleta) {
205
+ return normalizeEmisor(emisor, esBoleta);
206
+ }
207
+
208
+ _buildReceptor(receptor, esBoleta) {
209
+ return normalizeReceptor(receptor, esBoleta);
210
+ }
211
+
212
+ _buildDetalle(det) {
213
+ return (Array.isArray(det) ? det : [det]).map(item => ({
214
+ ...item,
215
+ NmbItem: sanitizeSiiText(item.NmbItem),
216
+ ...(item.DscItem ? { DscItem: sanitizeSiiText(item.DscItem) } : {}),
217
+ }));
218
+ }
219
+
220
+ // ============================================
221
+ // TIMBRAJE (TED)
222
+ // ============================================
223
+
224
+ timbrar(caf, timestampOverride) {
225
+ const enc = this.datos.Encabezado;
226
+ const det = this.datos.Detalle;
227
+ const primerItem = Array.isArray(det) ? det[0] : det;
228
+ const tipoDte = Number(enc.IdDoc.TipoDTE);
229
+ const esBoleta = TIPOS_BOLETA.includes(tipoDte);
230
+
231
+ this.tmstFirma = timestampOverride || new Date().toISOString().replace(/\.\d{3}Z$/, '');
232
+
233
+ const rznRecepRaw = sanitizeSiiText((enc.Receptor.RznSocRecep || '').substring(0, 40));
234
+ const it1Raw = sanitizeSiiText((primerItem.NmbItem || 'Producto').substring(0, 40));
235
+ const rznRecepXml = this._escapeXmlText(rznRecepRaw);
236
+ const it1Xml = this._escapeXmlText(it1Raw);
237
+
238
+ const dd = {
239
+ RE: enc.Emisor.RUTEmisor,
240
+ TD: enc.IdDoc.TipoDTE,
241
+ F: enc.IdDoc.Folio,
242
+ FE: enc.IdDoc.FchEmis,
243
+ RR: enc.Receptor.RUTRecep || (esBoleta ? '' : undefined),
244
+ MNT: enc.Totales.MntTotal,
245
+ CAF: caf.getCafXml(),
246
+ TSTED: this.tmstFirma,
247
+ };
248
+
249
+ const ddString = this._buildDDString(dd, rznRecepXml, it1Xml);
250
+ const firma = caf.sign(ddString);
251
+
252
+ this.tedXml = `<TED version="1.0"><DD><RE>${dd.RE}</RE><TD>${dd.TD}</TD><F>${dd.F}</F><FE>${dd.FE}</FE><RR>${dd.RR}</RR><RSR>${rznRecepXml}</RSR><MNT>${dd.MNT}</MNT><IT1>${it1Xml}</IT1>${dd.CAF}<TSTED>${dd.TSTED}</TSTED></DD><FRMT algoritmo="SHA1withRSA">${firma}</FRMT></TED>`;
253
+
254
+ this.documento.Documento.TED = '__TED_PLACEHOLDER__';
255
+ this.documento.Documento.TmstFirma = '__TMSTFIRMA_PLACEHOLDER__';
256
+
257
+ return this;
258
+ }
259
+
260
+ _buildDDString(dd, rsrXml, it1Xml) {
261
+ return `<DD><RE>${dd.RE}</RE><TD>${dd.TD}</TD><F>${dd.F}</F><FE>${dd.FE}</FE><RR>${dd.RR}</RR><RSR>${rsrXml}</RSR><MNT>${dd.MNT}</MNT><IT1>${it1Xml}</IT1>${dd.CAF}<TSTED>${dd.TSTED}</TSTED></DD>`;
262
+ }
263
+
264
+ _escapeXmlText(value) {
265
+ return String(value || '')
266
+ .replace(/&/g, '&amp;')
267
+ .replace(/</g, '&lt;')
268
+ .replace(/>/g, '&gt;')
269
+ .replace(/"/g, '&quot;')
270
+ .replace(/'/g, '&apos;');
271
+ }
272
+
273
+ // ============================================
274
+ // FIRMA ELECTRÓNICA
275
+ // ============================================
276
+
277
+ firmar(certificado) {
278
+ const dteXml = this._buildXmlSinFirma();
279
+ const doc = new DOMParser().parseFromString(dteXml, 'application/xml');
280
+
281
+ // Canonicalizar documento
282
+ const documentoC14N = this._c14nDocumento(doc);
283
+ const documentoC14NBytes = Buffer.from(documentoC14N, 'utf8');
284
+ const digestValue = crypto.createHash('sha1').update(documentoC14NBytes).digest('base64');
285
+
286
+ // Construir SignedInfo (usando c14n centralizado)
287
+ const signedInfoParaFirmar = buildSignedInfo(this.id, digestValue, { expandTags: true, includeXsi: true });
288
+ const signedInfoParaGuardar = buildSignedInfo(this.id, digestValue, { expandTags: false, includeXsi: false });
289
+
290
+ // Firmar
291
+ const sign = crypto.createSign('RSA-SHA1');
292
+ sign.update(Buffer.from(signedInfoParaFirmar, 'utf8'));
293
+ const signatureValue = sign.sign(certificado.getPrivateKeyPem(), 'base64');
294
+ const formattedSignature = signatureValue.match(/.{1,76}/g).join('\n');
295
+
296
+ // Construir Signature (usando c14n centralizado)
297
+ const signatureXml = buildSignature(signedInfoParaGuardar, formattedSignature, {
298
+ modulus: certificado.getModulus(),
299
+ exponent: certificado.getExponent(),
300
+ certificate: certificado.getCertificateBase64(),
301
+ });
302
+
303
+ // Insertar firma
304
+ const xmlFirmado = dteXml.replace('</Documento></DTE>', `</Documento>${signatureXml}</DTE>`);
305
+ this.xml = formatBase64InXml(xmlFirmado);
306
+
307
+ return this;
308
+ }
309
+
310
+ // NOTA: _buildSignedInfo y _buildSignature movidos a utils/c14n.js (v2.6.0)
311
+
312
+ _buildXmlSinFirma() {
313
+ const builder = new XMLBuilder({
314
+ ignoreAttributes: false,
315
+ attributeNamePrefix: '@_',
316
+ format: false,
317
+ });
318
+
319
+ const dteConVersion = {
320
+ '@_xmlns': 'http://www.sii.cl/SiiDte',
321
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
322
+ '@_version': '1.0',
323
+ ...this.documento
324
+ };
325
+
326
+ let dteXml = builder.build({ DTE: dteConVersion });
327
+ dteXml = dteXml.replace('<TED>__TED_PLACEHOLDER__</TED>', this.tedXml);
328
+ dteXml = dteXml.replace('<TmstFirma>__TMSTFIRMA_PLACEHOLDER__</TmstFirma>', `<TmstFirma>${this.tmstFirma}</TmstFirma>`);
329
+
330
+ return dteXml;
331
+ }
332
+
333
+ // ============================================
334
+ // CANONICALIZACIÓN (C14N) - Usa utils/c14n.js
335
+ // ============================================
336
+
337
+ _c14nDocumento(doc) {
338
+ const documento = doc.getElementsByTagName('Documento')[0];
339
+ const dteRoot = doc.getElementsByTagName('DTE')[0];
340
+ if (!documento) return '';
341
+
342
+ const inheritedNs = new Map();
343
+ if (dteRoot) {
344
+ const defaultNs = dteRoot.getAttribute('xmlns');
345
+ if (defaultNs) inheritedNs.set('xmlns', defaultNs);
346
+ const xsiNs = dteRoot.getAttribute('xmlns:xsi');
347
+ if (xsiNs) inheritedNs.set('xmlns:xsi', xsiNs);
348
+ }
349
+
350
+ let c14n = '<Documento';
351
+ if (inheritedNs.has('xmlns')) c14n += ` xmlns="${inheritedNs.get('xmlns')}"`;
352
+ if (inheritedNs.has('xmlns:xsi')) c14n += ` xmlns:xsi="${inheritedNs.get('xmlns:xsi')}"`;
353
+ const id = documento.getAttribute('ID');
354
+ if (id) c14n += ` ID="${id}"`;
355
+ c14n += '>';
356
+
357
+ for (let i = 0; i < documento.childNodes.length; i++) {
358
+ c14n += serializeNode(documento.childNodes[i], inheritedNs);
359
+ }
360
+
361
+ c14n += '</Documento>';
362
+ return fixEntities(c14n);
363
+ }
364
+
365
+ // ============================================
366
+ // GETTERS
367
+ // ============================================
368
+
369
+ getXML() { return this.xml; }
370
+ getXMLSinFirma() { return this._buildXmlSinFirma(); }
371
+ getTED() { return this.tedXml; }
372
+ getId() { return this.id; }
373
+
374
+ /**
375
+ * Obtiene el tipo de DTE
376
+ * @returns {number}
377
+ */
378
+ getTipoDTE() {
379
+ return Number(this.datos?.Encabezado?.IdDoc?.TipoDTE);
380
+ }
381
+
382
+ /**
383
+ * Obtiene el folio del documento
384
+ * @returns {number}
385
+ */
386
+ getFolio() {
387
+ return Number(this.datos?.Encabezado?.IdDoc?.Folio);
388
+ }
389
+
390
+ /**
391
+ * Obtiene el monto total del documento
392
+ * @returns {number}
393
+ */
394
+ getMontoTotal() {
395
+ return Number(this.datos?.Encabezado?.Totales?.MntTotal || 0);
396
+ }
397
+ }
398
+
399
+ module.exports = DTE;