@devlas/dte-sii 2.12.13 → 2.12.14

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.
@@ -1063,17 +1063,23 @@ class SiiCertificacion {
1063
1063
  }
1064
1064
 
1065
1065
  // El botón real (avanzar()) solo cambia el action y submitea — no valida nada del lado
1066
- // cliente. La respuesta del SII distingue 3 casos por texto, no por status HTTP (siempre
1067
- // 200) ni por la palabra "error" (aparece en el <script> de todas las páginas del portal):
1068
- // "ha pasado al paso X" → avance REAL (éxito)
1069
- // "ya se encuentra en el paso X" no-op: no avanzó nada (PASO no coincidía, o el SII
1070
- // aún no habilita el siguiente paso)NO es un error
1071
- // cualquier otro texto → error real
1066
+ // cliente. La respuesta del SII distingue estos casos por texto, no por status HTTP
1067
+ // (siempre 200) ni por la palabra "error" (aparece en el <script> de todas las páginas
1068
+ // del portal):
1069
+ // "ha pasado al paso X" avance REAL a otro paso intermedio
1070
+ // "ha finalizado con la certificación..." → avance REAL llegó al paso final,
1071
+ // queda pendiente declarar cumplimiento
1072
+ // (pe_avance7→8, ver declararCumplimiento())
1073
+ // "ya se encuentra en el paso X" → no-op: no avanzó nada (PASO no
1074
+ // coincidía, o el SII aún no habilita el
1075
+ // siguiente paso) — NO es un error
1076
+ // cualquier otro texto → error real
1072
1077
  const texto = limpiarTextoPortal(body);
1073
- const avanzo = /ha pasado al paso/i.test(texto);
1078
+ const RE_AVANZO = /ha pasado al paso|ha finalizado con la certificaci/i;
1079
+ const avanzo = RE_AVANZO.test(texto);
1074
1080
  const sinCambio = /ya se encuentra en el paso/i.test(texto);
1075
1081
  const frases = texto.split(/(?<=\.)\s+/);
1076
- const mensaje = (frases.find(f => /ha pasado al paso|ya se encuentra en el paso/i.test(f)) || '').trim() || null;
1082
+ const mensaje = (frases.find(f => RE_AVANZO.test(f) || /ya se encuentra en el paso/i.test(f)) || '').trim() || null;
1077
1083
 
