@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,1184 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * SetParser - Parseo de Sets de Prueba del SII
5
+ *
6
+ * Extrae casos de prueba del HTML/texto descargado del portal SII
7
+ * y genera estructuras compatibles con los módulos de certificación.
8
+ *
9
+ * @module dte-sii/cert/SetParser
10
+ */
11
+
12
+ // ═══════════════════════════════════════════════════════════════
13
+ // FUNCIONES AUXILIARES
14
+ // ═══════════════════════════════════════════════════════════════
15
+
16
+ /**
17
+ * Detecta el tipo de set según el nombre
18
+ */
19
+ function detectarTipoSet(nombreSet) {
20
+ const nombre = nombreSet.toUpperCase();
21
+ if (nombre.includes('LIBRO DE COMPRAS PARA EXENTOS')) return 'LIBRO_COMPRAS_EXENTOS';
22
+ if (nombre.includes('LIBRO DE COMPRAS')) return 'LIBRO_COMPRAS';
23
+ if (nombre.includes('LIBRO DE VENTAS')) return 'LIBRO_VENTAS';
24
+ if (nombre.includes('LIBRO DE GUIAS')) return 'LIBRO_GUIAS';
25
+ if (nombre.includes('GUIA DE DESPACHO') || nombre.includes('GUIA')) return 'GUIA_DESPACHO';
26
+ if (nombre.includes('FACTURA EXENTA') || nombre.includes('NO AFECTA')) return 'FACTURA_EXENTA';
27
+ if (nombre.includes('EXPORTACION')) return 'EXPORTACION';
28
+ if (nombre.includes('LIQUIDACION')) return 'LIQUIDACION';
29
+ if (nombre.includes('FACTURA DE COMPRA') || nombre.includes('EMISOR DE FACTURA DE COMPRA')) return 'FACTURA_COMPRA';
30
+ if (nombre.includes('BASICO')) return 'BASICO';
31
+ return 'OTRO';
32
+ }
33
+
34
+ /**
35
+ * Detecta el tipo DTE a partir del nombre del documento
36
+ */
37
+ function detectarTipoDTE(documento) {
38
+ if (!documento) return null;
39
+ const doc = documento.toUpperCase();
40
+
41
+ if (doc.includes('LIQUIDACION') || doc.includes('LIQUIDACI')) {
42
+ return { codigo: 43, nombre: 'Liquidación Factura' };
43
+ }
44
+ if (doc.includes('FACTURA DE COMPRA')) {
45
+ return { codigo: 46, nombre: 'Factura de Compra' };
46
+ }
47
+ if (doc.includes('FACTURA DE EXPORTACION') || doc.includes('FACTURA DE EXPORTACI')) {
48
+ return { codigo: 110, nombre: 'Factura de Exportación' };
49
+ }
50
+ if (doc.includes('NOTA DE CREDITO') && doc.includes('EXPORTACION')) {
51
+ return { codigo: 112, nombre: 'NC Exportación' };
52
+ }
53
+ if (doc.includes('NOTA DE DEBITO') && doc.includes('EXPORTACION')) {
54
+ return { codigo: 111, nombre: 'ND Exportación' };
55
+ }
56
+ if (doc.includes('NO AFECTA') || doc.includes('EXENTA')) {
57
+ return { codigo: 34, nombre: 'Factura Exenta' };
58
+ }
59
+ if (doc.includes('GUIA DE DESPACHO') || doc.includes('GUIA')) {
60
+ return { codigo: 52, nombre: 'Guía de Despacho' };
61
+ }
62
+ if (doc.includes('NOTA DE CREDITO') || doc.includes('NOTA DE CRÉDITO')) {
63
+ return { codigo: 61, nombre: 'Nota de Crédito' };
64
+ }
65
+ if (doc.includes('NOTA DE DEBITO') || doc.includes('NOTA DE DÉBITO')) {
66
+ return { codigo: 56, nombre: 'Nota de Débito' };
67
+ }
68
+ if (doc.includes('FACTURA')) {
69
+ return { codigo: 33, nombre: 'Factura Electrónica' };
70
+ }
71
+
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Determina código de referencia según razón
77
+ */
78
+ function determinarCodRef(razon) {
79
+ if (!razon) return 1;
80
+ const r = razon.toUpperCase();
81
+ if (r.includes('ANULA')) return 1;
82
+ if (r.includes('CORRIGE') && r.includes('GIRO')) return 2;
83
+ if (r.includes('CORRIGE') && r.includes('TEXTO')) return 2;
84
+ if (r.includes('DEVOLUCION')) return 3;
85
+ if (r.includes('MODIFICA')) return 3;
86
+ return 1;
87
+ }
88
+
89
+ /**
90
+ * Determina indicador de traslado para guías
91
+ */
92
+ function determinarIndTraslado(motivo) {
93
+ if (!motivo) return 1;
94
+ const m = motivo.toUpperCase();
95
+ if (m.includes('TRASLADO') && (m.includes('INTERNO') || m.includes('BODEGA'))) return 5;
96
+ if (m.includes('VENTA')) return 1;
97
+ if (m.includes('CONSIGNACION')) return 2;
98
+ if (m.includes('ENTREGA GRATUITA')) return 3;
99
+ if (m.includes('COMPROBANTE')) return 4;
100
+ if (m.includes('TRASLADO')) return 5;
101
+ if (m.includes('DEVOLUCION')) return 6;
102
+ return 1;
103
+ }
104
+
105
+ /**
106
+ * Determina tipo de despacho
107
+ */
108
+ function determinarTpoDespacho(trasladoPor) {
109
+ if (!trasladoPor) return null;
110
+ const t = trasladoPor.toUpperCase();
111
+ // IMPORTANTE: Buscar EMISOR primero porque frases como
112
+ // "EMISOR DEL DOCUMENTO AL LOCAL DEL CLIENTE" contienen ambas palabras
113
+ if (t.includes('EMISOR')) return 2;
114
+ if (t.includes('CLIENTE')) return 1;
115
+ return null;
116
+ }
117
+
118
+ /**
119
+ * Mapea tipo documento de texto a código SII
120
+ */
121
+ function mapearTipoDocLibro(tipo) {
122
+ const t = tipo.toUpperCase();
123
+ if (t.includes('FACTURA DE COMPRA ELECTRONICA')) return 46;
124
+ if (t.includes('FACTURA EXENTA ELECTRONICA') || (t.includes('NO AFECTA') && t.includes('ELECTRONICA'))) return 34;
125
+ if (t.includes('FACTURA ELECTRONICA')) return 33;
126
+ if (t.includes('FACTURA EXENTA') || t.includes('FACTURA NO AFECTA')) return 30; // exenta papel
127
+ if (t === 'FACTURA') return 30;
128
+ if (t.includes('NOTA DE CREDITO') && (t.includes('ELECTRONICA') || t.includes('ELECTRONICO'))) return 61;
129
+ if (t.includes('NOTA DE CREDITO')) return 60;
130
+ if (t.includes('NOTA DE DEBITO') && (t.includes('ELECTRONICA') || t.includes('ELECTRONICO'))) return 56;
131
+ if (t.includes('NOTA DE DEBITO')) return 55;
132
+ return 30;
133
+ }
134
+
135
+ // ═══════════════════════════════════════════════════════════════
136
+ // PARSER PRINCIPAL
137
+ // ═══════════════════════════════════════════════════════════════
138
+
139
+ /**
140
+ * Extrae los casos de prueba del texto del set descargado del SII
141
+ *
142
+ * @param {string} texto - Texto plano del set (HTML limpio)
143
+ * @returns {Object} Datos extraídos con sets y casos
144
+ */
145
+ function extraerCasosDelSet(texto) {
146
+ const resultado = {
147
+ sets: [],
148
+ totalCasos: 0,
149
+ };
150
+
151
+ const lineas = texto.split('\n');
152
+
153
+ let setActual = null;
154
+ let casoActual = null;
155
+ let enCabecera = false;
156
+ let enLibroCompras = false;
157
+ let enLibroComprasExentos = false;
158
+ let docActual = null;
159
+ let esperandoMontos = false;
160
+
161
+ for (let i = 0; i < lineas.length; i++) {
162
+ const linea = lineas[i];
163
+ const lineaTrim = linea.trim();
164
+
165
+ // Detectar inicio de SET
166
+ // Regex flexible: ATENCION puede tener tilde corrupta (�), tilde UTF-8 (Ó) o sin tilde (O)
167
+ const matchSet = lineaTrim.match(/^SET\s+(.+?)\s*[-:]\s*NUMERO DE ATENCI[^\d:]*[:\s]*(\d+)/i);
168
+ if (matchSet) {
169
+ // Guardar caso anterior si existe
170
+ if (casoActual && setActual) {
171
+ setActual.casos.push(casoActual);
172
+ casoActual = null;
173
+ }
174
+ // Guardar documento actual del libro si existe
175
+ if (docActual && setActual) {
176
+ setActual.documentosLibro.push(docActual);
177
+ docActual = null;
178
+ }
179
+ // Guardar set anterior si existe
180
+ if (setActual) {
181
+ resultado.sets.push(setActual);
182
+ }
183
+
184
+ const nombreSet = matchSet[1].trim();
185
+ const tipoSet = detectarTipoSet(nombreSet);
186
+ setActual = {
187
+ nombre: nombreSet,
188
+ numeroAtencion: matchSet[2],
189
+ tipo: tipoSet,
190
+ casos: [],
191
+ instrucciones: [],
192
+ documentosLibro: [], // Para SET LIBRO DE COMPRAS
193
+ observaciones: null,
194
+ factorProporcionalidad: null,
195
+ };
196
+ enLibroCompras = (tipoSet === 'LIBRO_COMPRAS');
197
+ enLibroComprasExentos = (tipoSet === 'LIBRO_COMPRAS_EXENTOS');
198
+ esperandoMontos = false;
199
+ continue;
200
+ }
201
+
202
+ // Detectar inicio de CASO (formato: CASO 4670590-1)
203
+ const matchCaso = lineaTrim.match(/^CASO\s+(\d+)-(\d+)\s*$/i);
204
+ if (matchCaso) {
205
+ // Guardar caso anterior si existe
206
+ if (casoActual && setActual) {
207
+ setActual.casos.push(casoActual);
208
+ }
209
+ // Guardar documento actual si existe
210
+ if (docActual && setActual) {
211
+ setActual.documentosLibro.push(docActual);
212
+ docActual = null;
213
+ }
214
+ enLibroCompras = false;
215
+ enLibroComprasExentos = false;
216
+
217
+ casoActual = {
218
+ setId: matchCaso[1],
219
+ numeroCaso: parseInt(matchCaso[2]),
220
+ id: `${matchCaso[1]}-${matchCaso[2]}`,
221
+ documento: null,
222
+ tipoDTE: null,
223
+ items: [],
224
+ referencia: null,
225
+ razonReferencia: null,
226
+ descuentoGlobal: null,
227
+ motivo: null,
228
+ trasladoPor: null,
229
+ moneda: null,
230
+ unidadMedida: null,
231
+ // Campos de exportación
232
+ formaPago: null,
233
+ modalidadVenta: null,
234
+ clausulaVenta: null,
235
+ totalClausula: null,
236
+ viaTransporte: null,
237
+ puertoEmbarque: null,
238
+ puertoDesembarque: null,
239
+ tipoBulto: null,
240
+ totalBultos: null,
241
+ flete: null,
242
+ seguro: null,
243
+ paisDestino: null,
244
+ comisionExtranjero: null,
245
+ raw: [],
246
+ };
247
+ enCabecera = true;
248
+ continue;
249
+ }
250
+
251
+ // Ignorar línea de separación ======
252
+ if (lineaTrim.match(/^=+$/)) {
253
+ enCabecera = false;
254
+ continue;
255
+ }
256
+
257
+ // Si estamos en un caso, parsear contenido
258
+ if (casoActual) {
259
+ casoActual.raw.push(lineaTrim);
260
+
261
+ // Detectar tipo de documento
262
+ const matchDocumento = lineaTrim.match(/^DOCUMENTO\s+(.+)$/i);
263
+ if (matchDocumento) {
264
+ casoActual.documento = matchDocumento[1].trim();
265
+ casoActual.tipoDTE = detectarTipoDTE(casoActual.documento);
266
+ continue;
267
+ }
268
+
269
+ // Detectar referencia (puede tener : o no)
270
+ const matchRef = lineaTrim.match(/^REFERENCIA[:\s]+(.+)$/i);
271
+ if (matchRef) {
272
+ casoActual.referencia = matchRef[1].trim();
273
+ // Extraer caso referenciado
274
+ const matchCasoRef = casoActual.referencia.match(/CASO\s+(\d+-\d+)/i);
275
+ if (matchCasoRef) {
276
+ casoActual.casoReferenciado = matchCasoRef[1];
277
+ }
278
+ continue;
279
+ }
280
+
281
+ // Detectar razón referencia
282
+ const matchRazon = lineaTrim.match(/^RAZON\s+REFERENCIA\s+(.+)$/i);
283
+ if (matchRazon) {
284
+ casoActual.razonReferencia = matchRazon[1].trim();
285
+ continue;
286
+ }
287
+
288
+ // Detectar motivo (para guías)
289
+ const matchMotivo = lineaTrim.match(/^MOTIVO[:\s]+(.+)$/i);
290
+ if (matchMotivo) {
291
+ casoActual.motivo = matchMotivo[1].trim();
292
+ continue;
293
+ }
294
+
295
+ // Detectar traslado por
296
+ const matchTraslado = lineaTrim.match(/^TRASLADO\s+POR[:\s]+(.+)$/i);
297
+ if (matchTraslado) {
298
+ casoActual.trasladoPor = matchTraslado[1].trim();
299
+ continue;
300
+ }
301
+
302
+ // Detectar descuento global
303
+ const matchDescGlobal = lineaTrim.match(/^DESCUENTO\s+GLOBAL.+?(\d+)%/i);
304
+ if (matchDescGlobal) {
305
+ casoActual.descuentoGlobal = parseInt(matchDescGlobal[1]);
306
+ continue;
307
+ }
308
+
309
+ // Detectar moneda
310
+ const matchMoneda = lineaTrim.match(/^MONEDA\s+DE\s+LA\s+OPERACION[:\s]+(.+)$/i);
311
+ if (matchMoneda) {
312
+ casoActual.moneda = matchMoneda[1].trim();
313
+ continue;
314
+ }
315
+
316
+ // ===== CAMPOS DE EXPORTACIÓN =====
317
+ const matchFormaPago = lineaTrim.match(/^FORMA\s+DE\s+PAGO\s+EXPORTACION[:\s]+(.+)$/i);
318
+ if (matchFormaPago) {
319
+ casoActual.formaPago = matchFormaPago[1].trim();
320
+ continue;
321
+ }
322
+
323
+ const matchModalidad = lineaTrim.match(/^MODALIDAD\s+DE\s+VENTA[:\s]+(.+)$/i);
324
+ if (matchModalidad) {
325
+ casoActual.modalidadVenta = matchModalidad[1].trim();
326
+ continue;
327
+ }
328
+
329
+ const matchClausula = lineaTrim.match(/^CLAUSULA\s+DE\s+VENTA.+?[:\s]+(.+)$/i);
330
+ if (matchClausula) {
331
+ casoActual.clausulaVenta = matchClausula[1].trim();
332
+ continue;
333
+ }
334
+
335
+ const matchTotalClausula = lineaTrim.match(/^TOTAL\s+CLAUSULA\s+DE\s+VENTA[:\s]+(.+)$/i);
336
+ if (matchTotalClausula) {
337
+ casoActual.totalClausula = parseFloat(matchTotalClausula[1].trim());
338
+ continue;
339
+ }
340
+
341
+ const matchVia = lineaTrim.match(/^VIA\s+DE\s+TRANSPORTE[:\s]+(.+)$/i);
342
+ if (matchVia) {
343
+ casoActual.viaTransporte = matchVia[1].trim();
344
+ continue;
345
+ }
346
+
347
+ const matchPuertoEmb = lineaTrim.match(/^PUERTO\s+DE\s+EMBARQUE[:\s]+(.+)$/i);
348
+ if (matchPuertoEmb) {
349
+ casoActual.puertoEmbarque = matchPuertoEmb[1].trim();
350
+ continue;
351
+ }
352
+
353
+ const matchPuertoDes = lineaTrim.match(/^PUERTO\s+DE\s+DESEMBARQUE[:\s]+(.+)$/i);
354
+ if (matchPuertoDes) {
355
+ casoActual.puertoDesembarque = matchPuertoDes[1].trim();
356
+ continue;
357
+ }
358
+
359
+ const matchTipoBulto = lineaTrim.match(/^TIPO\s+DE\s+BULTO[:\s]+(.+)$/i);
360
+ if (matchTipoBulto) {
361
+ casoActual.tipoBulto = matchTipoBulto[1].trim();
362
+ continue;
363
+ }
364
+
365
+ const matchTotalBultos = lineaTrim.match(/^TOTAL\s+BULTOS[:\s]+(\d+)/i);
366
+ if (matchTotalBultos) {
367
+ casoActual.totalBultos = parseInt(matchTotalBultos[1]);
368
+ continue;
369
+ }
370
+
371
+ const matchFlete = lineaTrim.match(/^FLETE[^:]*[:\s]+(.+)$/i);
372
+ if (matchFlete) {
373
+ casoActual.flete = parseFloat(matchFlete[1].trim());
374
+ continue;
375
+ }
376
+
377
+ const matchSeguro = lineaTrim.match(/^SEGURO[^:]*[:\s]+(.+)$/i);
378
+ if (matchSeguro) {
379
+ casoActual.seguro = parseFloat(matchSeguro[1].trim());
380
+ continue;
381
+ }
382
+
383
+ const matchPais = lineaTrim.match(/^PAIS\s+RECEPTOR.+?[:\s]+(.+)$/i);
384
+ if (matchPais) {
385
+ casoActual.paisDestino = matchPais[1].trim();
386
+ continue;
387
+ }
388
+
389
+ const matchComision = lineaTrim.match(/^COMISIONES?\s+EN\s+EL\s+EXTRANJERO.+?(\d+)%/i);
390
+ if (matchComision) {
391
+ casoActual.comisionExtranjero = parseInt(matchComision[1]);
392
+ continue;
393
+ }
394
+
395
+ // Ignorar cabeceras de items
396
+ if (lineaTrim.match(/^ITEM\s+(CANTIDAD|VALOR)/i)) {
397
+ continue;
398
+ }
399
+
400
+ // ===== PARSING DE ITEMS =====
401
+ // Dividir por tabs o múltiples espacios para manejar diferentes formatos
402
+ const partes = lineaTrim.split(/\t+|\s{2,}/).map(p => p.trim()).filter(p => p);
403
+
404
+ // Formato con tabs: "NOMBRE CANTIDAD UNIDAD PRECIO" o "NOMBRE CANTIDAD PRECIO DESCUENTO"
405
+ if (partes.length >= 2 && !lineaTrim.startsWith('DOCUMENTO') && !lineaTrim.startsWith('REFERENCIA')
406
+ && !lineaTrim.startsWith('RAZON') && !lineaTrim.includes(':')) {
407
+
408
+ const nombre = partes[0];
409
+
410
+ // Ignorar si parece una cabecera
411
+ if (nombre === 'ITEM' || nombre.includes('CANTIDAD') || nombre.includes('UNITARIO')) {
412
+ continue;
413
+ }
414
+
415
+ // Detectar formato según número de partes
416
+ if (partes.length === 4) {
417
+ // Puede ser: NOMBRE, CANTIDAD, UNIDAD, PRECIO o NOMBRE, CANTIDAD, PRECIO, DESCUENTO
418
+ const posibleUnidad = partes[2];
419
+ if (posibleUnidad.match(/^[A-Za-z]+$/) && !posibleUnidad.includes('%')) {
420
+ // Es una unidad de medida: NOMBRE, QTY, UNIDAD, PRECIO
421
+ casoActual.items.push({
422
+ nombre: nombre,
423
+ cantidad: parseInt(partes[1]) || 1,
424
+ unidadMedida: posibleUnidad,
425
+ precioUnitario: parseInt(partes[3]) || 0,
426
+ });
427
+ } else {
428
+ // Es descuento: NOMBRE, QTY, PRECIO, DESCUENTO
429
+ casoActual.items.push({
430
+ nombre: nombre,
431
+ cantidad: parseInt(partes[1]) || 1,
432
+ precioUnitario: parseInt(partes[2]) || 0,
433
+ descuento: partes[3],
434
+ });
435
+ }
436
+ continue;
437
+ }
438
+
439
+ if (partes.length === 3) {
440
+ // NOMBRE, CANTIDAD, PRECIO/VALOR
441
+ const qty = parseInt(partes[1]);
442
+ const precio = parseInt(partes[2]);
443
+ if (!isNaN(qty) && !isNaN(precio)) {
444
+ casoActual.items.push({
445
+ nombre: nombre,
446
+ cantidad: qty,
447
+ precioUnitario: precio,
448
+ });
449
+ continue;
450
+ }
451
+ }
452
+
453
+ if (partes.length === 2) {
454
+ // NOMBRE, CANTIDAD (guías sin precio) o NOMBRE, VALOR (NC/ND)
455
+ const valor = parseInt(partes[1]);
456
+ if (!isNaN(valor)) {
457
+ // Para NC/ND que modifican monto, el segundo valor es el precio unitario modificado
458
+ if (casoActual.razonReferencia?.includes('MODIFICA MONTO')) {
459
+ casoActual.items.push({
460
+ nombre: nombre,
461
+ cantidad: 1,
462
+ precioUnitario: valor,
463
+ });
464
+ } else {
465
+ // Para guías o NC por devolución, es la cantidad
466
+ casoActual.items.push({
467
+ nombre: nombre,
468
+ cantidad: valor,
469
+ });
470
+ }
471
+ continue;
472
+ }
473
+ }
474
+ }
475
+
476
+ // Fallback: items con formato de espacios simples
477
+ // "ITEM 1 56" - nombre con espacio, luego cantidad
478
+ const matchItemSimple = lineaTrim.match(/^(ITEM\s+\d+|[A-Z][^0-9]+?)\s+(\d+)$/i);
479
+ if (matchItemSimple) {
480
+ casoActual.items.push({
481
+ nombre: matchItemSimple[1].trim(),
482
+ cantidad: parseInt(matchItemSimple[2]),
483
+ });
484
+ continue;
485
+ }
486
+ }
487
+
488
+ // ===== SET LIBRO DE COMPRAS PARA EXENTOS =====
489
+ // Formato columnar: TIPO FOLIO [MONTO_EXENTO] [MONTO_AFECTO] (todo en una línea)
490
+ // Observaciones van en línea siguiente con prefijo "OBS:"
491
+ if (enLibroComprasExentos && setActual && !casoActual) {
492
+ if (lineaTrim.includes('TIPO DOCUMENTO') && lineaTrim.includes('FOLIO')) continue;
493
+ if (lineaTrim.includes('MONTO EXENTO') && lineaTrim.includes('MONTO AFECTO')) continue;
494
+ if (lineaTrim.match(/^=+$/)) continue;
495
+
496
+ // OBS: observación para el documento actual
497
+ if (lineaTrim.match(/^OBS:/i)) {
498
+ if (docActual) docActual.observacion = lineaTrim.replace(/^OBS:\s*/i, '').trim();
499
+ continue;
500
+ }
501
+
502
+ // Parsear línea columnar: TIPO FOLIO [MONTO_EXENTO] [MONTO_AFECTO]
503
+ const partes = lineaTrim.split(/\t+|\s{2,}/).map(p => p.trim()).filter(p => p);
504
+ if (partes.length >= 2 && /^\d+$/.test(partes[1])) {
505
+ if (docActual) setActual.documentosLibro.push(docActual);
506
+ const tipoDoc = partes[0].toUpperCase();
507
+ docActual = {
508
+ tipoDocumento: tipoDoc,
509
+ folio: parseInt(partes[1]),
510
+ observacion: null,
511
+ montoExento: null,
512
+ montoAfecto: null,
513
+ ivaUsoComun: false,
514
+ codigoIvaNoRec: null,
515
+ };
516
+ if (partes.length >= 4) {
517
+ // Dos montos: exento + afecto
518
+ docActual.montoExento = parseInt(partes[2]) || null;
519
+ docActual.montoAfecto = parseInt(partes[3]) || null;
520
+ } else if (partes.length === 3) {
521
+ // Un monto: determinar columna por tipo de documento
522
+ const esExento = /EXENTA|EXENTO|DEBITO/i.test(tipoDoc);
523
+ if (esExento) docActual.montoExento = parseInt(partes[2]) || null;
524
+ else docActual.montoAfecto = parseInt(partes[2]) || null;
525
+ }
526
+ continue;
527
+ }
528
+ continue;
529
+ }
530
+
531
+ // ===== SET LIBRO DE COMPRAS =====
532
+ if (enLibroCompras && setActual && !casoActual) {
533
+ // Detectar cabecera de tabla
534
+ if (lineaTrim.includes('TIPO DOCUMENTO') && lineaTrim.includes('FOLIO')) {
535
+ continue;
536
+ }
537
+ if (lineaTrim.includes('MONTO EXENTO') && lineaTrim.includes('MONTO AFECTO')) {
538
+ continue;
539
+ }
540
+
541
+ // Detectar documento con folio
542
+ // Formato: "FACTURA 234" o "FACTURA ELECTRONICA 32"
543
+ const matchDocFolio = lineaTrim.match(/^(FACTURA DE COMPRA ELECTRONICA|FACTURA ELECTRONICA|FACTURA|NOTA DE CREDITO)\s+(\d+)\s*$/i);
544
+ if (matchDocFolio) {
545
+ // Guardar documento anterior si existe
546
+ if (docActual) {
547
+ setActual.documentosLibro.push(docActual);
548
+ }
549
+ docActual = {
550
+ tipoDocumento: matchDocFolio[1].trim().toUpperCase(),
551
+ folio: parseInt(matchDocFolio[2]),
552
+ observacion: null,
553
+ montoExento: null,
554
+ montoAfecto: null,
555
+ codigoIvaNoRec: null,
556
+ ivaUsoComun: false,
557
+ };
558
+ esperandoMontos = false;
559
+ continue;
560
+ }
561
+
562
+ // Detectar observación del documento
563
+ if (docActual && !esperandoMontos) {
564
+ // Líneas de observación conocidas
565
+ if (lineaTrim.match(/^(FACTURA DEL GIRO|FACTURA CON IVA|NOTA DE CREDITO POR|ENTREGA GRATUITA|COMPRA CON RETENCION)/i)) {
566
+ docActual.observacion = lineaTrim;
567
+ // Detectar casos especiales
568
+ if (lineaTrim.includes('IVA USO COMUN')) {
569
+ docActual.ivaUsoComun = true;
570
+ }
571
+ if (lineaTrim.includes('ENTREGA GRATUITA')) {
572
+ docActual.codigoIvaNoRec = 4; // IVA no recuperable - entrega gratuita
573
+ }
574
+ if (lineaTrim.includes('RETENCION TOTAL')) {
575
+ docActual.retencionTotal = true;
576
+ }
577
+ esperandoMontos = true;
578
+ continue;
579
+ }
580
+ }
581
+
582
+ // Detectar montos (puede ser una o dos columnas)
583
+ if (docActual && esperandoMontos) {
584
+ // Formato: " 7893 3897" (exento afecto) o " 4305" (solo afecto)
585
+ const matchDosMonto = lineaTrim.match(/^\s*(\d+)\s+(\d+)\s*$/);
586
+ if (matchDosMonto) {
587
+ docActual.montoExento = parseInt(matchDosMonto[1]);
588
+ docActual.montoAfecto = parseInt(matchDosMonto[2]);
589
+ esperandoMontos = false;
590
+ continue;
591
+ }
592
+
593
+ // Solo monto afecto (con espacios iniciales)
594
+ const matchUnMonto = lineaTrim.match(/^\s*(\d+)\s*$/);
595
+ if (matchUnMonto) {
596
+ docActual.montoAfecto = parseInt(matchUnMonto[1]);
597
+ esperandoMontos = false;
598
+ continue;
599
+ }
600
+ }
601
+
602
+ // Detectar factor de proporcionalidad
603
+ const matchFactor = lineaTrim.match(/FACTOR\s+DE\s+PROPORCIONALIDAD.+?(\d+\.?\d*)/i);
604
+ if (matchFactor) {
605
+ setActual.factorProporcionalidad = parseFloat(matchFactor[1]);
606
+ setActual.observaciones = `Factor proporcionalidad IVA: ${matchFactor[1]}`;
607
+ continue;
608
+ }
609
+ }
610
+
611
+ // Línea separadora de sets
612
+ if (lineaTrim.match(/^-{20,}$/)) {
613
+ // Guardar caso actual si existe
614
+ if (casoActual && setActual) {
615
+ setActual.casos.push(casoActual);
616
+ casoActual = null;
617
+ }
618
+ // Guardar documento actual si existe
619
+ if (docActual && setActual) {
620
+ setActual.documentosLibro.push(docActual);
621
+ docActual = null;
622
+ }
623
+ enLibroCompras = false;
624
+ enLibroComprasExentos = false;
625
+ esperandoMontos = false;
626
+ continue;
627
+ }
628
+ }
629
+
630
+ // Guardar últimos elementos
631
+ if (casoActual && setActual) {
632
+ setActual.casos.push(casoActual);
633
+ }
634
+ if (docActual && setActual) {
635
+ setActual.documentosLibro.push(docActual);
636
+ }
637
+ if (setActual) {
638
+ resultado.sets.push(setActual);
639
+ }
640
+
641
+ // Contar total de casos
642
+ for (const set of resultado.sets) {
643
+ resultado.totalCasos += set.casos.length;
644
+ }
645
+
646
+ return resultado;
647
+ }
648
+
649
+ // ═══════════════════════════════════════════════════════════════
650
+ // GENERADORES DE ESTRUCTURAS
651
+ // ═══════════════════════════════════════════════════════════════
652
+
653
+ /**
654
+ * Genera estructura para SET BASICO (Facturas 33, NC 61, ND 56)
655
+ */
656
+ function generarEstructuraSetBasico(set) {
657
+ const casosFactura = [];
658
+ const casosNC = [];
659
+ const casosND = [];
660
+
661
+ for (const caso of set.casos) {
662
+ const tipoCodigo = caso.tipoDTE?.codigo;
663
+
664
+ if (tipoCodigo === 33) {
665
+ casosFactura.push({
666
+ id: caso.id,
667
+ items: caso.items.map(item => ({
668
+ nombre: item.nombre,
669
+ cantidad: item.cantidad,
670
+ precio: item.precioUnitario,
671
+ ...(item.descuento ? { descuentoPct: parseInt(item.descuento) } : {}),
672
+ ...(item.nombre.includes('EXENTO') || item.nombre.includes('SERVICIO EXENTO') ? { exento: true } : {}),
673
+ })),
674
+ ...(caso.descuentoGlobal ? { descuentoGlobalPct: caso.descuentoGlobal } : {}),
675
+ });
676
+ } else if (tipoCodigo === 61) {
677
+ casosNC.push({
678
+ id: caso.id,
679
+ tipoDte: 61,
680
+ referenciaCaso: caso.casoReferenciado,
681
+ codRef: determinarCodRef(caso.razonReferencia),
682
+ razonRef: caso.razonReferencia,
683
+ items: caso.items.length > 0 ? caso.items.map(item => ({
684
+ nombre: item.nombre,
685
+ cantidad: item.cantidad,
686
+ precio: item.precioUnitario || 0,
687
+ })) : [{ nombre: caso.razonReferencia, cantidad: 1, precio: 0 }],
688
+ ...(caso.razonReferencia?.includes('ANULA') ? { itemsFromCaso: caso.casoReferenciado } : {}),
689
+ });
690
+ } else if (tipoCodigo === 56) {
691
+ casosND.push({
692
+ id: caso.id,
693
+ tipoDte: 56,
694
+ referenciaCaso: caso.casoReferenciado,
695
+ codRef: determinarCodRef(caso.razonReferencia),
696
+ razonRef: caso.razonReferencia,
697
+ items: caso.items.length > 0 ? caso.items.map(item => ({
698
+ nombre: item.nombre,
699
+ cantidad: item.cantidad,
700
+ precio: item.precioUnitario || 0,
701
+ })) : [{ nombre: caso.razonReferencia, cantidad: 1, precio: 0 }],
702
+ });
703
+ }
704
+ }
705
+
706
+ return {
707
+ numeroAtencion: set.numeroAtencion,
708
+ casosFactura,
709
+ casosNC,
710
+ casosND,
711
+ cafRequired: {
712
+ 33: casosFactura.length,
713
+ 61: casosNC.length,
714
+ 56: casosND.length,
715
+ },
716
+ };
717
+ }
718
+
719
+ /**
720
+ * Genera estructura para SET FACTURA EXENTA (34, NC 61, ND 56)
721
+ */
722
+ function generarEstructuraSetExenta(set) {
723
+ const casosFactura = [];
724
+ const casosNC = [];
725
+ const casosND = [];
726
+
727
+ for (const caso of set.casos) {
728
+ const tipoCodigo = caso.tipoDTE?.codigo;
729
+
730
+ if (tipoCodigo === 34) {
731
+ casosFactura.push({
732
+ id: caso.id,
733
+ items: caso.items.map(item => ({
734
+ nombre: item.nombre,
735
+ cantidad: item.cantidad,
736
+ unidad: item.unidadMedida || 'UN',
737
+ precio: item.precioUnitario,
738
+ exento: true,
739
+ })),
740
+ });
741
+ } else if (tipoCodigo === 61) {
742
+ casosNC.push({
743
+ id: caso.id,
744
+ referenciaCaso: caso.casoReferenciado,
745
+ codRef: determinarCodRef(caso.razonReferencia),
746
+ razonRef: caso.razonReferencia,
747
+ items: caso.items.map(item => ({
748
+ nombre: item.nombre,
749
+ cantidad: item.cantidad || 1,
750
+ precio: item.precioUnitario || 0,
751
+ exento: true,
752
+ })),
753
+ });
754
+ } else if (tipoCodigo === 56) {
755
+ casosND.push({
756
+ id: caso.id,
757
+ referenciaCaso: caso.casoReferenciado,
758
+ codRef: determinarCodRef(caso.razonReferencia),
759
+ razonRef: caso.razonReferencia,
760
+ items: caso.items.map(item => ({
761
+ nombre: item.nombre,
762
+ cantidad: item.cantidad || 1,
763
+ precio: item.precioUnitario || 0,
764
+ exento: true,
765
+ })),
766
+ });
767
+ }
768
+ }
769
+
770
+ return {
771
+ numeroAtencion: set.numeroAtencion,
772
+ casosFactura,
773
+ casosNC,
774
+ casosND,
775
+ cafRequired: {
776
+ 34: casosFactura.length,
777
+ 61: casosNC.length,
778
+ 56: casosND.length,
779
+ },
780
+ };
781
+ }
782
+
783
+ /**
784
+ * Genera estructura para SET GUIA DE DESPACHO (52)
785
+ */
786
+ function generarEstructuraSetGuia(set) {
787
+ const casos = [];
788
+
789
+ for (const caso of set.casos) {
790
+ const indTraslado = determinarIndTraslado(caso.motivo);
791
+ const tpoDespacho = determinarTpoDespacho(caso.trasladoPor);
792
+
793
+ casos.push({
794
+ id: caso.id,
795
+ indTraslado,
796
+ ...(tpoDespacho ? { tpoDespacho } : {}),
797
+ items: caso.items.map(item => ({
798
+ nombre: item.nombre,
799
+ cantidad: item.cantidad,
800
+ ...(item.precioUnitario ? { precio: item.precioUnitario } : { monto: 0 }),
801
+ })),
802
+ });
803
+ }
804
+
805
+ return {
806
+ numeroAtencion: set.numeroAtencion,
807
+ casos,
808
+ cafRequired: { 52: casos.length },
809
+ };
810
+ }
811
+
812
+ /**
813
+ * Genera estructura para Libro de Guías a partir del SET GUIA DE DESPACHO
814
+ */
815
+ function generarEstructuraLibroGuiasDesdeSetGuia(setGuia, receptorConfig = {}) {
816
+ const casos = [];
817
+ const receptorBase = receptorConfig.rut && receptorConfig.razon_social
818
+ ? { rut: receptorConfig.rut, razon: receptorConfig.razon_social }
819
+ : null;
820
+
821
+ (setGuia?.casos || []).forEach((caso) => {
822
+ const items = (caso.items || []).map((item) => ({
823
+ nombre: item.nombre,
824
+ cantidad: item.cantidad || 1,
825
+ precio: item.precio ?? 0,
826
+ }));
827
+
828
+ const totalFromPrecio = (caso.items || []).reduce((acc, item) => (
829
+ acc + ((item.precio || 0) * (item.cantidad || 1))
830
+ ), 0);
831
+ const totalFromMonto = (caso.items || []).reduce((acc, item) => (
832
+ acc + (item.monto || 0)
833
+ ), 0);
834
+ const total = totalFromPrecio + totalFromMonto;
835
+ const hasMontoOnly = (caso.items || []).some((item) => (
836
+ item.monto !== undefined && item.precio == null
837
+ ));
838
+
839
+ const tpoOper = caso.indTraslado === 1
840
+ ? 1
841
+ : (Number.isFinite(Number(caso.indTraslado)) ? Number(caso.indTraslado) : 2);
842
+ const entry = {
843
+ tpoOper,
844
+ items,
845
+ };
846
+
847
+ if (hasMontoOnly) {
848
+ entry.mntTotalOverride = total;
849
+ }
850
+ if (tpoOper === 1 && receptorBase) {
851
+ entry.receptor = receptorBase;
852
+ }
853
+ casos.push(entry);
854
+ });
855
+
856
+ return {
857
+ numeroAtencion: setGuia?.numeroAtencion,
858
+ casos,
859
+ };
860
+ }
861
+
862
+ /**
863
+ * Genera estructura para SET FACTURA DE COMPRA (46, NC 61, ND 56)
864
+ */
865
+ function generarEstructuraSetFacturaCompra(set) {
866
+ let casoFactura = null;
867
+ let casoNC = null;
868
+ let casoND = null;
869
+
870
+ for (const caso of set.casos) {
871
+ const tipoCodigo = caso.tipoDTE?.codigo;
872
+
873
+ if (tipoCodigo === 46) {
874
+ casoFactura = {
875
+ id: caso.id,
876
+ items: caso.items.map(item => ({
877
+ nombre: item.nombre,
878
+ cantidad: item.cantidad,
879
+ precio: item.precioUnitario,
880
+ })),
881
+ };
882
+ } else if (tipoCodigo === 61) {
883
+ casoNC = {
884
+ id: caso.id,
885
+ referenciaCaso: caso.casoReferenciado,
886
+ codRef: determinarCodRef(caso.razonReferencia),
887
+ razonRef: caso.razonReferencia,
888
+ items: caso.items.map(item => ({
889
+ nombre: item.nombre,
890
+ cantidad: item.cantidad,
891
+ precio: item.precioUnitario,
892
+ })),
893
+ };
894
+ } else if (tipoCodigo === 56) {
895
+ casoND = {
896
+ id: caso.id,
897
+ referenciaCaso: caso.casoReferenciado,
898
+ codRef: determinarCodRef(caso.razonReferencia),
899
+ razonRef: caso.razonReferencia,
900
+ items: casoNC?.items || [],
901
+ };
902
+ }
903
+ }
904
+
905
+ return {
906
+ numeroAtencion: set.numeroAtencion,
907
+ casoFactura,
908
+ casoNC,
909
+ casoND,
910
+ cafRequired: {
911
+ 46: casoFactura ? 1 : 0,
912
+ 61: casoNC ? 1 : 0,
913
+ 56: casoND ? 1 : 0,
914
+ },
915
+ };
916
+ }
917
+
918
+ /**
919
+ * Genera estructura para SET LIQUIDACIONES (43)
920
+ */
921
+ function generarEstructuraSetLiquidaciones(set) {
922
+ const casos = [];
923
+
924
+ for (const caso of set.casos) {
925
+ casos.push({
926
+ id: caso.id,
927
+ items: caso.items.map(item => ({
928
+ nombre: item.nombre,
929
+ cantidad: item.cantidad,
930
+ totalLinea: item.totalLinea || item.precioUnitario * item.cantidad,
931
+ ...(item.nombre.includes('EXENTO') ? { exento: true } : {}),
932
+ })),
933
+ });
934
+ }
935
+
936
+ return {
937
+ numeroAtencion: set.numeroAtencion,
938
+ casos,
939
+ cafRequired: { 43: casos.length },
940
+ };
941
+ }
942
+
943
+ /**
944
+ * Genera estructura para SET EXPORTACION (110, 111, 112)
945
+ */
946
+ function generarEstructuraSetExportacion(set) {
947
+ const casos = [];
948
+
949
+ for (const caso of set.casos) {
950
+ casos.push({
951
+ id: caso.id,
952
+ tipoDTE: caso.tipoDTE?.codigo,
953
+ items: caso.items.map(item => ({
954
+ nombre: item.nombre,
955
+ cantidad: item.cantidad,
956
+ unidad: item.unidadMedida,
957
+ precio: item.precioUnitario || item.valorLinea,
958
+ })),
959
+ moneda: caso.moneda,
960
+ formaPago: caso.formaPago,
961
+ modalidadVenta: caso.modalidadVenta,
962
+ clausulaVenta: caso.clausulaVenta,
963
+ totalClausula: caso.totalClausula,
964
+ viaTransporte: caso.viaTransporte,
965
+ puertoEmbarque: caso.puertoEmbarque,
966
+ puertoDesembarque: caso.puertoDesembarque,
967
+ tipoBulto: caso.tipoBulto,
968
+ totalBultos: caso.totalBultos,
969
+ flete: caso.flete,
970
+ seguro: caso.seguro,
971
+ paisDestino: caso.paisDestino,
972
+ comisionExtranjero: caso.comisionExtranjero,
973
+ ...(caso.casoReferenciado ? {
974
+ referenciaCaso: caso.casoReferenciado,
975
+ razonRef: caso.razonReferencia,
976
+ } : {}),
977
+ });
978
+ }
979
+
980
+ return {
981
+ numeroAtencion: set.numeroAtencion,
982
+ casos,
983
+ cafRequired: {
984
+ 110: casos.filter(c => c.tipoDTE === 110).length,
985
+ 111: casos.filter(c => c.tipoDTE === 111).length,
986
+ 112: casos.filter(c => c.tipoDTE === 112).length,
987
+ },
988
+ };
989
+ }
990
+
991
+ /**
992
+ * Genera estructura para LIBRO DE COMPRAS (IECV)
993
+ */
994
+ function generarEstructuraLibroCompras(set) {
995
+ const detalle = [];
996
+ const resumen = {};
997
+
998
+ for (const doc of set.documentosLibro) {
999
+ const tipoDoc = mapearTipoDocLibro(doc.tipoDocumento);
1000
+ const tasaIva = 0.19;
1001
+
1002
+ const detalleDoc = {
1003
+ TpoDoc: tipoDoc,
1004
+ NroDoc: doc.folio,
1005
+ TasaImp: tasaIva,
1006
+ FchDoc: new Date().toISOString().split('T')[0], // Se debe ajustar
1007
+ RUTDoc: '17096073-4', // RUT por defecto para certificación
1008
+ RznSoc: 'Razon Social',
1009
+ };
1010
+
1011
+ if (doc.montoExento) detalleDoc.MntExe = doc.montoExento;
1012
+ if (doc.montoAfecto) {
1013
+ detalleDoc.MntNeto = doc.montoAfecto;
1014
+ detalleDoc.MntIVA = Math.round(doc.montoAfecto * tasaIva);
1015
+ }
1016
+
1017
+ if (doc.ivaUsoComun) {
1018
+ detalleDoc.TpoImp = 1;
1019
+ detalleDoc.IVAUsoComun = Math.round((doc.montoAfecto || 0) * tasaIva);
1020
+ }
1021
+
1022
+ if (doc.codigoIvaNoRec) {
1023
+ detalleDoc.IVANoRec = {
1024
+ CodIVANoRec: doc.codigoIvaNoRec,
1025
+ MntIVANoRec: Math.round((doc.montoAfecto || 0) * tasaIva),
1026
+ };
1027
+ }
1028
+
1029
+ if (doc.retencionTotal && tipoDoc === 46) {
1030
+ detalleDoc.OtrosImp = {
1031
+ CodImp: 15,
1032
+ TasaImp: tasaIva,
1033
+ MntImp: Math.round((doc.montoAfecto || 0) * tasaIva),
1034
+ };
1035
+ detalleDoc.IVARetTotal = Math.round((doc.montoAfecto || 0) * tasaIva);
1036
+ }
1037
+
1038
+ // Referencia para notas de crédito
1039
+ if (tipoDoc === 60 && doc.observacion) {
1040
+ const matchRef = doc.observacion.match(/FACTURA(?:\s+ELECTRONICA)?\s+(\d+)/i);
1041
+ if (matchRef) {
1042
+ detalleDoc.TpoDocRef = doc.observacion.includes('ELECTRONICA') ? 33 : 30;
1043
+ detalleDoc.FolioDocRef = parseInt(matchRef[1]);
1044
+ }
1045
+ }
1046
+
1047
+ detalleDoc.MntTotal = (detalleDoc.MntNeto || 0) + (detalleDoc.MntIVA || 0) + (detalleDoc.MntExe || 0);
1048
+ if (doc.retencionTotal && tipoDoc === 46) {
1049
+ detalleDoc.MntTotal = detalleDoc.MntNeto || 0; // Sin IVA pagado
1050
+ }
1051
+
1052
+ detalle.push(detalleDoc);
1053
+
1054
+ // Acumular resumen
1055
+ if (!resumen[tipoDoc]) {
1056
+ resumen[tipoDoc] = {
1057
+ TpoDoc: tipoDoc,
1058
+ TotDoc: 0,
1059
+ TotMntExe: 0,
1060
+ TotMntNeto: 0,
1061
+ TotMntIVA: 0,
1062
+ TotMntTotal: 0,
1063
+ };
1064
+ }
1065
+ resumen[tipoDoc].TotDoc++;
1066
+ resumen[tipoDoc].TotMntExe += detalleDoc.MntExe || 0;
1067
+ resumen[tipoDoc].TotMntNeto += detalleDoc.MntNeto || 0;
1068
+ resumen[tipoDoc].TotMntIVA += detalleDoc.MntIVA || 0;
1069
+ resumen[tipoDoc].TotMntTotal += detalleDoc.MntTotal || 0;
1070
+ }
1071
+
1072
+ return {
1073
+ numeroAtencion: set.numeroAtencion,
1074
+ factorProporcionalidad: set.factorProporcionalidad || 0.6,
1075
+ detalle,
1076
+ resumen: Object.values(resumen),
1077
+ documentosOriginales: set.documentosLibro,
1078
+ };
1079
+ }
1080
+
1081
+ // ═══════════════════════════════════════════════════════════════
1082
+ // FUNCIÓN PRINCIPAL
1083
+ // ═══════════════════════════════════════════════════════════════
1084
+
1085
+ /**
1086
+ * Genera estructuras compatibles con los scripts de certificación
1087
+ * a partir de los datos extraídos del set
1088
+ *
1089
+ * @param {Object} datosExtraidos - Resultado de extraerCasosDelSet()
1090
+ * @param {Object} [receptorConfig] - Configuración del receptor para libros
1091
+ * @returns {Object} Estructuras para cada set/libro
1092
+ */
1093
+ function generarEstructurasParaScripts(datosExtraidos, receptorConfig = {}) {
1094
+ const estructuras = {
1095
+ setBasico: null,
1096
+ setFacturaExenta: null,
1097
+ setGuiaDespacho: null,
1098
+ setFacturaCompra: null,
1099
+ setLiquidaciones: null,
1100
+ setExportacion1: null,
1101
+ setExportacion2: null,
1102
+ libroVentas: null,
1103
+ libroCompras: null,
1104
+ libroComprasExentos: null,
1105
+ libroGuias: null,
1106
+ };
1107
+
1108
+ for (const set of datosExtraidos.sets) {
1109
+ switch (set.tipo) {
1110
+ case 'BASICO':
1111
+ estructuras.setBasico = generarEstructuraSetBasico(set);
1112
+ break;
1113
+ case 'FACTURA_EXENTA':
1114
+ estructuras.setFacturaExenta = generarEstructuraSetExenta(set);
1115
+ break;
1116
+ case 'GUIA_DESPACHO':
1117
+ estructuras.setGuiaDespacho = generarEstructuraSetGuia(set);
1118
+ if (!estructuras.libroGuias) {
1119
+ estructuras.libroGuias = generarEstructuraLibroGuiasDesdeSetGuia(estructuras.setGuiaDespacho, receptorConfig);
1120
+ }
1121
+ break;
1122
+ case 'FACTURA_COMPRA':
1123
+ estructuras.setFacturaCompra = generarEstructuraSetFacturaCompra(set);
1124
+ break;
1125
+ case 'LIQUIDACION':
1126
+ estructuras.setLiquidaciones = generarEstructuraSetLiquidaciones(set);
1127
+ break;
1128
+ case 'EXPORTACION':
1129
+ if (set.nombre.includes('(1)')) {
1130
+ estructuras.setExportacion1 = generarEstructuraSetExportacion(set);
1131
+ } else if (set.nombre.includes('(2)')) {
1132
+ estructuras.setExportacion2 = generarEstructuraSetExportacion(set);
1133
+ }
1134
+ break;
1135
+ case 'LIBRO_COMPRAS':
1136
+ estructuras.libroCompras = generarEstructuraLibroCompras(set);
1137
+ break;
1138
+ case 'LIBRO_COMPRAS_EXENTOS':
1139
+ // Misma estructura que LIBRO_COMPRAS pero marcado como exentos
1140
+ estructuras.libroComprasExentos = generarEstructuraLibroCompras(set);
1141
+ break;
1142
+ case 'LIBRO_VENTAS':
1143
+ // El libro de ventas se genera con los datos del set básico o exento
1144
+ estructuras.libroVentas = { numeroAtencion: set.numeroAtencion, instruccion: 'Usar DTEs del SET BASICO' };
1145
+ break;
1146
+ case 'LIBRO_GUIAS':
1147
+ // El libro de guías se genera con los datos del set guía
1148
+ estructuras.libroGuias = estructuras.libroGuias || { numeroAtencion: set.numeroAtencion, instruccion: 'Usar DTEs del SET GUIA DE DESPACHO' };
1149
+ break;
1150
+ }
1151
+ }
1152
+
1153
+ return estructuras;
1154
+ }
1155
+
1156
+ // ═══════════════════════════════════════════════════════════════
1157
+ // EXPORTS
1158
+ // ═══════════════════════════════════════════════════════════════
1159
+
1160
+ module.exports = {
1161
+ // Función principal de extracción
1162
+ extraerCasosDelSet,
1163
+
1164
+ // Generador de estructuras
1165
+ generarEstructurasParaScripts,
1166
+
1167
+ // Generadores individuales (para uso avanzado)
1168
+ generarEstructuraSetBasico,
1169
+ generarEstructuraSetExenta,
1170
+ generarEstructuraSetGuia,
1171
+ generarEstructuraSetFacturaCompra,
1172
+ generarEstructuraSetLiquidaciones,
1173
+ generarEstructuraSetExportacion,
1174
+ generarEstructuraLibroCompras,
1175
+ generarEstructuraLibroGuiasDesdeSetGuia,
1176
+
1177
+ // Helpers de detección
1178
+ detectarTipoDTE,
1179
+ detectarTipoSet,
1180
+ determinarCodRef,
1181
+ determinarIndTraslado,
1182
+ determinarTpoDespacho,
1183
+ mapearTipoDocLibro,
1184
+ };