@devlas/dte-sii 2.16.0 → 2.17.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.
- package/FolioService.js +119 -3
- package/cert/CertRunner.js +220 -59
- package/cert/Simulacion.js +41 -12
- package/dte-sii.d.ts +38 -0
- package/package.json +1 -1
package/FolioService.js
CHANGED
|
@@ -142,6 +142,24 @@ class FolioService {
|
|
|
142
142
|
* @returns {string|null} - Ruta al CAF o null
|
|
143
143
|
*/
|
|
144
144
|
findLatestCaf(tipoDte) {
|
|
145
|
+
return this.listarCafs(tipoDte)[0] ?? null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Todos los CAF de un tipo que hay en disco para este RUT y ambiente, del más
|
|
150
|
+
* reciente al más viejo.
|
|
151
|
+
*
|
|
152
|
+
* `findLatestCaf` devuelve solo el primero, que alcanza mientras un CAF cubra
|
|
153
|
+
* la cantidad completa. Cuando el SII raciona el timbraje eso deja de valer: un
|
|
154
|
+
* pedido de 4 folios puede haber quedado repartido en un CAF de 3 y otro de 1, y
|
|
155
|
+
* mirando solo el último se concluye "no alcanza" y se vuelven a pedir folios
|
|
156
|
+
* nuevos, dejando los anteriores timbrados y sin usar (que es justo lo que el SII
|
|
157
|
+
* cuenta en contra del cupo).
|
|
158
|
+
*
|
|
159
|
+
* @param {number} tipoDte
|
|
160
|
+
* @returns {string[]} rutas ordenadas por mtime descendente
|
|
161
|
+
*/
|
|
162
|
+
listarCafs(tipoDte) {
|
|
145
163
|
const matches = [];
|
|
146
164
|
|
|
147
165
|
// Helper para buscar recursivamente
|
|
@@ -195,9 +213,8 @@ class FolioService {
|
|
|
195
213
|
const canonicalDir = path.join(this.debugDir, 'caf', this.ambiente, this.rutEmisor, String(tipoDte));
|
|
196
214
|
searchRecursive(canonicalDir);
|
|
197
215
|
|
|
198
|
-
if (!matches.length) return null;
|
|
199
216
|
matches.sort((a, b) => b.mtime - a.mtime);
|
|
200
|
-
return matches
|
|
217
|
+
return matches.map(m => m.filePath);
|
|
201
218
|
}
|
|
202
219
|
|
|
203
220
|
/**
|
|
@@ -456,7 +473,16 @@ class FolioService {
|
|
|
456
473
|
result.errorCode === 'MAX_AUTOR_INSUFICIENTE' ||
|
|
457
474
|
result.errorCode === 'TIMBRAJE_BLOQUEADO';
|
|
458
475
|
|
|
459
|
-
|
|
476
|
+
// ⚠️ `!== 0` estricto. FOLIOS_DISP es el total de folios timbrados y sin utilizar,
|
|
477
|
+
// que es exactamente el conjunto anulable (el SII solo anula folios "que no han sido
|
|
478
|
+
// recepcionados"). Con 0 la anulación no puede prosperar y solo gasta requests: el
|
|
479
|
+
// 19/08/2026 una corrida intentó anular 4 folios ya emitidos, uno por uno, para
|
|
480
|
+
// terminar en `SIN_FOLIOS_ANULABLES` y un mensaje que culpaba a folios inexistentes.
|
|
481
|
+
// Con el timbraje BLOQUEADO el SII no publica el campo y queda `null`, y ese caso sí
|
|
482
|
+
// necesita intentarlo.
|
|
483
|
+
const hayQueAnular = result.foliosDisp !== 0;
|
|
484
|
+
|
|
485
|
+
if (!result.success && recuperable && permitirAnular && hayQueAnular) {
|
|
460
486
|
console.warn(
|
|
461
487
|
`[FolioService] Tipo ${tipoDte}: ${result.errorCode} — anulando folios sin utilizar y reintentando...`
|
|
462
488
|
);
|
|
@@ -541,6 +567,96 @@ class FolioService {
|
|
|
541
567
|
return { ok: true, cafPath: result.cafPath, otorgados, ...base };
|
|
542
568
|
}
|
|
543
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Cubre `cantidad` folios en varios timbrajes cuando el SII no autoriza tantos de
|
|
572
|
+
* una vez.
|
|
573
|
+
*
|
|
574
|
+
* `solicitarCafExacto` exige que UN CAF cubra todo y aborta si `MAX_AUTOR` no da.
|
|
575
|
+
* Eso es correcto para no agravar el racionamiento cuando hay folios sin usar
|
|
576
|
+
* (anular/reobtener sí destraba ese caso), pero deja sin salida el caso contrario:
|
|
577
|
+
* `FOLIOS_DISP=0` y `MAX_AUTOR` chico porque el SII raciona por historial. Ahí no
|
|
578
|
+
* hay nada que anular ni que reobtener, y la única vía es pedir de a tandas.
|
|
579
|
+
*
|
|
580
|
+
* Medido el 19/08/2026 (RUT 76543210-K, maullin): el set de simulación necesita 4
|
|
581
|
+
* folios de tipo 33, el SII autorizaba 3 con FOLIOS_DISP=0, y el tope siguió en 3
|
|
582
|
+
* durante 24 h. Esperar no lo mueve.
|
|
583
|
+
*
|
|
584
|
+
* Los sets aceptan varios CAF por tipo (`SetBase._tomarFolio` recorre la lista y
|
|
585
|
+
* salta al siguiente cuando se agota un rango), así que juntar tandas es
|
|
586
|
+
* transparente para quien genera los documentos.
|
|
587
|
+
*
|
|
588
|
+
* ⚠️ Cada tanda sube `FOLIOS_DISP` y puede bajar el `MAX_AUTOR` de la siguiente. Por
|
|
589
|
+
* eso se corta apenas una tanda no aporta folios nuevos, en vez de insistir: cada
|
|
590
|
+
* pedido inútil deja folios timbrados que después juegan en contra.
|
|
591
|
+
*
|
|
592
|
+
* @param {Object} params
|
|
593
|
+
* @param {number} params.tipoDte
|
|
594
|
+
* @param {number} params.cantidad - folios a cubrir en total
|
|
595
|
+
* @param {number} [params.maxTandas=4] - tope duro de viajes al portal
|
|
596
|
+
* @param {Object} [params.topeInicial] - resultado de `consultarTope` ya en mano, para
|
|
597
|
+
* no repetir el sondeo de la primera tanda (cuesta ~3 requests al portal).
|
|
598
|
+
* @returns {Promise<{ ok: boolean, cafPaths: string[], otorgados: number,
|
|
599
|
+
* maxAutor: number|null, foliosDisp: number|null, errorCode?: string, error?: string }>}
|
|
600
|
+
*/
|
|
601
|
+
async solicitarCafPorTandas({ tipoDte, cantidad, maxTandas = 4, topeInicial = null }) {
|
|
602
|
+
if (!this.cafSolicitor) {
|
|
603
|
+
throw new Error('FolioService: CafSolicitor no inicializado (se requiere pfxPath y pfxPassword)');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const objetivo = Number(cantidad);
|
|
607
|
+
const cafPaths = [];
|
|
608
|
+
let cubiertos = 0;
|
|
609
|
+
let tope = null;
|
|
610
|
+
|
|
611
|
+
for (let tanda = 1; tanda <= maxTandas && cubiertos < objetivo; tanda++) {
|
|
612
|
+
tope = (tanda === 1 && topeInicial) ? topeInicial : await this.consultarTope({ tipoDte });
|
|
613
|
+
const faltan = objetivo - cubiertos;
|
|
614
|
+
|
|
615
|
+
// `sinTope` = el SII no publica MAX_AUTOR, o sea no está racionando este tipo:
|
|
616
|
+
// se pide todo lo que falta de una.
|
|
617
|
+
const pedir = tope.sinTope ? faltan : Math.min(tope.maxAutor ?? 0, faltan);
|
|
618
|
+
if (pedir <= 0) {
|
|
619
|
+
console.warn(
|
|
620
|
+
`[FolioService] Tipo ${tipoDte}: tanda ${tanda} sin cupo ` +
|
|
621
|
+
`(MAX_AUTOR=${tope.maxAutor}, FOLIOS_DISP=${tope.foliosDisp}) — se corta con ${cubiertos}/${objetivo}`
|
|
622
|
+
);
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
console.log(
|
|
627
|
+
`[FolioService] Tipo ${tipoDte}: tanda ${tanda} pide ${pedir} folio(s) ` +
|
|
628
|
+
`(faltan ${faltan}; MAX_AUTOR=${tope.maxAutor ?? 'sin tope'}, FOLIOS_DISP=${tope.foliosDisp ?? 'sin dato'})`
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
const res = await this.cafSolicitor.solicitar({ tipoDte, cantidad: pedir, minCantidad: 1 });
|
|
632
|
+
const otorgados = res.success ? this._contarFoliosCaf(res.cafPath) : 0;
|
|
633
|
+
if (!otorgados) {
|
|
634
|
+
console.warn(
|
|
635
|
+
`[FolioService] Tipo ${tipoDte}: tanda ${tanda} no entregó folios ` +
|
|
636
|
+
`(${res.errorCode || 'sin código'}: ${res.error || 'sin detalle'}) — se corta`
|
|
637
|
+
);
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
cafPaths.push(res.cafPath);
|
|
642
|
+
cubiertos += otorgados;
|
|
643
|
+
console.log(`[FolioService] Tipo ${tipoDte}: tanda ${tanda} entregó ${otorgados} folio(s) — ${cubiertos}/${objetivo}`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const base = { maxAutor: tope?.maxAutor ?? null, foliosDisp: tope?.foliosDisp ?? null };
|
|
647
|
+
if (cubiertos < objetivo) {
|
|
648
|
+
return {
|
|
649
|
+
ok: false, cafPaths, otorgados: cubiertos, ...base,
|
|
650
|
+
errorCode: 'TOPE_SII_INSUFICIENTE',
|
|
651
|
+
error:
|
|
652
|
+
`El SII autorizó ${cubiertos} folio(s) de los ${objetivo} que necesita el tipo ${tipoDte}, ` +
|
|
653
|
+
`repartidos en ${cafPaths.length} timbraje(s). No hay folios sin utilizar que anular ni recuperar ` +
|
|
654
|
+
`(FOLIOS_DISP=${base.foliosDisp}): el tope lo fija el SII según el historial de timbraje y emisión.`,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
return { ok: true, cafPaths, otorgados: cubiertos, ...base };
|
|
658
|
+
}
|
|
659
|
+
|
|
544
660
|
/**
|
|
545
661
|
* Consulta el estado de folios en el SII
|
|
546
662
|
* @param {Object} params - Parámetros
|
package/cert/CertRunner.js
CHANGED
|
@@ -492,51 +492,116 @@ class CertRunner {
|
|
|
492
492
|
|
|
493
493
|
_cafReusable(tipoDte, cantidad) {
|
|
494
494
|
try {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
//
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
495
|
+
// ⚠️ Se recorren TODOS los CAF del tipo, no solo el último.
|
|
496
|
+
//
|
|
497
|
+
// Cuando el SII raciona el timbraje, un pedido de 4 folios puede haber quedado
|
|
498
|
+
// repartido en un CAF de 3 y otro de 1 (ver FolioService.solicitarCafPorTandas).
|
|
499
|
+
// Mirando solo el último se concluía "no alcanza" y se pedían folios nuevos,
|
|
500
|
+
// dejando los anteriores timbrados y sin usar — que es exactamente lo que el SII
|
|
501
|
+
// cuenta en contra del cupo, o sea que el reintento empeoraba el problema que
|
|
502
|
+
// intentaba resolver.
|
|
503
|
+
// `listarCafs` es de 2.17.0. El fallback cubre a un `folioService` inyectado con la
|
|
504
|
+
// forma vieja: sin él, la excepción caería en el catch de abajo y el resultado sería
|
|
505
|
+
// "no hay CAF reusable", que dispara un timbraje nuevo teniendo folios en mano.
|
|
506
|
+
const rutas = typeof this.folioService.listarCafs === 'function'
|
|
507
|
+
? this.folioService.listarCafs(tipoDte)
|
|
508
|
+
: [this.folioService.findLatestCaf(tipoDte)].filter(Boolean);
|
|
509
|
+
if (!rutas.length) return null;
|
|
508
510
|
|
|
509
511
|
// Require local, igual que en _construirCafObjects (línea ~2110): `../index` cierra
|
|
510
512
|
// el ciclo con este archivo, así que importarlo arriba deja CAF sin definir.
|
|
511
513
|
const { CAF } = require('../index');
|
|
512
|
-
const caf = new CAF(fs.readFileSync(cafPath, 'utf8'));
|
|
513
|
-
|
|
514
|
-
// Doble chequeo del RUT, aunque findLatestCaf ya filtre. Firmar un timbre con el CAF
|
|
515
|
-
// de otro contribuyente hace que el SII rechace el envío ENTERO con `RFR - Rechazado
|
|
516
|
-
// por Error en Firma`, y el síntoma aparece etapas más adelante (la declaración de
|
|
517
|
-
// simulación se traba porque el envío tiene rechazos). Un fallo así de caro no puede
|
|
518
|
-
// depender de que una función de otro archivo siga filtrando bien.
|
|
519
|
-
const rutCaf = String(caf.da?.RE ?? '').replace(/\./g, '').trim().toUpperCase();
|
|
520
514
|
const rutMio = String(this.config.emisor.rut).replace(/\./g, '').trim().toUpperCase();
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
515
|
+
|
|
516
|
+
const candidatos = [];
|
|
517
|
+
const rangosVistos = new Set();
|
|
518
|
+
const descartes = [];
|
|
519
|
+
|
|
520
|
+
for (const cafPath of rutas) {
|
|
521
|
+
if (!fs.existsSync(cafPath)) continue;
|
|
522
|
+
// Ya se emitió con él: reusarlo repetiría folios y el SII rechaza el envío entero.
|
|
523
|
+
if (fs.existsSync(`${cafPath}.usado`)) {
|
|
524
|
+
descartes.push('ya se usó para emitir');
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const rango = this._rangoDelCaf(cafPath);
|
|
529
|
+
if (rango && this._rangoYaConsumido(tipoDte, rango.desde, rango.hasta)) {
|
|
530
|
+
descartes.push(`folios ${rango.desde}-${rango.hasta} ya emitidos (otra copia del mismo CAF)`);
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const caf = new CAF(fs.readFileSync(cafPath, 'utf8'));
|
|
535
|
+
|
|
536
|
+
// Doble chequeo del RUT, aunque listarCafs ya filtre. Firmar un timbre con el CAF
|
|
537
|
+
// de otro contribuyente hace que el SII rechace el envío ENTERO con `RFR - Rechazado
|
|
538
|
+
// por Error en Firma`, y el síntoma aparece etapas más adelante (la declaración de
|
|
539
|
+
// simulación se traba porque el envío tiene rechazos). Un fallo así de caro no puede
|
|
540
|
+
// depender de que una función de otro archivo siga filtrando bien.
|
|
541
|
+
const rutCaf = String(caf.da?.RE ?? '').replace(/\./g, '').trim().toUpperCase();
|
|
542
|
+
if (rutCaf !== rutMio) {
|
|
543
|
+
console.warn(
|
|
544
|
+
`[CertRunner] CAF previo del tipo ${tipoDte} DESCARTADO: es del RUT ${rutCaf}, ` +
|
|
545
|
+
`no del emisor ${rutMio} (${cafPath})`
|
|
546
|
+
);
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const desde = caf.getFolioDesde();
|
|
551
|
+
const hasta = caf.getFolioHasta();
|
|
552
|
+
if (!Number.isFinite(desde) || !Number.isFinite(hasta)) continue;
|
|
553
|
+
|
|
554
|
+
// El MISMO CAF vive en dos árboles con el mismo contenido (ver _foliosUsadosPath),
|
|
555
|
+
// así que sin deduplicar por rango se contarían dos veces los mismos folios y el
|
|
556
|
+
// set se quedaría sin numeración a mitad de camino.
|
|
557
|
+
const clave = `${desde}-${hasta}`;
|
|
558
|
+
if (rangosVistos.has(clave)) continue;
|
|
559
|
+
rangosVistos.add(clave);
|
|
560
|
+
|
|
561
|
+
candidatos.push({ path: cafPath, desde, hasta });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!candidatos.length) {
|
|
565
|
+
const motivos = [...new Set(descartes)].join('; ');
|
|
566
|
+
console.log(
|
|
567
|
+
` Tipo ${tipoDte}: sin CAF previo reusable${motivos ? ` (${motivos})` : ''} — se pedirá uno nuevo`
|
|
525
568
|
);
|
|
526
569
|
return null;
|
|
527
570
|
}
|
|
528
571
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
// Cuántos folios de ese rango quedan libres según el contador de ESTA corrida.
|
|
534
|
-
// Si el helper ya consumió parte del rango, lo que queda es lo que sirve.
|
|
572
|
+
// Descuento del contador de ESTA corrida: si el helper ya reservó folios de este
|
|
573
|
+
// tipo, no vuelven a estar disponibles. Va sobre el total y no por CAF porque el
|
|
574
|
+
// contador es por tipo, no por rango.
|
|
535
575
|
const yaUsados = this.folioHelper.usedFolios.get(tipoDte)?.size ?? 0;
|
|
536
|
-
const disponibles = (hasta - desde + 1) - yaUsados;
|
|
537
|
-
if (disponibles
|
|
576
|
+
const disponibles = candidatos.reduce((n, c) => n + (c.hasta - c.desde + 1), 0) - yaUsados;
|
|
577
|
+
if (disponibles <= 0) return null;
|
|
538
578
|
|
|
539
|
-
|
|
579
|
+
// Se juntan desde el folio más bajo, y solo los que hagan falta.
|
|
580
|
+
//
|
|
581
|
+
// Los dos criterios importan. Del folio más bajo porque los documentos quedan
|
|
582
|
+
// numerados como si vinieran de un rango contiguo, y porque gastar primero lo más
|
|
583
|
+
// viejo es lo que baja FOLIOS_DISP más rápido. Y solo los necesarios porque
|
|
584
|
+
// `_marcarCafsConsumidos` marca como usados TODOS los CAF que reciba: incluir uno
|
|
585
|
+
// que el plan no llega a tocar quema esos folios sin haber emitido nada con ellos.
|
|
586
|
+
candidatos.sort((a, b) => a.desde - b.desde);
|
|
587
|
+
const elegidos = [];
|
|
588
|
+
let acumulado = 0;
|
|
589
|
+
for (const c of candidatos) {
|
|
590
|
+
if (acumulado >= cantidad) break;
|
|
591
|
+
elegidos.push(c);
|
|
592
|
+
acumulado += (c.hasta - c.desde + 1);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return {
|
|
596
|
+
path: elegidos[0].path,
|
|
597
|
+
paths: elegidos.map(e => e.path),
|
|
598
|
+
desde: elegidos[0].desde,
|
|
599
|
+
hasta: elegidos[elegidos.length - 1].hasta,
|
|
600
|
+
/** Folios libres que aportan los CAF elegidos. */
|
|
601
|
+
total: Math.min(acumulado, disponibles),
|
|
602
|
+
/** false = hay folios en disco, pero no alcanzan para el plan. */
|
|
603
|
+
alcanza: disponibles >= cantidad,
|
|
604
|
+
};
|
|
540
605
|
} catch (err) {
|
|
541
606
|
// Un CAF ilegible no puede tumbar la corrida: se pide uno nuevo y listo.
|
|
542
607
|
console.warn(`[CertRunner] No se pudo evaluar el CAF previo del tipo ${tipoDte}: ${err.message}`);
|
|
@@ -621,6 +686,36 @@ class CertRunner {
|
|
|
621
686
|
);
|
|
622
687
|
continue;
|
|
623
688
|
}
|
|
689
|
+
|
|
690
|
+
// ── Cupo corto pero nada que limpiar ────────────────────────────────────
|
|
691
|
+
//
|
|
692
|
+
// FOLIOS_DISP es, textual en el formulario del SII, "considerando el timbraje
|
|
693
|
+
// histórico de documentos y de los documentos emitidos y/o anulados, la empresa
|
|
694
|
+
// posee folios disponibles o sin utilizar, por un total de N". O sea:
|
|
695
|
+
// timbrados - emitidos - anulados. Y anulable es exactamente ese conjunto: la
|
|
696
|
+
// página de anulación aclara que solo acepta "folios que no han sido
|
|
697
|
+
// recepcionados por el SII".
|
|
698
|
+
//
|
|
699
|
+
// Con N=0 la limpieza no puede anular nada, pero igual se intentaba, porque los
|
|
700
|
+
// candidatos NO salen de ahí: `consultarFolios` los saca del listado de
|
|
701
|
+
// TIMBRAJES (af_anular2), que lista los rangos autorizados sin decir si se usaron.
|
|
702
|
+
// Medido el 19/08/2026 (RUT 76543210-K): MAX_AUTOR=3 para 4 folios con
|
|
703
|
+
// FOLIOS_DISP=0, y la corrida gastó 43 de sus 70 requests intentando anular
|
|
704
|
+
// folios ya emitidos, uno por uno, para terminar con "0 anulados" y un error que
|
|
705
|
+
// además decía lo contrario de lo que pasaba ("el SII tiene bloqueado el
|
|
706
|
+
// timbraje", cuando el SII estaba autorizando 3).
|
|
707
|
+
//
|
|
708
|
+
// ⚠️ `=== 0` estricto, nunca `!foliosDisp`: con el timbraje BLOQUEADO el SII no
|
|
709
|
+
// publica el campo y queda `null` (ver FolioService.consultarTope), y ese caso
|
|
710
|
+
// sí necesita la limpieza, que es lo único que puede destrabarlo.
|
|
711
|
+
if (tope.foliosDisp === 0) {
|
|
712
|
+
console.log(
|
|
713
|
+
` Tipo ${tipoDte}: sin limpieza previa — MAX_AUTOR=${tope.maxAutor} para ` +
|
|
714
|
+
`${necesarios} folio(s), pero FOLIOS_DISP=0: no hay folios sin utilizar que ` +
|
|
715
|
+
'anular ni recuperar. El tope lo fija el SII por historial, se pedirá en tandas.'
|
|
716
|
+
);
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
624
719
|
// Un solo mensaje según el motivo: con el timbraje bloqueado el SII no publica
|
|
625
720
|
// MAX_AUTOR ni FOLIOS_DISP, así que el texto de racionamiento saldría con `null`
|
|
626
721
|
// en los dos números y confundiría a quien lea el log.
|
|
@@ -691,10 +786,13 @@ class CertRunner {
|
|
|
691
786
|
// Reusarlos es seguro: los sets se envían recién cuando TODOS los CAF están en mano,
|
|
692
787
|
// así que si el intento anterior falló, esos folios nunca se emitieron.
|
|
693
788
|
const previo = this._cafReusable(Number(tipoDte), Number(cantidad));
|
|
694
|
-
if (previo) {
|
|
695
|
-
cafs[tipoDte] = previo.path;
|
|
789
|
+
if (previo?.alcanza) {
|
|
790
|
+
cafs[tipoDte] = previo.paths.length > 1 ? previo.paths : previo.path;
|
|
696
791
|
emitProgress(STEPS.CAF_OK, { tipo: Number(tipoDte) });
|
|
697
|
-
console.log(
|
|
792
|
+
console.log(
|
|
793
|
+
` ✓ CAF tipo ${tipoDte} reusado del intento anterior (folios ${previo.desde}-${previo.hasta}` +
|
|
794
|
+
`${previo.paths.length > 1 ? `, ${previo.paths.length} CAF` : ''})`
|
|
795
|
+
);
|
|
698
796
|
continue;
|
|
699
797
|
}
|
|
700
798
|
|
|
@@ -711,10 +809,18 @@ class CertRunner {
|
|
|
711
809
|
// devuelve sin gastar cupo. Por eso va acá: después de reusar lo que hay en disco y
|
|
712
810
|
// antes de `solicitarCafExacto`, que pide folios nuevos y, si falla, anula.
|
|
713
811
|
//
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
//
|
|
717
|
-
|
|
812
|
+
// Se intenta cuando hay folios timbrados y sin utilizar, que es el pool sobre el que
|
|
813
|
+
// trabaja: `FOLIOS_DISP > 0`, o el timbraje bloqueado (ahí el SII no publica el campo
|
|
814
|
+
// y queda `null`, así que hay que intentar a ciegas). Con FOLIOS_DISP=0 todos los
|
|
815
|
+
// folios timbrados ya se emitieron y reobtener devolvería folios consumidos:
|
|
816
|
+
// `listarReobtenibles` filtra los anulados pero no distingue usados, así que emitir
|
|
817
|
+
// con ellos haría que el SII rechace el envío entero por folio repetido.
|
|
818
|
+
//
|
|
819
|
+
// Antes el gate era solo `bloqueado`, y con cupo corto sin bloqueo se saltaba
|
|
820
|
+
// derecho a pedir folios nuevos teniendo folios ya autorizados a mano. Pedir gasta
|
|
821
|
+
// cupo; reobtener no.
|
|
822
|
+
const topeTipo = this._topeConsultado?.[tipoDte];
|
|
823
|
+
if (topeTipo?.bloqueado || (topeTipo?.foliosDisp ?? 0) > 0) {
|
|
718
824
|
const reob = await this.folioService.reobtenerCaf({
|
|
719
825
|
tipoDte: Number(tipoDte), cantidad: Number(cantidad),
|
|
720
826
|
});
|
|
@@ -733,32 +839,82 @@ class CertRunner {
|
|
|
733
839
|
console.warn(`[CertRunner] Tipo ${tipoDte}: reobtención no sirvió — ${reob.motivo}`);
|
|
734
840
|
}
|
|
735
841
|
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
842
|
+
// ── Cómo pedir: de una o en tandas ──────────────────────────────────────
|
|
843
|
+
//
|
|
844
|
+
// `solicitarCafExacto` exige que UN CAF cubra la cantidad y, si no, anula folios
|
|
845
|
+
// sin utilizar y reintenta. Eso es lo correcto cuando el cupo está corto POR esos
|
|
846
|
+
// folios (FOLIOS_DISP > 0) y cuando el timbraje está bloqueado.
|
|
847
|
+
//
|
|
848
|
+
// Con FOLIOS_DISP=0 no aplica: no hay nada que anular ni que reobtener, el tope lo
|
|
849
|
+
// fija el SII por historial y no se mueve esperando (medido: 24 h con el mismo
|
|
850
|
+
// MAX_AUTOR=3 para 4 folios, RUT 76543210-K). Ahí la única vía es juntar el rango
|
|
851
|
+
// en varios timbrajes, que los sets soportan de fábrica porque `_tomarFolio` acepta
|
|
852
|
+
// una lista de CAF y salta al siguiente cuando se agota un rango.
|
|
853
|
+
const racionadoPorHistorial =
|
|
854
|
+
topeTipo?.foliosDisp === 0 &&
|
|
855
|
+
topeTipo?.maxAutor != null &&
|
|
856
|
+
topeTipo.maxAutor < Number(cantidad);
|
|
857
|
+
|
|
858
|
+
// Folios que ya están en disco pero no alcanzaban solos. En el camino normal se
|
|
859
|
+
// ignoran (se pide un rango que cubra todo y estos quedan para después), pero acá
|
|
860
|
+
// son la mitad del asunto: si el intento anterior alcanzó a timbrar 3 de 4, pedir
|
|
861
|
+
// los 4 de nuevo desperdicia esos 3 y encima sube FOLIOS_DISP, que es lo que aprieta
|
|
862
|
+
// el tope. Se cuentan como cubiertos y al SII se le pide solo la diferencia.
|
|
863
|
+
const parciales = racionadoPorHistorial && previo && !previo.alcanza ? previo : null;
|
|
864
|
+
const faltante = Number(cantidad) - (parciales?.total ?? 0);
|
|
865
|
+
if (parciales) {
|
|
866
|
+
console.log(
|
|
867
|
+
` Tipo ${tipoDte}: ${parciales.total} folio(s) ya timbrados en disco ` +
|
|
868
|
+
`(${parciales.desde}-${parciales.hasta}) — se pedirán ${faltante} al SII`
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const res = racionadoPorHistorial
|
|
873
|
+
? await this.folioService.solicitarCafPorTandas({
|
|
874
|
+
tipoDte: Number(tipoDte),
|
|
875
|
+
cantidad: faltante,
|
|
876
|
+
topeInicial: topeTipo,
|
|
877
|
+
})
|
|
878
|
+
: await this.folioService.solicitarCafExacto({
|
|
879
|
+
tipoDte: Number(tipoDte),
|
|
880
|
+
cantidad: Number(cantidad),
|
|
881
|
+
});
|
|
744
882
|
|
|
745
883
|
if (!res.ok) {
|
|
746
884
|
const detalle = [
|
|
747
885
|
res.maxAutor != null ? `MAX_AUTOR=${res.maxAutor}` : null,
|
|
748
886
|
res.foliosDisp != null ? `FOLIOS_DISP=${res.foliosDisp}` : null,
|
|
749
887
|
].filter(Boolean).join(', ');
|
|
888
|
+
// Dos causas con remedios opuestos. Antes las dos salían con el mismo texto, y en
|
|
889
|
+
// el caso de abajo el mensaje mandaba a anular folios que no existen: el 19/08/2026
|
|
890
|
+
// un comercio recibió "el SII tiene bloqueado el timbraje" mientras el SII le
|
|
891
|
+
// estaba autorizando 3 folios y no tenía ni uno sin utilizar.
|
|
892
|
+
const remedio = res.foliosDisp === 0
|
|
893
|
+
? 'No hay folios sin utilizar que anular ni recuperar: el tope lo fija el SII ' +
|
|
894
|
+
'según el historial de timbraje y emisión de este tipo de documento.'
|
|
895
|
+
: 'El SII limita el timbraje cuando hay folios ya autorizados sin utilizar; ' +
|
|
896
|
+
'emite o anula documentos electrónicos de este tipo y reintenta.';
|
|
897
|
+
const cubiertos = res.otorgados + (parciales?.total ?? 0);
|
|
750
898
|
throw new Error(
|
|
751
|
-
`Folios insuficientes para tipo ${tipoDte}: se requieren ${cantidad} y
|
|
752
|
-
|
|
753
|
-
`El SII limita el timbraje cuando hay folios ya autorizados sin utilizar; ` +
|
|
754
|
-
`emite o anula documentos electrónicos de este tipo y reintenta. ` +
|
|
899
|
+
`Folios insuficientes para tipo ${tipoDte}: se requieren ${cantidad} y hay ` +
|
|
900
|
+
`${cubiertos}${detalle ? ` (${detalle})` : ''}. ${remedio} ` +
|
|
755
901
|
`[${res.errorCode}] ${res.error || ''}`.trim()
|
|
756
902
|
);
|
|
757
903
|
}
|
|
758
904
|
|
|
759
|
-
|
|
905
|
+
// `solicitarCafPorTandas` devuelve varios; `solicitarCafExacto`, uno solo. Los
|
|
906
|
+
// parciales van primero: el set numera de menor a mayor y son los folios más viejos,
|
|
907
|
+
// así que gastarlos antes es también lo que baja FOLIOS_DISP más rápido.
|
|
908
|
+
const rutas = [
|
|
909
|
+
...(parciales?.paths ?? []),
|
|
910
|
+
...(res.cafPaths ?? (res.cafPath ? [res.cafPath] : [])),
|
|
911
|
+
];
|
|
912
|
+
cafs[tipoDte] = rutas.length > 1 ? rutas : rutas[0];
|
|
760
913
|
emitProgress(STEPS.CAF_OK, { tipo: Number(tipoDte) });
|
|
761
|
-
console.log(
|
|
914
|
+
console.log(
|
|
915
|
+
` ✓ CAF tipo ${tipoDte} (${res.otorgados + (parciales?.total ?? 0)} folios` +
|
|
916
|
+
`${rutas.length > 1 ? ` en ${rutas.length} CAF` : ''})`
|
|
917
|
+
);
|
|
762
918
|
}
|
|
763
919
|
|
|
764
920
|
return cafs;
|
|
@@ -2395,12 +2551,17 @@ class CertRunner {
|
|
|
2395
2551
|
// Solicitar CAFs frescos
|
|
2396
2552
|
const cafs = await this.solicitarCafs(cafRequired);
|
|
2397
2553
|
|
|
2398
|
-
// Cargar objetos CAF
|
|
2554
|
+
// Cargar objetos CAF. Un tipo puede traer VARIOS CAF cuando el SII racionó el
|
|
2555
|
+
// timbraje y el rango se juntó en tandas: `Simulacion._tomarFolio` recorre la lista.
|
|
2399
2556
|
const cafObjects = {};
|
|
2400
2557
|
const { CAF } = require('../index');
|
|
2401
|
-
for (const [tipo,
|
|
2402
|
-
const
|
|
2403
|
-
|
|
2558
|
+
for (const [tipo, ref] of Object.entries(cafs)) {
|
|
2559
|
+
const rutas = Array.isArray(ref) ? ref : [ref];
|
|
2560
|
+
const objetos = rutas.map(p => new CAF(fs.readFileSync(p, 'utf8')))
|
|
2561
|
+
// De menor a mayor folio: los documentos del plan quedan numerados como si
|
|
2562
|
+
// vinieran de un rango contiguo.
|
|
2563
|
+
.sort((a, b) => a.getFolioDesde() - b.getFolioDesde());
|
|
2564
|
+
cafObjects[tipo] = objetos.length > 1 ? objetos : objetos[0];
|
|
2404
2565
|
}
|
|
2405
2566
|
|
|
2406
2567
|
// Crear simulación
|
package/cert/Simulacion.js
CHANGED
|
@@ -38,10 +38,49 @@ class Simulacion {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Reserva el siguiente folio de un tipo, aceptando UNO o VARIOS CAF.
|
|
43
|
+
*
|
|
44
|
+
* El SII raciona el timbraje: cuando el cupo por solicitud es menor a lo que pide el
|
|
45
|
+
* plan, los folios llegan repartidos en varios CAF (ver
|
|
46
|
+
* `FolioService.solicitarCafPorTandas`). Cada CAF firma con su propia llave, así que
|
|
47
|
+
* hay que devolver también CUÁL se usó, para timbrar el documento con ese y no con el
|
|
48
|
+
* primero de la lista.
|
|
49
|
+
*
|
|
50
|
+
* Los sets ya funcionaban así (`SetBase._tomarFolio`); la simulación no, porque recibía
|
|
51
|
+
* un objeto CAF por tipo. Ese era el único punto que impedía repartir un rango.
|
|
52
|
+
*
|
|
53
|
+
* @private
|
|
54
|
+
* @returns {{ caf: Object, folio: number }}
|
|
55
|
+
*/
|
|
56
|
+
_tomarFolio(cafRef, tipoDte, folioHelper) {
|
|
57
|
+
const lista = (Array.isArray(cafRef) ? cafRef : [cafRef]).filter(Boolean);
|
|
58
|
+
if (!lista.length) {
|
|
59
|
+
throw new Error(`No hay CAF para tipo ${tipoDte}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let ultimoError = null;
|
|
63
|
+
for (const caf of lista) {
|
|
64
|
+
try {
|
|
65
|
+
const folio = folioHelper.reserveNextFolio({
|
|
66
|
+
tipoDte,
|
|
67
|
+
folioDesde: caf.getFolioDesde(),
|
|
68
|
+
folioHasta: caf.getFolioHasta(),
|
|
69
|
+
});
|
|
70
|
+
return { caf, folio };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
// Rango agotado: se pasa al CAF siguiente. Cualquier otro error sí es fatal.
|
|
73
|
+
if (!/No hay más folios disponibles/.test(err.message)) throw err;
|
|
74
|
+
ultimoError = err;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
throw ultimoError;
|
|
78
|
+
}
|
|
79
|
+
|
|
41
80
|
/**
|
|
42
81
|
* Genera el EnvioDTE de simulación a partir de las estructuras
|
|
43
82
|
* @param {Object} estructuras - Estructuras del set de pruebas
|
|
44
|
-
* @param {Object} cafs - { tipoDte: CAF } pre-cargados
|
|
83
|
+
* @param {Object} cafs - { tipoDte: CAF | CAF[] } pre-cargados
|
|
45
84
|
* @param {Object} folioHelper - Helper para gestionar folios
|
|
46
85
|
* @param {Object} [options] - Opciones
|
|
47
86
|
* @param {string} [options.fechaEmision] - Fecha de emisión (default: hoy)
|
|
@@ -61,17 +100,7 @@ class Simulacion {
|
|
|
61
100
|
|
|
62
101
|
for (const doc of plan) {
|
|
63
102
|
const tipoDte = Number(doc.tipoDte || 33);
|
|
64
|
-
const caf = cafs[tipoDte];
|
|
65
|
-
if (!caf) {
|
|
66
|
-
throw new Error(`No hay CAF para tipo ${tipoDte}`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Obtener folio desde el CAF
|
|
70
|
-
const folio = folioHelper.reserveNextFolio({
|
|
71
|
-
tipoDte,
|
|
72
|
-
folioDesde: caf.getFolioDesde(),
|
|
73
|
-
folioHasta: caf.getFolioHasta(),
|
|
74
|
-
});
|
|
103
|
+
const { caf, folio } = this._tomarFolio(cafs[tipoDte], tipoDte, folioHelper);
|
|
75
104
|
const base = doc.referenciaCaso ? docRefs[doc.referenciaCaso] : null;
|
|
76
105
|
|
|
77
106
|
// Resolver items
|
package/dte-sii.d.ts
CHANGED
|
@@ -653,6 +653,44 @@ export class FolioService {
|
|
|
653
653
|
* rechazan).
|
|
654
654
|
*/
|
|
655
655
|
reobtenerCaf(options: { tipoDte: number; cantidad: number }): Promise<ReobtenerCafResult | null>;
|
|
656
|
+
/** El CAF más reciente de un tipo para este RUT y ambiente, o null. */
|
|
657
|
+
findLatestCaf(tipoDte: number): string | null;
|
|
658
|
+
/**
|
|
659
|
+
* Todos los CAF de un tipo en disco, del más reciente al más viejo.
|
|
660
|
+
*
|
|
661
|
+
* Hace falta cuando el SII raciona el timbraje y un rango queda repartido en
|
|
662
|
+
* varios CAF: mirando solo el último se concluye "no alcanza" y se piden folios
|
|
663
|
+
* nuevos, dejando los anteriores timbrados y sin usar.
|
|
664
|
+
*/
|
|
665
|
+
listarCafs(tipoDte: number): string[];
|
|
666
|
+
/**
|
|
667
|
+
* Cubre `cantidad` folios en varios timbrajes cuando el SII no autoriza tantos de
|
|
668
|
+
* una vez.
|
|
669
|
+
*
|
|
670
|
+
* Es para el caso `FOLIOS_DISP = 0` con `MAX_AUTOR` corto: no hay folios sin usar
|
|
671
|
+
* que anular ni recuperar, el tope lo fija el SII por historial y no se mueve
|
|
672
|
+
* esperando. Se corta apenas una tanda no aporta folios nuevos, porque cada
|
|
673
|
+
* timbraje parcial sube `FOLIOS_DISP` y puede bajar el tope siguiente.
|
|
674
|
+
*/
|
|
675
|
+
solicitarCafPorTandas(options: {
|
|
676
|
+
tipoDte: number;
|
|
677
|
+
cantidad: number;
|
|
678
|
+
maxTandas?: number;
|
|
679
|
+
topeInicial?: FolioTope | null;
|
|
680
|
+
}): Promise<SolicitarCafTandasResult>;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export interface SolicitarCafTandasResult {
|
|
684
|
+
ok: boolean;
|
|
685
|
+
/** CAF obtenidos, uno por tanda. Los sets y la simulación aceptan la lista completa. */
|
|
686
|
+
cafPaths: string[];
|
|
687
|
+
/** Folios cubiertos entre todas las tandas. */
|
|
688
|
+
otorgados: number;
|
|
689
|
+
maxAutor: number | null;
|
|
690
|
+
foliosDisp: number | null;
|
|
691
|
+
/** `TOPE_SII_INSUFICIENTE` cuando el SII no llegó a cubrir la cantidad pedida. */
|
|
692
|
+
errorCode?: string;
|
|
693
|
+
error?: string;
|
|
656
694
|
}
|
|
657
695
|
|
|
658
696
|
export interface FolioRegistryOptions {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.17.0",
|
|
4
4
|
"description": "Facturación y boletas electrónicas para el SII de Chile. Genera, timbra, firma y envía DTEs, libros electrónicos y automatiza la certificación.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "dte-sii.d.ts",
|