@devlas/dte-sii 2.16.0 → 2.18.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 +142 -7
- package/cert/CertRunner.js +373 -59
- package/cert/Simulacion.js +41 -12
- package/dte-sii.d.ts +48 -1
- 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
|
/**
|
|
@@ -345,7 +362,7 @@ class FolioService {
|
|
|
345
362
|
*
|
|
346
363
|
* @returns {Promise<{ ok: boolean, cafPath?: string, motivo?: string, disponibles?: number }>}
|
|
347
364
|
*/
|
|
348
|
-
async reobtenerCaf({ tipoDte, cantidad }) {
|
|
365
|
+
async reobtenerCaf({ tipoDte, cantidad, yaEmitido = null }) {
|
|
349
366
|
if (!this.cafSolicitor) {
|
|
350
367
|
return { ok: false, motivo: 'CafSolicitor no inicializado' };
|
|
351
368
|
}
|
|
@@ -358,11 +375,30 @@ class FolioService {
|
|
|
358
375
|
return { ok: false, motivo: err.message };
|
|
359
376
|
}
|
|
360
377
|
|
|
361
|
-
|
|
362
|
-
|
|
378
|
+
// ⚠️ El listado del portal NO dice si un folio ya se emitió.
|
|
379
|
+
//
|
|
380
|
+
// `anulado` es lo único que marca, y eso deja pasar los folios que ya viajaron al SII
|
|
381
|
+
// dentro de un envío. Emitir de nuevo con ellos hace que el SII rechace cada documento
|
|
382
|
+
// con `(DTE-3-101) Folio para este Tipo de documento ya fue recibido en el SII`.
|
|
383
|
+
//
|
|
384
|
+
// Pasó de verdad (24/08/2026): la corrida descartó los CAF en disco por tener folios
|
|
385
|
+
// ya emitidos y, tres líneas después, la reobtención trajo esos mismos folios del
|
|
386
|
+
// portal y los usó. Los 7 documentos del tipo 61 fueron rechazados.
|
|
387
|
+
//
|
|
388
|
+
// Por eso el llamador puede pasar `yaEmitido`: es el mismo registro con el que descarta
|
|
389
|
+
// los CAF de disco (`CertRunner._rangoYaConsumido`), aplicado también acá. Sin ese dato
|
|
390
|
+
// la reobtención no puede saberlo, porque el SII no lo publica.
|
|
391
|
+
const emitidos = yaEmitido
|
|
392
|
+
? rangos.filter(r => yaEmitido({ folioDesde: r.folioDesde, folioHasta: r.folioHasta }))
|
|
393
|
+
: [];
|
|
394
|
+
const emitidosSet = new Set(emitidos);
|
|
395
|
+
const usables = rangos.filter(r => !r.anulado && !emitidosSet.has(r));
|
|
396
|
+
const anulados = rangos.filter(r => r.anulado).length;
|
|
363
397
|
console.log(
|
|
364
398
|
`[FolioService] Tipo ${tipoDte}: ${rangos.length} rango(s) reobtenible(s), ` +
|
|
365
|
-
`${usables.length} usable(s)
|
|
399
|
+
`${usables.length} usable(s)` +
|
|
400
|
+
`${anulados ? `, ${anulados} anulado(s) descartado(s)` : ''}` +
|
|
401
|
+
`${emitidos.length ? `, ${emitidos.length} ya emitido(s) descartado(s)` : ''}`
|
|
366
402
|
);
|
|
367
403
|
|
|
368
404
|
const total = usables.reduce((n, r) => n + r.cantidad, 0);
|
|
@@ -456,7 +492,16 @@ class FolioService {
|
|
|
456
492
|
result.errorCode === 'MAX_AUTOR_INSUFICIENTE' ||
|
|
457
493
|
result.errorCode === 'TIMBRAJE_BLOQUEADO';
|
|
458
494
|
|
|
459
|
-
|
|
495
|
+
// ⚠️ `!== 0` estricto. FOLIOS_DISP es el total de folios timbrados y sin utilizar,
|
|
496
|
+
// que es exactamente el conjunto anulable (el SII solo anula folios "que no han sido
|
|
497
|
+
// recepcionados"). Con 0 la anulación no puede prosperar y solo gasta requests: el
|
|
498
|
+
// 19/08/2026 una corrida intentó anular 4 folios ya emitidos, uno por uno, para
|
|
499
|
+
// terminar en `SIN_FOLIOS_ANULABLES` y un mensaje que culpaba a folios inexistentes.
|
|
500
|
+
// Con el timbraje BLOQUEADO el SII no publica el campo y queda `null`, y ese caso sí
|
|
501
|
+
// necesita intentarlo.
|
|
502
|
+
const hayQueAnular = result.foliosDisp !== 0;
|
|
503
|
+
|
|
504
|
+
if (!result.success && recuperable && permitirAnular && hayQueAnular) {
|
|
460
505
|
console.warn(
|
|
461
506
|
`[FolioService] Tipo ${tipoDte}: ${result.errorCode} — anulando folios sin utilizar y reintentando...`
|
|
462
507
|
);
|
|
@@ -541,6 +586,96 @@ class FolioService {
|
|
|
541
586
|
return { ok: true, cafPath: result.cafPath, otorgados, ...base };
|
|
542
587
|
}
|
|
543
588
|
|
|
589
|
+
/**
|
|
590
|
+
* Cubre `cantidad` folios en varios timbrajes cuando el SII no autoriza tantos de
|
|
591
|
+
* una vez.
|
|
592
|
+
*
|
|
593
|
+
* `solicitarCafExacto` exige que UN CAF cubra todo y aborta si `MAX_AUTOR` no da.
|
|
594
|
+
* Eso es correcto para no agravar el racionamiento cuando hay folios sin usar
|
|
595
|
+
* (anular/reobtener sí destraba ese caso), pero deja sin salida el caso contrario:
|
|
596
|
+
* `FOLIOS_DISP=0` y `MAX_AUTOR` chico porque el SII raciona por historial. Ahí no
|
|
597
|
+
* hay nada que anular ni que reobtener, y la única vía es pedir de a tandas.
|
|
598
|
+
*
|
|
599
|
+
* Medido el 19/08/2026 (RUT 76543210-K, maullin): el set de simulación necesita 4
|
|
600
|
+
* folios de tipo 33, el SII autorizaba 3 con FOLIOS_DISP=0, y el tope siguió en 3
|
|
601
|
+
* durante 24 h. Esperar no lo mueve.
|
|
602
|
+
*
|
|
603
|
+
* Los sets aceptan varios CAF por tipo (`SetBase._tomarFolio` recorre la lista y
|
|
604
|
+
* salta al siguiente cuando se agota un rango), así que juntar tandas es
|
|
605
|
+
* transparente para quien genera los documentos.
|
|
606
|
+
*
|
|
607
|
+
* ⚠️ Cada tanda sube `FOLIOS_DISP` y puede bajar el `MAX_AUTOR` de la siguiente. Por
|
|
608
|
+
* eso se corta apenas una tanda no aporta folios nuevos, en vez de insistir: cada
|
|
609
|
+
* pedido inútil deja folios timbrados que después juegan en contra.
|
|
610
|
+
*
|
|
611
|
+
* @param {Object} params
|
|
612
|
+
* @param {number} params.tipoDte
|
|
613
|
+
* @param {number} params.cantidad - folios a cubrir en total
|
|
614
|
+
* @param {number} [params.maxTandas=4] - tope duro de viajes al portal
|
|
615
|
+
* @param {Object} [params.topeInicial] - resultado de `consultarTope` ya en mano, para
|
|
616
|
+
* no repetir el sondeo de la primera tanda (cuesta ~3 requests al portal).
|
|
617
|
+
* @returns {Promise<{ ok: boolean, cafPaths: string[], otorgados: number,
|
|
618
|
+
* maxAutor: number|null, foliosDisp: number|null, errorCode?: string, error?: string }>}
|
|
619
|
+
*/
|
|
620
|
+
async solicitarCafPorTandas({ tipoDte, cantidad, maxTandas = 4, topeInicial = null }) {
|
|
621
|
+
if (!this.cafSolicitor) {
|
|
622
|
+
throw new Error('FolioService: CafSolicitor no inicializado (se requiere pfxPath y pfxPassword)');
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const objetivo = Number(cantidad);
|
|
626
|
+
const cafPaths = [];
|
|
627
|
+
let cubiertos = 0;
|
|
628
|
+
let tope = null;
|
|
629
|
+
|
|
630
|
+
for (let tanda = 1; tanda <= maxTandas && cubiertos < objetivo; tanda++) {
|
|
631
|
+
tope = (tanda === 1 && topeInicial) ? topeInicial : await this.consultarTope({ tipoDte });
|
|
632
|
+
const faltan = objetivo - cubiertos;
|
|
633
|
+
|
|
634
|
+
// `sinTope` = el SII no publica MAX_AUTOR, o sea no está racionando este tipo:
|
|
635
|
+
// se pide todo lo que falta de una.
|
|
636
|
+
const pedir = tope.sinTope ? faltan : Math.min(tope.maxAutor ?? 0, faltan);
|
|
637
|
+
if (pedir <= 0) {
|
|
638
|
+
console.warn(
|
|
639
|
+
`[FolioService] Tipo ${tipoDte}: tanda ${tanda} sin cupo ` +
|
|
640
|
+
`(MAX_AUTOR=${tope.maxAutor}, FOLIOS_DISP=${tope.foliosDisp}) — se corta con ${cubiertos}/${objetivo}`
|
|
641
|
+
);
|
|
642
|
+
break;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
console.log(
|
|
646
|
+
`[FolioService] Tipo ${tipoDte}: tanda ${tanda} pide ${pedir} folio(s) ` +
|
|
647
|
+
`(faltan ${faltan}; MAX_AUTOR=${tope.maxAutor ?? 'sin tope'}, FOLIOS_DISP=${tope.foliosDisp ?? 'sin dato'})`
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
const res = await this.cafSolicitor.solicitar({ tipoDte, cantidad: pedir, minCantidad: 1 });
|
|
651
|
+
const otorgados = res.success ? this._contarFoliosCaf(res.cafPath) : 0;
|
|
652
|
+
if (!otorgados) {
|
|
653
|
+
console.warn(
|
|
654
|
+
`[FolioService] Tipo ${tipoDte}: tanda ${tanda} no entregó folios ` +
|
|
655
|
+
`(${res.errorCode || 'sin código'}: ${res.error || 'sin detalle'}) — se corta`
|
|
656
|
+
);
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
cafPaths.push(res.cafPath);
|
|
661
|
+
cubiertos += otorgados;
|
|
662
|
+
console.log(`[FolioService] Tipo ${tipoDte}: tanda ${tanda} entregó ${otorgados} folio(s) — ${cubiertos}/${objetivo}`);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const base = { maxAutor: tope?.maxAutor ?? null, foliosDisp: tope?.foliosDisp ?? null };
|
|
666
|
+
if (cubiertos < objetivo) {
|
|
667
|
+
return {
|
|
668
|
+
ok: false, cafPaths, otorgados: cubiertos, ...base,
|
|
669
|
+
errorCode: 'TOPE_SII_INSUFICIENTE',
|
|
670
|
+
error:
|
|
671
|
+
`El SII autorizó ${cubiertos} folio(s) de los ${objetivo} que necesita el tipo ${tipoDte}, ` +
|
|
672
|
+
`repartidos en ${cafPaths.length} timbraje(s). No hay folios sin utilizar que anular ni recuperar ` +
|
|
673
|
+
`(FOLIOS_DISP=${base.foliosDisp}): el tope lo fija el SII según el historial de timbraje y emisión.`,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
return { ok: true, cafPaths, otorgados: cubiertos, ...base };
|
|
677
|
+
}
|
|
678
|
+
|
|
544
679
|
/**
|
|
545
680
|
* Consulta el estado de folios en el SII
|
|
546
681
|
* @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;
|
|
578
|
+
|
|
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
|
+
}
|
|
538
594
|
|
|
539
|
-
return {
|
|
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,12 +809,34 @@ 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
|
+
// ── Cuándo reobtener: solo si NO se pueden pedir folios nuevos ──────────
|
|
813
|
+
//
|
|
814
|
+
// La reobtención es el último recurso antes de anular, no un atajo para ahorrar
|
|
815
|
+
// cupo, porque tiene un riesgo que pedir folios nuevos no tiene: el listado del
|
|
816
|
+
// portal **no marca los folios ya emitidos**, y emitir de nuevo con uno hace que el
|
|
817
|
+
// SII rechace el documento con `(DTE-3-101) Folio ... ya fue recibido en el SII`.
|
|
818
|
+
//
|
|
819
|
+
// El filtro `yaEmitido` de abajo tapa lo que sabemos, pero no alcanza solo: el
|
|
820
|
+
// registro local puede estar incompleto (corridas viejas, disco efímero). Medido el
|
|
821
|
+
// 24/08/2026: de los folios reobtenidos del tipo 61, los rangos 1-3 y 10 sí estaban
|
|
822
|
+
// registrados, pero el 4-6 no, y el SII también lo tenía. Los 7 documentos del envío
|
|
823
|
+
// fueron rechazados.
|
|
824
|
+
//
|
|
825
|
+
// Por eso vuelve a exigirse que el cupo NO alcance: con timbraje bloqueado (el SII no
|
|
826
|
+
// publica MAX_AUTOR y queda `null`) o con un tope racionado por debajo de lo que hace
|
|
827
|
+
// falta. Con cupo holgado —el caso de esa corrida, MAX_AUTOR=19 para 7 folios— se
|
|
828
|
+
// piden folios nuevos, que es la vía sin riesgo.
|
|
829
|
+
const topeTipo = this._topeConsultado?.[tipoDte];
|
|
830
|
+
const cupoAlcanza = topeTipo?.sinTope === true
|
|
831
|
+
|| (topeTipo?.maxAutor != null && topeTipo.maxAutor >= Number(cantidad));
|
|
832
|
+
if (topeTipo?.bloqueado || (!cupoAlcanza && (topeTipo?.foliosDisp ?? 0) > 0)) {
|
|
718
833
|
const reob = await this.folioService.reobtenerCaf({
|
|
719
834
|
tipoDte: Number(tipoDte), cantidad: Number(cantidad),
|
|
835
|
+
// El portal lista folios ya emitidos sin marcarlos: el único que lo sabe es este
|
|
836
|
+
// registro, el mismo con el que se descartan los CAF de disco unas líneas arriba.
|
|
837
|
+
// Sin pasarlo, la reobtención devuelve folios ya recibidos por el SII y todo el
|
|
838
|
+
// envío se rechaza documento por documento.
|
|
839
|
+
yaEmitido: (r) => this._rangoYaConsumido(Number(tipoDte), r.folioDesde, r.folioHasta),
|
|
720
840
|
});
|
|
721
841
|
if (reob.ok) {
|
|
722
842
|
// Puede ser más de uno: el SII entrega los folios reobtenidos de a uno y cada
|
|
@@ -733,32 +853,82 @@ class CertRunner {
|
|
|
733
853
|
console.warn(`[CertRunner] Tipo ${tipoDte}: reobtención no sirvió — ${reob.motivo}`);
|
|
734
854
|
}
|
|
735
855
|
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
856
|
+
// ── Cómo pedir: de una o en tandas ──────────────────────────────────────
|
|
857
|
+
//
|
|
858
|
+
// `solicitarCafExacto` exige que UN CAF cubra la cantidad y, si no, anula folios
|
|
859
|
+
// sin utilizar y reintenta. Eso es lo correcto cuando el cupo está corto POR esos
|
|
860
|
+
// folios (FOLIOS_DISP > 0) y cuando el timbraje está bloqueado.
|
|
861
|
+
//
|
|
862
|
+
// Con FOLIOS_DISP=0 no aplica: no hay nada que anular ni que reobtener, el tope lo
|
|
863
|
+
// fija el SII por historial y no se mueve esperando (medido: 24 h con el mismo
|
|
864
|
+
// MAX_AUTOR=3 para 4 folios, RUT 76543210-K). Ahí la única vía es juntar el rango
|
|
865
|
+
// en varios timbrajes, que los sets soportan de fábrica porque `_tomarFolio` acepta
|
|
866
|
+
// una lista de CAF y salta al siguiente cuando se agota un rango.
|
|
867
|
+
const racionadoPorHistorial =
|
|
868
|
+
topeTipo?.foliosDisp === 0 &&
|
|
869
|
+
topeTipo?.maxAutor != null &&
|
|
870
|
+
topeTipo.maxAutor < Number(cantidad);
|
|
871
|
+
|
|
872
|
+
// Folios que ya están en disco pero no alcanzaban solos. En el camino normal se
|
|
873
|
+
// ignoran (se pide un rango que cubra todo y estos quedan para después), pero acá
|
|
874
|
+
// son la mitad del asunto: si el intento anterior alcanzó a timbrar 3 de 4, pedir
|
|
875
|
+
// los 4 de nuevo desperdicia esos 3 y encima sube FOLIOS_DISP, que es lo que aprieta
|
|
876
|
+
// el tope. Se cuentan como cubiertos y al SII se le pide solo la diferencia.
|
|
877
|
+
const parciales = racionadoPorHistorial && previo && !previo.alcanza ? previo : null;
|
|
878
|
+
const faltante = Number(cantidad) - (parciales?.total ?? 0);
|
|
879
|
+
if (parciales) {
|
|
880
|
+
console.log(
|
|
881
|
+
` Tipo ${tipoDte}: ${parciales.total} folio(s) ya timbrados en disco ` +
|
|
882
|
+
`(${parciales.desde}-${parciales.hasta}) — se pedirán ${faltante} al SII`
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const res = racionadoPorHistorial
|
|
887
|
+
? await this.folioService.solicitarCafPorTandas({
|
|
888
|
+
tipoDte: Number(tipoDte),
|
|
889
|
+
cantidad: faltante,
|
|
890
|
+
topeInicial: topeTipo,
|
|
891
|
+
})
|
|
892
|
+
: await this.folioService.solicitarCafExacto({
|
|
893
|
+
tipoDte: Number(tipoDte),
|
|
894
|
+
cantidad: Number(cantidad),
|
|
895
|
+
});
|
|
744
896
|
|
|
745
897
|
if (!res.ok) {
|
|
746
898
|
const detalle = [
|
|
747
899
|
res.maxAutor != null ? `MAX_AUTOR=${res.maxAutor}` : null,
|
|
748
900
|
res.foliosDisp != null ? `FOLIOS_DISP=${res.foliosDisp}` : null,
|
|
749
901
|
].filter(Boolean).join(', ');
|
|
902
|
+
// Dos causas con remedios opuestos. Antes las dos salían con el mismo texto, y en
|
|
903
|
+
// el caso de abajo el mensaje mandaba a anular folios que no existen: el 19/08/2026
|
|
904
|
+
// un comercio recibió "el SII tiene bloqueado el timbraje" mientras el SII le
|
|
905
|
+
// estaba autorizando 3 folios y no tenía ni uno sin utilizar.
|
|
906
|
+
const remedio = res.foliosDisp === 0
|
|
907
|
+
? 'No hay folios sin utilizar que anular ni recuperar: el tope lo fija el SII ' +
|
|
908
|
+
'según el historial de timbraje y emisión de este tipo de documento.'
|
|
909
|
+
: 'El SII limita el timbraje cuando hay folios ya autorizados sin utilizar; ' +
|
|
910
|
+
'emite o anula documentos electrónicos de este tipo y reintenta.';
|
|
911
|
+
const cubiertos = res.otorgados + (parciales?.total ?? 0);
|
|
750
912
|
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. ` +
|
|
913
|
+
`Folios insuficientes para tipo ${tipoDte}: se requieren ${cantidad} y hay ` +
|
|
914
|
+
`${cubiertos}${detalle ? ` (${detalle})` : ''}. ${remedio} ` +
|
|
755
915
|
`[${res.errorCode}] ${res.error || ''}`.trim()
|
|
756
916
|
);
|
|
757
917
|
}
|
|
758
918
|
|
|
759
|
-
|
|
919
|
+
// `solicitarCafPorTandas` devuelve varios; `solicitarCafExacto`, uno solo. Los
|
|
920
|
+
// parciales van primero: el set numera de menor a mayor y son los folios más viejos,
|
|
921
|
+
// así que gastarlos antes es también lo que baja FOLIOS_DISP más rápido.
|
|
922
|
+
const rutas = [
|
|
923
|
+
...(parciales?.paths ?? []),
|
|
924
|
+
...(res.cafPaths ?? (res.cafPath ? [res.cafPath] : [])),
|
|
925
|
+
];
|
|
926
|
+
cafs[tipoDte] = rutas.length > 1 ? rutas : rutas[0];
|
|
760
927
|
emitProgress(STEPS.CAF_OK, { tipo: Number(tipoDte) });
|
|
761
|
-
console.log(
|
|
928
|
+
console.log(
|
|
929
|
+
` ✓ CAF tipo ${tipoDte} (${res.otorgados + (parciales?.total ?? 0)} folios` +
|
|
930
|
+
`${rutas.length > 1 ? ` en ${rutas.length} CAF` : ''})`
|
|
931
|
+
);
|
|
762
932
|
}
|
|
763
933
|
|
|
764
934
|
return cafs;
|
|
@@ -961,6 +1131,7 @@ class CertRunner {
|
|
|
961
1131
|
await sleep(intervalo);
|
|
962
1132
|
} else {
|
|
963
1133
|
console.log(` [ERR] Contenido rechazado por SII (ENVIO CON ERRORES O REPAROS): ${(result.nombresConError || []).join(', ')}`);
|
|
1134
|
+
await this._diagnosticarEnviosConError(sets, result.nombresConError, debugPrefix);
|
|
964
1135
|
break;
|
|
965
1136
|
}
|
|
966
1137
|
} else if (result.allRejected) {
|
|
@@ -986,6 +1157,144 @@ class CertRunner {
|
|
|
986
1157
|
return lastResult;
|
|
987
1158
|
}
|
|
988
1159
|
|
|
1160
|
+
/**
|
|
1161
|
+
* Cuando el portal dice "ENVIO CON ERRORES O REPAROS", pregunta POR QUÉ.
|
|
1162
|
+
*
|
|
1163
|
+
* El portal de certificación solo da el titular: nombra el set y dice que tiene errores
|
|
1164
|
+
* o reparos, sin un solo dato del documento culpable. El detalle vive en la consulta de
|
|
1165
|
+
* estado del envío, que se hace con el TrackId — y ese TrackId lo tenemos en la mano
|
|
1166
|
+
* desde que se subió el set.
|
|
1167
|
+
*
|
|
1168
|
+
* Hasta acá nadie preguntaba. Caso real (24/08/2026): una corrida quedó trabada con
|
|
1169
|
+
* "SET BASICO, SET GUIA DE DESPACHO" con errores, y en las 105 respuestas HTTP de la
|
|
1170
|
+
* etapa los TrackIds de esos sets aparecían solo en el `DTEUpload` que los creó y en el
|
|
1171
|
+
* formulario que los declaró. Ni el comercio ni nosotros teníamos forma de saber qué
|
|
1172
|
+
* corregir; la corrida siguió a libros, avance y simulación arrastrando el problema.
|
|
1173
|
+
*
|
|
1174
|
+
* ⚠️ Se consulta por SOAP (`QueryEstUp.jws`), NO por el REST de `consultarEstado()`: ese
|
|
1175
|
+
* apunta a `apicert.sii.cl/recursos/v1/boleta.electronica.envio/`, que es el servicio de
|
|
1176
|
+
* BOLETAS. Pasarle el TrackId de un set de facturas devuelve un resultado que no
|
|
1177
|
+
* corresponde, y el diagnóstico saldría equivocado justo cuando más se necesita.
|
|
1178
|
+
*
|
|
1179
|
+
* La respuesta cruda queda en el debug dir y también en la captura HTTP de la librería.
|
|
1180
|
+
*
|
|
1181
|
+
* Nunca es fatal: es diagnóstico. Si la consulta falla, se deja constancia y se sigue.
|
|
1182
|
+
*
|
|
1183
|
+
* @private
|
|
1184
|
+
* @param {Object} sets - El mismo mapa que se declaró: { setBasico: { trackId }, ... }
|
|
1185
|
+
* @param {string[]} nombresConError - Nombres tal como los escribe el portal
|
|
1186
|
+
* @param {string} debugPrefix - Prefijo de los archivos de debug de esta declaración
|
|
1187
|
+
*/
|
|
1188
|
+
async _diagnosticarEnviosConError(sets, nombresConError, debugPrefix) {
|
|
1189
|
+
// Espejo de la tabla `patterns` de SiiCertificacion.declararAvance: ahí se traduce de
|
|
1190
|
+
// fila del portal a clave, acá de vuelta a la clave para encontrar el TrackId.
|
|
1191
|
+
const NOMBRE_A_CLAVE = {
|
|
1192
|
+
'SET BASICO': 'setBasico',
|
|
1193
|
+
'SET GUIA DE DESPACHO': 'setGuiaDespacho',
|
|
1194
|
+
'SET FACTURA EXENTA': 'setFacturaExenta',
|
|
1195
|
+
'SET CASO GENERAL FACTURA COMPRA': 'setFacturaCompra',
|
|
1196
|
+
'SET DE SIMULACION': 'setSimulacion',
|
|
1197
|
+
'LIBRO DE VENTAS': 'libroVentas',
|
|
1198
|
+
'LIBRO DE COMPRAS': 'libroCompras',
|
|
1199
|
+
'LIBRO DE COMPRAS PARA EXENTOS': 'libroComprasExentos',
|
|
1200
|
+
'LIBRO DE GUIAS': 'libroGuias',
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
const nombres = nombresConError || [];
|
|
1204
|
+
if (!nombres.length) return;
|
|
1205
|
+
|
|
1206
|
+
console.log('\n──────────────────────────────────────────────────────────');
|
|
1207
|
+
console.log(' DIAGNÓSTICO: consultando al SII por qué rechazó');
|
|
1208
|
+
console.log('──────────────────────────────────────────────────────────');
|
|
1209
|
+
|
|
1210
|
+
let enviador = null;
|
|
1211
|
+
try {
|
|
1212
|
+
enviador = new EnviadorSII(this.certificado, this.ambiente);
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
console.warn(` [!] No se pudo crear el consultor de estado: ${err.message}`);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const rut = this.config.emisor.rut;
|
|
1219
|
+
|
|
1220
|
+
// Se consultan TODOS los envíos declarados, no solo los marcados. El contraste es la
|
|
1221
|
+
// mitad del diagnóstico: si el que falló dice RPR y los demás EPR, el problema es de
|
|
1222
|
+
// ese documento; si todos vuelven RCT o RFR, es de la carátula o de la firma y el
|
|
1223
|
+
// portal solo alcanzó a marcar los que ya procesó.
|
|
1224
|
+
const conError = new Set(nombres.map(n => NOMBRE_A_CLAVE[n]).filter(Boolean));
|
|
1225
|
+
const claves = [...new Set([...conError, ...Object.keys(sets || {})])];
|
|
1226
|
+
|
|
1227
|
+
for (const clave of claves) {
|
|
1228
|
+
const nombre = Object.keys(NOMBRE_A_CLAVE).find(n => NOMBRE_A_CLAVE[n] === clave) || clave;
|
|
1229
|
+
const marca = conError.has(clave) ? '[CON ERRORES]' : '[sin marca en el portal]';
|
|
1230
|
+
const trackId = sets?.[clave]?.trackId;
|
|
1231
|
+
|
|
1232
|
+
if (!trackId) {
|
|
1233
|
+
console.log(` [?] ${nombre} ${marca}: sin TrackId a mano — no se puede consultar el detalle`);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
console.log(`\n ${nombre} ${marca} — TrackId ${trackId}`);
|
|
1238
|
+
|
|
1239
|
+
try {
|
|
1240
|
+
const res = await enviador.consultarEstadoSoap(trackId, rut);
|
|
1241
|
+
|
|
1242
|
+
// Cruda y completa: el resumen de abajo es para leer en el momento, el archivo es
|
|
1243
|
+
// para poder revisar después qué dijo exactamente el SII.
|
|
1244
|
+
try {
|
|
1245
|
+
fs.writeFileSync(
|
|
1246
|
+
path.join(this.debugDir, `${debugPrefix}-estado-${clave}-${trackId}.xml`),
|
|
1247
|
+
res?.xmlRaw || res?.respuesta || JSON.stringify(res, null, 2),
|
|
1248
|
+
'utf8'
|
|
1249
|
+
);
|
|
1250
|
+
} catch { /* el debug no puede tumbar el diagnóstico */ }
|
|
1251
|
+
|
|
1252
|
+
if (!res || res.ok === false) {
|
|
1253
|
+
console.log(` sin estado: ${res?.error || 'sin detalle'}`);
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
console.log(` estado=${res.estado ?? '?'} ${res.mensaje || ''}`.trimEnd());
|
|
1258
|
+
if (res.glosa) console.log(` glosa: ${res.glosa}`);
|
|
1259
|
+
this._imprimirDetalleEstado(res.xmlRaw);
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
console.log(` falló la consulta de estado: ${err.message}`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
console.log('──────────────────────────────────────────────────────────\n');
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Saca a la luz el detalle por documento que trae el XML de estado.
|
|
1269
|
+
*
|
|
1270
|
+
* `consultarEstadoSoap` parsea solo ESTADO, GLOSA y NUM_ATENCION, que es lo que necesita
|
|
1271
|
+
* para decidir si seguir esperando. El detalle de los reparos viene en el mismo XML y se
|
|
1272
|
+
* perdía: son las líneas que nombran el documento culpable.
|
|
1273
|
+
*
|
|
1274
|
+
* No se asume una estructura fija — el SII varía los nombres según el tipo de envío — así
|
|
1275
|
+
* que se imprimen los campos que aparezcan y el XML completo queda en el archivo.
|
|
1276
|
+
* @private
|
|
1277
|
+
*/
|
|
1278
|
+
_imprimirDetalleEstado(xml) {
|
|
1279
|
+
if (!xml) return;
|
|
1280
|
+
|
|
1281
|
+
// Cualquier bloque que hable de reparos, rechazos o detalle por documento.
|
|
1282
|
+
const bloques = xml.match(/<(DETALLE_REP|REPARO|RECHAZO|DETALLE)[\s\S]{0,600}?<\/\1>/gi) || [];
|
|
1283
|
+
for (const b of bloques.slice(0, 20)) {
|
|
1284
|
+
const campos = [...b.matchAll(/<([A-Z_]+)>([^<]+)<\/\1>/gi)]
|
|
1285
|
+
.map(m => `${m[1]}=${m[2].trim()}`)
|
|
1286
|
+
.filter(t => !/^DETALLE(_REP)?=/.test(t));
|
|
1287
|
+
if (campos.length) console.log(` · ${campos.join(' | ')}`);
|
|
1288
|
+
}
|
|
1289
|
+
if (bloques.length > 20) console.log(` · (+${bloques.length - 20} más — ver el XML en el debug)`);
|
|
1290
|
+
|
|
1291
|
+
// Contadores del envío: cuántos documentos aceptó, cuántos objetó. Aunque no haya
|
|
1292
|
+
// bloques de detalle, esto ya dice si el problema es de uno o de todos.
|
|
1293
|
+
const contadores = [...xml.matchAll(/<(NUM_DOC[A-Z_]*|ACEPTADOS|RECHAZADOS|REPAROS)>([^<]+)<\/\1>/gi)]
|
|
1294
|
+
.map(m => `${m[1]}=${m[2].trim()}`);
|
|
1295
|
+
if (contadores.length) console.log(` · ${contadores.join(' | ')}`);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
989
1298
|
/**
|
|
990
1299
|
* Declara avance de los sets ejecutados con reintentos automáticos
|
|
991
1300
|
* @param {Object} [resultadosExt] - Resultados externos (usa this.resultados si no se pasa)
|
|
@@ -2395,12 +2704,17 @@ class CertRunner {
|
|
|
2395
2704
|
// Solicitar CAFs frescos
|
|
2396
2705
|
const cafs = await this.solicitarCafs(cafRequired);
|
|
2397
2706
|
|
|
2398
|
-
// Cargar objetos CAF
|
|
2707
|
+
// Cargar objetos CAF. Un tipo puede traer VARIOS CAF cuando el SII racionó el
|
|
2708
|
+
// timbraje y el rango se juntó en tandas: `Simulacion._tomarFolio` recorre la lista.
|
|
2399
2709
|
const cafObjects = {};
|
|
2400
2710
|
const { CAF } = require('../index');
|
|
2401
|
-
for (const [tipo,
|
|
2402
|
-
const
|
|
2403
|
-
|
|
2711
|
+
for (const [tipo, ref] of Object.entries(cafs)) {
|
|
2712
|
+
const rutas = Array.isArray(ref) ? ref : [ref];
|
|
2713
|
+
const objetos = rutas.map(p => new CAF(fs.readFileSync(p, 'utf8')))
|
|
2714
|
+
// De menor a mayor folio: los documentos del plan quedan numerados como si
|
|
2715
|
+
// vinieran de un rango contiguo.
|
|
2716
|
+
.sort((a, b) => a.getFolioDesde() - b.getFolioDesde());
|
|
2717
|
+
cafObjects[tipo] = objetos.length > 1 ? objetos : objetos[0];
|
|
2404
2718
|
}
|
|
2405
2719
|
|
|
2406
2720
|
// 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
|
@@ -652,7 +652,54 @@ export class FolioService {
|
|
|
652
652
|
* el portal reporta como anulados (los documentos emitidos con esos folios se
|
|
653
653
|
* rechazan).
|
|
654
654
|
*/
|
|
655
|
-
reobtenerCaf(options: {
|
|
655
|
+
reobtenerCaf(options: {
|
|
656
|
+
tipoDte: number;
|
|
657
|
+
cantidad: number;
|
|
658
|
+
/**
|
|
659
|
+
* Descarta los rangos que ya se emitieron. El listado del portal solo marca los
|
|
660
|
+
* ANULADOS, así que sin esto la reobtención devuelve folios que el SII ya recibió y
|
|
661
|
+
* cada documento vuelve rechazado con `DTE-3-101`.
|
|
662
|
+
*/
|
|
663
|
+
yaEmitido?: (rango: { folioDesde: number; folioHasta: number }) => boolean;
|
|
664
|
+
}): Promise<ReobtenerCafResult | null>;
|
|
665
|
+
/** El CAF más reciente de un tipo para este RUT y ambiente, o null. */
|
|
666
|
+
findLatestCaf(tipoDte: number): string | null;
|
|
667
|
+
/**
|
|
668
|
+
* Todos los CAF de un tipo en disco, del más reciente al más viejo.
|
|
669
|
+
*
|
|
670
|
+
* Hace falta cuando el SII raciona el timbraje y un rango queda repartido en
|
|
671
|
+
* varios CAF: mirando solo el último se concluye "no alcanza" y se piden folios
|
|
672
|
+
* nuevos, dejando los anteriores timbrados y sin usar.
|
|
673
|
+
*/
|
|
674
|
+
listarCafs(tipoDte: number): string[];
|
|
675
|
+
/**
|
|
676
|
+
* Cubre `cantidad` folios en varios timbrajes cuando el SII no autoriza tantos de
|
|
677
|
+
* una vez.
|
|
678
|
+
*
|
|
679
|
+
* Es para el caso `FOLIOS_DISP = 0` con `MAX_AUTOR` corto: no hay folios sin usar
|
|
680
|
+
* que anular ni recuperar, el tope lo fija el SII por historial y no se mueve
|
|
681
|
+
* esperando. Se corta apenas una tanda no aporta folios nuevos, porque cada
|
|
682
|
+
* timbraje parcial sube `FOLIOS_DISP` y puede bajar el tope siguiente.
|
|
683
|
+
*/
|
|
684
|
+
solicitarCafPorTandas(options: {
|
|
685
|
+
tipoDte: number;
|
|
686
|
+
cantidad: number;
|
|
687
|
+
maxTandas?: number;
|
|
688
|
+
topeInicial?: FolioTope | null;
|
|
689
|
+
}): Promise<SolicitarCafTandasResult>;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export interface SolicitarCafTandasResult {
|
|
693
|
+
ok: boolean;
|
|
694
|
+
/** CAF obtenidos, uno por tanda. Los sets y la simulación aceptan la lista completa. */
|
|
695
|
+
cafPaths: string[];
|
|
696
|
+
/** Folios cubiertos entre todas las tandas. */
|
|
697
|
+
otorgados: number;
|
|
698
|
+
maxAutor: number | null;
|
|
699
|
+
foliosDisp: number | null;
|
|
700
|
+
/** `TOPE_SII_INSUFICIENTE` cuando el SII no llegó a cubrir la cantidad pedida. */
|
|
701
|
+
errorCode?: string;
|
|
702
|
+
error?: string;
|
|
656
703
|
}
|
|
657
704
|
|
|
658
705
|
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.18.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",
|