@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.
@@ -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 78480527-1): cada set pedía sus
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
- const cafPath = this.folioService.findLatestCaf(tipoDte);
496
- if (!cafPath || !fs.existsSync(cafPath)) return null;
497
- // Ya se emitió con él: reusarlo repetiría folios y el SII rechaza el envío entero.
498
- if (fs.existsSync(`${cafPath}.usado`)) {
499
- console.log(` Tipo ${tipoDte}: el CAF previo ya se usó para emitir se pedirá uno nuevo`);
500
- return null;
501
- }
502
- const rango = this._rangoDelCaf(cafPath);
503
- if (rango && this._rangoYaConsumido(tipoDte, rango.desde, rango.hasta)) {
504
- console.log(` Tipo ${tipoDte}: los folios ${rango.desde}-${rango.hasta} ya se emitieron `
505
- + '(otra copia del mismo CAF) se pedirá uno nuevo');
506
- return null;
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
- if (rutCaf !== rutMio) {
522
- console.warn(
523
- `[CertRunner] CAF previo del tipo ${tipoDte} DESCARTADO: es del RUT ${rutCaf}, ` +
524
- `no del emisor ${rutMio} (${cafPath})`
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
- const desde = caf.getFolioDesde();
530
- const hasta = caf.getFolioHasta();
531
- if (!Number.isFinite(desde) || !Number.isFinite(hasta)) return null;
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 < cantidad) return null;
576
+ const disponibles = candidatos.reduce((n, c) => n + (c.hasta - c.desde + 1), 0) - yaUsados;
577
+ if (disponibles <= 0) return null;
538
578
 
539
- return { path: cafPath, desde, hasta };
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 78480527-1) ese margen disparaba la limpieza teniendo cupo de
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 77967443-6): tipo 56 bloqueado por
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 77967443-6): 6 reintentos de ENVIAR_SETS quemaron 66 folios
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(` ✓ CAF tipo ${tipoDte} reusado del intento anterior (folios ${previo.desde}-${previo.hasta})`);
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
- // Solo se intenta con el timbraje bloqueado. Con cupo disponible pedir es más simple
715
- // y no cuesta nada; la reobtención implica un request por rango para descartar los
716
- // anulados, que el listado del SII no distingue.
717
- if (this._topeConsultado?.[tipoDte]?.bloqueado) {
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
- // solicitarCafExacto (no ConFallback): la simulación genera un plan fijo de
737
- // documentos y muere si falta un folio, así que un CAF corto no sirve. El
738
- // método aborta antes de emitir si el SII no autoriza lo suficiente, anula
739
- // folios sin utilizar para destrabar el tope, y reintenta.
740
- const res = await this.folioService.solicitarCafExacto({
741
- tipoDte: Number(tipoDte),
742
- cantidad: Number(cantidad),
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 el SII ` +
752
- `otorgó ${res.otorgados}${detalle ? ` (${detalle})` : ''}. ` +
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
- cafs[tipoDte] = res.cafPath;
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(` ✓ CAF tipo ${tipoDte} (${res.otorgados} folios)`);
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, cafPath] of Object.entries(cafs)) {
2402
- const cafXml = fs.readFileSync(cafPath, 'utf8');
2403
- cafObjects[tipo] = new CAF(cafXml);
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 78206276-K): la simulación quedó `RFR - Rechazado por
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 78206276-K no ha solicitado Set de
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 78441936-3 con curl crudo (independiente de este
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 78441936-3:
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 78441936-3): el flujo reportaba "Boleta NO autorizada"
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 78441936-3): palena devolvió 3377 bytes con
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 77967443-6).
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
- RUTDoc: '17096073-4', // RUT por defecto para certificación
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
 
@@ -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
@@ -6,7 +6,7 @@
6
6
  * Facturación y boletas electrónicas para el SII de Chile.
7
7
  *
8
8
  * @version 2.5.2
9
- * @author Devlas SpA <hola@devlas.cl>
9
+ * @author Devlas SpA <ti@devlas.cl>
10
10
  * @license MIT
11
11
  */
12
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devlas/dte-sii",
3
- "version": "2.15.0",
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 78206276-K, maullin). Un solo ítem, "CAPACITACION USO PLC'S
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 — ver docs/EXTRACCION_RUT_CERTIFICADO.md).
186
+ * contra Acepta, IDOK y Signapis).
187
187
  * @param {forge.pki.Certificate} cert - Certificado
188
188
  * @returns {string|null} RUT o null
189
189
  */