@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.
package/cert/SetExenta.js CHANGED
@@ -1,490 +1,490 @@
1
1
  // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
2
  // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
- /**
4
- * Set Factura Exenta
5
- *
6
- * Tipos DTE: 34 (Factura Exenta), 61 (NC), 56 (ND)
7
- *
8
- * Flujo similar a SetBasico pero:
9
- * - Todos los items son exentos
10
- * - Totales sin IVA
11
- *
12
- * @module dte-sii/cert/SetExenta
13
- */
14
-
15
- const SetBase = require('./SetBase');
16
- const { SET_LABELS, SETS_DTE } = require('./types');
17
-
18
- class SetExenta extends SetBase {
19
- constructor(deps) {
20
- super(deps);
21
-
22
- this.key = 'exenta';
23
- this.label = SET_LABELS.exenta;
24
- this.tiposDte = SETS_DTE.exenta; // [34, 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 exenta
34
- */
35
- _validarCasos(casos) {
36
- super._validarCasos(casos);
37
-
38
- if (!casos.casosFactura?.length) {
39
- throw new Error('SetExenta: casosFactura es requerido');
40
- }
41
- }
42
-
43
- /**
44
- * @override
45
- * Calcula folios necesarios por tipo
46
- */
47
- _calcularCantidadFolios(casos, tipoDte) {
48
- if (casos.cafRequired?.[tipoDte]) {
49
- return casos.cafRequired[tipoDte];
50
- }
51
-
52
- switch (tipoDte) {
53
- case 34: return casos.casosFactura?.length || 1;
54
- case 61: return casos.casosNC?.length || 1;
55
- case 56: return casos.casosND?.length || 1;
56
- default: return 1;
57
- }
58
- }
59
-
60
- /**
61
- * @override
62
- * Genera DTEs del set exenta
63
- *
64
- * @param {Object} casos - { casosFactura, casosNC, casosND, cafRequired }
65
- * @param {Object} cafs - { 34: cafPath, 56: cafPath, 61: cafPath }
66
- * @returns {Promise<DTE[]>}
67
- */
68
- async generarDtes(casos, cafs) {
69
- const dtes = [];
70
-
71
- // 1. Generar facturas exentas primero
72
- this.logger.log(' 📄 Generando facturas exentas...');
73
- for (const caso of casos.casosFactura || []) {
74
- const dte = await this._generarFacturaExenta(caso, cafs[34]);
75
- dtes.push(dte);
76
- }
77
-
78
- // 2. Generar notas de crédito
79
- this.logger.log(' 📄 Generando notas de crédito...');
80
- for (const caso of casos.casosNC || []) {
81
- const dte = await this._generarNotaCredito(caso, cafs[61]);
82
- dtes.push(dte);
83
- }
84
-
85
- // 3. Generar notas de débito
86
- this.logger.log(' 📄 Generando notas de débito...');
87
- for (const caso of casos.casosND || []) {
88
- const dte = await this._generarNotaDebito(caso, cafs[56]);
89
- dtes.push(dte);
90
- }
91
-
92
- return dtes;
93
- }
94
-
95
- // ═══════════════════════════════════════════════════════════════
96
- // Generadores de DTEs
97
- // ═══════════════════════════════════════════════════════════════
98
-
99
- /**
100
- * Genera factura exenta (tipo 34)
101
- * @private
102
- */
103
- async _generarFacturaExenta(caso, cafPath) {
104
- const { DTE, CAF, buildDetalle, calcularTotalesDesdeDetalle, buildSetReferencia } = require('../index');
105
- const fs = require('fs');
106
-
107
- const cafXml = fs.readFileSync(cafPath, 'utf8');
108
- const caf = new CAF(cafXml);
109
- const folio = this._reservarFolio(caf, cafXml);
110
-
111
- // Marcar todos los items como exentos
112
- const items = this._normalizarItemsExentos(caso.items);
113
- const detalle = buildDetalle(items, { allowIndExe: true });
114
- const totales = calcularTotalesDesdeDetalle(detalle, { soloExento: true });
115
-
116
- const fechaEmision = this._getFechaEmision();
117
- const setReferencia = buildSetReferencia(caso.id, fechaEmision);
118
-
119
- const dteDatos = {
120
- Encabezado: {
121
- IdDoc: {
122
- TipoDTE: 34,
123
- Folio: folio,
124
- FchEmis: fechaEmision,
125
- },
126
- Emisor: this._buildEmisor(),
127
- Receptor: this._buildReceptor(),
128
- Totales: totales,
129
- },
130
- Detalle: detalle,
131
- Referencia: [setReferencia],
132
- };
133
-
134
- const dte = new DTE(dteDatos);
135
- this._timbrarYFirmar(dte, caf);
136
-
137
- // Guardar referencia para NC/ND
138
- this._docRefs[caso.id] = {
139
- tipoDte: 34,
140
- folio,
141
- fecha: fechaEmision,
142
- detalle,
143
- totales,
144
- items,
145
- };
146
-
147
- this.logger.log(` ✓ Factura Exenta caso ${caso.id}: folio ${folio}`);
148
- return dte;
149
- }
150
-
151
- /**
152
- * Genera nota de crédito exenta (tipo 61)
153
- * @private
154
- */
155
- async _generarNotaCredito(caso, cafPath) {
156
- const { DTE, CAF, buildDetalle, calcularTotalesDesdeDetalle, buildSetReferencia } = require('../index');
157
- const fs = require('fs');
158
-
159
- const cafXml = fs.readFileSync(cafPath, 'utf8');
160
- const caf = new CAF(cafXml);
161
- const folio = this._reservarFolio(caf, cafXml);
162
-
163
- // Obtener documento referenciado
164
- const docRef = this._docRefs[caso.referenciaCaso];
165
- if (!docRef) {
166
- throw new Error(`SetExenta: Documento referencia ${caso.referenciaCaso} no encontrado para NC ${caso.id}`);
167
- }
168
-
169
- // Determinar items según tipo de NC
170
- const items = this._resolverItemsNC(caso, docRef);
171
- const detalle = buildDetalle(items, { allowIndExe: true });
172
- const totales = calcularTotalesDesdeDetalle(detalle, { soloExento: true });
173
-
174
- const fechaEmision = this._getFechaEmision();
175
- const setReferencia = buildSetReferencia(caso.id, fechaEmision);
176
-
177
- // Referencia al documento original
178
- const docReferencia = {
179
- NroLinRef: 2,
180
- TpoDocRef: docRef.tipoDte,
181
- FolioRef: docRef.folio,
182
- FchRef: docRef.fecha,
183
- CodRef: caso.codRef,
184
- RazonRef: caso.razonRef,
185
- };
186
-
187
- const dteDatos = {
188
- Encabezado: {
189
- IdDoc: {
190
- TipoDTE: 61,
191
- Folio: folio,
192
- FchEmis: fechaEmision,
193
- },
194
- Emisor: this._buildEmisor(),
195
- Receptor: this._buildReceptor(),
196
- Totales: totales,
197
- },
198
- Detalle: detalle,
199
- Referencia: [setReferencia, docReferencia],
200
- };
201
-
202
- const dte = new DTE(dteDatos);
203
- this._timbrarYFirmar(dte, caf);
204
-
205
- // Guardar referencia
206
- this._docRefs[caso.id] = {
207
- tipoDte: 61,
208
- folio,
209
- fecha: fechaEmision,
210
- detalle,
211
- totales,
212
- items,
213
- };
214
-
215
- this.logger.log(` ✓ NC Exenta caso ${caso.id}: folio ${folio} (ref: ${caso.referenciaCaso})`);
216
- return dte;
217
- }
218
-
219
- /**
220
- * Genera nota de débito exenta (tipo 56)
221
- * @private
222
- */
223
- async _generarNotaDebito(caso, cafPath) {
224
- const { DTE, CAF, buildDetalle, calcularTotalesDesdeDetalle, buildSetReferencia } = require('../index');
225
- const fs = require('fs');
226
-
227
- const cafXml = fs.readFileSync(cafPath, 'utf8');
228
- const caf = new CAF(cafXml);
229
- const folio = this._reservarFolio(caf, cafXml);
230
-
231
- // Obtener documento referenciado
232
- const docRef = this._docRefs[caso.referenciaCaso];
233
- if (!docRef) {
234
- throw new Error(`SetExenta: Documento referencia ${caso.referenciaCaso} no encontrado para ND ${caso.id}`);
235
- }
236
-
237
- // Determinar items según tipo de ND
238
- const items = this._resolverItemsND(caso, docRef);
239
- const detalle = buildDetalle(items, { allowIndExe: true });
240
- const totales = calcularTotalesDesdeDetalle(detalle, { soloExento: true });
241
-
242
- const fechaEmision = this._getFechaEmision();
243
- const setReferencia = buildSetReferencia(caso.id, fechaEmision);
244
-
245
- // Referencia al documento original
246
- const docReferencia = {
247
- NroLinRef: 2,
248
- TpoDocRef: docRef.tipoDte,
249
- FolioRef: docRef.folio,
250
- FchRef: docRef.fecha,
251
- CodRef: caso.codRef,
252
- RazonRef: caso.razonRef,
253
- };
254
-
255
- const dteDatos = {
256
- Encabezado: {
257
- IdDoc: {
258
- TipoDTE: 56,
259
- Folio: folio,
260
- FchEmis: fechaEmision,
261
- },
262
- Emisor: this._buildEmisor(),
263
- Receptor: this._buildReceptor(),
264
- Totales: totales,
265
- },
266
- Detalle: detalle,
267
- Referencia: [setReferencia, docReferencia],
268
- };
269
-
270
- const dte = new DTE(dteDatos);
271
- this._timbrarYFirmar(dte, caf);
272
-
273
- this.logger.log(` ✓ ND Exenta caso ${caso.id}: folio ${folio} (ref: ${caso.referenciaCaso})`);
274
- return dte;
275
- }
276
-
277
- // ═══════════════════════════════════════════════════════════════
278
- // Helpers
279
- // ═══════════════════════════════════════════════════════════════
280
-
281
- /**
282
- * Normaliza items marcándolos como exentos
283
- * @private
284
- */
285
- _normalizarItemsExentos(items) {
286
- return (items || []).map(item => ({
287
- nombre: item.nombre,
288
- cantidad: item.cantidad || 1,
289
- precio: item.precio || 0,
290
- unidad: item.unidad || 'UN',
291
- exento: true, // Siempre exento
292
- }));
293
- }
294
-
295
- /**
296
- * Resuelve items para NC según codRef
297
- * @private
298
- */
299
- _resolverItemsNC(caso, docRef) {
300
- const razon = (caso.razonRef || '').toUpperCase();
301
-
302
- // CodRef 2: Corrige giro/texto - usar item dummy
303
- if (caso.codRef === 2) {
304
- return [{
305
- nombre: caso.razonRef || 'CORRECCION',
306
- cantidad: 1,
307
- precio: 0,
308
- exento: true,
309
- }];
310
- }
311
-
312
- // CodRef 3: Modifica montos
313
- if (caso.codRef === 3) {
314
- const esDevolucion = razon.includes('DEVOLUCION') || razon.includes('DEVOLUCIÓN');
315
- const esModificaMonto = razon.includes('MODIFICA MONTO');
316
-
317
- // DEVOLUCION: usar items de factura original completos
318
- if (esDevolucion && docRef.items?.length) {
319
- return docRef.items.map(item => ({
320
- ...item,
321
- exento: true,
322
- }));
323
- }
324
-
325
- // MODIFICA MONTO: usar CANTIDAD de factura original con PRECIO nuevo del SII
326
- if (esModificaMonto && caso.items?.length && docRef.items?.length) {
327
- return caso.items.map(ncItem => {
328
- const nombreNc = (ncItem.nombre || '').toUpperCase().trim();
329
- const itemOriginal = docRef.items.find(i =>
330
- (i.nombre || '').toUpperCase().trim() === nombreNc
331
- );
332
-
333
- if (itemOriginal && itemOriginal.cantidad) {
334
- return {
335
- nombre: ncItem.nombre,
336
- cantidad: itemOriginal.cantidad, // Cantidad de factura ORIGINAL
337
- precio: ncItem.precio, // Precio NUEVO del SII
338
- unidad: ncItem.unidad || itemOriginal.unidad || 'UN',
339
- exento: true,
340
- };
341
- }
342
- return { ...ncItem, exento: true };
343
- });
344
- }
345
-
346
- // Otros casos de codRef 3: usar items del caso
347
- if (caso.items?.length) {
348
- return this._normalizarItemsExentos(caso.items);
349
- }
350
- return docRef.items || [];
351
- }
352
-
353
- // CodRef 1: Anula documento completo
354
- if (caso.codRef === 1) {
355
- return docRef.items || [];
356
- }
357
-
358
- // Default: usar items de la NC o del documento
359
- return caso.items?.length
360
- ? this._normalizarItemsExentos(caso.items)
361
- : docRef.items || [];
362
- }
363
-
364
- /**
365
- * Resuelve items para ND según codRef
366
- * @private
367
- */
368
- _resolverItemsND(caso, docRef) {
369
- const razon = (caso.razonRef || '').toUpperCase();
370
- const esModificaMonto = razon.includes('MODIFICA MONTO');
371
-
372
- // Si no hay items, usar item dummy con la razón
373
- if (!caso.items?.length) {
374
- // Para ANULA o similar, usar items del documento referenciado si existen
375
- if (docRef.items?.length) {
376
- return docRef.items.map(i => ({ ...i, exento: true }));
377
- }
378
- return [{
379
- nombre: caso.razonRef || 'CORRECCION',
380
- cantidad: 1,
381
- precio: 0,
382
- exento: true,
383
- }];
384
- }
385
-
386
- // MODIFICA MONTO: usar CANTIDAD de documento original con PRECIO nuevo del SII
387
- if (caso.codRef === 3 && esModificaMonto && docRef.items?.length) {
388
- return caso.items.map(ndItem => {
389
- const nombreNd = (ndItem.nombre || '').toUpperCase().trim();
390
- const itemOriginal = docRef.items.find(i =>
391
- (i.nombre || '').toUpperCase().trim() === nombreNd
392
- );
393
-
394
- if (itemOriginal && itemOriginal.cantidad) {
395
- return {
396
- nombre: ndItem.nombre,
397
- cantidad: itemOriginal.cantidad, // Cantidad de documento ORIGINAL
398
- precio: ndItem.precio, // Precio NUEVO del SII
399
- unidad: ndItem.unidad || itemOriginal.unidad || 'UN',
400
- exento: true,
401
- };
402
- }
403
- return { ...ndItem, exento: true };
404
- });
405
- }
406
-
407
- // Default: usar items del caso
408
- return this._normalizarItemsExentos(caso.items);
409
- }
410
-
411
- /**
412
- * Reserva el siguiente folio disponible
413
- * @private
414
- */
415
- _reservarFolio(caf, cafXml) {
416
- const cafFingerprint = this.folioHelper.createCafFingerprint(cafXml);
417
- const folio = this.folioHelper.reserveNextFolio({
418
- rutEmisor: this.config.emisor.rut,
419
- tipoDte: caf.getTipoDTE(),
420
- folioDesde: caf.getFolioDesde(),
421
- folioHasta: caf.getFolioHasta(),
422
- ambiente: this.config.ambiente || 'certificacion',
423
- cafFingerprint,
424
- });
425
- return folio;
426
- }
427
-
428
- /**
429
- * Obtiene la fecha de emisión
430
- * @private
431
- */
432
- _getFechaEmision() {
433
- if (this._fechaEmision) return this._fechaEmision;
434
- const now = new Date();
435
- this._fechaEmision = now.toISOString().split('T')[0];
436
- return this._fechaEmision;
437
- }
438
-
439
- /**
440
- * Construye datos del emisor
441
- * @private
442
- */
443
- _buildEmisor() {
444
- const e = this.config.emisor;
445
- return {
446
- RUTEmisor: e.rut,
447
- RznSoc: e.razon_social,
448
- GiroEmis: e.giro,
449
- Acteco: e.acteco,
450
- DirOrigen: e.direccion,
451
- CmnaOrigen: e.comuna,
452
- CiudadOrigen: e.ciudad || e.comuna || 'Santiago',
453
- };
454
- }
455
-
456
- /**
457
- * Construye datos del receptor
458
- * @private
459
- */
460
- _buildReceptor() {
461
- const r = this.config.receptor;
462
- return {
463
- RUTRecep: r.rut,
464
- RznSocRecep: r.razon_social,
465
- GiroRecep: r.giro,
466
- DirRecep: r.direccion,
467
- CmnaRecep: r.comuna,
468
- CiudadRecep: r.ciudad || r.comuna,
469
- };
470
- }
471
-
472
- /**
473
- * Timbra y firma el DTE
474
- * @private
475
- */
476
- _timbrarYFirmar(dte, caf) {
477
- const { Certificado } = require('../index');
478
- const fs = require('fs');
479
-
480
- const pfxBuffer = fs.readFileSync(this.config.certificado.path);
481
- const cert = new Certificado(pfxBuffer, this.config.certificado.password);
482
-
483
- const timestamp = new Date().toISOString().replace('Z', '');
484
-
485
- dte.generarXML().timbrar(caf, timestamp);
486
- dte.firmar(cert);
487
- }
488
- }
489
-
490
- module.exports = SetExenta;
3
+ /**
4
+ * Set Factura Exenta
5
+ *
6
+ * Tipos DTE: 34 (Factura Exenta), 61 (NC), 56 (ND)
7
+ *
8
+ * Flujo similar a SetBasico pero:
9
+ * - Todos los items son exentos
10
+ * - Totales sin IVA
11
+ *
12
+ * @module dte-sii/cert/SetExenta
13
+ */
14
+
15
+ const SetBase = require('./SetBase');
16
+ const { SET_LABELS, SETS_DTE } = require('./types');
17
+
18
+ class SetExenta extends SetBase {
19
+ constructor(deps) {
20
+ super(deps);
21
+
22
+ this.key = 'exenta';
23
+ this.label = SET_LABELS.exenta;
24
+ this.tiposDte = SETS_DTE.exenta; // [34, 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 exenta
34
+ */
35
+ _validarCasos(casos) {
36
+ super._validarCasos(casos);
37
+
38
+ if (!casos.casosFactura?.length) {
39
+ throw new Error('SetExenta: casosFactura es requerido');
40
+ }
41
+ }
42
+
43
+ /**
44
+ * @override
45
+ * Calcula folios necesarios por tipo
46
+ */
47
+ _calcularCantidadFolios(casos, tipoDte) {
48
+ if (casos.cafRequired?.[tipoDte]) {
49
+ return casos.cafRequired[tipoDte];
50
+ }
51
+
52
+ switch (tipoDte) {
53
+ case 34: return casos.casosFactura?.length || 1;
54
+ case 61: return casos.casosNC?.length || 1;
55
+ case 56: return casos.casosND?.length || 1;
56
+ default: return 1;
57
+ }
58
+ }
59
+
60
+ /**
61
+ * @override
62
+ * Genera DTEs del set exenta
63
+ *
64
+ * @param {Object} casos - { casosFactura, casosNC, casosND, cafRequired }
65
+ * @param {Object} cafs - { 34: cafPath, 56: cafPath, 61: cafPath }
66
+ * @returns {Promise<DTE[]>}
67
+ */
68
+ async generarDtes(casos, cafs) {
69
+ const dtes = [];
70
+
71
+ // 1. Generar facturas exentas primero
72
+ this.logger.log(' 📄 Generando facturas exentas...');
73
+ for (const caso of casos.casosFactura || []) {
74
+ const dte = await this._generarFacturaExenta(caso, cafs[34]);
75
+ dtes.push(dte);
76
+ }
77
+
78
+ // 2. Generar notas de crédito
79
+ this.logger.log(' 📄 Generando notas de crédito...');
80
+ for (const caso of casos.casosNC || []) {
81
+ const dte = await this._generarNotaCredito(caso, cafs[61]);
82
+ dtes.push(dte);
83
+ }
84
+
85
+ // 3. Generar notas de débito
86
+ this.logger.log(' 📄 Generando notas de débito...');
87
+ for (const caso of casos.casosND || []) {
88
+ const dte = await this._generarNotaDebito(caso, cafs[56]);
89
+ dtes.push(dte);
90
+ }
91
+
92
+ return dtes;
93
+ }
94
+
95
+ // ═══════════════════════════════════════════════════════════════
96
+ // Generadores de DTEs
97
+ // ═══════════════════════════════════════════════════════════════
98
+
99
+ /**
100
+ * Genera factura exenta (tipo 34)
101
+ * @private
102
+ */
103
+ async _generarFacturaExenta(caso, cafPath) {
104
+ const { DTE, CAF, buildDetalle, calcularTotalesDesdeDetalle, buildSetReferencia } = require('../index');
105
+ const fs = require('fs');
106
+
107
+ const cafXml = fs.readFileSync(cafPath, 'utf8');
108
+ const caf = new CAF(cafXml);
109
+ const folio = this._reservarFolio(caf, cafXml);
110
+
111
+ // Marcar todos los items como exentos
112
+ const items = this._normalizarItemsExentos(caso.items);
113
+ const detalle = buildDetalle(items, { allowIndExe: true });
114
+ const totales = calcularTotalesDesdeDetalle(detalle, { soloExento: true });
115
+
116
+ const fechaEmision = this._getFechaEmision();
117
+ const setReferencia = buildSetReferencia(caso.id, fechaEmision);
118
+
119
+ const dteDatos = {
120
+ Encabezado: {
121
+ IdDoc: {
122
+ TipoDTE: 34,
123
+ Folio: folio,
124
+ FchEmis: fechaEmision,
125
+ },
126
+ Emisor: this._buildEmisor(),
127
+ Receptor: this._buildReceptor(),
128
+ Totales: totales,
129
+ },
130
+ Detalle: detalle,
131
+ Referencia: [setReferencia],
132
+ };
133
+
134
+ const dte = new DTE(dteDatos);
135
+ this._timbrarYFirmar(dte, caf);
136
+
137
+ // Guardar referencia para NC/ND
138
+ this._docRefs[caso.id] = {
139
+ tipoDte: 34,
140
+ folio,
141
+ fecha: fechaEmision,
142
+ detalle,
143
+ totales,
144
+ items,
145
+ };
146
+
147
+ this.logger.log(` ✓ Factura Exenta caso ${caso.id}: folio ${folio}`);
148
+ return dte;
149
+ }
150
+
151
+ /**
152
+ * Genera nota de crédito exenta (tipo 61)
153
+ * @private
154
+ */
155
+ async _generarNotaCredito(caso, cafPath) {
156
+ const { DTE, CAF, buildDetalle, calcularTotalesDesdeDetalle, buildSetReferencia } = require('../index');
157
+ const fs = require('fs');
158
+
159
+ const cafXml = fs.readFileSync(cafPath, 'utf8');
160
+ const caf = new CAF(cafXml);
161
+ const folio = this._reservarFolio(caf, cafXml);
162
+
163
+ // Obtener documento referenciado
164
+ const docRef = this._docRefs[caso.referenciaCaso];
165
+ if (!docRef) {
166
+ throw new Error(`SetExenta: Documento referencia ${caso.referenciaCaso} no encontrado para NC ${caso.id}`);
167
+ }
168
+
169
+ // Determinar items según tipo de NC
170
+ const items = this._resolverItemsNC(caso, docRef);
171
+ const detalle = buildDetalle(items, { allowIndExe: true });
172
+ const totales = calcularTotalesDesdeDetalle(detalle, { soloExento: true });
173
+
174
+ const fechaEmision = this._getFechaEmision();
175
+ const setReferencia = buildSetReferencia(caso.id, fechaEmision);
176
+
177
+ // Referencia al documento original
178
+ const docReferencia = {
179
+ NroLinRef: 2,
180
+ TpoDocRef: docRef.tipoDte,
181
+ FolioRef: docRef.folio,
182
+ FchRef: docRef.fecha,
183
+ CodRef: caso.codRef,
184
+ RazonRef: caso.razonRef,
185
+ };
186
+
187
+ const dteDatos = {
188
+ Encabezado: {
189
+ IdDoc: {
190
+ TipoDTE: 61,
191
+ Folio: folio,
192
+ FchEmis: fechaEmision,
193
+ },
194
+ Emisor: this._buildEmisor(),
195
+ Receptor: this._buildReceptor(),
196
+ Totales: totales,
197
+ },
198
+ Detalle: detalle,
199
+ Referencia: [setReferencia, docReferencia],
200
+ };
201
+
202
+ const dte = new DTE(dteDatos);
203
+ this._timbrarYFirmar(dte, caf);
204
+
205
+ // Guardar referencia
206
+ this._docRefs[caso.id] = {
207
+ tipoDte: 61,
208
+ folio,
209
+ fecha: fechaEmision,
210
+ detalle,
211
+ totales,
212
+ items,
213
+ };
214
+
215
+ this.logger.log(` ✓ NC Exenta caso ${caso.id}: folio ${folio} (ref: ${caso.referenciaCaso})`);
216
+ return dte;
217
+ }
218
+
219
+ /**
220
+ * Genera nota de débito exenta (tipo 56)
221
+ * @private
222
+ */
223
+ async _generarNotaDebito(caso, cafPath) {
224
+ const { DTE, CAF, buildDetalle, calcularTotalesDesdeDetalle, buildSetReferencia } = require('../index');
225
+ const fs = require('fs');
226
+
227
+ const cafXml = fs.readFileSync(cafPath, 'utf8');
228
+ const caf = new CAF(cafXml);
229
+ const folio = this._reservarFolio(caf, cafXml);
230
+
231
+ // Obtener documento referenciado
232
+ const docRef = this._docRefs[caso.referenciaCaso];
233
+ if (!docRef) {
234
+ throw new Error(`SetExenta: Documento referencia ${caso.referenciaCaso} no encontrado para ND ${caso.id}`);
235
+ }
236
+
237
+ // Determinar items según tipo de ND
238
+ const items = this._resolverItemsND(caso, docRef);
239
+ const detalle = buildDetalle(items, { allowIndExe: true });
240
+ const totales = calcularTotalesDesdeDetalle(detalle, { soloExento: true });
241
+
242
+ const fechaEmision = this._getFechaEmision();
243
+ const setReferencia = buildSetReferencia(caso.id, fechaEmision);
244
+
245
+ // Referencia al documento original
246
+ const docReferencia = {
247
+ NroLinRef: 2,
248
+ TpoDocRef: docRef.tipoDte,
249
+ FolioRef: docRef.folio,
250
+ FchRef: docRef.fecha,
251
+ CodRef: caso.codRef,
252
+ RazonRef: caso.razonRef,
253
+ };
254
+
255
+ const dteDatos = {
256
+ Encabezado: {
257
+ IdDoc: {
258
+ TipoDTE: 56,
259
+ Folio: folio,
260
+ FchEmis: fechaEmision,
261
+ },
262
+ Emisor: this._buildEmisor(),
263
+ Receptor: this._buildReceptor(),
264
+ Totales: totales,
265
+ },
266
+ Detalle: detalle,
267
+ Referencia: [setReferencia, docReferencia],
268
+ };
269
+
270
+ const dte = new DTE(dteDatos);
271
+ this._timbrarYFirmar(dte, caf);
272
+
273
+ this.logger.log(` ✓ ND Exenta caso ${caso.id}: folio ${folio} (ref: ${caso.referenciaCaso})`);
274
+ return dte;
275
+ }
276
+
277
+ // ═══════════════════════════════════════════════════════════════
278
+ // Helpers
279
+ // ═══════════════════════════════════════════════════════════════
280
+
281
+ /**
282
+ * Normaliza items marcándolos como exentos
283
+ * @private
284
+ */
285
+ _normalizarItemsExentos(items) {
286
+ return (items || []).map(item => ({
287
+ nombre: item.nombre,
288
+ cantidad: item.cantidad || 1,
289
+ precio: item.precio || 0,
290
+ unidad: item.unidad || 'UN',
291
+ exento: true, // Siempre exento
292
+ }));
293
+ }
294
+
295
+ /**
296
+ * Resuelve items para NC según codRef
297
+ * @private
298
+ */
299
+ _resolverItemsNC(caso, docRef) {
300
+ const razon = (caso.razonRef || '').toUpperCase();
301
+
302
+ // CodRef 2: Corrige giro/texto - usar item dummy
303
+ if (caso.codRef === 2) {
304
+ return [{
305
+ nombre: caso.razonRef || 'CORRECCION',
306
+ cantidad: 1,
307
+ precio: 0,
308
+ exento: true,
309
+ }];
310
+ }
311
+
312
+ // CodRef 3: Modifica montos
313
+ if (caso.codRef === 3) {
314
+ const esDevolucion = razon.includes('DEVOLUCION') || razon.includes('DEVOLUCIÓN');
315
+ const esModificaMonto = razon.includes('MODIFICA MONTO');
316
+
317
+ // DEVOLUCION: usar items de factura original completos
318
+ if (esDevolucion && docRef.items?.length) {
319
+ return docRef.items.map(item => ({
320
+ ...item,
321
+ exento: true,
322
+ }));
323
+ }
324
+
325
+ // MODIFICA MONTO: usar CANTIDAD de factura original con PRECIO nuevo del SII
326
+ if (esModificaMonto && caso.items?.length && docRef.items?.length) {
327
+ return caso.items.map(ncItem => {
328
+ const nombreNc = (ncItem.nombre || '').toUpperCase().trim();
329
+ const itemOriginal = docRef.items.find(i =>
330
+ (i.nombre || '').toUpperCase().trim() === nombreNc
331
+ );
332
+
333
+ if (itemOriginal && itemOriginal.cantidad) {
334
+ return {
335
+ nombre: ncItem.nombre,
336
+ cantidad: itemOriginal.cantidad, // Cantidad de factura ORIGINAL
337
+ precio: ncItem.precio, // Precio NUEVO del SII
338
+ unidad: ncItem.unidad || itemOriginal.unidad || 'UN',
339
+ exento: true,
340
+ };
341
+ }
342
+ return { ...ncItem, exento: true };
343
+ });
344
+ }
345
+
346
+ // Otros casos de codRef 3: usar items del caso
347
+ if (caso.items?.length) {
348
+ return this._normalizarItemsExentos(caso.items);
349
+ }
350
+ return docRef.items || [];
351
+ }
352
+
353
+ // CodRef 1: Anula documento completo
354
+ if (caso.codRef === 1) {
355
+ return docRef.items || [];
356
+ }
357
+
358
+ // Default: usar items de la NC o del documento
359
+ return caso.items?.length
360
+ ? this._normalizarItemsExentos(caso.items)
361
+ : docRef.items || [];
362
+ }
363
+
364
+ /**
365
+ * Resuelve items para ND según codRef
366
+ * @private
367
+ */
368
+ _resolverItemsND(caso, docRef) {
369
+ const razon = (caso.razonRef || '').toUpperCase();
370
+ const esModificaMonto = razon.includes('MODIFICA MONTO');
371
+
372
+ // Si no hay items, usar item dummy con la razón
373
+ if (!caso.items?.length) {
374
+ // Para ANULA o similar, usar items del documento referenciado si existen
375
+ if (docRef.items?.length) {
376
+ return docRef.items.map(i => ({ ...i, exento: true }));
377
+ }
378
+ return [{
379
+ nombre: caso.razonRef || 'CORRECCION',
380
+ cantidad: 1,
381
+ precio: 0,
382
+ exento: true,
383
+ }];
384
+ }
385
+
386
+ // MODIFICA MONTO: usar CANTIDAD de documento original con PRECIO nuevo del SII
387
+ if (caso.codRef === 3 && esModificaMonto && docRef.items?.length) {
388
+ return caso.items.map(ndItem => {
389
+ const nombreNd = (ndItem.nombre || '').toUpperCase().trim();
390
+ const itemOriginal = docRef.items.find(i =>
391
+ (i.nombre || '').toUpperCase().trim() === nombreNd
392
+ );
393
+
394
+ if (itemOriginal && itemOriginal.cantidad) {
395
+ return {
396
+ nombre: ndItem.nombre,
397
+ cantidad: itemOriginal.cantidad, // Cantidad de documento ORIGINAL
398
+ precio: ndItem.precio, // Precio NUEVO del SII
399
+ unidad: ndItem.unidad || itemOriginal.unidad || 'UN',
400
+ exento: true,
401
+ };
402
+ }
403
+ return { ...ndItem, exento: true };
404
+ });
405
+ }
406
+
407
+ // Default: usar items del caso
408
+ return this._normalizarItemsExentos(caso.items);
409
+ }
410
+
411
+ /**
412
+ * Reserva el siguiente folio disponible
413
+ * @private
414
+ */
415
+ _reservarFolio(caf, cafXml) {
416
+ const cafFingerprint = this.folioHelper.createCafFingerprint(cafXml);
417
+ const folio = this.folioHelper.reserveNextFolio({
418
+ rutEmisor: this.config.emisor.rut,
419
+ tipoDte: caf.getTipoDTE(),
420
+ folioDesde: caf.getFolioDesde(),
421
+ folioHasta: caf.getFolioHasta(),
422
+ ambiente: this.config.ambiente || 'certificacion',
423
+ cafFingerprint,
424
+ });
425
+ return folio;
426
+ }
427
+
428
+ /**
429
+ * Obtiene la fecha de emisión
430
+ * @private
431
+ */
432
+ _getFechaEmision() {
433
+ if (this._fechaEmision) return this._fechaEmision;
434
+ const now = new Date();
435
+ this._fechaEmision = now.toISOString().split('T')[0];
436
+ return this._fechaEmision;
437
+ }
438
+
439
+ /**
440
+ * Construye datos del emisor
441
+ * @private
442
+ */
443
+ _buildEmisor() {
444
+ const e = this.config.emisor;
445
+ return {
446
+ RUTEmisor: e.rut,
447
+ RznSoc: e.razon_social,
448
+ GiroEmis: e.giro,
449
+ Acteco: e.acteco,
450
+ DirOrigen: e.direccion,
451
+ CmnaOrigen: e.comuna,
452
+ CiudadOrigen: e.ciudad || e.comuna || 'Santiago',
453
+ };
454
+ }
455
+
456
+ /**
457
+ * Construye datos del receptor
458
+ * @private
459
+ */
460
+ _buildReceptor() {
461
+ const r = this.config.receptor;
462
+ return {
463
+ RUTRecep: r.rut,
464
+ RznSocRecep: r.razon_social,
465
+ GiroRecep: r.giro,
466
+ DirRecep: r.direccion,
467
+ CmnaRecep: r.comuna,
468
+ CiudadRecep: r.ciudad || r.comuna,
469
+ };
470
+ }
471
+
472
+ /**
473
+ * Timbra y firma el DTE
474
+ * @private
475
+ */
476
+ _timbrarYFirmar(dte, caf) {
477
+ const { Certificado } = require('../index');
478
+ const fs = require('fs');
479
+
480
+ const pfxBuffer = fs.readFileSync(this.config.certificado.path);
481
+ const cert = new Certificado(pfxBuffer, this.config.certificado.password);
482
+
483
+ const timestamp = new Date().toISOString().replace('Z', '');
484
+
485
+ dte.generarXML().timbrar(caf, timestamp);
486
+ dte.firmar(cert);
487
+ }
488
+ }
489
+
490
+ module.exports = SetExenta;