@devlas/dte-sii 2.12.12 → 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.
package/EnviadorSII.js CHANGED
@@ -799,6 +799,18 @@ class EnviadorSII {
799
799
  timeoutMs: 30000,
800
800
  });
801
801
 
802
+ // El SII (maullín especialmente) a veces responde 5xx/503 en vez del XML de estado.
803
+ // Distinguirlo explícitamente para no reportarlo como "estado no encontrado" genérico
804
+ // — es una caída transitoria del SII, no un problema del envío ni del parseo.
805
+ if (!response.ok) {
806
+ return {
807
+ ok: false,
808
+ error: `SII no disponible (HTTP ${response.status})`,
809
+ httpStatus: response.status,
810
+ respuesta: response.text,
811
+ };
812
+ }
813
+
802
814
  // Usar decodeXmlEntities centralizado
803
815
  const decoded = decodeXmlEntities(response.text).replace(/
/g, '\n');
804
816
 
@@ -18,6 +18,23 @@
18
18
  const SiiSession = require('./SiiSession.js');
19
19
  const { STEPS, emitProgress } = require('./utils/progress');
20
20
 
21
+ /** Decodifica entidades HTML latinas (el portal SII las usa en vez de UTF-8 crudo) y limpia
22
+ * tags/whitespace, para poder aplicar regex de texto sobre las respuestas de forma confiable. */
23
+ function limpiarTextoPortal(html) {
24
+ const decodificar = (s) => s
25
+ .replace(/á/gi, 'á').replace(/é/gi, 'é').replace(/í/gi, 'í')
26
+ .replace(/ó/gi, 'ó').replace(/ú/gi, 'ú').replace(/ñ/gi, 'ñ')
27
+ .replace(/Á/g, 'Á').replace(/É/g, 'É').replace(/Í/g, 'Í')
28
+ .replace(/Ó/g, 'Ó').replace(/Ú/g, 'Ú').replace(/Ñ/g, 'Ñ')
29
+ .replace(/ /gi, ' ').replace(/&/gi, '&').replace(/"/gi, '"')
30
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(+n));
31
+ return decodificar(
32
+ html.replace(/<script[\s\S]*?<\/script>/gi, ' ')
33
+ .replace(/<style[\s\S]*?<\/style>/gi, ' ')
34
+ .replace(/<[^>]+>/g, ' ')
35
+ ).replace(/\s+/g, ' ').trim();
36
+ }
37
+
21
38
  /**
22
39
  * Etapas de certificacion DTE
23
40
  */
@@ -989,13 +1006,21 @@ class SiiCertificacion {
989
1006
 
990
1007
  const formHtml = formResponse.body || '';
991
1008
 
992
- // 3. Enviar formulario de avance final
993
- // El botón "Avanzar Siguiente Paso" cambia la acción a /pe_avance4
1009
+ // 3. Enviar formulario de avance final.
1010
+ // El botón "Avanzar Siguiente Paso" del portal es <input type="button" onclick="avanzar()">,
1011
+ // y avanzar() SOLO cambia el action del form a /pe_avance4 y lo submitea — no toca ningún
1012
+ // campo. TOTREG y PASO vienen en inputs hidden que YA trae el form de pe_avance2, y PASO
1013
+ // varía según la etapa actual (ej. "P01" para SET DE PRUEBAS, "P05" para DOCUMENTOS
1014
+ // IMPRESOS). Antes se hardcodeaba PASO:'P01' → solo funcionaba en la primera transición;
1015
+ // en cualquier otra etapa el SII lo ignoraba silenciosamente y respondía "la empresa ya
1016
+ // se encuentra en el paso X" (no avanzaba nada, pero tampoco reportaba error).
1017
+ const totregMatch = formHtml.match(/name="TOTREG"[^>]*value="([^"]*)"/i);
1018
+ const pasoMatch = formHtml.match(/name="PASO"[^>]*value="([^"]*)"/i);
994
1019
  const formData = {
995
1020
  RUT_EMP: this.rutEmpresa,
996
1021
  DV_EMP: this.dvEmpresa,
997
- TOTREG: '0',
998
- PASO: 'P01',
1022
+ TOTREG: totregMatch ? totregMatch[1] : '0',
1023
+ PASO: pasoMatch ? pasoMatch[1] : 'P01',
999
1024
  ACEPTAR: 'Avanzar Siguiente Paso',
1000
1025
  };
1001
1026
 
