@devlas/dte-sii 2.12.10 → 2.12.13

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 CHANGED
@@ -224,6 +224,8 @@ class CafSolicitor {
224
224
  // parsear <form action> + hidden fields → submit con esos campos + los propios.
225
225
  const authResponse = await this.session.ensureSession('/cvc_cgi/dte/of_solicita_folios');
226
226
 
227
+ this._saveDebug(debugDir, 'step0-ensureSession.html', authResponse.body || '');
228
+
227
229
  let response;
228
230
  if (this._requiresAuthentication(authResponse.body)) {
229
231
  // Sesión sigue inválida tras ensureSession — el chequeo de más abajo
@@ -235,6 +237,8 @@ class CafSolicitor {
235
237
  response = await this.session.submitForm(formAction, { ...hiddenFields, ...fields });
236
238
  }
237
239
 
240
+ this._saveDebug(debugDir, 'step1-submit.html', response.body || '');
241
+
238
242
  // Guardar sesión para reutilización
239
243
  if (this.sessionPath) {
240
244
  this.session.saveSession(this.sessionPath);
@@ -371,6 +375,19 @@ class CafSolicitor {
371
375
  */
372
376
  async _processMultiStepFlow(response, rut, dv, tipoDte, cantidad, debugDir) {
373
377
  let currentHtml = response.body || '';
378
+ const realFormAction = SiiSession.extractFormAction(currentHtml);
379
+
380
+ // Si el <form> real ya apunta a of_confirma_folio, el SII preseleccionó
381
+ // COD_DOCTO server-side (coincide con el tipoDte enviado en el paso 1) y no
382
+ // hace falta pasar por of_solicita_folios_dcto. Ese endpoint solo se dispara
383
+ // vía JS (changeRegregion) si el usuario cambia el <select> a mano — su sola
384
+ // presencia como string dentro de un <script> no significa que sea el
385
+ // próximo paso real. Ir directo a _processStep3, que ya arma COD_DOCTO/
386
+ // CANT_DOCTOS explícitamente (extractInputValues no lee <select>, así que
387
+ // dejarlo entrar a la rama de abajo pierde el tipo de documento).
388
+ if (realFormAction && realFormAction.includes('of_confirma_folio')) {
389
+ return this._processStep3(response, rut, dv, tipoDte, cantidad, debugDir);
390
+ }
374
391
 
375
392
  // Paso 2: of_solicita_folios_dcto
376
393
  if (currentHtml.includes('of_solicita_folios_dcto')) {
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(/&#xd;/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(/&aacute;/gi, 'á').replace(/&eacute;/gi, 'é').replace(/&iacute;/gi, 'í')
26
+ .replace(/&oacute;/gi, 'ó').replace(/&uacute;/gi, 'ú').replace(/&ntilde;/gi, 'ñ')
27
+ .replace(/&Aacute;/g, 'Á').replace(/&Eacute;/g, 'É').replace(/&Iacute;/g, 'Í')
28
+ .replace(/&Oacute;/g, 'Ó').replace(/&Uacute;/g, 'Ú').replace(/&Ntilde;/g, 'Ñ')
29
+ .replace(/&nbsp;/gi, ' ').replace(/&amp;/gi, '&').replace(/&quot;/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,23 @@ 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 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
1072
+ const texto = limpiarTextoPortal(body);
1073
+ const avanzo = /ha pasado al paso/i.test(texto);
1074
+ const sinCambio = /ya se encuentra en el paso/i.test(texto);
1075
+ 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;
1042
1077
 
1043
1078
  return {
1044
- success: !hasError || hasSuccess,
1079
+ success: avanzo,
1080
+ sinCambio,
1081
+ mensaje,
1045
1082
  rawHtml: body,
1046
1083
  };
1047
1084
 
@@ -1172,10 +1209,30 @@ class SiiCertificacion {
1172
1209
  );
1173
1210
 
1174
1211
  const body = formResponse.body || '';
1175
- const hasError = body.includes('Error') || body.includes('error');
1212
+
1213
+ // El chequeo anterior (body.includes('error')) era demasiado laxo: cualquier página con
1214
+ // la palabra "error" (incluso dentro de un <script>) daba falso negativo, y no detectaba
1215
+ // el caso REAL de bloqueo, que el SII devuelve con HTTP 200 y texto explícito:
1216
+ // "El contribuyente ... no ha finalizado su certificación, encontrándose en el paso
1217
+ // DOCUMENTOS IMPRESOS, por lo que no podrá efectuar la Declaración de Cumplimiento."
1218
+ // Eso pasa mientras el SII no haya APROBADO las muestras impresas (quedan "Por revisar").
1219
+ const texto = limpiarTextoPortal(body);
1220
+
1221
+ const RE_BLOQUEO = /no ha finalizado su certificaci|no podr[aá] efectuar la Declaraci/i;
1222
+ const bloqueado = RE_BLOQUEO.test(texto);
1223
+
1224
+ // Tomar la FRASE que contiene el motivo — no el primer "El contribuyente..." que aparezca
1225
+ // (la página arranca con un párrafo descriptivo que también empieza así, y antes se
1226
+ // devolvía ese texto genérico en vez del motivo real del bloqueo).
1227
+ const frases = texto.split(/(?<=\.)\s+/);
1228
+ const mensaje = bloqueado
1229
+ ? (frases.find(f => RE_BLOQUEO.test(f)) || '').trim() || null
1230
+ : (frases.find(f => /declaraci[oó]n de cumplimiento|ha finalizado|certificad[ao]/i.test(f)) || '').trim() || null;
1176
1231
 
1177
1232
  return {
1178
- success: !hasError,
1233
+ success: !bloqueado,
1234
+ bloqueado,
1235
+ mensaje,
1179
1236
  rawHtml: body,
1180
1237
  };
1181
1238
 
package/SiiSession.js CHANGED
@@ -456,6 +456,22 @@ class SiiSession {
456
456
  }
457
457
  }
458
458
 
459
+ // Pantalla "ESCOJA COMO DESEA INGRESAR" — aparece cuando el certificado está
460
+ // habilitado para representación electrónica de otros RUT. No es un fallo de
461
+ // autenticación (aunque su <title> genérico "Autenticación" coincida con el
462
+ // check de más arriba y por eso el caller podría malinterpretarlo como tal):
463
+ // hay que seguir el link "Continuar" (GET al mismo recurso) para llegar al
464
+ // formulario real.
465
+ if (response.body && response.body.includes('ESCOJA COMO DESEA INGRESAR')) {
466
+ const continuarMatch = response.body.match(/<a\s+href="([^"]+)"[^>]*>\s*Continuar\s*<\/a>/i);
467
+ if (continuarMatch) {
468
+ const continuarUrl = new URL(continuarMatch[1], targetUrl).toString();
469
+ const continuado = await this.request(continuarUrl, { method: 'GET' });
470
+ const continuadoResult = await this.followRedirects(continuado);
471
+ response = continuadoResult.response;
472
+ }
473
+ }
474
+
459
475
  // Si hay redirección a af_anular1
460
476
  if (response.body && response.body.includes('/cvc_cgi/dte/af_anular1')) {
461
477
  const continued = await this.request(targetUrl, { method: 'GET' });
@@ -914,6 +914,27 @@ class CertRunner {
914
914
  if (_pendientesAun.length > 0 && (_i + 1) % 4 === 0) {
915
915
  console.log(` [...] Aún esperando (${Math.round((_i + 1) * 15 / 60)} min): ${_pendientesAun.join(', ')}`);
916
916
  }
917
+ // Cross-check SOAP: el portal se queda en S21 ("declarado") para un envío que el
918
+ // SOAP ya reporta RECHAZADO (LNC = período ocupado → "ENVIO CON ERRORES O REPAROS"
919
+ // en pe_avance3). Ese estado NUNCA pasa a REVISADO CONFORME; sin este chequeo se
920
+ // espera el timeout completo (10 min) al pedo. Consultar SOAP cada 2 intentos (~30s):
921
+ // si algún pendiente está rechazado, cortar y dejar que phase b reintente período.
922
+ // Solo se corta ante un rechazo POSITIVO (ok && esRechazado); errores de red/503 se
923
+ // ignoran para no cortar una espera legítima.
924
+ if (_enviadorSoap && _pendientesAun.length > 0 && (_i + 1) % 2 === 0) {
925
+ for (const _n of _pendientesAun) {
926
+ const _k = _SII_NOMBRE_A_KEY[_n];
927
+ const _tid = _k && resultados[_k]?.trackId;
928
+ if (!_tid) continue;
929
+ try {
930
+ const _soap = await _enviadorSoap.consultarEstadoSoap(_tid, _rutEmisor);
931
+ if (_soap.ok && _soap.esRechazado) {
932
+ console.log(`\n [SOAP] ${_n} (${_tid}): ${_soap.estado} — ${_soap.glosa || 'rechazado'}. No llegará a CONFORME → reintentando período.`);
933
+ return { ok: false, estadosFinal: _ss };
934
+ }
935
+ } catch { /* SOAP inestable (503) — ignorar y seguir con el portal */ }
936
+ }
937
+ }
917
938
  }
918
939
  console.log('\n[!] Timeout (10 min). El SII aún no responde. Verifica con --avance más tarde.');
919
940
  return { ok: false, estadosFinal: _ss };
@@ -932,16 +953,19 @@ class CertRunner {
932
953
  const _enviadorSoap = this._createLibroEnviador();
933
954
  const _rutEmisor = this.config.emisor.rut;
934
955
 
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;
956
+ // Polling hasta 5 minutos (20 intentos × 15s). Si los libros no llegan a LOK
957
+ // en ese lapso se declara igual (ver más abajo "Declarando igual"). El SII de
958
+ // certificación (maullín) suele dejar los libros en LSO mucho tiempo sin avanzar,
959
+ // así que esperar 30 min (el valor anterior) solo agregaba demora sin cambiar el
960
+ // resultado. LSO y otros intermedios igual esperan hasta LOK o rechazo, pero acotado.
961
+ const _MAX_SOAP_POLLS = 20;
938
962
  const _SOAP_INTERVAL_MS = 15000;
939
963
  let _soapPendientes = new Set(_librosParaConsultar.map(l => l.key));
940
964
  const _soapFinalStates = {}; // key → resultado SOAP terminal (LOK/LNC/LRH/etc.)
941
965
  const _soapErrCount = {}; // key → contador de errores SOAP consecutivos (ok:false o throw)
942
966
  const _MAX_SOAP_ERR = 10; // reintentos antes de desistir por errores transitorios
943
967
 
944
- console.log('\n[SOAP] Consultando estado de envíos (espera hasta 15 min)...');
968
+ console.log('\n[SOAP] Consultando estado de envíos (espera hasta 5 min)...');
945
969
  await sleep(15000); // espera inicial — SII tarda al menos 15s en validar schema
946
970
  for (let _pi = 0; _pi < _MAX_SOAP_POLLS && _soapPendientes.size > 0; _pi++) {
947
971
  for (const { key, nombre } of _librosParaConsultar) {
@@ -957,9 +981,9 @@ class CertRunner {
957
981
  _soapErrCount[key] = (_soapErrCount[key] || 0) + 1;
958
982
  if (_soapErrCount[key] >= _MAX_SOAP_ERR) {
959
983
  _soapPendientes.delete(key);
960
- console.log(` [SOAP] ${nombre} (${_tid}): [?] demasiados errores — desistiendo`);
984
+ console.log(` [SOAP] ${nombre} (${_tid}): [?] demasiados errores — desistiendo (${_detalle})`);
961
985
  } else {
962
- console.log(` [SOAP] ${nombre} (${_tid}): [?] error transitorio (${_soapErrCount[key]}/${_MAX_SOAP_ERR}) — reintentando...`);
986
+ console.log(` [SOAP] ${nombre} (${_tid}): [?] error transitorio (${_soapErrCount[key]}/${_MAX_SOAP_ERR}) — reintentando... (${_detalle})`);
963
987
  }
964
988
  } else {
965
989
  _soapErrCount[key] = 0; // reset en cualquier respuesta válida
@@ -988,7 +1012,7 @@ class CertRunner {
988
1012
  }
989
1013
  if (_soapPendientes.size > 0) {
990
1014
  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.`);
1015
+ console.log(` [SOAP] ${_nombresTimeout.join(', ')} siguen en proceso tras 5 min. Declarando igual — verifica correos del SII.`);
992
1016
  }
993
1017
 
994
1018
  // 4. Declarar + retry automático:
@@ -1640,6 +1664,42 @@ class CertRunner {
1640
1664
  }
1641
1665
  }
1642
1666
 
1667
+ /**
1668
+ * Cierre final de la certificación DTE, en un solo paso:
1669
+ * 1. "Avanzar Siguiente Paso" (pe_avance4) — solo funciona si el SII ya APROBÓ las muestras
1670
+ * impresas. Mientras estén "Por revisar" el portal responde "ya se encuentra en el paso
1671
+ * DOCUMENTOS IMPRESOS" y no avanza (no es un error: hay que esperar al SII).
1672
+ * 2. "Declaración de Cumplimiento de Requisitos" (pe_avance7 → pe_avance8).
1673
+ *
1674
+ * Reemplaza los 3 pasos manuales que el usuario debía hacer a mano en el portal del SII.
1675
+ * @returns {Promise<Object>} { success, bloqueado, mensaje, avance, declaracion }
1676
+ */
1677
+ async declararCumplimientoFinal() {
1678
+ console.log('\n' + '═'.repeat(60));
1679
+ console.log('CIERRE FINAL: AVANZAR PASO + DECLARAR CUMPLIMIENTO');
1680
+ console.log('═'.repeat(60));
1681
+
1682
+ console.log('\n → Avanzando al siguiente paso (pe_avance4)...');
1683
+ const avance = await this.avanzarSiguientePaso().catch(e => ({ success: false, error: e.message }));
1684
+ console.log(avance?.success ? ' ✓ Avance enviado' : ` [!] Avance no aplicado: ${avance?.error || 'sin cambios'}`);
1685
+
1686
+ console.log('\n → Declarando cumplimiento de requisitos (pe_avance7 → pe_avance8)...');
1687
+ const declaracion = await this.siiCert.declararCumplimiento();
1688
+
1689
+ if (declaracion.bloqueado) {
1690
+ console.log(`\n [!] El SII NO permite declarar todavía: ${declaracion.mensaje || 'certificación no finalizada'}`);
1691
+ console.log(' Las muestras impresas siguen pendientes de revisión del SII.');
1692
+ return { success: false, bloqueado: true, mensaje: declaracion.mensaje, avance, declaracion };
1693
+ }
1694
+ if (!declaracion.success) {
1695
+ console.log(`\n [ERR] Error declarando cumplimiento: ${declaracion.error || 'desconocido'}`);
1696
+ return { success: false, bloqueado: false, mensaje: declaracion.error, avance, declaracion };
1697
+ }
1698
+
1699
+ console.log('\n [OK] DECLARACIÓN DE CUMPLIMIENTO EFECTUADA');
1700
+ return { success: true, bloqueado: false, mensaje: declaracion.mensaje, avance, declaracion };
1701
+ }
1702
+
1643
1703
  /**
1644
1704
  * Espera a que los libros sean aprobados y luego avanza al siguiente paso
1645
1705
  * @param {Object} [options] - { maxIntentos, intervalo }
@@ -2305,9 +2365,11 @@ class CertRunner {
2305
2365
  const { cookies, makeReq } = await this._autenticarPfeInternet();
2306
2366
 
2307
2367
  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'];
2368
+ // RUT del titular del certificado desde el objeto Certificado (parsea el PFX), NO del
2369
+ // nombre de archivo: cuando la API inyecta un PFX temporal (p.ej. "1-<ts>-cert.pfx") el
2370
+ // nombre no contiene el RUT → certRut salía basura validarUsuario "No hay usuario a validar".
2371
+ const certRutFull = (this.certificado?.rut || path.basename(this.config.certificado.path, '.pfx')).replace(/\./g, '');
2372
+ const [certRut, certDv = '0'] = certRutFull.includes('-') ? certRutFull.split('-') : [certRutFull, '0'];
2311
2373
 
2312
2374
  const gwtHeaders = {
2313
2375
  'Content-Type': 'text/x-gwt-rpc; charset=UTF-8',
@@ -2386,7 +2448,10 @@ class CertRunner {
2386
2448
  ].join(CRLF);
2387
2449
 
2388
2450
  console.log(` → Subiendo ${archivo.filename}...`);
2389
- const uploadResp = await makeReq(`${PFE_BASE}uploadFile${archivo.uploadN}`, {
2451
+ // El portal SII exige el RUT de la empresa como query params (?re={rut}&dve={dv}),
2452
+ // igual que downloadFile. Sin ellos responde HTTP 200 con "no vienen los parametros
2453
+ // necesarios" y el envío no se registra → la etapa nunca avanza a DOCUMENTOS IMPRESOS.
2454
+ const uploadResp = await makeReq(`${PFE_BASE}uploadFile${archivo.uploadN}?re=${empRut}&dve=${empDv}`, {
2390
2455
  method: 'POST',
2391
2456
  body: multipartBody,
2392
2457
  headers: {
@@ -2403,19 +2468,54 @@ class CertRunner {
2403
2468
  }
2404
2469
 
2405
2470
  const respLow = respBody.toLowerCase();
2406
- const hasError = uploadResp.status >= 400
2407
- || (respLow.includes('error') && !respLow.includes('procesado'))
2471
+ // La página de error del portal GWT devuelve HTTP 200 con "no vienen los parametros
2472
+ // necesarios" / <title>ERROR</title> → NO es éxito aunque el status sea 200. Antes se
2473
+ // tomaba status===200 como éxito y se marcaba el intercambio confirmado en falso.
2474
+ const esPaginaError = respLow.includes('no vienen los parametros')
2475
+ || respLow.includes('<title>error')
2408
2476
  || respLow.includes('rechaz');
2409
- const hasSuccess = respLow.includes('procesado') || respLow.includes('exitosamente')
2410
- || respLow.includes('cargado') || uploadResp.status === 200;
2477
+ const hasError = uploadResp.status >= 400 || esPaginaError;
2411
2478
 
2412
- if (hasError && !hasSuccess) {
2479
+ if (hasError) {
2413
2480
  throw new Error(`Error al subir ${archivo.filename}: HTTP ${uploadResp.status} — ${respBody.substring(0, 200)}`);
2414
2481
  }
2415
2482
  console.log(` ✓ ${archivo.filename} subido (HTTP ${uploadResp.status})`);
2416
2483
  }
2417
2484
 
2418
- return { success: true, resultado: 'Los 3 archivos XML de intercambio subidos correctamente' };
2485
+ // === PASO 3: Confirmar el intercambio (GWT RPC) el "commit" que avanza a DOCUMENTOS
2486
+ // IMPRESOS. Sin esto los 3 XML quedan subidos pero la postulación NO avanza. Secuencia
2487
+ // capturada del portal real (F12): validarSetContriPostSeguimiento + updatePostulacion-
2488
+ // Seguimiento ×3 (una por cada documento subido, ids 40/41/43). Antes hacíamos solo una
2489
+ // (43) → por eso no avanzaba: faltaban las otras dos + la validación previa.
2490
+ console.log(' → pfeInternet: validarSetContriPostSeguimiento...');
2491
+ const validSetPayload =
2492
+ `7|0|7|${PFE_BASE}|${PFE_POLICY}|${PFE_SVC}|validarSetContriPostSeguimiento|` +
2493
+ `java.lang.Integer/3438268394|java.lang.String/2004016611|${empDv}|` +
2494
+ `1|2|3|4|2|5|6|5|${empRut}|7|`;
2495
+ const validSetResp = await makeReq(`${PFE_BASE}facade`, {
2496
+ method: 'POST', body: validSetPayload, headers: gwtHeaders, cookies,
2497
+ });
2498
+ if (debugDir) fs.writeFileSync(path.join(debugDir, 'pfe-validarset-resp.txt'), (validSetResp.body || '').substring(0, 800), 'utf8');
2499
+
2500
+ // updatePostulacionSeguimiento × 3 (ids 40, 41, 43 — uno por documento de intercambio)
2501
+ const SEGUIMIENTO_IDS = [40, 41, 43];
2502
+ for (const seguimientoId of SEGUIMIENTO_IDS) {
2503
+ console.log(` → pfeInternet: updatePostulacionSeguimiento (id=${seguimientoId})...`);
2504
+ const segPayload =
2505
+ `7|0|7|${PFE_BASE}|${PFE_POLICY}|${PFE_SVC}|updatePostulacionSeguimiento|` +
2506
+ `cl.sii.sdi.dim.pfe.to.PostulacionSeguimientoTo/3921005560|java.lang.Integer/3438268394|${empDv}|` +
2507
+ `1|2|3|4|1|5|5|6|${seguimientoId}|0|0|0|0|7|6|${empRut}|`;
2508
+ const segResp = await makeReq(`${PFE_BASE}facade`, {
2509
+ method: 'POST', body: segPayload, headers: gwtHeaders, cookies,
2510
+ });
2511
+ if (debugDir) fs.writeFileSync(path.join(debugDir, `pfe-seguimiento-${seguimientoId}-resp.txt`), (segResp.body || '').substring(0, 500), 'utf8');
2512
+ if (!/\/\/OK/.test(segResp.body || '')) {
2513
+ throw new Error(`updatePostulacionSeguimiento(${seguimientoId}) no confirmó: ${(segResp.body || '').substring(0, 200)}`);
2514
+ }
2515
+ }
2516
+ console.log(' ✓ Intercambio confirmado (3 seguimientos) — postulación avanza a DOCUMENTOS IMPRESOS');
2517
+
2518
+ return { success: true, resultado: 'Los 3 archivos XML de intercambio subidos y confirmados (avanza a DOCUMENTOS IMPRESOS)' };
2419
2519
  }
2420
2520
 
2421
2521
  // ═══════════════════════════════════════════════════════════════
@@ -2717,11 +2817,18 @@ class CertRunner {
2717
2817
  (() => { const m = leeResp.body.match(/"(El estado de la postulacion[^"\\]*(?:\\.[^"\\]*)*)"/i); return m ? m[1].replace(/\\x27/g, "'").replace(/\\x22/g, '"') : null; })()
2718
2818
  );
2719
2819
  if (errorEstadoMsg) {
2820
+ // OJO: el SII devuelve "El estado de la postulacion NO es: 'DOCUMENTOS IMPRESOS'" en DOS
2821
+ // situaciones distintas, y el mensaje no las distingue:
2822
+ // a) la postulación TODAVÍA NO llegó a DOCUMENTOS IMPRESOS (falta completar intercambio)
2823
+ // b) la postulación YA PASÓ esa etapa (p.ej. DECLARACION EFECTUADA = certificación
2824
+ // terminada) → subir muestras de nuevo ya no aplica, no hay nada que esperar.
2825
+ // Por eso el hint sugiere verificar la etapa con --avance en vez de asumir que hay una
2826
+ // revisión en curso.
2720
2827
  console.log(` → Portal SII bloqueado: ${errorEstadoMsg}`);
2721
2828
  return {
2722
2829
  success: false, blocked: true, estado: 'BLOQUEADO',
2723
2830
  error: errorEstadoMsg,
2724
- hint: 'Espere a que el SII procese la revisión en curso (APROBADO/RECHAZADO) antes de re-subir.',
2831
+ 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
2832
  };
2726
2833
  }
2727
2834
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devlas/dte-sii",
3
- "version": "2.12.10",
3
+ "version": "2.12.13",
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",