@devlas/dte-sii 2.5.11 → 2.5.13

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.
@@ -1,429 +1,429 @@
1
1
  // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
2
  // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
- /**
4
- * IntercambioCert.js
5
- *
6
- * Módulo para generar respuestas de Intercambio de Información DTE
7
- * - Respuesta Recepción de Envío
8
- * - Respuesta Aprobación Comercial (ResultadoDTE)
9
- * - Envío de Recibos (Recepción de Mercaderías)
10
- *
11
- * @module dte-sii/cert/IntercambioCert
12
- */
13
-
14
- const fs = require('fs');
15
- const path = require('path');
16
- const { XMLParser, XMLBuilder } = require('fast-xml-parser');
17
- const { SignedXml } = require('xml-crypto');
18
-
19
- const DECLARACION_RECIBO = 'El acuse de recibo que se declara en este acto, de acuerdo a lo dispuesto en la letra b) del Art. 4, y la letra c) del Art. 5 de la Ley 19.983, acredita que la entrega de mercaderias o servicio(s) prestado(s) ha(n) sido recibido(s).';
20
-
21
- class IntercambioCert {
22
- /**
23
- * @param {Object} options
24
- * @param {Object} options.certificado - Instancia de Certificado
25
- * @param {Object} options.emisor - Datos del emisor { rut, razonSocial }
26
- * @param {Object} [options.contacto] - Datos de contacto { nombre, mail, fono }
27
- * @param {string} [options.debugDir] - Directorio para guardar XMLs
28
- */
29
- constructor({ certificado, emisor, contacto = {}, debugDir }) {
30
- this.certificado = certificado;
31
- this.emisor = emisor;
32
- this.contacto = contacto;
33
- this.debugDir = debugDir;
34
- }
35
-
36
- /**
37
- * Obtiene las utilidades del paquete
38
- * @private
39
- */
40
- _lib() {
41
- const { formatRut, normalizeArray, sanitizeSiiText } = require('../index');
42
- return { formatRut, normalizeArray, sanitizeSiiText };
43
- }
44
-
45
- /**
46
- * Formatea fecha/hora en formato SII
47
- * @private
48
- */
49
- _formatSiiDateTime(date = new Date()) {
50
- const pad = (n) => String(n).padStart(2, '0');
51
- return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
52
- }
53
-
54
- /**
55
- * Firma un XML con el certificado
56
- * @private
57
- */
58
- _signXml({ xml, referenceXpath, insertAfterXpath }) {
59
- const sig = new SignedXml();
60
- sig.privateKey = this.certificado.getPrivateKeyPem();
61
- sig.publicCert = this.certificado.getCertificatePem();
62
- sig.canonicalizationAlgorithm = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
63
- sig.signatureAlgorithm = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
64
- sig.addReference({
65
- xpath: referenceXpath,
66
- transforms: ['http://www.w3.org/2000/09/xmldsig#enveloped-signature'],
67
- digestAlgorithm: 'http://www.w3.org/2000/09/xmldsig#sha1',
68
- });
69
- sig.getKeyInfoContent = () => {
70
- const certBase64 = this.certificado.getCertificateBase64();
71
- const modulus = this.certificado.getModulus();
72
- const exponent = this.certificado.getExponent();
73
- return `<KeyValue><RSAKeyValue><Modulus>${modulus}</Modulus><Exponent>${exponent}</Exponent></RSAKeyValue></KeyValue><X509Data><X509Certificate>${certBase64}</X509Certificate></X509Data>`;
74
- };
75
-
76
- sig.computeSignature(xml, {
77
- location: {
78
- reference: insertAfterXpath || referenceXpath,
79
- action: 'after',
80
- },
81
- });
82
-
83
- return sig.getSignedXml();
84
- }
85
-
86
- /**
87
- * Parsea un EnvioDTE XML y extrae los documentos
88
- * @param {string} xml - XML del EnvioDTE
89
- * @returns {Object} Metadatos del envío
90
- */
91
- parseEnvioDTE(xml) {
92
- const { normalizeArray } = this._lib();
93
-
94
- const parser = new XMLParser({
95
- ignoreAttributes: false,
96
- attributeNamePrefix: '@_',
97
- trimValues: true,
98
- parseTagValue: true,
99
- });
100
-
101
- const data = parser.parse(xml);
102
- const envio = data?.EnvioDTE || data?.EnvioDTEB || data?.EnvioDTETraslado || data?.EnvioBOLETA || {};
103
- const setDte = envio?.SetDTE || data?.SetDTE || {};
104
- const caratula = setDte?.Caratula || {};
105
- const setId = setDte?.['@_ID'] || setDte?.ID || 'SetDTE';
106
-
107
- const rutEmisor = caratula?.RutEmisor || null;
108
- const rutEnvia = caratula?.RutEnvia || null;
109
- const rutReceptor = caratula?.RutReceptor || null;
110
-
111
- const dtes = normalizeArray(setDte?.DTE);
112
- const documentos = dtes.map((dte) => dte?.Documento || dte?.DTE?.Documento || dte?.DTE?.Documento || {}).filter(Boolean);
113
-
114
- const items = documentos.map((doc) => {
115
- const encabezado = doc?.Encabezado || {};
116
- const idDoc = encabezado?.IdDoc || {};
117
- const emisor = encabezado?.Emisor || {};
118
- const receptor = encabezado?.Receptor || {};
119
- const totales = encabezado?.Totales || {};
120
-
121
- return {
122
- tipoDTE: parseInt(idDoc?.TipoDTE ?? idDoc?.TpoDoc ?? 0, 10) || 0,
123
- folio: parseInt(idDoc?.Folio ?? 0, 10) || 0,
124
- fchEmis: idDoc?.FchEmis || null,
125
- rutEmisor: emisor?.RUTEmisor || emisor?.RutEmisor || rutEmisor || null,
126
- rutRecep: receptor?.RUTRecep || receptor?.RutRecep || rutReceptor || null,
127
- mntTotal: parseInt(totales?.MntTotal ?? 0, 10) || 0,
128
- };
129
- });
130
-
131
- return {
132
- setId,
133
- rutEmisor,
134
- rutEnvia,
135
- rutReceptor,
136
- items,
137
- };
138
- }
139
-
140
- /**
141
- * Genera la Respuesta de Recepción de Envío
142
- * @param {Object} meta - Metadatos del EnvioDTE parseado
143
- * @param {string} [envioNombre] - Nombre del envío
144
- * @returns {string} XML firmado
145
- */
146
- generarRespuestaRecepcionEnvio(meta, envioNombre) {
147
- const { formatRut, sanitizeSiiText } = this._lib();
148
- const now = this._formatSiiDateTime();
149
- const resultId = `R_ENVIO_${Date.now()}`;
150
- const rutEmpresa = this.emisor.rut;
151
-
152
- const recepcionDte = meta.items.map((item) => {
153
- const ok = formatRut(item.rutRecep || '') === formatRut(rutEmpresa);
154
- return {
155
- TipoDTE: item.tipoDTE,
156
- Folio: item.folio,
157
- FchEmis: item.fchEmis,
158
- RUTEmisor: formatRut(item.rutEmisor || ''),
159
- RUTRecep: formatRut(item.rutRecep || ''),
160
- MntTotal: item.mntTotal,
161
- EstadoRecepDTE: ok ? 0 : 3,
162
- RecepDTEGlosa: ok ? 'DTE Recibido OK.' : 'DTE No Recibido - Error en RUT Receptor.',
163
- };
164
- });
165
-
166
- const data = {
167
- RespuestaDTE: {
168
- '@_xmlns': 'http://www.sii.cl/SiiDte',
169
- '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
170
- '@_xsi:schemaLocation': 'http://www.sii.cl/SiiDte RespuestaEnvioDTE_v10.xsd',
171
- '@_version': '1.0',
172
- Resultado: {
173
- '@_ID': resultId,
174
- Caratula: {
175
- '@_version': '1.0',
176
- RutResponde: formatRut(rutEmpresa),
177
- RutRecibe: formatRut(meta.rutEmisor || ''),
178
- IdRespuesta: 1,
179
- NroDetalles: 1,
180
- ...(this.contacto?.nombre ? { NmbContacto: sanitizeSiiText(this.contacto.nombre) } : {}),
181
- ...(this.contacto?.mail ? { MailContacto: this.contacto.mail } : {}),
182
- TmstFirmaResp: now,
183
- },
184
- RecepcionEnvio: {
185
- NmbEnvio: envioNombre || `ENVIO_DTE_${Date.now()}`,
186
- FchRecep: now,
187
- CodEnvio: 1,
188
- EnvioDTEID: meta.setId,
189
- RutEmisor: formatRut(meta.rutEmisor || ''),
190
- RutReceptor: formatRut(rutEmpresa),
191
- EstadoRecepEnv: 0,
192
- RecepEnvGlosa: 'Envío recibido conforme.',
193
- NroDTE: meta.items.length,
194
- RecepcionDTE: recepcionDte,
195
- },
196
- },
197
- },
198
- };
199
-
200
- const builder = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false });
201
- let xml = builder.build(data);
202
-
203
- xml = this._signXml({
204
- xml,
205
- referenceXpath: `//*[local-name()='Resultado' and @ID='${resultId}']`,
206
- insertAfterXpath: `//*[local-name()='Resultado' and @ID='${resultId}']`,
207
- });
208
-
209
- return xml;
210
- }
211
-
212
- /**
213
- * Genera la Respuesta de Aprobación Comercial (ResultadoDTE)
214
- * @param {Object} meta - Metadatos del EnvioDTE parseado
215
- * @returns {string} XML firmado
216
- */
217
- generarRespuestaAprobacionComercial(meta) {
218
- const { formatRut, sanitizeSiiText } = this._lib();
219
- const now = this._formatSiiDateTime();
220
- const resultId = `R_DTE_${Date.now()}`;
221
- const rutEmpresa = this.emisor.rut;
222
-
223
- const resultados = meta.items.map((item, idx) => {
224
- const ok = formatRut(item.rutRecep || '') === formatRut(rutEmpresa);
225
- return {
226
- TipoDTE: item.tipoDTE,
227
- Folio: item.folio,
228
- FchEmis: item.fchEmis,
229
- RUTEmisor: formatRut(item.rutEmisor || ''),
230
- RUTRecep: formatRut(item.rutRecep || ''),
231
- MntTotal: item.mntTotal,
232
- CodEnvio: idx + 1,
233
- EstadoDTE: ok ? 0 : 2,
234
- EstadoDTEGlosa: ok ? 'DTE Aceptado OK' : 'DTE Rechazado',
235
- ...(ok ? {} : { CodRchDsc: -1 }),
236
- };
237
- });
238
-
239
- const data = {
240
- RespuestaDTE: {
241
- '@_xmlns': 'http://www.sii.cl/SiiDte',
242
- '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
243
- '@_xsi:schemaLocation': 'http://www.sii.cl/SiiDte RespuestaEnvioDTE_v10.xsd',
244
- '@_version': '1.0',
245
- Resultado: {
246
- '@_ID': resultId,
247
- Caratula: {
248
- '@_version': '1.0',
249
- RutResponde: formatRut(rutEmpresa),
250
- RutRecibe: formatRut(meta.rutEmisor || ''),
251
- IdRespuesta: 1,
252
- NroDetalles: meta.items.length,
253
- ...(this.contacto?.nombre ? { NmbContacto: sanitizeSiiText(this.contacto.nombre) } : {}),
254
- ...(this.contacto?.mail ? { MailContacto: this.contacto.mail } : {}),
255
- TmstFirmaResp: now,
256
- },
257
- ResultadoDTE: resultados,
258
- },
259
- },
260
- };
261
-
262
- const builder = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false });
263
- let xml = builder.build(data);
264
-
265
- xml = this._signXml({
266
- xml,
267
- referenceXpath: `//*[local-name()='Resultado' and @ID='${resultId}']`,
268
- insertAfterXpath: `//*[local-name()='Resultado' and @ID='${resultId}']`,
269
- });
270
-
271
- return xml;
272
- }
273
-
274
- /**
275
- * Genera el Envío de Recibos (Recepción de Mercaderías)
276
- * @param {Object} meta - Metadatos del EnvioDTE parseado
277
- * @returns {string} XML firmado
278
- */
279
- generarEnvioRecibos(meta) {
280
- const { formatRut, sanitizeSiiText } = this._lib();
281
- const now = this._formatSiiDateTime();
282
- const setId = `SetRecibos_${Date.now()}`;
283
- const rutEmpresa = this.emisor.rut;
284
- const rutFirma = this.certificado.rut || rutEmpresa;
285
-
286
- const recibos = meta.items.map((item, idx) => {
287
- const docId = `REC-${idx + 1}`;
288
- return {
289
- '@_version': '1.0',
290
- DocumentoRecibo: {
291
- '@_ID': docId,
292
- TipoDoc: item.tipoDTE,
293
- Folio: item.folio,
294
- FchEmis: item.fchEmis,
295
- RUTEmisor: formatRut(item.rutEmisor || ''),
296
- RUTRecep: formatRut(item.rutRecep || ''),
297
- MntTotal: item.mntTotal,
298
- Recinto: 'Oficina central',
299
- RutFirma: formatRut(rutFirma),
300
- Declaracion: DECLARACION_RECIBO,
301
- TmstFirmaRecibo: now,
302
- },
303
- };
304
- });
305
-
306
- const data = {
307
- EnvioRecibos: {
308
- '@_xmlns': 'http://www.sii.cl/SiiDte',
309
- '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
310
- '@_xsi:schemaLocation': 'http://www.sii.cl/SiiDte EnvioRecibos_v10.xsd',
311
- '@_version': '1.0',
312
- SetRecibos: {
313
- '@_ID': setId,
314
- Caratula: {
315
- '@_version': '1.0',
316
- RutResponde: formatRut(rutEmpresa),
317
- RutRecibe: formatRut(meta.rutEmisor || ''),
318
- ...(this.contacto?.nombre ? { NmbContacto: sanitizeSiiText(this.contacto.nombre) } : {}),
319
- ...(this.contacto?.fono ? { FonoContacto: this.contacto.fono } : {}),
320
- ...(this.contacto?.mail ? { MailContacto: this.contacto.mail } : {}),
321
- TmstFirmaEnv: now,
322
- },
323
- Recibo: recibos,
324
- },
325
- },
326
- };
327
-
328
- const builder = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false });
329
- let xml = builder.build(data);
330
-
331
- // Firmar cada DocumentoRecibo
332
- meta.items.forEach((_, idx) => {
333
- const docId = `REC-${idx + 1}`;
334
- xml = this._signXml({
335
- xml,
336
- referenceXpath: `//*[local-name()='DocumentoRecibo' and @ID='${docId}']`,
337
- insertAfterXpath: `//*[local-name()='DocumentoRecibo' and @ID='${docId}']`,
338
- });
339
- });
340
-
341
- // Firmar SetRecibos
342
- xml = this._signXml({
343
- xml,
344
- referenceXpath: `//*[local-name()='SetRecibos' and @ID='${setId}']`,
345
- insertAfterXpath: `//*[local-name()='SetRecibos' and @ID='${setId}']`,
346
- });
347
-
348
- return xml;
349
- }
350
-
351
- /**
352
- * Genera todas las respuestas de intercambio
353
- * @param {string} envioDteXml - XML del EnvioDTE de entrada
354
- * @param {Object} [options]
355
- * @param {string} [options.envioNombre] - Nombre del envío
356
- * @param {string} [options.outDir] - Directorio de salida (override debugDir)
357
- * @returns {Object} Resultado con rutas de archivos generados
358
- */
359
- async generarIntercambio(envioDteXml, options = {}) {
360
- const outDir = options.outDir || this.debugDir || './debug/intercambio';
361
- fs.mkdirSync(outDir, { recursive: true });
362
-
363
- console.log('\n' + '═'.repeat(60));
364
- console.log('📬 INTERCAMBIO DE INFORMACIÓN DTE');
365
- console.log('═'.repeat(60));
366
-
367
- // Parsear EnvioDTE
368
- console.log('\n📋 Parseando EnvioDTE...');
369
- const meta = this.parseEnvioDTE(envioDteXml);
370
-
371
- if (!meta?.items?.length) {
372
- throw new Error('No se encontraron DTEs en el EnvioDTE de entrada');
373
- }
374
-
375
- console.log(` ✓ ${meta.items.length} documentos encontrados`);
376
- console.log(` ✓ Emisor: ${meta.rutEmisor}`);
377
- console.log(` ✓ Receptor: ${meta.rutReceptor}`);
378
-
379
- meta.items.forEach((item, idx) => {
380
- console.log(` ${idx + 1}. Tipo ${item.tipoDTE} Folio ${item.folio} - $${item.mntTotal.toLocaleString('es-CL')}`);
381
- });
382
-
383
- // Generar respuestas
384
- console.log('\n📝 Generando respuestas de intercambio...');
385
-
386
- // 1. Respuesta Recepción de Envío
387
- console.log(' 1. Respuesta Recepción de Envío...');
388
- const recepcionXml = this.generarRespuestaRecepcionEnvio(meta, options.envioNombre);
389
- const recepcionPath = path.join(outDir, 'respuesta-recepcion-envio.xml');
390
- fs.writeFileSync(recepcionPath, recepcionXml, 'utf8');
391
- console.log(` ✓ ${recepcionPath}`);
392
-
393
- // 2. Respuesta Aprobación Comercial
394
- console.log(' 2. Respuesta Aprobación Comercial...');
395
- const aprobacionXml = this.generarRespuestaAprobacionComercial(meta);
396
- const aprobacionPath = path.join(outDir, 'respuesta-aprobacion-comercial.xml');
397
- fs.writeFileSync(aprobacionPath, aprobacionXml, 'utf8');
398
- console.log(` ✓ ${aprobacionPath}`);
399
-
400
- // 3. Envío de Recibos
401
- console.log(' 3. Envío de Recibos (Mercaderías)...');
402
- const recibosXml = this.generarEnvioRecibos(meta);
403
- const recibosPath = path.join(outDir, 'envio-recibos.xml');
404
- fs.writeFileSync(recibosPath, recibosXml, 'utf8');
405
- console.log(` ✓ ${recibosPath}`);
406
-
407
- console.log('\n' + '═'.repeat(60));
408
- console.log('✅ INTERCAMBIO GENERADO');
409
- console.log('═'.repeat(60));
410
- console.log(` 📂 Directorio: ${outDir}`);
411
- console.log(' 📄 Archivos generados:');
412
- console.log(' - respuesta-recepcion-envio.xml');
413
- console.log(' - respuesta-aprobacion-comercial.xml');
414
- console.log(' - envio-recibos.xml');
415
-
416
- return {
417
- success: true,
418
- outDir,
419
- meta,
420
- files: {
421
- recepcion: recepcionPath,
422
- aprobacion: aprobacionPath,
423
- recibos: recibosPath,
424
- },
425
- };
426
- }
427
- }
428
-
429
- module.exports = IntercambioCert;
3
+ /**
4
+ * IntercambioCert.js
5
+ *
6
+ * Módulo para generar respuestas de Intercambio de Información DTE
7
+ * - Respuesta Recepción de Envío
8
+ * - Respuesta Aprobación Comercial (ResultadoDTE)
9
+ * - Envío de Recibos (Recepción de Mercaderías)
10
+ *
11
+ * @module dte-sii/cert/IntercambioCert
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { XMLParser, XMLBuilder } = require('fast-xml-parser');
17
+ const { SignedXml } = require('xml-crypto');
18
+
19
+ const DECLARACION_RECIBO = 'El acuse de recibo que se declara en este acto, de acuerdo a lo dispuesto en la letra b) del Art. 4, y la letra c) del Art. 5 de la Ley 19.983, acredita que la entrega de mercaderias o servicio(s) prestado(s) ha(n) sido recibido(s).';
20
+
21
+ class IntercambioCert {
22
+ /**
23
+ * @param {Object} options
24
+ * @param {Object} options.certificado - Instancia de Certificado
25
+ * @param {Object} options.emisor - Datos del emisor { rut, razonSocial }
26
+ * @param {Object} [options.contacto] - Datos de contacto { nombre, mail, fono }
27
+ * @param {string} [options.debugDir] - Directorio para guardar XMLs
28
+ */
29
+ constructor({ certificado, emisor, contacto = {}, debugDir }) {
30
+ this.certificado = certificado;
31
+ this.emisor = emisor;
32
+ this.contacto = contacto;
33
+ this.debugDir = debugDir;
34
+ }
35
+
36
+ /**
37
+ * Obtiene las utilidades del paquete
38
+ * @private
39
+ */
40
+ _lib() {
41
+ const { formatRut, normalizeArray, sanitizeSiiText } = require('../index');
42
+ return { formatRut, normalizeArray, sanitizeSiiText };
43
+ }
44
+
45
+ /**
46
+ * Formatea fecha/hora en formato SII
47
+ * @private
48
+ */
49
+ _formatSiiDateTime(date = new Date()) {
50
+ const pad = (n) => String(n).padStart(2, '0');
51
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
52
+ }
53
+
54
+ /**
55
+ * Firma un XML con el certificado
56
+ * @private
57
+ */
58
+ _signXml({ xml, referenceXpath, insertAfterXpath }) {
59
+ const sig = new SignedXml();
60
+ sig.privateKey = this.certificado.getPrivateKeyPem();
61
+ sig.publicCert = this.certificado.getCertificatePem();
62
+ sig.canonicalizationAlgorithm = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315';
63
+ sig.signatureAlgorithm = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1';
64
+ sig.addReference({
65
+ xpath: referenceXpath,
66
+ transforms: ['http://www.w3.org/2000/09/xmldsig#enveloped-signature'],
67
+ digestAlgorithm: 'http://www.w3.org/2000/09/xmldsig#sha1',
68
+ });
69
+ sig.getKeyInfoContent = () => {
70
+ const certBase64 = this.certificado.getCertificateBase64();
71
+ const modulus = this.certificado.getModulus();
72
+ const exponent = this.certificado.getExponent();
73
+ return `<KeyValue><RSAKeyValue><Modulus>${modulus}</Modulus><Exponent>${exponent}</Exponent></RSAKeyValue></KeyValue><X509Data><X509Certificate>${certBase64}</X509Certificate></X509Data>`;
74
+ };
75
+
76
+ sig.computeSignature(xml, {
77
+ location: {
78
+ reference: insertAfterXpath || referenceXpath,
79
+ action: 'after',
80
+ },
81
+ });
82
+
83
+ return sig.getSignedXml();
84
+ }
85
+
86
+ /**
87
+ * Parsea un EnvioDTE XML y extrae los documentos
88
+ * @param {string} xml - XML del EnvioDTE
89
+ * @returns {Object} Metadatos del envío
90
+ */
91
+ parseEnvioDTE(xml) {
92
+ const { normalizeArray } = this._lib();
93
+
94
+ const parser = new XMLParser({
95
+ ignoreAttributes: false,
96
+ attributeNamePrefix: '@_',
97
+ trimValues: true,
98
+ parseTagValue: true,
99
+ });
100
+
101
+ const data = parser.parse(xml);
102
+ const envio = data?.EnvioDTE || data?.EnvioDTEB || data?.EnvioDTETraslado || data?.EnvioBOLETA || {};
103
+ const setDte = envio?.SetDTE || data?.SetDTE || {};
104
+ const caratula = setDte?.Caratula || {};
105
+ const setId = setDte?.['@_ID'] || setDte?.ID || 'SetDTE';
106
+
107
+ const rutEmisor = caratula?.RutEmisor || null;
108
+ const rutEnvia = caratula?.RutEnvia || null;
109
+ const rutReceptor = caratula?.RutReceptor || null;
110
+
111
+ const dtes = normalizeArray(setDte?.DTE);
112
+ const documentos = dtes.map((dte) => dte?.Documento || dte?.DTE?.Documento || dte?.DTE?.Documento || {}).filter(Boolean);
113
+
114
+ const items = documentos.map((doc) => {
115
+ const encabezado = doc?.Encabezado || {};
116
+ const idDoc = encabezado?.IdDoc || {};
117
+ const emisor = encabezado?.Emisor || {};
118
+ const receptor = encabezado?.Receptor || {};
119
+ const totales = encabezado?.Totales || {};
120
+
121
+ return {
122
+ tipoDTE: parseInt(idDoc?.TipoDTE ?? idDoc?.TpoDoc ?? 0, 10) || 0,
123
+ folio: parseInt(idDoc?.Folio ?? 0, 10) || 0,
124
+ fchEmis: idDoc?.FchEmis || null,
125
+ rutEmisor: emisor?.RUTEmisor || emisor?.RutEmisor || rutEmisor || null,
126
+ rutRecep: receptor?.RUTRecep || receptor?.RutRecep || rutReceptor || null,
127
+ mntTotal: parseInt(totales?.MntTotal ?? 0, 10) || 0,
128
+ };
129
+ });
130
+
131
+ return {
132
+ setId,
133
+ rutEmisor,
134
+ rutEnvia,
135
+ rutReceptor,
136
+ items,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Genera la Respuesta de Recepción de Envío
142
+ * @param {Object} meta - Metadatos del EnvioDTE parseado
143
+ * @param {string} [envioNombre] - Nombre del envío
144
+ * @returns {string} XML firmado
145
+ */
146
+ generarRespuestaRecepcionEnvio(meta, envioNombre) {
147
+ const { formatRut, sanitizeSiiText } = this._lib();
148
+ const now = this._formatSiiDateTime();
149
+ const resultId = `R_ENVIO_${Date.now()}`;
150
+ const rutEmpresa = this.emisor.rut;
151
+
152
+ const recepcionDte = meta.items.map((item) => {
153
+ const ok = formatRut(item.rutRecep || '') === formatRut(rutEmpresa);
154
+ return {
155
+ TipoDTE: item.tipoDTE,
156
+ Folio: item.folio,
157
+ FchEmis: item.fchEmis,
158
+ RUTEmisor: formatRut(item.rutEmisor || ''),
159
+ RUTRecep: formatRut(item.rutRecep || ''),
160
+ MntTotal: item.mntTotal,
161
+ EstadoRecepDTE: ok ? 0 : 3,
162
+ RecepDTEGlosa: ok ? 'DTE Recibido OK.' : 'DTE No Recibido - Error en RUT Receptor.',
163
+ };
164
+ });
165
+
166
+ const data = {
167
+ RespuestaDTE: {
168
+ '@_xmlns': 'http://www.sii.cl/SiiDte',
169
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
170
+ '@_xsi:schemaLocation': 'http://www.sii.cl/SiiDte RespuestaEnvioDTE_v10.xsd',
171
+ '@_version': '1.0',
172
+ Resultado: {
173
+ '@_ID': resultId,
174
+ Caratula: {
175
+ '@_version': '1.0',
176
+ RutResponde: formatRut(rutEmpresa),
177
+ RutRecibe: formatRut(meta.rutEmisor || ''),
178
+ IdRespuesta: 1,
179
+ NroDetalles: 1,
180
+ ...(this.contacto?.nombre ? { NmbContacto: sanitizeSiiText(this.contacto.nombre) } : {}),
181
+ ...(this.contacto?.mail ? { MailContacto: this.contacto.mail } : {}),
182
+ TmstFirmaResp: now,
183
+ },
184
+ RecepcionEnvio: {
185
+ NmbEnvio: envioNombre || `ENVIO_DTE_${Date.now()}`,
186
+ FchRecep: now,
187
+ CodEnvio: 1,
188
+ EnvioDTEID: meta.setId,
189
+ RutEmisor: formatRut(meta.rutEmisor || ''),
190
+ RutReceptor: formatRut(rutEmpresa),
191
+ EstadoRecepEnv: 0,
192
+ RecepEnvGlosa: 'Envío recibido conforme.',
193
+ NroDTE: meta.items.length,
194
+ RecepcionDTE: recepcionDte,
195
+ },
196
+ },
197
+ },
198
+ };
199
+
200
+ const builder = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false });
201
+ let xml = builder.build(data);
202
+
203
+ xml = this._signXml({
204
+ xml,
205
+ referenceXpath: `//*[local-name()='Resultado' and @ID='${resultId}']`,
206
+ insertAfterXpath: `//*[local-name()='Resultado' and @ID='${resultId}']`,
207
+ });
208
+
209
+ return xml;
210
+ }
211
+
212
+ /**
213
+ * Genera la Respuesta de Aprobación Comercial (ResultadoDTE)
214
+ * @param {Object} meta - Metadatos del EnvioDTE parseado
215
+ * @returns {string} XML firmado
216
+ */
217
+ generarRespuestaAprobacionComercial(meta) {
218
+ const { formatRut, sanitizeSiiText } = this._lib();
219
+ const now = this._formatSiiDateTime();
220
+ const resultId = `R_DTE_${Date.now()}`;
221
+ const rutEmpresa = this.emisor.rut;
222
+
223
+ const resultados = meta.items.map((item, idx) => {
224
+ const ok = formatRut(item.rutRecep || '') === formatRut(rutEmpresa);
225
+ return {
226
+ TipoDTE: item.tipoDTE,
227
+ Folio: item.folio,
228
+ FchEmis: item.fchEmis,
229
+ RUTEmisor: formatRut(item.rutEmisor || ''),
230
+ RUTRecep: formatRut(item.rutRecep || ''),
231
+ MntTotal: item.mntTotal,
232
+ CodEnvio: idx + 1,
233
+ EstadoDTE: ok ? 0 : 2,
234
+ EstadoDTEGlosa: ok ? 'DTE Aceptado OK' : 'DTE Rechazado',
235
+ ...(ok ? {} : { CodRchDsc: -1 }),
236
+ };
237
+ });
238
+
239
+ const data = {
240
+ RespuestaDTE: {
241
+ '@_xmlns': 'http://www.sii.cl/SiiDte',
242
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
243
+ '@_xsi:schemaLocation': 'http://www.sii.cl/SiiDte RespuestaEnvioDTE_v10.xsd',
244
+ '@_version': '1.0',
245
+ Resultado: {
246
+ '@_ID': resultId,
247
+ Caratula: {
248
+ '@_version': '1.0',
249
+ RutResponde: formatRut(rutEmpresa),
250
+ RutRecibe: formatRut(meta.rutEmisor || ''),
251
+ IdRespuesta: 1,
252
+ NroDetalles: meta.items.length,
253
+ ...(this.contacto?.nombre ? { NmbContacto: sanitizeSiiText(this.contacto.nombre) } : {}),
254
+ ...(this.contacto?.mail ? { MailContacto: this.contacto.mail } : {}),
255
+ TmstFirmaResp: now,
256
+ },
257
+ ResultadoDTE: resultados,
258
+ },
259
+ },
260
+ };
261
+
262
+ const builder = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false });
263
+ let xml = builder.build(data);
264
+
265
+ xml = this._signXml({
266
+ xml,
267
+ referenceXpath: `//*[local-name()='Resultado' and @ID='${resultId}']`,
268
+ insertAfterXpath: `//*[local-name()='Resultado' and @ID='${resultId}']`,
269
+ });
270
+
271
+ return xml;
272
+ }
273
+
274
+ /**
275
+ * Genera el Envío de Recibos (Recepción de Mercaderías)
276
+ * @param {Object} meta - Metadatos del EnvioDTE parseado
277
+ * @returns {string} XML firmado
278
+ */
279
+ generarEnvioRecibos(meta) {
280
+ const { formatRut, sanitizeSiiText } = this._lib();
281
+ const now = this._formatSiiDateTime();
282
+ const setId = `SetRecibos_${Date.now()}`;
283
+ const rutEmpresa = this.emisor.rut;
284
+ const rutFirma = this.certificado.rut || rutEmpresa;
285
+
286
+ const recibos = meta.items.map((item, idx) => {
287
+ const docId = `REC-${idx + 1}`;
288
+ return {
289
+ '@_version': '1.0',
290
+ DocumentoRecibo: {
291
+ '@_ID': docId,
292
+ TipoDoc: item.tipoDTE,
293
+ Folio: item.folio,
294
+ FchEmis: item.fchEmis,
295
+ RUTEmisor: formatRut(item.rutEmisor || ''),
296
+ RUTRecep: formatRut(item.rutRecep || ''),
297
+ MntTotal: item.mntTotal,
298
+ Recinto: 'Oficina central',
299
+ RutFirma: formatRut(rutFirma),
300
+ Declaracion: DECLARACION_RECIBO,
301
+ TmstFirmaRecibo: now,
302
+ },
303
+ };
304
+ });
305
+
306
+ const data = {
307
+ EnvioRecibos: {
308
+ '@_xmlns': 'http://www.sii.cl/SiiDte',
309
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
310
+ '@_xsi:schemaLocation': 'http://www.sii.cl/SiiDte EnvioRecibos_v10.xsd',
311
+ '@_version': '1.0',
312
+ SetRecibos: {
313
+ '@_ID': setId,
314
+ Caratula: {
315
+ '@_version': '1.0',
316
+ RutResponde: formatRut(rutEmpresa),
317
+ RutRecibe: formatRut(meta.rutEmisor || ''),
318
+ ...(this.contacto?.nombre ? { NmbContacto: sanitizeSiiText(this.contacto.nombre) } : {}),
319
+ ...(this.contacto?.fono ? { FonoContacto: this.contacto.fono } : {}),
320
+ ...(this.contacto?.mail ? { MailContacto: this.contacto.mail } : {}),
321
+ TmstFirmaEnv: now,
322
+ },
323
+ Recibo: recibos,
324
+ },
325
+ },
326
+ };
327
+
328
+ const builder = new XMLBuilder({ ignoreAttributes: false, attributeNamePrefix: '@_', format: false });
329
+ let xml = builder.build(data);
330
+
331
+ // Firmar cada DocumentoRecibo
332
+ meta.items.forEach((_, idx) => {
333
+ const docId = `REC-${idx + 1}`;
334
+ xml = this._signXml({
335
+ xml,
336
+ referenceXpath: `//*[local-name()='DocumentoRecibo' and @ID='${docId}']`,
337
+ insertAfterXpath: `//*[local-name()='DocumentoRecibo' and @ID='${docId}']`,
338
+ });
339
+ });
340
+
341
+ // Firmar SetRecibos
342
+ xml = this._signXml({
343
+ xml,
344
+ referenceXpath: `//*[local-name()='SetRecibos' and @ID='${setId}']`,
345
+ insertAfterXpath: `//*[local-name()='SetRecibos' and @ID='${setId}']`,
346
+ });
347
+
348
+ return xml;
349
+ }
350
+
351
+ /**
352
+ * Genera todas las respuestas de intercambio
353
+ * @param {string} envioDteXml - XML del EnvioDTE de entrada
354
+ * @param {Object} [options]
355
+ * @param {string} [options.envioNombre] - Nombre del envío
356
+ * @param {string} [options.outDir] - Directorio de salida (override debugDir)
357
+ * @returns {Object} Resultado con rutas de archivos generados
358
+ */
359
+ async generarIntercambio(envioDteXml, options = {}) {
360
+ const outDir = options.outDir || this.debugDir || './debug/intercambio';
361
+ fs.mkdirSync(outDir, { recursive: true });
362
+
363
+ console.log('\n' + '═'.repeat(60));
364
+ console.log('📬 INTERCAMBIO DE INFORMACIÓN DTE');
365
+ console.log('═'.repeat(60));
366
+
367
+ // Parsear EnvioDTE
368
+ console.log('\n📋 Parseando EnvioDTE...');
369
+ const meta = this.parseEnvioDTE(envioDteXml);
370
+
371
+ if (!meta?.items?.length) {
372
+ throw new Error('No se encontraron DTEs en el EnvioDTE de entrada');
373
+ }
374
+
375
+ console.log(` ✓ ${meta.items.length} documentos encontrados`);
376
+ console.log(` ✓ Emisor: ${meta.rutEmisor}`);
377
+ console.log(` ✓ Receptor: ${meta.rutReceptor}`);
378
+
379
+ meta.items.forEach((item, idx) => {
380
+ console.log(` ${idx + 1}. Tipo ${item.tipoDTE} Folio ${item.folio} - $${item.mntTotal.toLocaleString('es-CL')}`);
381
+ });
382
+
383
+ // Generar respuestas
384
+ console.log('\n📝 Generando respuestas de intercambio...');
385
+
386
+ // 1. Respuesta Recepción de Envío
387
+ console.log(' 1. Respuesta Recepción de Envío...');
388
+ const recepcionXml = this.generarRespuestaRecepcionEnvio(meta, options.envioNombre);
389
+ const recepcionPath = path.join(outDir, 'respuesta-recepcion-envio.xml');
390
+ fs.writeFileSync(recepcionPath, recepcionXml, 'utf8');
391
+ console.log(` ✓ ${recepcionPath}`);
392
+
393
+ // 2. Respuesta Aprobación Comercial
394
+ console.log(' 2. Respuesta Aprobación Comercial...');
395
+ const aprobacionXml = this.generarRespuestaAprobacionComercial(meta);
396
+ const aprobacionPath = path.join(outDir, 'respuesta-aprobacion-comercial.xml');
397
+ fs.writeFileSync(aprobacionPath, aprobacionXml, 'utf8');
398
+ console.log(` ✓ ${aprobacionPath}`);
399
+
400
+ // 3. Envío de Recibos
401
+ console.log(' 3. Envío de Recibos (Mercaderías)...');
402
+ const recibosXml = this.generarEnvioRecibos(meta);
403
+ const recibosPath = path.join(outDir, 'envio-recibos.xml');
404
+ fs.writeFileSync(recibosPath, recibosXml, 'utf8');
405
+ console.log(` ✓ ${recibosPath}`);
406
+
407
+ console.log('\n' + '═'.repeat(60));
408
+ console.log('✅ INTERCAMBIO GENERADO');
409
+ console.log('═'.repeat(60));
410
+ console.log(` 📂 Directorio: ${outDir}`);
411
+ console.log(' 📄 Archivos generados:');
412
+ console.log(' - respuesta-recepcion-envio.xml');
413
+ console.log(' - respuesta-aprobacion-comercial.xml');
414
+ console.log(' - envio-recibos.xml');
415
+
416
+ return {
417
+ success: true,
418
+ outDir,
419
+ meta,
420
+ files: {
421
+ recepcion: recepcionPath,
422
+ aprobacion: aprobacionPath,
423
+ recibos: recibosPath,
424
+ },
425
+ };
426
+ }
427
+ }
428
+
429
+ module.exports = IntercambioCert;