@devlas/dte-sii 2.13.3 → 2.14.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 +139 -4
- package/DTE.js +2 -2
- package/FolioService.js +149 -4
- package/Signer.js +2 -2
- package/SiiSession.js +35 -0
- package/cert/CertRunner.js +415 -21
- package/cert/SetBase.js +52 -1
- package/cert/SetBasico.js +3 -21
- package/cert/SetCompra.js +3 -21
- package/cert/SetExenta.js +3 -21
- package/cert/SetGuia.js +1 -15
- package/dte-sii.d.ts +29 -0
- package/package.json +2 -2
- package/utils/c14n.js +22 -17
- package/utils/httpDebug.js +14 -0
package/cert/CertRunner.js
CHANGED
|
@@ -98,6 +98,10 @@ class CertRunner {
|
|
|
98
98
|
this._siiCert = null;
|
|
99
99
|
this._setsProvider = null;
|
|
100
100
|
this._estructuras = null;
|
|
101
|
+
// CAF timbrados por adelantado para toda la corrida (ver precargarCafsDeSets).
|
|
102
|
+
// Mientras sea null, cada set pide los suyos como antes.
|
|
103
|
+
this._cafsPrecargados = null;
|
|
104
|
+
this._planPrecargado = null;
|
|
101
105
|
|
|
102
106
|
// Caché de sesión SII en memoria (evita logins múltiples durante la misma ejecución)
|
|
103
107
|
// Se puede inyectar un cookieJar ya obtenido vía config.cookieJar para reutilizar sesión.
|
|
@@ -262,6 +266,219 @@ class CertRunner {
|
|
|
262
266
|
Object.assign(this.resultados, resultadosJson);
|
|
263
267
|
}
|
|
264
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Timbra de una sola vez TODOS los folios que la corrida va a necesitar, sumados por
|
|
271
|
+
* tipo entre los cuatro sets. Llamar antes de ejecutar el primer set.
|
|
272
|
+
*
|
|
273
|
+
* El problema que resuelve (medido el 13/08/2026, RUT 78480527-1): cada set pedía sus
|
|
274
|
+
* propios folios cuando le tocaba, así que del tipo 56 se pedían tres tandas separadas
|
|
275
|
+
* —básico 1, exenta 2, compra 1— en la misma corrida. El SII raciona el timbraje según
|
|
276
|
+
* cuántos folios tengas autorizados sin usar, así que la primera tanda le bajaba el tope
|
|
277
|
+
* de 100 a 1 y la segunda se estrellaba con `MAX_AUTOR=1 < 2`. La corrida moría a mitad,
|
|
278
|
+
* y lo único que la destrababa era esperar a que el SII procesara lo ya emitido.
|
|
279
|
+
*
|
|
280
|
+
* Pedir el total junto no rompe el orden de emisión, que sí es obligatorio: las notas de
|
|
281
|
+
* crédito y débito llevan `Referencia` al documento que corrigen (ver SetBasico), o sea
|
|
282
|
+
* que se emiten después de sus facturas, set por set. Pero el timbraje es independiente
|
|
283
|
+
* de la emisión — se pueden tener folios en mano sin haber emitido nada — así que es lo
|
|
284
|
+
* único de la secuencia que sí se puede consolidar.
|
|
285
|
+
*
|
|
286
|
+
* @param {Object[]} planes - cafRequired de cada set, ej. [{33:4,56:1}, {34:3,56:2}]
|
|
287
|
+
* @returns {Promise<Object>} { 33: cafPath, 56: cafPath, ... }
|
|
288
|
+
*/
|
|
289
|
+
async precargarCafsDeSets(planes) {
|
|
290
|
+
const total = {};
|
|
291
|
+
for (const plan of planes) {
|
|
292
|
+
for (const [tipoDte, cantidad] of Object.entries(plan || {})) {
|
|
293
|
+
total[tipoDte] = (total[tipoDte] || 0) + Number(cantidad || 0);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const resumen = Object.entries(total).map(([t, c]) => `${t}x${c}`).join(', ');
|
|
298
|
+
console.log(`\n Timbrando el total de la corrida de una vez: ${resumen}`);
|
|
299
|
+
|
|
300
|
+
this._cafsPrecargados = await this.solicitarCafs(total);
|
|
301
|
+
this._planPrecargado = total;
|
|
302
|
+
return this._cafsPrecargados;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Devuelve los CAF precargados que le tocan a un set.
|
|
307
|
+
*
|
|
308
|
+
* No vuelve a limpiar `folioHelper.counters`: con un CAF por tipo compartido entre sets,
|
|
309
|
+
* el contador tiene que seguir avanzando donde lo dejó el set anterior. Reiniciarlo haría
|
|
310
|
+
* que dos sets emitieran el MISMO folio, que el SII rechaza como duplicado.
|
|
311
|
+
* @private
|
|
312
|
+
*/
|
|
313
|
+
_cafsDelPlan(cafRequired, nombreSet) {
|
|
314
|
+
const cafs = {};
|
|
315
|
+
for (const tipoDte of Object.keys(cafRequired)) {
|
|
316
|
+
const cafPath = this._cafsPrecargados[tipoDte];
|
|
317
|
+
if (!cafPath) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`${nombreSet} necesita folios del tipo ${tipoDte} pero no se timbraron en la ` +
|
|
320
|
+
`precarga (se timbró: ${Object.keys(this._cafsPrecargados).join(', ') || 'nada'}). ` +
|
|
321
|
+
`Es un error de programación: el plan de precarga quedó desalineado con los sets.`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
cafs[tipoDte] = cafPath;
|
|
325
|
+
}
|
|
326
|
+
return cafs;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Busca en disco un CAF de este tipo, timbrado por una corrida anterior, que todavía
|
|
331
|
+
* tenga folios suficientes sin usar. Devuelve null si no hay ninguno servible.
|
|
332
|
+
*
|
|
333
|
+
* Solo mira CAF del ambiente y RUT actuales (`findLatestCaf` ya acota por eso) y exige
|
|
334
|
+
* que el rango alcance para lo que se necesita: un CAF corto no sirve, porque la
|
|
335
|
+
* simulación arma un plan fijo de documentos y muere si falta un folio.
|
|
336
|
+
* @private
|
|
337
|
+
*/
|
|
338
|
+
/**
|
|
339
|
+
* Registro de rangos de folios ya emitidos, por RUT.
|
|
340
|
+
*
|
|
341
|
+
* ⚠️ Va por RANGO y no por ruta de archivo, y eso no es un detalle de implementación.
|
|
342
|
+
* El mismo CAF se guarda en DOS árboles distintos con el mismo contenido:
|
|
343
|
+
*
|
|
344
|
+
* debug/auto-caf/{rut}/{ts}/{tipo}/archivo.xml
|
|
345
|
+
* debug/caf/{ambiente}/{rut}/{tipo}/{ts}/caf-{tipo}-{desde}-{hasta}.xml
|
|
346
|
+
*
|
|
347
|
+
* `findLatestCaf` puede devolver cualquiera de las dos. Marcar la copia que se usó deja
|
|
348
|
+
* la otra intacta, y la etapa siguiente la encuentra "sin usar" y repite los folios.
|
|
349
|
+
* Pasó el 14/08/2026: ENVIAR_SETS marcó `caf-33-4523-4526.xml` a las 03:53:00 y
|
|
350
|
+
* SIMULACION reusó `auto-caf/.../33/archivo.xml` a las 03:53:5x — los mismos folios.
|
|
351
|
+
*
|
|
352
|
+
* Lo que identifica a un folio es (RUT, tipo, número), no dónde quedó el archivo.
|
|
353
|
+
* @private
|
|
354
|
+
*/
|
|
355
|
+
_foliosUsadosPath() {
|
|
356
|
+
const rutLimpio = String(this.config.emisor.rut || '').replace(/[^0-9kK]/g, '');
|
|
357
|
+
return path.join(this.stateDir, `folios-usados-${rutLimpio}.json`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** @returns {Record<string, Array<[number, number]>>} rangos consumidos por tipo de DTE */
|
|
361
|
+
_cargarFoliosUsados() {
|
|
362
|
+
try {
|
|
363
|
+
const p = this._foliosUsadosPath();
|
|
364
|
+
if (!fs.existsSync(p)) return {};
|
|
365
|
+
const data = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
366
|
+
return (data && typeof data === 'object') ? data : {};
|
|
367
|
+
} catch { return {}; }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Lee (tipo, desde, hasta) de un CAF en disco. `null` si no se pudo. */
|
|
371
|
+
_rangoDelCaf(cafPath) {
|
|
372
|
+
try {
|
|
373
|
+
const { CAF } = require('../index');
|
|
374
|
+
const caf = new CAF(fs.readFileSync(cafPath, 'utf8'));
|
|
375
|
+
const tipo = Number(caf.da?.TD);
|
|
376
|
+
const desde = caf.getFolioDesde();
|
|
377
|
+
const hasta = caf.getFolioHasta();
|
|
378
|
+
if (!Number.isFinite(tipo) || !Number.isFinite(desde) || !Number.isFinite(hasta)) return null;
|
|
379
|
+
return { tipo, desde, hasta };
|
|
380
|
+
} catch { return null; }
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Marca los folios de estos CAF como emitidos, para que no se reusen nunca.
|
|
385
|
+
*
|
|
386
|
+
* Se llama en un `finally`, apenas se intentó el envío y sin mirar si salió bien: si
|
|
387
|
+
* el envío viajó, el SII ya vio esos folios aunque después lo rechace. Quemar un folio
|
|
388
|
+
* de más es barato; repetirlo cuesta la etapa entera.
|
|
389
|
+
* @private
|
|
390
|
+
*/
|
|
391
|
+
_marcarCafsConsumidos(cafs) {
|
|
392
|
+
const registro = this._cargarFoliosUsados();
|
|
393
|
+
let cambio = false;
|
|
394
|
+
|
|
395
|
+
for (const ruta of Object.values(cafs || {})) {
|
|
396
|
+
for (const p of (Array.isArray(ruta) ? ruta : [ruta])) {
|
|
397
|
+
// El `.usado` se mantiene: no es la fuente de verdad, pero deja el estado a la
|
|
398
|
+
// vista de quien mire el directorio.
|
|
399
|
+
try { fs.writeFileSync(`${p}.usado`, new Date().toISOString(), 'utf8'); }
|
|
400
|
+
catch (e) { console.warn(`[CertRunner] No se pudo marcar ${p} como usado: ${e.message}`); }
|
|
401
|
+
|
|
402
|
+
const r = this._rangoDelCaf(p);
|
|
403
|
+
if (!r) continue;
|
|
404
|
+
const clave = String(r.tipo);
|
|
405
|
+
registro[clave] = registro[clave] || [];
|
|
406
|
+
if (!registro[clave].some(([d, h]) => d === r.desde && h === r.hasta)) {
|
|
407
|
+
registro[clave].push([r.desde, r.hasta]);
|
|
408
|
+
cambio = true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!cambio) return;
|
|
414
|
+
try {
|
|
415
|
+
fs.mkdirSync(path.dirname(this._foliosUsadosPath()), { recursive: true });
|
|
416
|
+
fs.writeFileSync(this._foliosUsadosPath(), JSON.stringify(registro, null, 2), 'utf8');
|
|
417
|
+
} catch (e) {
|
|
418
|
+
console.warn(`[CertRunner] No se pudo guardar el registro de folios usados: ${e.message}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** ¿Algún folio de [desde, hasta] ya se emitió? */
|
|
423
|
+
_rangoYaConsumido(tipoDte, desde, hasta) {
|
|
424
|
+
const rangos = this._cargarFoliosUsados()[String(tipoDte)] || [];
|
|
425
|
+
return rangos.some(([d, h]) => desde <= h && hasta >= d);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
_cafReusable(tipoDte, cantidad) {
|
|
429
|
+
try {
|
|
430
|
+
const cafPath = this.folioService.findLatestCaf(tipoDte);
|
|
431
|
+
if (!cafPath || !fs.existsSync(cafPath)) return null;
|
|
432
|
+
// Ya se emitió con él: reusarlo repetiría folios y el SII rechaza el envío entero.
|
|
433
|
+
if (fs.existsSync(`${cafPath}.usado`)) {
|
|
434
|
+
console.log(` Tipo ${tipoDte}: el CAF previo ya se usó para emitir — se pedirá uno nuevo`);
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
const rango = this._rangoDelCaf(cafPath);
|
|
438
|
+
if (rango && this._rangoYaConsumido(tipoDte, rango.desde, rango.hasta)) {
|
|
439
|
+
console.log(` Tipo ${tipoDte}: los folios ${rango.desde}-${rango.hasta} ya se emitieron `
|
|
440
|
+
+ '(otra copia del mismo CAF) — se pedirá uno nuevo');
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Require local, igual que en _construirCafObjects (línea ~2110): `../index` cierra
|
|
445
|
+
// el ciclo con este archivo, así que importarlo arriba deja CAF sin definir.
|
|
446
|
+
const { CAF } = require('../index');
|
|
447
|
+
const caf = new CAF(fs.readFileSync(cafPath, 'utf8'));
|
|
448
|
+
|
|
449
|
+
// Doble chequeo del RUT, aunque findLatestCaf ya filtre. Firmar un timbre con el CAF
|
|
450
|
+
// de otro contribuyente hace que el SII rechace el envío ENTERO con `RFR - Rechazado
|
|
451
|
+
// por Error en Firma`, y el síntoma aparece etapas más adelante (la declaración de
|
|
452
|
+
// simulación se traba porque el envío tiene rechazos). Un fallo así de caro no puede
|
|
453
|
+
// depender de que una función de otro archivo siga filtrando bien.
|
|
454
|
+
const rutCaf = String(caf.da?.RE ?? '').replace(/\./g, '').trim().toUpperCase();
|
|
455
|
+
const rutMio = String(this.config.emisor.rut).replace(/\./g, '').trim().toUpperCase();
|
|
456
|
+
if (rutCaf !== rutMio) {
|
|
457
|
+
console.warn(
|
|
458
|
+
`[CertRunner] CAF previo del tipo ${tipoDte} DESCARTADO: es del RUT ${rutCaf}, ` +
|
|
459
|
+
`no del emisor ${rutMio} (${cafPath})`
|
|
460
|
+
);
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const desde = caf.getFolioDesde();
|
|
465
|
+
const hasta = caf.getFolioHasta();
|
|
466
|
+
if (!Number.isFinite(desde) || !Number.isFinite(hasta)) return null;
|
|
467
|
+
|
|
468
|
+
// Cuántos folios de ese rango quedan libres según el contador de ESTA corrida.
|
|
469
|
+
// Si el helper ya consumió parte del rango, lo que queda es lo que sirve.
|
|
470
|
+
const yaUsados = this.folioHelper.usedFolios.get(tipoDte)?.size ?? 0;
|
|
471
|
+
const disponibles = (hasta - desde + 1) - yaUsados;
|
|
472
|
+
if (disponibles < cantidad) return null;
|
|
473
|
+
|
|
474
|
+
return { path: cafPath, desde, hasta };
|
|
475
|
+
} catch (err) {
|
|
476
|
+
// Un CAF ilegible no puede tumbar la corrida: se pide uno nuevo y listo.
|
|
477
|
+
console.warn(`[CertRunner] No se pudo evaluar el CAF previo del tipo ${tipoDte}: ${err.message}`);
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
265
482
|
/**
|
|
266
483
|
* Solicita CAFs frescos para los tipos especificados
|
|
267
484
|
* @param {Object} cafRequired - { 33: 4, 56: 1, 61: 3 }
|
|
@@ -269,7 +486,11 @@ class CertRunner {
|
|
|
269
486
|
*/
|
|
270
487
|
async solicitarCafs(cafRequired) {
|
|
271
488
|
const cafs = {};
|
|
272
|
-
|
|
489
|
+
// Lo que el sondeo vio en ESTA llamada. Se reinicia para no arrastrar el estado del
|
|
490
|
+
// SII de una corrida anterior: el bloqueo de timbraje se destraba y se vuelve a
|
|
491
|
+
// trabar, así que un dato viejo mandaría a reobtener cuando ya no hace falta.
|
|
492
|
+
this._topeConsultado = {};
|
|
493
|
+
|
|
273
494
|
// Limpiar contadores para nuevos CAFs
|
|
274
495
|
this.folioHelper.counters.clear();
|
|
275
496
|
this.folioHelper.usedFolios.clear();
|
|
@@ -302,14 +523,30 @@ class CertRunner {
|
|
|
302
523
|
// El sondeo cuesta ~3 requests, no emite nada y responde la única pregunta
|
|
303
524
|
// que importa: ¿el SII está racionando este tipo?
|
|
304
525
|
const tope = await this.folioService.consultarTope({ tipoDte: Number(tipoDte) });
|
|
526
|
+
// Se guarda para el bucle de abajo: ahí se decide si conviene intentar la
|
|
527
|
+
// reobtención (solo tiene sentido con el timbraje bloqueado) y sondear de nuevo
|
|
528
|
+
// sería otro viaje al portal para saber algo que ya sabemos.
|
|
529
|
+
this._topeConsultado = { ...(this._topeConsultado ?? {}), [tipoDte]: tope };
|
|
305
530
|
const necesarios = cafRequired[tipoDte];
|
|
306
531
|
|
|
307
532
|
// `sinTope` = el SII ni siquiera publica MAX_AUTOR, o sea no está limitando
|
|
308
533
|
// este tipo. Limpiar ahí es puro costo.
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
// quedar contra la pared,
|
|
312
|
-
|
|
534
|
+
//
|
|
535
|
+
// Con tope publicado alcanza con que el cupo CUBRA lo necesario. Antes se exigía
|
|
536
|
+
// un margen de 3x para "actuar antes de quedar contra la pared", pero medido el
|
|
537
|
+
// 13/08/2026 (RUT 78480527-1) ese margen disparaba la limpieza teniendo cupo de
|
|
538
|
+
// sobra —tipo 61 con MAX_AUTOR=6 para 3 folios, tipo 33 con 4 para 4— y en los
|
|
539
|
+
// tres tipos el resultado fue `0 anulados`: el SII rechazó cada anulación.
|
|
540
|
+
//
|
|
541
|
+
// O sea que la limpieza preventiva no compra nada. Y si algún día funcionara sería
|
|
542
|
+
// peor: el SII cuenta los folios anulados de los últimos 6 meses EN CONTRA del cupo
|
|
543
|
+
// (ver el comentario largo más abajo), así que anular de más acerca el bloqueo en
|
|
544
|
+
// vez de alejarlo. Se limpia solo cuando el cupo realmente no da.
|
|
545
|
+
// `bloqueado` gana sobre todo lo demás: el SII no autoriza nada para este tipo y
|
|
546
|
+
// no publica MAX_AUTOR, así que sin este chequeo `sinTope` daba true y se saltaba
|
|
547
|
+
// la limpieza — que es lo único que puede destrabarlo.
|
|
548
|
+
const holgado = !tope.bloqueado
|
|
549
|
+
&& (tope.sinTope || (tope.maxAutor !== null && tope.maxAutor >= necesarios));
|
|
313
550
|
if (holgado) {
|
|
314
551
|
console.log(
|
|
315
552
|
` Tipo ${tipoDte}: sin limpieza previa — ` +
|
|
@@ -319,23 +556,44 @@ class CertRunner {
|
|
|
319
556
|
);
|
|
320
557
|
continue;
|
|
321
558
|
}
|
|
322
|
-
|
|
559
|
+
// Un solo mensaje según el motivo: con el timbraje bloqueado el SII no publica
|
|
560
|
+
// MAX_AUTOR ni FOLIOS_DISP, así que el texto de racionamiento saldría con `null`
|
|
561
|
+
// en los dos números y confundiría a quien lea el log.
|
|
323
562
|
console.warn(
|
|
324
|
-
|
|
325
|
-
|
|
563
|
+
tope.bloqueado
|
|
564
|
+
? `[CertRunner] Tipo ${tipoDte}: el SII tiene el timbraje BLOQUEADO para este tipo ` +
|
|
565
|
+
`(no autoriza folios nuevos) — limpiando folios sin usar antes de pedir...`
|
|
566
|
+
: `[CertRunner] Tipo ${tipoDte}: MAX_AUTOR=${tope.maxAutor} ajustado para ${necesarios} ` +
|
|
567
|
+
`folio(s) (FOLIOS_DISP=${tope.foliosDisp}) — limpiando folios sin usar...`
|
|
326
568
|
);
|
|
327
569
|
|
|
328
|
-
// ACOTADA a propósito.
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
570
|
+
// ACOTADA a propósito. Sin corte por antigüedad la limpieza barre el historial
|
|
571
|
+
// completo de la empresa: en un RUT con volumen real eso son cientos de
|
|
572
|
+
// anulaciones contra el SII y, peor, el SII cuenta los folios anulados de los
|
|
573
|
+
// últimos 6 meses EN CONTRA del cupo de timbraje — la limpieza terminaría
|
|
574
|
+
// provocando el bloqueo que intenta evitar. Verificado 2026-07-22: sin acotar
|
|
575
|
+
// anuló 296 folios de un RUT (rango 1851–2909) antes de detenerla a mano.
|
|
576
|
+
//
|
|
577
|
+
// Pero la ventana no puede ser la misma en los dos ambientes:
|
|
578
|
+
//
|
|
579
|
+
// producción — el historial son documentos reales del comercio. Anular ahí es
|
|
580
|
+
// destructivo y el riesgo de arriba es el que manda: 1 día.
|
|
581
|
+
// maullin — TODO el historial es de pruebas, propias o de corridas viejas.
|
|
582
|
+
// Con 1 día quedaba un callejón sin salida: los folios que
|
|
583
|
+
// bloquean el timbraje suelen ser de semanas atrás, el filtro los
|
|
584
|
+
// descartaba antes de intentarlos, y la corrida se quedaba
|
|
585
|
+
// reintentando para siempre contra algo que nunca iba a cambiar.
|
|
586
|
+
// Caso real (14/08/2026, RUT 77967443-6): tipo 56 bloqueado por
|
|
587
|
+
// folios del 22-07, seis intentos idénticos, cero anulados.
|
|
588
|
+
//
|
|
589
|
+
// Ojo: esto es el ÚLTIMO recurso. Antes de llegar acá se intenta reusar el CAF
|
|
590
|
+
// previo (ver _cafReusable), que resuelve el mismo bloqueo sin anular nada y sin
|
|
591
|
+
// gastar el cupo de timbraje. Anular es para cuando el folio quedó autorizado
|
|
592
|
+
// pero su CAF ya no está en disco.
|
|
593
|
+
const DIAS_LIMPIEZA = this.ambiente === 'produccion' ? 1 : 180;
|
|
336
594
|
const limpieza = await this.folioService.anularFolios({
|
|
337
595
|
tipoDte: Number(tipoDte),
|
|
338
|
-
soloUltimosDias:
|
|
596
|
+
soloUltimosDias: DIAS_LIMPIEZA,
|
|
339
597
|
maxRangos: 10,
|
|
340
598
|
});
|
|
341
599
|
if (limpieza.totalAnulados > 0) {
|
|
@@ -352,8 +610,64 @@ class CertRunner {
|
|
|
352
610
|
|
|
353
611
|
for (const [tipoDte, cantidad] of Object.entries(cafRequired)) {
|
|
354
612
|
emitProgress(STEPS.CAF_REQUESTING, { tipo: Number(tipoDte) });
|
|
613
|
+
|
|
614
|
+
// ── ¿Ya tenemos folios de un intento anterior? ──────────────────────────
|
|
615
|
+
//
|
|
616
|
+
// Cuando una etapa falla DESPUÉS de haber timbrado algunos tipos (típico: se cae en
|
|
617
|
+
// el último y se pierde todo), el reintento volvía a pedir todo de cero. Los folios
|
|
618
|
+
// del intento anterior quedaban timbrados y sin usar, y el SII cuenta exactamente eso
|
|
619
|
+
// para negar el timbraje: "usted tiene disponible una cantidad de folios suficiente".
|
|
620
|
+
//
|
|
621
|
+
// O sea que cada reintento empeoraba el bloqueo que intentaba superar. Medido el
|
|
622
|
+
// 14/08/2026 (RUT 77967443-6): 6 reintentos de ENVIAR_SETS quemaron 66 folios
|
|
623
|
+
// —tipos 33, 34, 46 y 52, seis rangos cada uno— sin emitir un solo documento,
|
|
624
|
+
// mientras el tipo 56 seguía bloqueado.
|
|
625
|
+
//
|
|
626
|
+
// Reusarlos es seguro: los sets se envían recién cuando TODOS los CAF están en mano,
|
|
627
|
+
// así que si el intento anterior falló, esos folios nunca se emitieron.
|
|
628
|
+
const previo = this._cafReusable(Number(tipoDte), Number(cantidad));
|
|
629
|
+
if (previo) {
|
|
630
|
+
cafs[tipoDte] = previo.path;
|
|
631
|
+
emitProgress(STEPS.CAF_OK, { tipo: Number(tipoDte) });
|
|
632
|
+
console.log(` ✓ CAF tipo ${tipoDte} reusado del intento anterior (folios ${previo.desde}-${previo.hasta})`);
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
|
|
355
636
|
console.log(` Tipo ${tipoDte}: ${cantidad} folios...`);
|
|
356
637
|
|
|
638
|
+
// ── Con el timbraje bloqueado, recuperar antes que pedir ────────────────
|
|
639
|
+
//
|
|
640
|
+
// El SII bloquea el timbraje cuando el contribuyente ya tiene folios sin usar, y su
|
|
641
|
+
// propio mensaje da la salida: "debe emitir y enviar documentos electrónicos al SII
|
|
642
|
+
// o anular folios". Emitir es lo que levanta el bloqueo; anular lo AGRAVA, porque
|
|
643
|
+
// los folios anulados pesan 6 meses en contra del cupo.
|
|
644
|
+
//
|
|
645
|
+
// Para emitir hace falta el CAF de esos folios, y la reobtención del portal lo
|
|
646
|
+
// devuelve sin gastar cupo. Por eso va acá: después de reusar lo que hay en disco y
|
|
647
|
+
// antes de `solicitarCafExacto`, que pide folios nuevos y, si falla, anula.
|
|
648
|
+
//
|
|
649
|
+
// Solo se intenta con el timbraje bloqueado. Con cupo disponible pedir es más simple
|
|
650
|
+
// y no cuesta nada; la reobtención implica un request por rango para descartar los
|
|
651
|
+
// anulados, que el listado del SII no distingue.
|
|
652
|
+
if (this._topeConsultado?.[tipoDte]?.bloqueado) {
|
|
653
|
+
const reob = await this.folioService.reobtenerCaf({
|
|
654
|
+
tipoDte: Number(tipoDte), cantidad: Number(cantidad),
|
|
655
|
+
});
|
|
656
|
+
if (reob.ok) {
|
|
657
|
+
// Puede ser más de uno: el SII entrega los folios reobtenidos de a uno y cada
|
|
658
|
+
// CAF firma con su propia llave, así que se pasan todos y el set toma de cada
|
|
659
|
+
// uno según el folio (ver SetBase._tomarFolio).
|
|
660
|
+
cafs[tipoDte] = reob.cafPaths.length > 1 ? reob.cafPaths : reob.cafPaths[0];
|
|
661
|
+
emitProgress(STEPS.CAF_OK, { tipo: Number(tipoDte) });
|
|
662
|
+
console.log(
|
|
663
|
+
` ✓ CAF tipo ${tipoDte} recuperado del SII: ${reob.cafPaths.length} CAF ` +
|
|
664
|
+
`de folios ya autorizados, sin gastar cupo`
|
|
665
|
+
);
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
console.warn(`[CertRunner] Tipo ${tipoDte}: reobtención no sirvió — ${reob.motivo}`);
|
|
669
|
+
}
|
|
670
|
+
|
|
357
671
|
// solicitarCafExacto (no ConFallback): la simulación genera un plan fijo de
|
|
358
672
|
// documentos y muere si falta un folio, así que un CAF corto no sirve. El
|
|
359
673
|
// método aborta antes de emitir si el SII no autoriza lo suficiente, anula
|
|
@@ -442,7 +756,13 @@ class CertRunner {
|
|
|
442
756
|
|
|
443
757
|
const cafRequired = setData.cafRequired ||
|
|
444
758
|
(typeof cafFallback === 'function' ? cafFallback(setData) : cafFallback);
|
|
445
|
-
|
|
759
|
+
|
|
760
|
+
// Si ya se timbró el total de la corrida por adelantado (ver precargarCafsDeSets),
|
|
761
|
+
// este set reusa esos CAF en vez de pedir los suyos. Pedir acá volvería a partir el
|
|
762
|
+
// timbraje en tandas, que es justamente lo que gatilla el racionamiento del SII.
|
|
763
|
+
const cafs = this._cafsPrecargados
|
|
764
|
+
? this._cafsDelPlan(cafRequired, ClaseSet.name)
|
|
765
|
+
: await this.solicitarCafs(cafRequired);
|
|
446
766
|
|
|
447
767
|
const set = new ClaseSet({
|
|
448
768
|
config: {
|
|
@@ -460,7 +780,23 @@ class CertRunner {
|
|
|
460
780
|
enviador: this._createEnviador(enviadorNombre),
|
|
461
781
|
});
|
|
462
782
|
|
|
463
|
-
|
|
783
|
+
// Un folio NO se reutiliza nunca. Se marca el CAF apenas se intentó enviar, sin mirar
|
|
784
|
+
// si salió bien: si el envío viajó, el SII ya vio esos folios aunque después lo
|
|
785
|
+
// rechace, y reemitirlos invalida el envío entero.
|
|
786
|
+
//
|
|
787
|
+
// Hace falta marcarlo en DISCO porque el contador de folios vive en memoria y cada
|
|
788
|
+
// etapa corre en un proceso aparte. Sin esto, la etapa siguiente reusa el mismo CAF y
|
|
789
|
+
// empieza el rango de nuevo. Pasó el 14/08/2026: ENVIAR_SETS emitió los folios
|
|
790
|
+
// 4519-4522 del tipo 33 y SIMULACION reusó ese CAF reemitiéndolos — el SII devolvió
|
|
791
|
+
// `RFR - Rechazado por Error en Firma` para todo el envío de simulación.
|
|
792
|
+
//
|
|
793
|
+
// Quemar un folio de más es barato; repetirlo cuesta la etapa completa.
|
|
794
|
+
let resultado;
|
|
795
|
+
try {
|
|
796
|
+
resultado = await set.ejecutar(setData, cafs);
|
|
797
|
+
} finally {
|
|
798
|
+
this._marcarCafsConsumidos(cafs);
|
|
799
|
+
}
|
|
464
800
|
this.resultados[resultadoKey] = resultado;
|
|
465
801
|
return resultado;
|
|
466
802
|
}
|
|
@@ -1548,6 +1884,29 @@ class CertRunner {
|
|
|
1548
1884
|
return null;
|
|
1549
1885
|
}
|
|
1550
1886
|
|
|
1887
|
+
/**
|
|
1888
|
+
* Pregunta al SII en qué estado quedó un envío, por su trackId.
|
|
1889
|
+
*
|
|
1890
|
+
* Es la única forma de distinguir "el SII todavía lo está procesando" de "el SII lo
|
|
1891
|
+
* rechazó": mirando el avance de la postulación los dos casos se ven igual, la etapa
|
|
1892
|
+
* simplemente no se mueve. Devuelve `null` si no se pudo consultar, para que quien
|
|
1893
|
+
* llama decida sin quedarse colgado de una consulta que falló.
|
|
1894
|
+
*
|
|
1895
|
+
* @param {string} trackId
|
|
1896
|
+
* @returns {Promise<{estado: string, glosa?: string, esRechazado: boolean, esExitoso: boolean} | null>}
|
|
1897
|
+
*/
|
|
1898
|
+
async consultarEstadoEnvio(trackId) {
|
|
1899
|
+
if (!trackId) return null;
|
|
1900
|
+
try {
|
|
1901
|
+
const enviador = new EnviadorSII(this.certificado, this.ambiente);
|
|
1902
|
+
const r = await enviador.consultarEstadoSoap(trackId, this.config.emisor.rut);
|
|
1903
|
+
return r && r.estado ? r : null;
|
|
1904
|
+
} catch (e) {
|
|
1905
|
+
console.log(` [!] No se pudo consultar el estado del envío ${trackId}: ${e.message}`);
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1551
1910
|
/**
|
|
1552
1911
|
* Crea un enviador de libros
|
|
1553
1912
|
* @private
|
|
@@ -2018,7 +2377,13 @@ class CertRunner {
|
|
|
2018
2377
|
// Enviar al SII
|
|
2019
2378
|
console.log('\n Enviando al SII...');
|
|
2020
2379
|
const enviador = this._createEnviador();
|
|
2021
|
-
|
|
2380
|
+
let resultado;
|
|
2381
|
+
try {
|
|
2382
|
+
resultado = await enviador.enviar(envioDte);
|
|
2383
|
+
} finally {
|
|
2384
|
+
// Mismo criterio que en los sets: los folios de la simulación quedan quemados.
|
|
2385
|
+
this._marcarCafsConsumidos(cafs);
|
|
2386
|
+
}
|
|
2022
2387
|
|
|
2023
2388
|
const result = {
|
|
2024
2389
|
success: !!resultado?.trackId,
|
|
@@ -2237,8 +2602,11 @@ class CertRunner {
|
|
|
2237
2602
|
console.log(`\n ¡SIMULACIÓN APROBADA! Etapa actual: ${etapaActual}`);
|
|
2238
2603
|
return { success: true, etapa: etapaActual };
|
|
2239
2604
|
} else if (esRechazado) {
|
|
2605
|
+
// `rechazado` distingue "el SII dijo que no" de "el SII todavía no dijo nada".
|
|
2606
|
+
// Sin esa marca, quien clasifica la etapa lo lee como espera y la reintenta en
|
|
2607
|
+
// loop, cuando lo que hay que hacer es corregir y reenviar.
|
|
2240
2608
|
console.log(` [ERR] SIMULACIÓN: ${simEstado.estado}`);
|
|
2241
|
-
return { success: false, error:
|
|
2609
|
+
return { success: false, rechazado: true, error: `Simulación rechazada por el SII: ${simEstado.estado}` };
|
|
2242
2610
|
} else {
|
|
2243
2611
|
console.log(` [...] SIMULACIÓN: ${simEstado.estado || 'EN REVISION'}`);
|
|
2244
2612
|
}
|
|
@@ -2254,6 +2622,32 @@ class CertRunner {
|
|
|
2254
2622
|
await sleep(intervalo);
|
|
2255
2623
|
}
|
|
2256
2624
|
|
|
2625
|
+
// ⚠️ Que se agoten los intentos NO significa "el SII se está tomando su tiempo".
|
|
2626
|
+
// El portal tampoco avanza cuando el envío fue RECHAZADO, y ahí esperar más no
|
|
2627
|
+
// sirve de nada: hay que corregir y reenviar. Distinguir los dos casos mirando el
|
|
2628
|
+
// avance es imposible — en ambos la etapa se queda quieta. La única fuente de
|
|
2629
|
+
// verdad es el estado del envío, así que se le pregunta al SII por el trackId.
|
|
2630
|
+
//
|
|
2631
|
+
// Caso real (14/08/2026, RUT 78206276-K): la simulación quedó `RFR - Rechazado por
|
|
2632
|
+
// Error en Firma` y este método devolvió "Timeout esperando aprobación". Quien lo
|
|
2633
|
+
// llamaba lo leyó como "sigue en revisión", siguió adelante y marcó la etapa como
|
|
2634
|
+
// completada sobre un envío que el SII había rechazado.
|
|
2635
|
+
const trackIdSim = this.resultados.simulacion?.trackId;
|
|
2636
|
+
const envio = await this.consultarEstadoEnvio(trackIdSim);
|
|
2637
|
+
if (envio?.esRechazado) {
|
|
2638
|
+
const glosa = envio.glosa || envio.descripcion || envio.mensaje || envio.estado;
|
|
2639
|
+
console.log(`\n [ERR] El SII RECHAZÓ el envío de simulación (${envio.estado}): ${glosa}`);
|
|
2640
|
+
return {
|
|
2641
|
+
success: false,
|
|
2642
|
+
rechazado: true,
|
|
2643
|
+
estadoEnvio: envio.estado,
|
|
2644
|
+
error: `El SII rechazó el envío de simulación (${envio.estado}): ${glosa}`,
|
|
2645
|
+
};
|
|
2646
|
+
}
|
|
2647
|
+
if (envio?.estado) {
|
|
2648
|
+
console.log(`\n [...] Envío de simulación ${trackIdSim} en estado ${envio.estado} — el SII sigue procesando`);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2257
2651
|
console.log('\n [!] Timeout esperando aprobación de simulación');
|
|
2258
2652
|
return { success: false, error: 'Timeout esperando aprobación' };
|
|
2259
2653
|
}
|
package/cert/SetBase.js
CHANGED
|
@@ -214,10 +214,61 @@ class SetBase {
|
|
|
214
214
|
cafs[tipoDte] = cafPath;
|
|
215
215
|
this.logger.log(` ✓ CAF tipo ${tipoDte}: ${cafPath}`);
|
|
216
216
|
}
|
|
217
|
-
|
|
217
|
+
|
|
218
218
|
return cafs;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Reserva el próximo folio y devuelve EL CAF QUE LO CONTIENE.
|
|
223
|
+
*
|
|
224
|
+
* `cafRef` acepta una ruta (comportamiento de siempre) o una lista de rutas. La lista
|
|
225
|
+
* hace falta porque el SII no siempre entrega un rango contiguo: al reobtener folios ya
|
|
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).
|
|
228
|
+
*
|
|
229
|
+
* Y no alcanza con concatenar numeraciones: **cada CAF trae su propia llave privada RSA**
|
|
230
|
+
* (`CAF.js` → `RSASK`) y el timbre de cada documento se firma con la llave del CAF que
|
|
231
|
+
* contiene ESE folio (`DTE.js` → `caf.sign(...)`). Por eso hay que devolver el par
|
|
232
|
+
* folio+CAF junto, y no solo el número: firmar el folio 3 con la llave del CAF del folio
|
|
233
|
+
* 1 produce un timbre inválido y el SII rechaza el envío completo con "Rechazado por
|
|
234
|
+
* Error en Firma".
|
|
235
|
+
*
|
|
236
|
+
* Antes este método estaba duplicado —idéntico, mismo hash— en SetBasico, SetGuia,
|
|
237
|
+
* SetExenta y SetCompra. Se unifica acá para que la lógica de qué llave firma qué
|
|
238
|
+
* documento viva en un solo lugar.
|
|
239
|
+
*
|
|
240
|
+
* @param {string|string[]} cafRef - ruta al CAF, o varias en orden de preferencia
|
|
241
|
+
* @returns {{ caf: Object, cafXml: string, folio: number }}
|
|
242
|
+
*/
|
|
243
|
+
_tomarFolio(cafRef) {
|
|
244
|
+
const { CAF } = require('../index');
|
|
245
|
+
const fs = require('fs');
|
|
246
|
+
const rutas = Array.isArray(cafRef) ? cafRef : [cafRef];
|
|
247
|
+
|
|
248
|
+
let ultimoError = null;
|
|
249
|
+
for (const ruta of rutas) {
|
|
250
|
+
const cafXml = fs.readFileSync(ruta, 'utf8');
|
|
251
|
+
const caf = new CAF(cafXml);
|
|
252
|
+
try {
|
|
253
|
+
const folio = this.folioHelper.reserveNextFolio({
|
|
254
|
+
rutEmisor: this.config.emisor.rut,
|
|
255
|
+
tipoDte: caf.getTipoDTE(),
|
|
256
|
+
folioDesde: caf.getFolioDesde(),
|
|
257
|
+
folioHasta: caf.getFolioHasta(),
|
|
258
|
+
ambiente: this.config.ambiente || 'certificacion',
|
|
259
|
+
cafFingerprint: this.folioHelper.createCafFingerprint(cafXml),
|
|
260
|
+
});
|
|
261
|
+
return { caf, cafXml, folio };
|
|
262
|
+
} catch (err) {
|
|
263
|
+
// `reserveNextFolio` lanza cuando el rango se agotó: se pasa al CAF siguiente.
|
|
264
|
+
// Cualquier otro error sí es fatal y se propaga.
|
|
265
|
+
if (!/No hay más folios disponibles/.test(err.message)) throw err;
|
|
266
|
+
ultimoError = err;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
throw ultimoError ?? new Error('No se recibió ningún CAF para reservar folio');
|
|
270
|
+
}
|
|
271
|
+
|
|
221
272
|
/**
|
|
222
273
|
* Calcula cuántos folios se necesitan para un tipo de DTE
|
|
223
274
|
* Subclases pueden sobrescribir para lógica específica
|
package/cert/SetBasico.js
CHANGED
|
@@ -104,11 +104,9 @@ class SetBasico extends SetBase {
|
|
|
104
104
|
const fs = require('fs');
|
|
105
105
|
|
|
106
106
|
// Cargar CAF
|
|
107
|
-
const cafXml =
|
|
108
|
-
const caf = new CAF(cafXml);
|
|
107
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
109
108
|
|
|
110
109
|
// Reservar folio
|
|
111
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
112
110
|
|
|
113
111
|
// Construir detalle e items
|
|
114
112
|
const detalle = buildDetalle(caso.items);
|
|
@@ -180,11 +178,9 @@ class SetBasico extends SetBase {
|
|
|
180
178
|
}
|
|
181
179
|
|
|
182
180
|
// Cargar CAF
|
|
183
|
-
const cafXml =
|
|
184
|
-
const caf = new CAF(cafXml);
|
|
181
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
185
182
|
|
|
186
183
|
// Reservar folio
|
|
187
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
188
184
|
|
|
189
185
|
// Determinar items a usar
|
|
190
186
|
let items = caso.itemsFromCaso
|
|
@@ -267,11 +263,9 @@ class SetBasico extends SetBase {
|
|
|
267
263
|
}
|
|
268
264
|
|
|
269
265
|
// Cargar CAF
|
|
270
|
-
const cafXml =
|
|
271
|
-
const caf = new CAF(cafXml);
|
|
266
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
272
267
|
|
|
273
268
|
// Reservar folio
|
|
274
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
275
269
|
|
|
276
270
|
// Items
|
|
277
271
|
const itemsFinal = caso.items || [{ nombre: 'SIN MONTO', cantidad: 1, precio: 0 }];
|
|
@@ -322,18 +316,6 @@ class SetBasico extends SetBase {
|
|
|
322
316
|
* Compatible con folioHelper de cert-base
|
|
323
317
|
* @private
|
|
324
318
|
*/
|
|
325
|
-
_reservarFolio(caf, cafXml) {
|
|
326
|
-
const cafFingerprint = this.folioHelper.createCafFingerprint(cafXml);
|
|
327
|
-
const folio = this.folioHelper.reserveNextFolio({
|
|
328
|
-
rutEmisor: this.config.emisor.rut,
|
|
329
|
-
tipoDte: caf.getTipoDTE(),
|
|
330
|
-
folioDesde: caf.getFolioDesde(),
|
|
331
|
-
folioHasta: caf.getFolioHasta(),
|
|
332
|
-
ambiente: this.config.ambiente || 'certificacion',
|
|
333
|
-
cafFingerprint,
|
|
334
|
-
});
|
|
335
|
-
return folio;
|
|
336
|
-
}
|
|
337
319
|
|
|
338
320
|
/**
|
|
339
321
|
* Obtiene la fecha de emisión en formato YYYY-MM-DD
|