@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 +12 -0
- package/SiiCertificacion.js +72 -9
- package/cert/CertRunner.js +185 -25
- package/package.json +1 -1
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,29 @@ 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 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:
|
|
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
|
-
|
|
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: !
|
|
1239
|
+
success: !bloqueado,
|
|
1240
|
+
bloqueado,
|
|
1241
|
+
mensaje,
|
|
1179
1242
|
rawHtml: body,
|
|
1180
1243
|
};
|
|
1181
1244
|
|
package/cert/CertRunner.js
CHANGED
|
@@ -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
|
-
|
|
584
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
936
|
-
//
|
|
937
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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
|
-
//
|
|
2309
|
-
|
|
2310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2407
|
-
|
|
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
|
|
2410
|
-
|| respLow.includes('cargado') || uploadResp.status === 200;
|
|
2530
|
+
const hasError = uploadResp.status >= 400 || esPaginaError;
|
|
2411
2531
|
|
|
2412
|
-
if (hasError
|
|
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
|
-
|
|
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: '
|
|
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.
|
|
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",
|