@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,553 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * FolioRegistry.js - Registro local de folios
5
+ *
6
+ * Maneja el control de folios usados, reservados y enviados al SII.
7
+ * Proporciona persistencia en archivo JSON y control de conflictos.
8
+ *
9
+ * @module FolioRegistry
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const crypto = require('crypto');
15
+
16
+ /**
17
+ * Clase para manejar el registro de folios
18
+ */
19
+ class FolioRegistry {
20
+ /**
21
+ * @param {Object} options - Opciones de configuración
22
+ * @param {string} [options.registryPath] - Ruta al archivo de registro
23
+ * @param {string} [options.baseDir] - Directorio base para el registro
24
+ */
25
+ constructor(options = {}) {
26
+ if (options.registryPath) {
27
+ this.registryPath = options.registryPath;
28
+ } else if (options.baseDir) {
29
+ this.registryPath = path.join(options.baseDir, 'debug', 'folios.json');
30
+ } else {
31
+ this.registryPath = path.resolve(__dirname, '..', '..', 'debug', 'folios.json');
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Asegura que existe el directorio del registro
37
+ * @private
38
+ */
39
+ _ensureDir() {
40
+ const dir = path.dirname(this.registryPath);
41
+ if (!fs.existsSync(dir)) {
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Carga el registro desde disco
48
+ * @returns {Object}
49
+ */
50
+ load() {
51
+ try {
52
+ if (!fs.existsSync(this.registryPath)) {
53
+ return { version: 1, entries: {} };
54
+ }
55
+ const raw = fs.readFileSync(this.registryPath, 'utf8');
56
+ const parsed = JSON.parse(raw);
57
+ if (!parsed || typeof parsed !== 'object' || !parsed.entries) {
58
+ return { version: 1, entries: {} };
59
+ }
60
+ return parsed;
61
+ } catch (error) {
62
+ return { version: 1, entries: {} };
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Guarda el registro a disco
68
+ * @param {Object} registry - Datos del registro
69
+ */
70
+ save(registry) {
71
+ this._ensureDir();
72
+ fs.writeFileSync(this.registryPath, JSON.stringify(registry, null, 2), 'utf8');
73
+ }
74
+
75
+ /**
76
+ * Crea un fingerprint único para un CAF
77
+ * @param {string} cafXml - XML del CAF
78
+ * @returns {string|undefined}
79
+ */
80
+ static createCafFingerprint(cafXml) {
81
+ if (!cafXml) return undefined;
82
+ return crypto.createHash('sha256').update(cafXml).digest('hex').slice(0, 12);
83
+ }
84
+
85
+ /**
86
+ * Construye la clave única para una entrada
87
+ * @param {Object} params - Parámetros
88
+ * @returns {string}
89
+ */
90
+ static buildKey({ rutEmisor, tipoDte, folioDesde, folioHasta, ambiente, cafFingerprint }) {
91
+ if (!rutEmisor || !tipoDte || !folioDesde || !folioHasta) {
92
+ throw new Error('Faltan datos para construir la clave del registro de folios');
93
+ }
94
+ return [
95
+ String(rutEmisor),
96
+ String(tipoDte),
97
+ String(folioDesde),
98
+ String(folioHasta),
99
+ String(ambiente || ''),
100
+ String(cafFingerprint || ''),
101
+ ].join('|');
102
+ }
103
+
104
+ /**
105
+ * Recolecta folios bloqueados (anulados)
106
+ * @param {Object} params - Parámetros de búsqueda
107
+ * @returns {Set<number>}
108
+ */
109
+ collectBlockedFolios({ registry, rutEmisor, tipoDte, ambiente }) {
110
+ const blocked = new Set();
111
+ if (!registry || !registry.entries) return blocked;
112
+
113
+ Object.values(registry.entries).forEach((entry) => {
114
+ const meta = entry && entry.meta ? entry.meta : {};
115
+ if (String(meta.rutEmisor) !== String(rutEmisor)) return;
116
+ if (String(meta.tipoDte) !== String(tipoDte)) return;
117
+ if (String(meta.ambiente || '') !== String(ambiente || '')) return;
118
+
119
+ const nota = String(meta.nota || '').toLowerCase();
120
+ if (nota.includes('anulad')) {
121
+ (entry.usedFolios || []).forEach((folio) => blocked.add(Number(folio)));
122
+ }
123
+
124
+ const sent = entry.sentFolios || {};
125
+ Object.keys(sent).forEach((folio) => {
126
+ if (sent[folio] && sent[folio].anulado) {
127
+ blocked.add(Number(folio));
128
+ }
129
+ });
130
+ });
131
+
132
+ return blocked;
133
+ }
134
+
135
+ /**
136
+ * Reserva el siguiente folio disponible
137
+ * @param {Object} params - Parámetros
138
+ * @returns {number} - Folio reservado
139
+ */
140
+ reserveNextFolio({
141
+ rutEmisor,
142
+ tipoDte,
143
+ folioDesde,
144
+ folioHasta,
145
+ ambiente,
146
+ cafFingerprint,
147
+ }) {
148
+ const desde = Number(folioDesde);
149
+ const hasta = Number(folioHasta);
150
+
151
+ if (!rutEmisor || !tipoDte || Number.isNaN(desde) || Number.isNaN(hasta)) {
152
+ throw new Error('Parámetros inválidos para reservar folio');
153
+ }
154
+ if (desde > hasta) {
155
+ throw new Error(`Rango de folios inválido (${desde}-${hasta})`);
156
+ }
157
+
158
+ const registry = this.load();
159
+ const blocked = this.collectBlockedFolios({ registry, rutEmisor, tipoDte, ambiente });
160
+ const key = FolioRegistry.buildKey({ rutEmisor, tipoDte, folioDesde: desde, folioHasta: hasta, ambiente, cafFingerprint });
161
+
162
+ const entry = registry.entries[key] || {
163
+ usedFolios: [],
164
+ lastReserved: null,
165
+ meta: {
166
+ rutEmisor,
167
+ tipoDte,
168
+ folioDesde: desde,
169
+ folioHasta: hasta,
170
+ ambiente: ambiente || null,
171
+ cafFingerprint: cafFingerprint || null,
172
+ },
173
+ };
174
+
175
+ const inRange = (folio) => Number(folio) >= desde && Number(folio) <= hasta;
176
+ const baseUsed = (entry.usedFolios || []).filter(inRange).map((f) => Number(f));
177
+ const usedSet = new Set(baseUsed);
178
+ const sentFolios = entry.sentFolios || {};
179
+
180
+ // Folios reutilizables: reservados pero no enviados ni bloqueados
181
+ const reusable = baseUsed
182
+ .filter((folio) => !sentFolios[String(folio)] && !blocked.has(Number(folio)))
183
+ .sort((a, b) => a - b);
184
+
185
+ let next = reusable.length
186
+ ? reusable[0]
187
+ : (entry.lastReserved ? Math.max(desde, Number(entry.lastReserved) + 1) : desde);
188
+
189
+ while (usedSet.has(next) || blocked.has(next)) {
190
+ next += 1;
191
+ }
192
+
193
+ if (next > hasta) {
194
+ throw new Error(`No hay más folios disponibles (${desde}-${hasta})`);
195
+ }
196
+
197
+ usedSet.add(next);
198
+ entry.usedFolios = Array.from(usedSet).filter(inRange).sort((a, b) => a - b);
199
+ entry.lastReserved = entry.usedFolios.length ? Math.max(...entry.usedFolios) : null;
200
+ entry.updatedAt = new Date().toISOString();
201
+
202
+ registry.entries[key] = entry;
203
+ this.save(registry);
204
+
205
+ return next;
206
+ }
207
+
208
+ /**
209
+ * Marca un folio como enviado al SII
210
+ * @param {Object} params - Parámetros
211
+ */
212
+ markFolioSent({
213
+ rutEmisor,
214
+ tipoDte,
215
+ folio,
216
+ folioDesde,
217
+ folioHasta,
218
+ ambiente,
219
+ cafFingerprint,
220
+ trackId,
221
+ sentAt,
222
+ extra,
223
+ }) {
224
+ const desde = Number(folioDesde);
225
+ const hasta = Number(folioHasta);
226
+ const folioNum = Number(folio);
227
+
228
+ if (!rutEmisor || !tipoDte || Number.isNaN(desde) || Number.isNaN(hasta) || Number.isNaN(folioNum)) {
229
+ throw new Error('Parámetros inválidos para marcar folio como enviado');
230
+ }
231
+ if (folioNum < desde || folioNum > hasta) {
232
+ throw new Error(`Folio ${folioNum} fuera de rango CAF (${desde}-${hasta})`);
233
+ }
234
+
235
+ const registry = this.load();
236
+ const key = FolioRegistry.buildKey({ rutEmisor, tipoDte, folioDesde: desde, folioHasta: hasta, ambiente, cafFingerprint });
237
+
238
+ const entry = registry.entries[key] || {
239
+ usedFolios: [],
240
+ lastReserved: null,
241
+ sentFolios: {},
242
+ meta: {
243
+ rutEmisor,
244
+ tipoDte,
245
+ folioDesde: desde,
246
+ folioHasta: hasta,
247
+ ambiente: ambiente || null,
248
+ cafFingerprint: cafFingerprint || null,
249
+ },
250
+ };
251
+
252
+ if (!entry.usedFolios) entry.usedFolios = [];
253
+ if (!entry.sentFolios) entry.sentFolios = {};
254
+
255
+ if (!entry.usedFolios.includes(folioNum)) {
256
+ entry.usedFolios.push(folioNum);
257
+ entry.usedFolios.sort((a, b) => a - b);
258
+ }
259
+
260
+ entry.sentFolios[String(folioNum)] = {
261
+ trackId: trackId || null,
262
+ sentAt: sentAt || new Date().toISOString(),
263
+ ...(extra ? { extra } : {}),
264
+ };
265
+
266
+ entry.updatedAt = new Date().toISOString();
267
+ registry.entries[key] = entry;
268
+ this.save(registry);
269
+ }
270
+
271
+ /**
272
+ * Libera folios del registro
273
+ * @param {Object} params - Parámetros
274
+ */
275
+ releaseFolios({
276
+ rutEmisor,
277
+ tipoDte,
278
+ folios,
279
+ folioDesde,
280
+ folioHasta,
281
+ ambiente,
282
+ cafFingerprint,
283
+ }) {
284
+ const desde = Number(folioDesde);
285
+ const hasta = Number(folioHasta);
286
+
287
+ if (!rutEmisor || !tipoDte || Number.isNaN(desde) || Number.isNaN(hasta)) {
288
+ throw new Error('Parámetros inválidos para liberar folios');
289
+ }
290
+
291
+ const foliosNum = (Array.isArray(folios) ? folios : [folios])
292
+ .map((f) => Number(f))
293
+ .filter((f) => !Number.isNaN(f));
294
+
295
+ if (!foliosNum.length) return;
296
+
297
+ const registry = this.load();
298
+ const key = FolioRegistry.buildKey({ rutEmisor, tipoDte, folioDesde: desde, folioHasta: hasta, ambiente, cafFingerprint });
299
+ const entry = registry.entries[key];
300
+
301
+ if (!entry) return;
302
+
303
+ const toRemove = new Set(foliosNum);
304
+ const inRange = (f) => Number(f) >= desde && Number(f) <= hasta;
305
+
306
+ entry.usedFolios = (entry.usedFolios || [])
307
+ .filter((f) => !toRemove.has(Number(f)))
308
+ .filter(inRange)
309
+ .map((f) => Number(f));
310
+
311
+ if (entry.sentFolios) {
312
+ Object.keys(entry.sentFolios).forEach((folioKey) => {
313
+ if (toRemove.has(Number(folioKey))) {
314
+ delete entry.sentFolios[folioKey];
315
+ }
316
+ });
317
+ }
318
+
319
+ entry.lastReserved = entry.usedFolios.length ? Math.max(...entry.usedFolios) : null;
320
+ entry.updatedAt = new Date().toISOString();
321
+ registry.entries[key] = entry;
322
+ this.save(registry);
323
+ }
324
+
325
+ /**
326
+ * Obtiene folios restantes disponibles
327
+ * @param {Object} params - Parámetros
328
+ * @returns {number}
329
+ */
330
+ getRemainingFolios({
331
+ rutEmisor,
332
+ tipoDte,
333
+ folioDesde,
334
+ folioHasta,
335
+ ambiente,
336
+ cafFingerprint,
337
+ }) {
338
+ const desde = Number(folioDesde);
339
+ const hasta = Number(folioHasta);
340
+ const total = hasta - desde + 1;
341
+
342
+ if (!fs.existsSync(this.registryPath)) {
343
+ return total;
344
+ }
345
+
346
+ try {
347
+ const registry = this.load();
348
+ const key = FolioRegistry.buildKey({
349
+ rutEmisor,
350
+ tipoDte,
351
+ folioDesde: desde,
352
+ folioHasta: hasta,
353
+ ambiente,
354
+ cafFingerprint
355
+ });
356
+ const entry = registry.entries[key];
357
+ const used = entry && Array.isArray(entry.usedFolios) ? entry.usedFolios.length : 0;
358
+ return Math.max(0, total - used);
359
+ } catch (_) {
360
+ return total;
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Obtiene estadísticas del registro para un tipo de DTE
366
+ * @param {Object} params - Parámetros
367
+ * @returns {Object}
368
+ */
369
+ getStats({ rutEmisor, tipoDte, ambiente }) {
370
+ const registry = this.load();
371
+ const stats = {
372
+ totalEntries: 0,
373
+ totalUsed: 0,
374
+ totalSent: 0,
375
+ ranges: [],
376
+ };
377
+
378
+ Object.entries(registry.entries).forEach(([key, entry]) => {
379
+ const meta = entry.meta || {};
380
+ if (String(meta.rutEmisor) !== String(rutEmisor)) return;
381
+ if (String(meta.tipoDte) !== String(tipoDte)) return;
382
+ if (ambiente && String(meta.ambiente || '') !== String(ambiente)) return;
383
+
384
+ stats.totalEntries += 1;
385
+ stats.totalUsed += (entry.usedFolios || []).length;
386
+ stats.totalSent += Object.keys(entry.sentFolios || {}).length;
387
+ stats.ranges.push({
388
+ folioDesde: meta.folioDesde,
389
+ folioHasta: meta.folioHasta,
390
+ used: (entry.usedFolios || []).length,
391
+ sent: Object.keys(entry.sentFolios || {}).length,
392
+ remaining: (meta.folioHasta - meta.folioDesde + 1) - (entry.usedFolios || []).length,
393
+ });
394
+ });
395
+
396
+ return stats;
397
+ }
398
+
399
+ /**
400
+ * Busca el CAF más reciente para un tipo de DTE en un directorio
401
+ * @param {number} tipoDte - Tipo de DTE (OBLIGATORIO)
402
+ * @param {string} [cafDir] - Directorio base de CAFs (legacy)
403
+ * @param {string} rutEmisor - RUT del emisor (OBLIGATORIO)
404
+ * @param {string} ambiente - 'certificacion' o 'produccion' (OBLIGATORIO)
405
+ * @param {string} [baseDir] - Directorio base del proyecto (para multi-tenant)
406
+ * @returns {string|null} - Ruta al CAF o null
407
+ */
408
+ static findLatestCaf(tipoDte, cafDir, rutEmisor, ambiente, baseDir = null) {
409
+ // Validar parámetros obligatorios
410
+ if (!tipoDte) throw new Error('findLatestCaf: tipoDte es obligatorio');
411
+ if (!rutEmisor) throw new Error('findLatestCaf: rutEmisor es obligatorio');
412
+ if (!ambiente) throw new Error('findLatestCaf: ambiente es obligatorio');
413
+ if (!['certificacion', 'produccion'].includes(ambiente)) {
414
+ throw new Error(`findLatestCaf: ambiente inválido "${ambiente}"`);
415
+ }
416
+
417
+ const base = baseDir || process.cwd();
418
+
419
+ // Buscar en estructura organizada: debug/caf/<ambiente>/<rut>/<tipoDte>/<fecha>/
420
+ const rutClean = rutEmisor.replace(/\./g, '').toUpperCase();
421
+ const organizedDir = path.resolve(base, 'debug', 'caf', ambiente, rutClean, String(tipoDte));
422
+
423
+ if (fs.existsSync(organizedDir)) {
424
+ // Obtener todas las subcarpetas de fecha, ordenadas de más reciente a más antigua
425
+ const dateDirs = fs.readdirSync(organizedDir)
426
+ .filter((d) => /^\d{4}-\d{2}-\d{2}$/.test(d))
427
+ .map((d) => path.join(organizedDir, d))
428
+ .filter((d) => fs.statSync(d).isDirectory())
429
+ .sort((a, b) => b.localeCompare(a)); // Ordenar fechas descendente
430
+
431
+ const matches = [];
432
+
433
+ // Buscar CAFs en cada carpeta de fecha
434
+ for (const dateDir of dateDirs) {
435
+ const files = fs.readdirSync(dateDir)
436
+ .filter((f) => f.endsWith('.xml') && f.startsWith(`caf-${tipoDte}-`))
437
+ .map((f) => path.join(dateDir, f));
438
+
439
+ files.forEach((filePath) => {
440
+ try {
441
+ const xml = fs.readFileSync(filePath, 'utf8');
442
+ if (!xml.includes('<AUTORIZACION')) return;
443
+ const stat = fs.statSync(filePath);
444
+ matches.push({ filePath, mtime: stat.mtimeMs });
445
+ } catch (_) {
446
+ // ignore
447
+ }
448
+ });
449
+ }
450
+
451
+ if (matches.length) {
452
+ matches.sort((a, b) => b.mtime - a.mtime);
453
+ return matches[0].filePath;
454
+ }
455
+ }
456
+
457
+ // Fallback: buscar en debug/auto-caf (legacy)
458
+ const dir = cafDir || path.resolve(base, 'debug', 'auto-caf');
459
+ if (!fs.existsSync(dir)) return null;
460
+
461
+ const files = fs.readdirSync(dir)
462
+ .filter((f) => f.endsWith('.xml') && f.startsWith('caf-solicitar-'))
463
+ .map((f) => path.join(dir, f));
464
+
465
+ const matches = [];
466
+ files.forEach((filePath) => {
467
+ try {
468
+ const xml = fs.readFileSync(filePath, 'utf8');
469
+ const tdMatch = xml.match(/<TD>(\d+)<\/TD>/i);
470
+ if (tdMatch && Number(tdMatch[1]) === Number(tipoDte)) {
471
+ const stat = fs.statSync(filePath);
472
+ matches.push({ filePath, mtime: stat.mtimeMs });
473
+ }
474
+ } catch (_) {
475
+ // ignore
476
+ }
477
+ });
478
+
479
+ if (!matches.length) return null;
480
+ matches.sort((a, b) => b.mtime - a.mtime);
481
+ return matches[0].filePath;
482
+ }
483
+
484
+ /**
485
+ * Resuelve la ruta de un CAF, buscando uno disponible si es necesario
486
+ * @param {Object} params - Parámetros
487
+ * @param {number} params.tipoDte - Tipo de DTE (OBLIGATORIO)
488
+ * @param {string} params.rutEmisor - RUT del emisor (OBLIGATORIO)
489
+ * @param {string} params.ambiente - 'certificacion' o 'produccion' (OBLIGATORIO)
490
+ * @param {string} [params.cafPath] - Ruta explícita al CAF
491
+ * @param {number} [params.requiredCount=1] - Folios mínimos requeridos
492
+ * @param {string} [params.cafDir] - Directorio de CAFs (legacy)
493
+ * @param {string} [params.baseDir] - Directorio base del proyecto (para multi-tenant)
494
+ * @param {string} [params.registryPath] - Ruta al registro de folios
495
+ * @returns {string|null} - Ruta al CAF o null
496
+ */
497
+ static resolveCafPath({
498
+ tipoDte,
499
+ rutEmisor,
500
+ ambiente,
501
+ cafPath = null,
502
+ requiredCount = 1,
503
+ cafDir,
504
+ baseDir = null,
505
+ registryPath = null,
506
+ }) {
507
+ // Validar parámetros obligatorios
508
+ if (!tipoDte) throw new Error('resolveCafPath: tipoDte es obligatorio');
509
+ if (!rutEmisor) throw new Error('resolveCafPath: rutEmisor es obligatorio');
510
+ if (!ambiente) throw new Error('resolveCafPath: ambiente es obligatorio');
511
+ if (!['certificacion', 'produccion'].includes(ambiente)) {
512
+ throw new Error(`resolveCafPath: ambiente inválido "${ambiente}"`);
513
+ }
514
+
515
+ const CAF = require('./CAF');
516
+ const registry = new FolioRegistry({ baseDir, registryPath });
517
+
518
+ let resolvedPath = cafPath;
519
+
520
+ // Si no hay CAF o no existe, buscar el más reciente (estructura organizada primero)
521
+ if (!resolvedPath || !fs.existsSync(resolvedPath)) {
522
+ resolvedPath = FolioRegistry.findLatestCaf(tipoDte, cafDir, rutEmisor, ambiente, baseDir);
523
+ }
524
+
525
+ // Validar si hay folios suficientes
526
+ if (resolvedPath && fs.existsSync(resolvedPath)) {
527
+ const cafXml = fs.readFileSync(resolvedPath, 'utf8');
528
+ const caf = new CAF(cafXml);
529
+ const cafFingerprint = FolioRegistry.createCafFingerprint(cafXml);
530
+
531
+ const remaining = registry.getRemainingFolios({
532
+ rutEmisor,
533
+ tipoDte: caf.getTipoDTE(),
534
+ folioDesde: caf.getFolioDesde(),
535
+ folioHasta: caf.getFolioHasta(),
536
+ ambiente,
537
+ cafFingerprint,
538
+ });
539
+
540
+ if (remaining < requiredCount) {
541
+ // Intentar buscar otro CAF más nuevo
542
+ const newerCaf = FolioRegistry.findLatestCaf(tipoDte, cafDir, rutEmisor, ambiente, baseDir);
543
+ if (newerCaf && newerCaf !== resolvedPath) {
544
+ resolvedPath = newerCaf;
545
+ }
546
+ }
547
+ }
548
+
549
+ return resolvedPath;
550
+ }
551
+ }
552
+
553
+ module.exports = FolioRegistry;