@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 +17 -0
- package/EnviadorSII.js +12 -0
- package/SiiCertificacion.js +66 -9
- package/SiiSession.js +16 -0
- package/cert/CertRunner.js +125 -18
- package/package.json +1 -1
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(/
/g, '\n');
|
|
804
816
|
|
package/SiiCertificacion.js
CHANGED
|
@@ -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"
|
|
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
|
-
|
|
1041
|
-
|
|
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:
|
|
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
|
-
|
|
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: !
|
|
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' });
|
package/cert/CertRunner.js
CHANGED
|
@@ -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
|
|
936
|
-
//
|
|
937
|
-
|
|
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
|
|
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
|
|
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
|
-
//
|
|
2309
|
-
|
|
2310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2407
|
-
|
|
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
|
|
2410
|
-
|| respLow.includes('cargado') || uploadResp.status === 200;
|
|
2477
|
+
const hasError = uploadResp.status >= 400 || esPaginaError;
|
|
2411
2478
|
|
|
2412
|
-
if (hasError
|
|
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
|
-
|
|
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: '
|
|
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.
|
|
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",
|