@@ -1037,11 +1062,29 @@ class SiiCertificacion {
1037
1062
  console.log(' [DEBUG] Respuesta avanzar guardada en:', debugPath);
1038
1063
  }
1039
1064
 
1040
- const hasError = body.toLowerCase().includes('error') && !body.includes('Error de Sesión');
1041
- const hasSuccess = body.includes('exitosamente') || body.includes('Avance declarado') || body.includes('actualizado');
1065
+ // El botón real (avanzar()) solo cambia el action y submitea — no valida nada del lado
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
1077
+ const texto = limpiarTextoPortal(body);
1078
+ const RE_AVANZO = /ha pasado al paso|ha finalizado con la certificaci/i;
1079
+ const avanzo = RE_AVANZO.test(texto);
1080
+ const sinCambio = /ya se encuentra en el paso/i.test(texto);
1081
+ const frases = texto.split(/(?<=\.)\s+/);
1082
+ const mensaje = (frases.find(f => RE_AVANZO.test(f) || /ya se encuentra en el paso/i.test(f)) || '').trim() || null;
1042
1083
 
1043
1084
  return {
1044
- success: !hasError || hasSuccess,
1085
+ success: avanzo,
1086
+ sinCambio,
1087
+ mensaje,
1045
1088
  rawHtml: body,
1046
1089
  };
1047
1090
 
@@ -1172,10 +1215,30 @@ class SiiCertificacion {
1172
1215
  );
1173
1216
 
1174
1217
  const body = formResponse.body || '';
1175
- const hasError = body.includes('Error') || body.includes('error');
1218
+
1219
+ // El chequeo anterior (body.includes('error')) era demasiado laxo: cualquier página con
1220
+ // la palabra "error" (incluso dentro de un <script>) daba falso negativo, y no detectaba
1221
+ // el caso REAL de bloqueo, que el SII devuelve con HTTP 200 y texto explícito:
1222
+ // "El contribuyente ... no ha finalizado su certificación, encontrándose en el paso
1223
+ // DOCUMENTOS IMPRESOS, por lo que no podrá efectuar la Declaración de Cumplimiento."
1224
+ // Eso pasa mientras el SII no haya APROBADO las muestras impresas (quedan "Por revisar").
1225
+ const texto = limpiarTextoPortal(body);
1226
+
1227
+ const RE_BLOQUEO = /no ha finalizado su certificaci|no podr[aá] efectuar la Declaraci/i;
1228
+ const bloqueado = RE_BLOQUEO.test(texto);
1229
+
1230
+ // Tomar la FRASE que contiene el motivo — no el primer "El contribuyente..." que aparezca
1231
+ // (la página arranca con un párrafo descriptivo que también empieza así, y antes se
1232
+ // devolvía ese texto genérico en vez del motivo real del bloqueo).
1233
+ const frases = texto.split(/(?<=\.)\s+/);
1234
+ const mensaje = bloqueado
1235
+ ? (frases.find(f => RE_BLOQUEO.test(f)) || '').trim() || null
1236
+ : (frases.find(f => /declaraci[oó]n de cumplimiento|ha finalizado|certificad[ao]/i.test(f)) || '').trim() || null;
1176
1237
 
1177
1238
  return {
1178
- success: !hasError,
1239
+ success: !bloqueado,
1240
+ bloqueado,
1241
+ mensaje,
1179
1242
  rawHtml: body,
1180
1243
  };
1181
1244
 
@@ -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 = {};
@@ -914,6 +967,27 @@ class CertRunner {
914
967
  if (_pendientesAun.length > 0 && (_i + 1) % 4 === 0) {
915
968
  console.log(` [...] Aún esperando (${Math.round((_i + 1) * 15 / 60)} min): ${_pendientesAun.join(', ')}`);
916
969
  }
970
+ // Cross-check SOAP: el portal se queda en S21 ("declarado") para un envío que el
971
+ // SOAP ya reporta RECHAZADO (LNC = período ocupado → "ENVIO CON ERRORES O REPAROS"
972
+ // en pe_avance3). Ese estado NUNCA pasa a REVISADO CONFORME; sin este chequeo se
973
+ // espera el timeout completo (10 min) al pedo. Consultar SOAP cada 2 intentos (~30s):
974
+ // si algún pendiente está rechazado, cortar y dejar que phase b reintente período.
975
+ // Solo se corta ante un rechazo POSITIVO (ok && esRechazado); errores de red/503 se
976
+ // ignoran para no cortar una espera legítima.
977
+ if (_enviadorSoap && _pendientesAun.length > 0 && (_i + 1) % 2 === 0) {
978
+ for (const _n of _pendientesAun) {
979
+ const _k = _SII_NOMBRE_A_KEY[_n];
980
+ const _tid = _k && resultados[_k]?.trackId;
981
+ if (!_tid) continue;
982
+ try {
983
+ const _soap = await _enviadorSoap.consultarEstadoSoap(_tid, _rutEmisor);
984
+ if (_soap.ok && _soap.esRechazado) {
985
+ console.log(`\n [SOAP] ${_n} (${_tid}): ${_soap.estado} — ${_soap.glosa || 'rechazado'}. No llegará a CONFORME → reintentando período.`);
986
+ return { ok: false, estadosFinal: _ss };
987
+ }
988
+ } catch { /* SOAP inestable (503) — ignorar y seguir con el portal */ }
989
+ }
990
+ }
917
991
  }
