@devlas/dte-sii 2.12.23 → 2.12.24
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 +89 -4
- package/SiiCertificacion.js +14 -1
- package/cert/CertRunner.js +13 -1
- package/package.json +1 -1
package/FolioService.js
CHANGED
|
@@ -311,7 +311,11 @@ class FolioService {
|
|
|
311
311
|
`[FolioService] Tipo ${tipoDte}: ${result.errorCode} — anulando folios sin utilizar y reintentando...`
|
|
312
312
|
);
|
|
313
313
|
try {
|
|
314
|
-
|
|
314
|
+
// Acotado: acá sí interesan los folios viejos (son los que inflan
|
|
315
|
+
// FOLIOS_DISP), pero sin barrer el historial entero — anular de a
|
|
316
|
+
// cientos satura al SII y suma al contador de "anulados últimos 6
|
|
317
|
+
// meses", que también juega en contra del cupo.
|
|
318
|
+
const anul = await this.anularFolios({ tipoDte, maxRangos: 20 });
|
|
315
319
|
|
|
316
320
|
// Desglosar los motivos reales del rechazo: no todos son "ya recepcionado"
|
|
317
321
|
// (folio consumido en un DTE enviado). También caen acá los folios ya
|
|
@@ -500,7 +504,53 @@ class FolioService {
|
|
|
500
504
|
* @param {string} [params.motivo]
|
|
501
505
|
* @returns {Promise<{ok:boolean, anulados:Array, rechazados:Array, totalAnulados:number, totalRechazados:number}>}
|
|
502
506
|
*/
|
|
503
|
-
|
|
507
|
+
/**
|
|
508
|
+
* @param {number} [params.soloUltimosDias] - Si se indica, solo se anulan los
|
|
509
|
+
* rangos timbrados dentro de esos días. Sirve para limpiar los sobrantes de
|
|
510
|
+
* una corrida reciente sin tocar el historial de la empresa: sin este filtro
|
|
511
|
+
* la función barre TODOS los rangos sin utilizar, lo que en un RUT con
|
|
512
|
+
* volumen real significa cientos de anulaciones contra el SII — y, peor, el
|
|
513
|
+
* propio SII cuenta los folios anulados de los últimos 6 meses en contra del
|
|
514
|
+
* cupo de timbraje. Verificado 2026-07-22: una limpieza sin acotar anuló 296
|
|
515
|
+
* folios de un RUT antes de detenerla.
|
|
516
|
+
*/
|
|
517
|
+
/**
|
|
518
|
+
* Ruta del registro de rangos ya anulados, por RUT y tipo de DTE.
|
|
519
|
+
*
|
|
520
|
+
* El SII sigue listando un folio anulado como "sin utilizar" —anulado es, en
|
|
521
|
+
* efecto, sin usar— así que `consultarFolios` lo devuelve para siempre y cada
|
|
522
|
+
* limpieza vuelve a intentarlo. Sin memoria entre corridas eso son
|
|
523
|
+
* round-trips al SII que solo pueden fallar, y peor: van comiendo el cupo de
|
|
524
|
+
* `maxRangos`, hasta dejar fuera a los sobrantes que sí hay que anular.
|
|
525
|
+
*/
|
|
526
|
+
_anuladosPath(tipoDte) {
|
|
527
|
+
const rutLimpio = String(this.rutEmisor || '').replace(/[^0-9kK]/g, '');
|
|
528
|
+
return path.join(this.debugDir, `folios-anulados-${rutLimpio}-${tipoDte}.json`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Set de claves "desde-hasta" que el SII ya reportó como anuladas. */
|
|
532
|
+
_cargarAnulados(tipoDte) {
|
|
533
|
+
try {
|
|
534
|
+
const raw = fs.readFileSync(this._anuladosPath(tipoDte), 'utf8');
|
|
535
|
+
const arr = JSON.parse(raw);
|
|
536
|
+
return new Set(Array.isArray(arr) ? arr : []);
|
|
537
|
+
} catch (_) {
|
|
538
|
+
// Sin registro previo (o ilegible): se parte de cero. Nunca es fatal —
|
|
539
|
+
// como mucho se repite el intento una vez más.
|
|
540
|
+
return new Set();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
_guardarAnulados(tipoDte, set) {
|
|
545
|
+
try {
|
|
546
|
+
fs.mkdirSync(this.debugDir, { recursive: true });
|
|
547
|
+
fs.writeFileSync(this._anuladosPath(tipoDte), JSON.stringify([...set]), 'utf8');
|
|
548
|
+
} catch (err) {
|
|
549
|
+
console.warn(`[FolioService] No se pudo persistir registro de anulados: ${err.message}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async anularFolios({ tipoDte, folioDesde = null, folioHasta = null, motivo = 'Folios no utilizados', maxRangos = 50, soloUltimosDias = null }) {
|
|
504
554
|
const debugStampA = new Date().toISOString().replace(/[:.]/g, '-');
|
|
505
555
|
const debugDirA = path.join(this.debugDir, 'auto-caf', 'anulacion', debugStampA);
|
|
506
556
|
fs.mkdirSync(debugDirA, { recursive: true });
|
|
@@ -509,18 +559,38 @@ class FolioService {
|
|
|
509
559
|
const anulados = [];
|
|
510
560
|
const rechazados = [];
|
|
511
561
|
const vistos = new Set(); // claves "folioDesde-folioHasta" ya procesadas en esta ejecución
|
|
562
|
+
// Rangos que el SII ya reportó como anulados en corridas anteriores: se
|
|
563
|
+
// saltan de entrada para no gastar el cupo de `maxRangos` en reintentos
|
|
564
|
+
// que solo pueden volver a fallar. Ver `_anuladosPath`.
|
|
565
|
+
const yaAnulados = this._cargarAnulados(tipoDte);
|
|
566
|
+
const yaAnuladosInicial = yaAnulados.size;
|
|
512
567
|
|
|
513
568
|
const maxPasadas = 4;
|
|
514
569
|
|
|
515
570
|
for (let pasada = 0; pasada < maxPasadas; pasada++) {
|
|
516
571
|
const consulta = await this.consultarFolios({ tipoDte });
|
|
517
572
|
|
|
573
|
+
// Corte por antigüedad: `fecha` viene como DD-MM-AAAA en cada rango.
|
|
574
|
+
// Un rango sin fecha parseable se descarta cuando el filtro está activo —
|
|
575
|
+
// ante la duda, no anular.
|
|
576
|
+
const limiteMs = Number.isFinite(soloUltimosDias)
|
|
577
|
+
? Date.now() - soloUltimosDias * 24 * 60 * 60 * 1000
|
|
578
|
+
: null;
|
|
579
|
+
|
|
518
580
|
// Filtrar rangos dentro del rango solicitado y que no hayamos intentado aún
|
|
519
581
|
let rangos = consulta.ranges.filter(r => {
|
|
520
582
|
if (Number.isFinite(folioDesde) && Number.isFinite(folioHasta)) {
|
|
521
583
|
if (r.folioDesde > folioHasta || r.folioHasta < folioDesde) return false;
|
|
522
584
|
}
|
|
523
|
-
|
|
585
|
+
if (limiteMs !== null) {
|
|
586
|
+
const { dia, mes, ano } = this._parseFecha(r.fecha);
|
|
587
|
+
if (!dia || !mes || !ano) return false;
|
|
588
|
+
const t = new Date(`${ano}-${mes}-${dia}T00:00:00`).getTime();
|
|
589
|
+
if (!Number.isFinite(t) || t < limiteMs) return false;
|
|
590
|
+
}
|
|
591
|
+
const clave = `${r.folioDesde}-${r.folioHasta}`;
|
|
592
|
+
if (yaAnulados.has(clave)) return false;
|
|
593
|
+
return !vistos.has(clave);
|
|
524
594
|
});
|
|
525
595
|
|
|
526
596
|
// Limitar a los más recientes para evitar operaciones masivas
|
|
@@ -574,6 +644,8 @@ class FolioService {
|
|
|
574
644
|
body3.includes('efectuado anteriormente') ||
|
|
575
645
|
/anulad[oa]\s+anteriormente/i.test(body3)) {
|
|
576
646
|
rechazados.push({ folioDesde: iniA, folioHasta: finA, count, reason: 'ya-anulado' });
|
|
647
|
+
// Se recuerda para que la próxima corrida no lo reintente.
|
|
648
|
+
yaAnulados.add(`${iniA}-${finA}`);
|
|
577
649
|
console.warn(`[FolioService] ✗ Rango ${iniA}-${finA}: ya anulado (af_anular3)`);
|
|
578
650
|
try { fs.writeFileSync(path.join(debugDirA, `rango-${iniA}-${finA}-af_anular3-error.html`), body3, 'utf8'); } catch (_) {}
|
|
579
651
|
continue;
|
|
@@ -603,6 +675,11 @@ class FolioService {
|
|
|
603
675
|
bodyBulk.includes('SOLICITUD ANULACION DE FOLIOS');
|
|
604
676
|
if (exitoBulk) {
|
|
605
677
|
anulados.push({ folioDesde: iniA, folioHasta: finA, count });
|
|
678
|
+
// Un rango recién anulado sigue apareciendo en `consultarFolios` como
|
|
679
|
+
// "sin utilizar": si no se recuerda acá, la próxima corrida lo
|
|
680
|
+
// reintenta y el SII contesta "ya anulado". Ésta es la fuente
|
|
681
|
+
// principal de los rangos zombis.
|
|
682
|
+
yaAnulados.add(`${iniA}-${finA}`);
|
|
606
683
|
try { fs.writeFileSync(path.join(debugDirA, `rango-${iniA}-${finA}-af_anular-ok.html`), bodyBulk, 'utf8'); } catch (_) {}
|
|
607
684
|
console.log(`[FolioService] ✓ Rango ${iniA}-${finA} (${count} folios) anulado en bulk`);
|
|
608
685
|
continue;
|
|
@@ -643,6 +720,7 @@ class FolioService {
|
|
|
643
720
|
const ok = bs.includes('ha autorizado la anulaci') || bs.includes('SOLICITUD ANULACION DE FOLIOS');
|
|
644
721
|
if (ok) {
|
|
645
722
|
anulados.push({ folioDesde: folio, folioHasta: folio, count: 1 });
|
|
723
|
+
yaAnulados.add(`${folio}-${folio}`);
|
|
646
724
|
} else {
|
|
647
725
|
const razon = this._parseAnulacionResult(bs).reason || 'error';
|
|
648
726
|
rechazados.push({ folioDesde: folio, folioHasta: folio, count: 1, reason: razon });
|
|
@@ -655,9 +733,16 @@ class FolioService {
|
|
|
655
733
|
if (pasada < maxPasadas - 1) await this._sleep(1500);
|
|
656
734
|
}
|
|
657
735
|
|
|
736
|
+
if (yaAnulados.size !== yaAnuladosInicial) {
|
|
737
|
+
this._guardarAnulados(tipoDte, yaAnulados);
|
|
738
|
+
}
|
|
739
|
+
|
|
658
740
|
const totalAnulados = anulados.reduce((s, r) => s + r.count, 0);
|
|
659
741
|
const totalRechazados = rechazados.reduce((s, r) => s + r.count, 0);
|
|
660
|
-
console.log(
|
|
742
|
+
console.log(
|
|
743
|
+
`[FolioService] Completado: ${totalAnulados} anulados, ${totalRechazados} rechazados` +
|
|
744
|
+
(yaAnulados.size ? ` (${yaAnulados.size} rango(s) en memoria, se omiten en la próxima corrida)` : '')
|
|
745
|
+
);
|
|
661
746
|
|
|
662
747
|
return { ok: true, anulados, rechazados, totalAnulados, totalRechazados };
|
|
663
748
|
}
|
package/SiiCertificacion.js
CHANGED
|
@@ -1553,7 +1553,20 @@ class SiiCertificacion {
|
|
|
1553
1553
|
|
|
1554
1554
|
if (algunoRechazado) {
|
|
1555
1555
|
emitProgress(STEPS.SETS_REJECTED);
|
|
1556
|
-
|
|
1556
|
+
// El detalle va en el error, no solo en `estados`: el llamador suele
|
|
1557
|
+
// interpolar `error` en el mensaje que ve el usuario, y un literal
|
|
1558
|
+
// "Sets rechazados" rendía el inútil "Sets rechazados: Sets rechazados"
|
|
1559
|
+
// sin decir cuál set ni con qué estado. Se nombran solo los rechazados
|
|
1560
|
+
// para no ahogar el dato entre los que sí pasaron.
|
|
1561
|
+
const detalle = Object.values(estadosRelevantes)
|
|
1562
|
+
.filter(e => e.esRechazado)
|
|
1563
|
+
.map(e => `${e.nombre}: ${e.estado}`)
|
|
1564
|
+
.join('; ');
|
|
1565
|
+
return {
|
|
1566
|
+
success: false,
|
|
1567
|
+
error: detalle || 'el SII rechazó uno o más sets',
|
|
1568
|
+
estados: estadosRelevantes,
|
|
1569
|
+
};
|
|
1557
1570
|
}
|
|
1558
1571
|
}
|
|
1559
1572
|
|
package/cert/CertRunner.js
CHANGED
|
@@ -262,7 +262,19 @@ class CertRunner {
|
|
|
262
262
|
// usados se rechazan sin efecto.
|
|
263
263
|
for (const tipoDte of Object.keys(cafRequired)) {
|
|
264
264
|
try {
|
|
265
|
-
|
|
265
|
+
// ACOTADA a propósito. Solo interesan los folios que ESTA certificación
|
|
266
|
+
// timbró y dejó sin usar, que son de hoy. Sin el corte por antigüedad la
|
|
267
|
+
// limpieza barre el historial completo de la empresa: en un RUT con
|
|
268
|
+
// volumen real eso son cientos de anulaciones contra el SII y, peor, el
|
|
269
|
+
// SII cuenta los folios anulados de los últimos 6 meses EN CONTRA del
|
|
270
|
+
// cupo de timbraje — la limpieza terminaría provocando el bloqueo que
|
|
271
|
+
// intenta evitar. Verificado 2026-07-22: sin acotar anuló 296 folios de
|
|
272
|
+
// un RUT (rango 1851–2909) antes de detenerla a mano.
|
|
273
|
+
const limpieza = await this.folioService.anularFolios({
|
|
274
|
+
tipoDte: Number(tipoDte),
|
|
275
|
+
soloUltimosDias: 1,
|
|
276
|
+
maxRangos: 10,
|
|
277
|
+
});
|
|
266
278
|
if (limpieza.totalAnulados > 0) {
|
|
267
279
|
console.log(
|
|
268
280
|
` Tipo ${tipoDte}: ${limpieza.totalAnulados} folio(s) sin usar de intentos previos anulados`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.24",
|
|
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",
|