1078
1084
  return {
1079
1085
  success: avanzo,
@@ -168,6 +168,20 @@ class CertRunner {
168
168
  * @returns {Promise<Object>} { success, estructuras, error }
169
169
  */
170
170
  async obtenerSets(options = {}) {
171
+ if (options.hydrateOnly) {
172
+ // Solo hidrata this._estructuras desde memoria o disco — NUNCA llama al SII. Necesario
173
+ // como modo aparte porque el resto de esta función SIEMPRE pide un set nuevo al SII
174
+ // (forceRefresh: true, ver abajo), lo que REINICIA la postulación real completa. Un
175
+ // llamado pensado solo para "popular estado" no puede arriesgarse a tocar esa rama.
176
+ if (!this._estructuras) {
177
+ const estructurasPath = path.join(this.debugDir, 'estructuras.json');
178
+ if (fs.existsSync(estructurasPath)) {
179
+ this._estructuras = JSON.parse(fs.readFileSync(estructurasPath, 'utf8'));
180
+ }
181
+ }
182
+ return { success: !!this._estructuras, estructuras: this._estructuras, hydrated: true };
183
+ }
184
+
171
185
  const resultado = await this.setsProvider.obtenerSets({
172
186
  setsOpcionales: options.setsOpcionales || {
173
187
  SET03: 'S', // SET GUIA DE DESPACHO
@@ -199,6 +213,28 @@ class CertRunner {
199
213
  return { success: true, estructuras: resultado.estructuras };
200
214
  }
201
215
 
216
+ /**
217
+ * Inyecta estructuras ya descargadas (desde BD/caché) sin pedirlas al SII. Formaliza lo que
218
+ * hoy hacen los callers a mano (`runner._estructuras = JSON.parse(...)`) con acceso directo
219
+ * a un campo "privado". Útil para reanudar en una fase posterior (libros, simulación) sin
220
+ * volver a llamar obtenerSets() — que SIEMPRE resetea la postulación real (forceRefresh).
221
+ * @param {Object} estructurasJson
222
+ */
223
+ setEstructuras(estructurasJson) {
224
+ this._estructuras = estructurasJson;
225
+ }
226
+
227
+ /**
228
+ * Inyecta resultados previos (trackIds de sets ya enviados) en this.resultados sin
229
+ * re-ejecutar los envíos. Hace merge — no borra resultados ya presentes que no vengan en
230
+ * resultadosJson. Formaliza `runner.resultados[k] = {...}` (acceso directo hoy en
231
+ * run-certificacion.js) con un método explícito.
232
+ * @param {Object} resultadosJson
233
+ */
234
+ setResultados(resultadosJson) {
235
+ Object.assign(this.resultados, resultadosJson);
236
+ }
237
+
202
238
  /**
203
239
  * Solicita CAFs frescos para los tipos especificados
204
240
  * @param {Object} cafRequired - { 33: 4, 56: 1, 61: 3 }
@@ -437,13 +473,18 @@ class CertRunner {
437
473
  * Declara avance de los sets ejecutados con reintentos automáticos
438
474
  * @param {Object} [resultadosExt] - Resultados externos (usa this.resultados si no se pasa)
439
475
  * @param {Object} [estructurasExt] - Estructuras externas (usa this._estructuras si no se pasa)
440
- * @param {Object} [options] - { maxIntentos, intervalo }
476
+ * @param {Object} [options] - { maxIntentos, intervalo, ignorarVacio }
477
+ * @param {boolean} [options.ignorarVacio] - Si this.resultados no tiene trackIds (runner
478
+ * recién creado, sin hidratar), en vez de devolver el error genérico "No hay sets para
479
+ * declarar" consulta el estado real del portal: si la etapa ya avanzó más allá de SET DE
480
+ * PRUEBAS, los sets YA fueron declarados y aprobados en una ejecución anterior — no hay
481
+ * nada que hacer y no es un error. Default false — comportamiento previo sin cambios.
441
482
  * @returns {Promise<Object>} Resultado de la declaración
442
483
  */
443
484
  async declararAvance(resultadosExt, estructurasExt, options = {}) {
444
485
  const resultados = resultadosExt || this.resultados;
445
486
  const estructuras = estructurasExt || this._estructuras;
446
- const { maxIntentos = 10, intervalo = 5000 } = options;
487
+ const { maxIntentos = 10, intervalo = 5000, ignorarVacio = false } = options;
447
488
 
448
489
  if (!estructuras) {
449
490
  throw new Error('No hay estructuras. Ejecutar obtenerSets() primero.');
@@ -470,6 +511,13 @@ class CertRunner {
470
511
  }
471
512
 
472
513
  if (Object.keys(sets).length === 0) {
514
+ if (ignorarVacio) {
515
+ const avance = await this.consultarAvance().catch(() => null);
516
+ const etapa = avance?.etapaActual;
517
+ if (etapa && etapa !== 'SET DE PRUEBAS') {
518
+ return { success: true, yaDeclarado: true, etapaActual: etapa };
519
+ }
520
+ }
473
521
  return { success: false, error: 'No hay sets para declarar' };
474
522
  }
475
523
 
@@ -519,6 +567,11 @@ class CertRunner {
519
567
  * @returns {Promise<Object>} Resultado con todos los libros
520
568
  */
521
569
  async ejecutarFase4Libros(options = {}) {
570
+ // Permite pasar las estructuras explícitamente (ej. runner recién creado, hidratado desde
571
+ // BD/caché en vez de obtenerSets()) — igual que ejecutarSimulacion(). Si no vienen, cae al
572
+ // estado interno (comportamiento previo, sin cambios para callers existentes).
573
+ const estructuras = options.estructuras || this._estructuras;
574
+
522
575
  // NOTA: Ya NO decrementamos aquí - cada libro decrementa su propio período
523
576
  emitProgress(STEPS.BOOKS_START);
524
577
  console.log('\n' + '═'.repeat(60));
@@ -580,8 +633,8 @@ class CertRunner {
580
633
  // con el mes de esas fechas. Usar currentMonth-1 causa "ENVIO CON ERRORES O REPAROS".
581
634
  let _periodoBase = null;
582
635
  const _fchDocMuestra =
583
- this._estructuras?.libroCompras?.detalle?.[0]?.FchDoc ||
584
- this._estructuras?.libroComprasExentos?.detalle?.[0]?.FchDoc;
636
+ estructuras?.libroCompras?.detalle?.[0]?.FchDoc ||
637
+ estructuras?.libroComprasExentos?.detalle?.[0]?.FchDoc;
585
638
  if (_fchDocMuestra) {
586
639
  const _dt = new Date(_fchDocMuestra);
587
640
  if (!isNaN(_dt.getTime())) {
@@ -729,7 +782,7 @@ class CertRunner {
729
782
  }
730
783
 
731
784
  // 4. Libro de Compras para Exentos (solo si el SII lo entregó y no está ya aprobado)
732
- if (this._estructuras?.libroComprasExentos) {
785
+ if (estructuras?.libroComprasExentos) {
733
786
  try {
734
787
  if (_estaConforme('LIBRO DE COMPRAS PARA EXENTOS')) {
735
788
  emitProgress(STEPS.BOOK_SKIPPED, { book: 'libroComprasExentos' });
@@ -867,7 +920,7 @@ class CertRunner {
867
920
  // NO es un error de período. Solo LNC/LRH son errores reales que requieren acción.
868
921
  const _esperarAprobacion = async (librosAVerificar) => {
869
922
  const _todosCandidatos = ['LIBRO DE VENTAS', 'LIBRO DE COMPRAS', 'LIBRO DE GUIAS'];
870
- if (this._estructuras?.libroComprasExentos) _todosCandidatos.push('LIBRO DE COMPRAS PARA EXENTOS');
923
+ if (estructuras?.libroComprasExentos) _todosCandidatos.push('LIBRO DE COMPRAS PARA EXENTOS');
871
924
  const _librosAVerif = librosAVerificar || _todosCandidatos.filter(n => !_estaConforme(n));
872
925
  console.log(`\nEsperando aprobacion del SII para: ${_librosAVerif.join(', ')}`);
873
926
  let _ss = {};
@@ -1069,7 +1122,7 @@ class CertRunner {
1069
1122
 
1070
1123
  // Construir la lista inicial de libros a verificar
1071
1124
  const _todosLibrosNombres = ['LIBRO DE VENTAS', 'LIBRO DE COMPRAS', 'LIBRO DE GUIAS'];
1072
- if (this._estructuras?.libroComprasExentos) _todosLibrosNombres.push('LIBRO DE COMPRAS PARA EXENTOS');
1125
+ if (estructuras?.libroComprasExentos) _todosLibrosNombres.push('LIBRO DE COMPRAS PARA EXENTOS');
1073
1126
  let _librosAVerificar = _todosLibrosNombres.filter(n => !_estaConforme(n));
1074
1127
 
1075
1128
  // Si SOAP detectó LNC/LRH para algún libro → excluirlos de la espera portal.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devlas/dte-sii",
3
- "version": "2.12.13",
3
+ "version": "2.12.14",
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",