918
992
  console.log('\n[!] Timeout (10 min). El SII aún no responde. Verifica con --avance más tarde.');
919
993
  return { ok: false, estadosFinal: _ss };
@@ -932,16 +1006,19 @@ class CertRunner {
932
1006
  const _enviadorSoap = this._createLibroEnviador();
933
1007
  const _rutEmisor = this.config.emisor.rut;
934
1008
 
935
- // Polling hasta 30 minutos (120 intentos × 15s).
936
- // LSO (schema OK, procesando contenido) y otros intermedios esperan hasta LOK o rechazo.
937
- const _MAX_SOAP_POLLS = 120;
1009
+ // Polling hasta 5 minutos (20 intentos × 15s). Si los libros no llegan a LOK
1010
+ // en ese lapso se declara igual (ver más abajo "Declarando igual"). El SII de
1011
+ // certificación (maullín) suele dejar los libros en LSO mucho tiempo sin avanzar,
1012
+ // así que esperar 30 min (el valor anterior) solo agregaba demora sin cambiar el
1013
+ // resultado. LSO y otros intermedios igual esperan hasta LOK o rechazo, pero acotado.
1014
+ const _MAX_SOAP_POLLS = 20;
938
1015
  const _SOAP_INTERVAL_MS = 15000;
939
1016
  let _soapPendientes = new Set(_librosParaConsultar.map(l => l.key));
940
1017
  const _soapFinalStates = {}; // key → resultado SOAP terminal (LOK/LNC/LRH/etc.)
941
1018
  const _soapErrCount = {}; // key → contador de errores SOAP consecutivos (ok:false o throw)
942
1019
  const _MAX_SOAP_ERR = 10; // reintentos antes de desistir por errores transitorios
943
1020
 
944
- console.log('\n[SOAP] Consultando estado de envíos (espera hasta 15 min)...');
1021
+ console.log('\n[SOAP] Consultando estado de envíos (espera hasta 5 min)...');
945
1022
  await sleep(15000); // espera inicial — SII tarda al menos 15s en validar schema
946
1023
  for (let _pi = 0; _pi < _MAX_SOAP_POLLS && _soapPendientes.size > 0; _pi++) {
947
1024
  for (const { key, nombre } of _librosParaConsultar) {
@@ -957,9 +1034,9 @@ class CertRunner {
957
1034
  _soapErrCount[key] = (_soapErrCount[key] || 0) + 1;
958
1035
  if (_soapErrCount[key] >= _MAX_SOAP_ERR) {
959
1036
  _soapPendientes.delete(key);
960
- console.log(` [SOAP] ${nombre} (${_tid}): [?] demasiados errores — desistiendo`);
1037
+ console.log(` [SOAP] ${nombre} (${_tid}): [?] demasiados errores — desistiendo (${_detalle})`);
961
1038
  } else {
962
- console.log(` [SOAP] ${nombre} (${_tid}): [?] error transitorio (${_soapErrCount[key]}/${_MAX_SOAP_ERR}) — reintentando...`);
1039
+ console.log(` [SOAP] ${nombre} (${_tid}): [?] error transitorio (${_soapErrCount[key]}/${_MAX_SOAP_ERR}) — reintentando... (${_detalle})`);
963
1040
  }
964
1041
  } else {
965
1042
  _soapErrCount[key] = 0; // reset en cualquier respuesta válida
@@ -988,7 +1065,7 @@ class CertRunner {
988
1065
  }
989
1066
  if (_soapPendientes.size > 0) {
990
1067
  const _nombresTimeout = _librosParaConsultar.filter(l => _soapPendientes.has(l.key)).map(l => l.nombre);
991
- console.log(` [SOAP] ${_nombresTimeout.join(', ')} siguen en proceso tras 15 min. Declarando igual — verifica correos del SII.`);
1068
+ console.log(` [SOAP] ${_nombresTimeout.join(', ')} siguen en proceso tras 5 min. Declarando igual — verifica correos del SII.`);
992
1069
  }
993
1070
 
994
1071
  // 4. Declarar + retry automático:
@@ -1045,7 +1122,7 @@ class CertRunner {
1045
1122
 
1046
1123
  // Construir la lista inicial de libros a verificar
1047
1124
  const _todosLibrosNombres = ['LIBRO DE VENTAS', 'LIBRO DE COMPRAS', 'LIBRO DE GUIAS'];
1048
- if (this._estructuras?.libroComprasExentos) _todosLibrosNombres.push('LIBRO DE COMPRAS PARA EXENTOS');
1125
+ if (estructuras?.libroComprasExentos) _todosLibrosNombres.push('LIBRO DE COMPRAS PARA EXENTOS');
1049
1126
  let _librosAVerificar = _todosLibrosNombres.filter(n => !_estaConforme(n));
1050
1127
 
1051
1128
  // Si SOAP detectó LNC/LRH para algún libro → excluirlos de la espera portal.
@@ -1640,6 +1717,42 @@ class CertRunner {
1640
1717
  }
1641
1718
  }
1642
1719
 
1720
+ /**
1721
+ * Cierre final de la certificación DTE, en un solo paso:
1722
+ * 1. "Avanzar Siguiente Paso" (pe_avance4) — solo funciona si el SII ya APROBÓ las muestras
1723
+ * impresas. Mientras estén "Por revisar" el portal responde "ya se encuentra en el paso
1724
+ * DOCUMENTOS IMPRESOS" y no avanza (no es un error: hay que esperar al SII).
1725
+ * 2. "Declaración de Cumplimiento de Requisitos" (pe_avance7 → pe_avance8).
1726
+ *
1727
+ * Reemplaza los 3 pasos manuales que el usuario debía hacer a mano en el portal del SII.
1728
+ * @returns {Promise<Object>} { success, bloqueado, mensaje, avance, declaracion }
1729
+ */
1730
+ async declararCumplimientoFinal() {
1731
+ console.log('\n' + '═'.repeat(60));
1732
+ console.log('CIERRE FINAL: AVANZAR PASO + DECLARAR CUMPLIMIENTO');
1733
+ console.log('═'.repeat(60));
1734
+
1735
+ console.log('\n → Avanzando al siguiente paso (pe_avance4)...');
1736
+ const avance = await this.avanzarSiguientePaso().catch(e => ({ success: false, error: e.message }));
1737
+ console.log(avance?.success ? ' ✓ Avance enviado' : ` [!] Avance no aplicado: ${avance?.error || 'sin cambios'}`);
1738
+
1739
+ console.log('\n → Declarando cumplimiento de requisitos (pe_avance7 → pe_avance8)...');
1740
+ const declaracion = await this.siiCert.declararCumplimiento();
1741
+
1742
+ if (declaracion.bloqueado) {
1743
+ console.log(`\n [!] El SII NO permite declarar todavía: ${declaracion.mensaje || 'certificación no finalizada'}`);
1744
+ console.log(' Las muestras impresas siguen pendientes de revisión del SII.');
1745
+ return { success: false, bloqueado: true, mensaje: declaracion.mensaje, avance, declaracion };
1746
+ }
1747
+ if (!declaracion.success) {
1748
+ console.log(`\n [ERR] Error declarando cumplimiento: ${declaracion.error || 'desconocido'}`);
1749
+ return { success: false, bloqueado: false, mensaje: declaracion.error, avance, declaracion };
1750
+ }
1751
+
1752
+ console.log('\n [OK] DECLARACIÓN DE CUMPLIMIENTO EFECTUADA');
1753
+ return { success: true, bloqueado: false, mensaje: declaracion.mensaje, avance, declaracion };
1754
+ }
1755
+
1643
1756
  /**
1644
1757
  * Espera a que los libros sean aprobados y luego avanza al siguiente paso
1645
1758
  * @param {Object} [options] - { maxIntentos, intervalo }
@@ -2305,9 +2418,11 @@ class CertRunner {
2305
2418
  const { cookies, makeReq } = await this._autenticarPfeInternet();
2306
2419
 
2307
2420
  const [empRut, empDv] = this.config.emisor.rut.replace(/\./g, '').split('-');
2308
- // Extraer RUT del certificado desde la ruta del PFX (e.g. "19925444-8.pfx")
2309
- const certBase = path.basename(this.config.certificado.path, '.pfx');
2310
- const [certRut, certDv = '0'] = certBase.includes('-') ? certBase.split('-') : [certBase, '0'];
2421
+ // RUT del titular del certificado desde el objeto Certificado (parsea el PFX), NO del
2422
+ // nombre de archivo: cuando la API inyecta un PFX temporal (p.ej. "1-<ts>-cert.pfx") el
2423
+ // nombre no contiene el RUT → certRut salía basura validarUsuario "No hay usuario a validar".
2424
+ const certRutFull = (this.certificado?.rut || path.basename(this.config.certificado.path, '.pfx')).replace(/\./g, '');
2425
+ const [certRut, certDv = '0'] = certRutFull.includes('-') ? certRutFull.split('-') : [certRutFull, '0'];
2311
2426
 
2312
2427
  const gwtHeaders = {
2313
2428
  'Content-Type': 'text/x-gwt-rpc; charset=UTF-8',
@@ -2386,7 +2501,10 @@ class CertRunner {
2386
2501
  ].join(CRLF);
2387
2502
 
2388
2503
  console.log(` → Subiendo ${archivo.filename}...`);
2389
- const uploadResp = await makeReq(`${PFE_BASE}uploadFile${archivo.uploadN}`, {
2504
+ // El portal SII exige el RUT de la empresa como query params (?re={rut}&dve={dv}),
2505
+ // igual que downloadFile. Sin ellos responde HTTP 200 con "no vienen los parametros
2506
+ // necesarios" y el envío no se registra → la etapa nunca avanza a DOCUMENTOS IMPRESOS.
2507
+ const uploadResp = await makeReq(`${PFE_BASE}uploadFile${archivo.uploadN}?re=${empRut}&dve=${empDv}`, {
2390
2508
  method: 'POST',
2391
2509
  body: multipartBody,
2392
2510
  headers: {
@@ -2403,19 +2521,54 @@ class CertRunner {
2403
2521
  }
2404
2522
 
2405
2523
  const respLow = respBody.toLowerCase();
2406
- const hasError = uploadResp.status >= 400
2407
- || (respLow.includes('error') && !respLow.includes('procesado'))
2524
+ // La página de error del portal GWT devuelve HTTP 200 con "no vienen los parametros
2525
+ // necesarios" / <title>ERROR</title> → NO es éxito aunque el status sea 200. Antes se
2526
+ // tomaba status===200 como éxito y se marcaba el intercambio confirmado en falso.
2527
+ const esPaginaError = respLow.includes('no vienen los parametros')
2528
+ || respLow.includes('<title>error')
2408
2529
  || respLow.includes('rechaz');
2409
- const hasSuccess = respLow.includes('procesado') || respLow.includes('exitosamente')
2410
- || respLow.includes('cargado') || uploadResp.status === 200;
2530
+ const hasError = uploadResp.status >= 400 || esPaginaError;
2411
2531
 
2412
- if (hasError && !hasSuccess) {
2532
+ if (hasError) {
2413
2533
  throw new Error(`Error al subir ${archivo.filename}: HTTP ${uploadResp.status} — ${respBody.substring(0, 200)}`);
2414
2534
  }
2415
2535
  console.log(` ✓ ${archivo.filename} subido (HTTP ${uploadResp.status})`);
2416
2536
  }
2417
2537
 
2418
- return { success: true, resultado: 'Los 3 archivos XML de intercambio subidos correctamente' };
2538
+ // === PASO 3: Confirmar el intercambio (GWT RPC) el "commit" que avanza a DOCUMENTOS
2539
+ // IMPRESOS. Sin esto los 3 XML quedan subidos pero la postulación NO avanza. Secuencia
2540
+ // capturada del portal real (F12): validarSetContriPostSeguimiento + updatePostulacion-
2541
+ // Seguimiento ×3 (una por cada documento subido, ids 40/41/43). Antes hacíamos solo una
2542
+ // (43) → por eso no avanzaba: faltaban las otras dos + la validación previa.
2543
+ console.log(' → pfeInternet: validarSetContriPostSeguimiento...');
2544
+ const validSetPayload =
2545
+ `7|0|7|${PFE_BASE}|${PFE_POLICY}|${PFE_SVC}|validarSetContriPostSeguimiento|` +
2546
+ `java.lang.Integer/3438268394|java.lang.String/2004016611|${empDv}|` +
2547
+ `1|2|3|4|2|5|6|5|${empRut}|7|`;
2548
+ const validSetResp = await makeReq(`${PFE_BASE}facade`, {
2549
+ method: 'POST', body: validSetPayload, headers: gwtHeaders, cookies,
2550
+ });
2551
+ if (debugDir) fs.writeFileSync(path.join(debugDir, 'pfe-validarset-resp.txt'), (validSetResp.body || '').substring(0, 800), 'utf8');
2552
+
2553
+ // updatePostulacionSeguimiento × 3 (ids 40, 41, 43 — uno por documento de intercambio)
2554
+ const SEGUIMIENTO_IDS = [40, 41, 43];
2555
+ for (const seguimientoId of SEGUIMIENTO_IDS) {
2556
+ console.log(` → pfeInternet: updatePostulacionSeguimiento (id=${seguimientoId})...`);
2557
+ const segPayload =
2558
+ `7|0|7|${PFE_BASE}|${PFE_POLICY}|${PFE_SVC}|updatePostulacionSeguimiento|` +
2559
+ `cl.sii.sdi.dim.pfe.to.PostulacionSeguimientoTo/3921005560|java.lang.Integer/3438268394|${empDv}|` +
2560
+ `1|2|3|4|1|5|5|6|${seguimientoId}|0|0|0|0|7|6|${empRut}|`;
2561
+ const segResp = await makeReq(`${PFE_BASE}facade`, {
2562
+ method: 'POST', body: segPayload, headers: gwtHeaders, cookies,
2563
+ });
2564
+ if (debugDir) fs.writeFileSync(path.join(debugDir, `pfe-seguimiento-${seguimientoId}-resp.txt`), (segResp.body || '').substring(0, 500), 'utf8');
2565
+ if (!/\/\/OK/.test(segResp.body || '')) {
2566
+ throw new Error(`updatePostulacionSeguimiento(${seguimientoId}) no confirmó: ${(segResp.body || '').substring(0, 200)}`);
2567
+ }
2568
+ }
2569
+ console.log(' ✓ Intercambio confirmado (3 seguimientos) — postulación avanza a DOCUMENTOS IMPRESOS');
2570
+
2571
+ return { success: true, resultado: 'Los 3 archivos XML de intercambio subidos y confirmados (avanza a DOCUMENTOS IMPRESOS)' };
2419
2572
  }
2420
2573
 
2421
2574
  // ═══════════════════════════════════════════════════════════════
@@ -2717,11 +2870,18 @@ class CertRunner {
2717
2870
  (() => { const m = leeResp.body.match(/"(El estado de la postulacion[^"\\]*(?:\\.[^"\\]*)*)"/i); return m ? m[1].replace(/\\x27/g, "'").replace(/\\x22/g, '"') : null; })()
2718
2871
  );
2719
2872
  if (errorEstadoMsg) {
2873
+ // OJO: el SII devuelve "El estado de la postulacion NO es: 'DOCUMENTOS IMPRESOS'" en DOS
2874
+ // situaciones distintas, y el mensaje no las distingue:
2875
+ // a) la postulación TODAVÍA NO llegó a DOCUMENTOS IMPRESOS (falta completar intercambio)
2876
+ // b) la postulación YA PASÓ esa etapa (p.ej. DECLARACION EFECTUADA = certificación
2877
+ // terminada) → subir muestras de nuevo ya no aplica, no hay nada que esperar.
2878
+ // Por eso el hint sugiere verificar la etapa con --avance en vez de asumir que hay una
2879
+ // revisión en curso.
2720
2880
  console.log(` → Portal SII bloqueado: ${errorEstadoMsg}`);
2721
2881
  return {
2722
2882
  success: false, blocked: true, estado: 'BLOQUEADO',
2723
2883
  error: errorEstadoMsg,
2724
- hint: 'Espere a que el SII procese la revisión en curso (APROBADO/RECHAZADO) antes de re-subir.',
2884
+ hint: 'La postulación no está en DOCUMENTOS IMPRESOS: puede que aún no haya llegado a esa etapa, o que YA la haya superado (certificación terminada). Verifica la etapa real con --avance.',
2725
2885
  };
2726
2886
  }
2727
2887
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devlas/dte-sii",
3
- "version": "2.12.12",
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",