@devlas/dte-sii 2.15.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/CafSolicitor.js +7 -7
- package/EnviadorSII.js +1 -1
- package/FolioService.js +121 -5
- package/README.md +146 -25
- package/SiiPortalAuth.js +394 -32
- package/SiiSession.js +2 -2
- package/cert/CertRunner.js +230 -69
- package/cert/SetBase.js +1 -1
- package/cert/SetParser.js +15 -1
- package/cert/Simulacion.js +41 -12
- package/dte-sii.d.ts +38 -0
- package/index.js +1 -1
- package/package.json +2 -2
- package/utils/c14n.js +1 -1
- package/utils/pfx.js +1 -1
- package/test-qdetestlibro.js +0 -174
package/cert/CertRunner.js
CHANGED
|
@@ -296,7 +296,7 @@ class CertRunner {
|
|
|
296
296
|
* Timbra de una sola vez TODOS los folios que la corrida va a necesitar, sumados por
|
|
297
297
|
* tipo entre los cuatro sets. Llamar antes de ejecutar el primer set.
|
|
298
298
|
*
|
|
299
|
-
* El problema que resuelve (medido el 13/08/2026, RUT
|
|
299
|
+
* El problema que resuelve (medido el 13/08/2026, RUT 79888999-0): cada set pedía sus
|
|
300
300
|
* propios folios cuando le tocaba, así que del tipo 56 se pedían tres tandas separadas
|
|
301
301
|
* —básico 1, exenta 2, compra 1— en la misma corrida. El SII raciona el timbraje según
|
|
302
302
|
* cuántos folios tengas autorizados sin usar, así que la primera tanda le bajaba el tope
|
|
@@ -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}`);
|
|
@@ -599,7 +664,7 @@ class CertRunner {
|
|
|
599
664
|
//
|
|
600
665
|
// Con tope publicado alcanza con que el cupo CUBRA lo necesario. Antes se exigía
|
|
601
666
|
// un margen de 3x para "actuar antes de quedar contra la pared", pero medido el
|
|
602
|
-
// 13/08/2026 (RUT
|
|
667
|
+
// 13/08/2026 (RUT 79888999-0) ese margen disparaba la limpieza teniendo cupo de
|
|
603
668
|
// sobra —tipo 61 con MAX_AUTOR=6 para 3 folios, tipo 33 con 4 para 4— y en los
|
|
604
669
|
// tres tipos el resultado fue `0 anulados`: el SII rechazó cada anulación.
|
|
605
670
|
//
|
|
@@ -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.
|
|
@@ -648,7 +743,7 @@ class CertRunner {
|
|
|
648
743
|
// bloquean el timbraje suelen ser de semanas atrás, el filtro los
|
|
649
744
|
// descartaba antes de intentarlos, y la corrida se quedaba
|
|
650
745
|
// reintentando para siempre contra algo que nunca iba a cambiar.
|
|
651
|
-
// Caso real (14/08/2026, RUT
|
|
746
|
+
// Caso real (14/08/2026, RUT 76543210-K): tipo 56 bloqueado por
|
|
652
747
|
// folios del 22-07, seis intentos idénticos, cero anulados.
|
|
653
748
|
//
|
|
654
749
|
// Ojo: esto es el ÚLTIMO recurso. Antes de llegar acá se intenta reusar el CAF
|
|
@@ -684,17 +779,20 @@ class CertRunner {
|
|
|
684
779
|
// para negar el timbraje: "usted tiene disponible una cantidad de folios suficiente".
|
|
685
780
|
//
|
|
686
781
|
// O sea que cada reintento empeoraba el bloqueo que intentaba superar. Medido el
|
|
687
|
-
// 14/08/2026 (RUT
|
|
782
|
+
// 14/08/2026 (RUT 76543210-K): 6 reintentos de ENVIAR_SETS quemaron 66 folios
|
|
688
783
|
// —tipos 33, 34, 46 y 52, seis rangos cada uno— sin emitir un solo documento,
|
|
689
784
|
// mientras el tipo 56 seguía bloqueado.
|
|
690
785
|
//
|
|
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
|
|
@@ -2693,7 +2854,7 @@ class CertRunner {
|
|
|
2693
2854
|
// avance es imposible — en ambos la etapa se queda quieta. La única fuente de
|
|
2694
2855
|
// verdad es el estado del envío, así que se le pregunta al SII por el trackId.
|
|
2695
2856
|
//
|
|
2696
|
-
// Caso real (14/08/2026, RUT
|
|
2857
|
+
// Caso real (14/08/2026, RUT 77111222-3): la simulación quedó `RFR - Rechazado por
|
|
2697
2858
|
// Error en Firma` y este método devolvió "Timeout esperando aprobación". Quien lo
|
|
2698
2859
|
// llamaba lo leyó como "sigue en revisión", siguió adelante y marcó la etapa como
|
|
2699
2860
|
// completada sobre un envío que el SII había rechazado.
|
|
@@ -3573,7 +3734,7 @@ class CertRunner {
|
|
|
3573
3734
|
// buscaba UNA sola frase ("El estado de la postulacion"), así que cualquier otro
|
|
3574
3735
|
// rechazo pasaba de largo, no se encontraba el ID de revisión y el usuario recibía
|
|
3575
3736
|
// "no se pudo extraer ID de revisión de: //OK[0,0,2,...".
|
|
3576
|
-
// Caso real (11/08/2026): "El contribuyente
|
|
3737
|
+
// Caso real (11/08/2026): "El contribuyente 77111222-3 no ha solicitado Set de
|
|
3577
3738
|
// pruebas." quedó completamente oculto detrás de ese mensaje.
|
|
3578
3739
|
//
|
|
3579
3740
|
// Ahora se reconoce cualquier frase con pinta de mensaje al usuario. Es deliberado
|
|
@@ -3974,7 +4135,7 @@ class CertRunner {
|
|
|
3974
4135
|
* - recuperar el estado de boleta cuando las banderas locales se resetearon (ese
|
|
3975
4136
|
* portal NO aparece en pe_avance, así que consultarEstadoAvance no lo cubre)
|
|
3976
4137
|
*
|
|
3977
|
-
* Verificado 2026-07-24 contra
|
|
4138
|
+
* Verificado 2026-07-24 contra 79555666-7 con curl crudo (independiente de este
|
|
3978
4139
|
* código) — misma respuesta `//OK[0,[],0,7]` cuando no hay P90 todavía.
|
|
3979
4140
|
*
|
|
3980
4141
|
* @returns {Promise<{success: boolean, inscrita: boolean, listaParaDeclarar: boolean, estado: string|null, error?: string}>}
|
|
@@ -4017,7 +4178,7 @@ class CertRunner {
|
|
|
4017
4178
|
//
|
|
4018
4179
|
// Darlas por (a) siempre hacía que, después de una declaración exitosa, se
|
|
4019
4180
|
// informara "esperando aprobación del SII (SOK)". La pantalla le pedía al usuario
|
|
4020
|
-
// esperar una etapa que ya había superado (visto el 17/08/2026, RUT
|
|
4181
|
+
// esperar una etapa que ya había superado (visto el 17/08/2026, RUT 79555666-7:
|
|
4021
4182
|
// declaró a las 14:01 con DECLARACION EFECTUADA y a las 14:5x seguía diciendo que
|
|
4022
4183
|
// faltaba el SOK).
|
|
4023
4184
|
//
|
|
@@ -4239,7 +4400,7 @@ class CertRunner {
|
|
|
4239
4400
|
// autorizada". Es la secuencia que ya usa CafSolicitor._processMultiStepFlow() para
|
|
4240
4401
|
// pedir folios de verdad, y por eso ESE camino sí funciona.
|
|
4241
4402
|
//
|
|
4242
|
-
// Caso real (17/08/2026, RUT
|
|
4403
|
+
// Caso real (17/08/2026, RUT 79555666-7): el flujo reportaba "Boleta NO autorizada"
|
|
4243
4404
|
// mientras el SII tenía a la empresa como FACTURADOR ELECTRONICO con BOLETA
|
|
4244
4405
|
// ELECTRONICA autorizada hasta el folio 103009 desde el 12/07/2026, y palena le
|
|
4245
4406
|
// entregaba folios reales de factura por el camino bien armado. Un comercio ya
|
|
@@ -4271,7 +4432,7 @@ class CertRunner {
|
|
|
4271
4432
|
// veía cero tipos y concluía "no autorizada" — un dato inventado, indistinguible
|
|
4272
4433
|
// de la respuesta real.
|
|
4273
4434
|
//
|
|
4274
|
-
// Visto el 17/08/2026 (RUT
|
|
4435
|
+
// Visto el 17/08/2026 (RUT 79555666-7): palena devolvió 3377 bytes con
|
|
4275
4436
|
// "No ha sido posible completar su solicitud... código LIBRUD-OFSF-DTE-3-1-02" y
|
|
4276
4437
|
// el flujo venía reportando "certificación incompleta" desde hacía días.
|
|
4277
4438
|
//
|
package/cert/SetBase.js
CHANGED
|
@@ -224,7 +224,7 @@ class SetBase {
|
|
|
224
224
|
* `cafRef` acepta una ruta (comportamiento de siempre) o una lista de rutas. La lista
|
|
225
225
|
* hace falta porque el SII no siempre entrega un rango contiguo: al reobtener folios ya
|
|
226
226
|
* autorizados los devuelve de a uno (folios 1-1 y 3-3 como CAF separados, visto el
|
|
227
|
-
* 14/08/2026 en el RUT
|
|
227
|
+
* 14/08/2026 en el RUT 76543210-K).
|
|
228
228
|
*
|
|
229
229
|
* Y no alcanza con concatenar numeraciones: **cada CAF trae su propia llave privada RSA**
|
|
230
230
|
* (`CAF.js` → `RSASK`) y el timbre de cada documento se firma con la llave del CAF que
|
package/cert/SetParser.js
CHANGED
|
@@ -1016,7 +1016,21 @@ function generarEstructuraLibroCompras(set, opts = {}) {
|
|
|
1016
1016
|
NroDoc: doc.folio,
|
|
1017
1017
|
TasaImp: tasaIva,
|
|
1018
1018
|
FchDoc: new Date().toISOString().split('T')[0], // Se debe ajustar
|
|
1019
|
-
|
|
1019
|
+
// Contraparte de relleno del libro de COMPRAS de certificación: los documentos del set
|
|
1020
|
+
// son ficticios y va emparejada con `RznSoc: 'Razon Social'`, también un marcador.
|
|
1021
|
+
//
|
|
1022
|
+
// ⚠️ Este valor SÍ llega al SII: `LibroCompras.generarDesdeEstructuras()` hace
|
|
1023
|
+
// `{ ...doc }` y el RUTDoc entra tal cual al XML que se declara. No es código muerto.
|
|
1024
|
+
//
|
|
1025
|
+
// Se usa el RUT del propio SII porque cumple las tres cosas que hacen falta: existe como
|
|
1026
|
+
// contribuyente (si el SII validara existencia, pasa), es público y no es dato personal
|
|
1027
|
+
// de nadie, y ya se usa en este repo como contraparte (`RutReceptor` de los envíos).
|
|
1028
|
+
//
|
|
1029
|
+
// ⚠️ Antes acá había un RUT tomado de la documentación de otro sistema de facturación,
|
|
1030
|
+
// que resultaba ser el de una persona real. NO poner un RUT de un ejemplo ajeno, y
|
|
1031
|
+
// evitar `11111111-1`: tiene DV válido pero es el dummy más conocido de Chile y varios
|
|
1032
|
+
// sistemas lo rechazan por eso.
|
|
1033
|
+
RUTDoc: '60803000-K',
|
|
1020
1034
|
RznSoc: 'Razon Social',
|
|
1021
1035
|
};
|
|
1022
1036
|
|
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/index.js
CHANGED
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",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"author": "Devlas SpA <ti@devlas.cl> (https://devlas.cl)",
|
|
9
9
|
"homepage": "https://github.com/devlas-cl/dte-sii",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node test/pfx.test.js && node test/folios-consolidados.test.js && node test/c14n-apostrofe.test.js && node test/precargar-plan-corrida.test.js"
|
|
11
|
+
"test": "node test/pfx.test.js && node test/folios-consolidados.test.js && node test/c14n-apostrofe.test.js && node test/precargar-plan-corrida.test.js && node test/respaldo-mipyme.test.js"
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
package/utils/c14n.js
CHANGED
|
@@ -51,7 +51,7 @@ function escapeText(text) {
|
|
|
51
51
|
* por qué. Y solo pasaba cuando algún texto traía un apóstrofe, que es lo que lo hacía
|
|
52
52
|
* tan difícil de ver: los mismos sets pasaban o fallaban según el contenido del set.
|
|
53
53
|
*
|
|
54
|
-
* Medido el 14/08/2026 (RUT
|
|
54
|
+
* Medido el 14/08/2026 (RUT 77111222-3, maullin). Un solo ítem, "CAPACITACION USO PLC'S
|
|
55
55
|
* CNC", en un único documento del Set Factura Exenta:
|
|
56
56
|
*
|
|
57
57
|
* Set Básico → EPR 0 documentos con digest divergente
|
package/utils/pfx.js
CHANGED
|
@@ -183,7 +183,7 @@ function extractSubjectFields(cert) {
|
|
|
183
183
|
* 1.3.6.1.4.1.8321.1 — reservado bajo la normativa chilena de firma
|
|
184
184
|
* electrónica (Ley 19.799). A diferencia de serialNumber/OU/CN, que son
|
|
185
185
|
* convención de cada CA, este OID es consistente entre CAs (verificado
|
|
186
|
-
* contra Acepta, IDOK y Signapis
|
|
186
|
+
* contra Acepta, IDOK y Signapis).
|
|
187
187
|
* @param {forge.pki.Certificate} cert - Certificado
|
|
188
188
|
* @returns {string|null} RUT o null
|
|
189
189
|
*/
|