@devlas/dte-sii 2.9.8 → 2.11.0
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 +100 -64
- package/LICENSE +27 -27
- package/LibroCompraVenta.js +141 -17
- package/LibroGuia.js +36 -25
- package/SiiCertificacion.js +85 -3
- package/SiiPortalAuth.js +85 -19
- package/WsReclamo.js +434 -434
- package/cert/BoletaCert.js +41 -4
- package/cert/CertRunner.js +1123 -1209
- package/cert/LibroCompras.js +3 -2
- package/cert/LibroGuias.js +2 -1
- package/cert/LibroVentas.js +2 -1
- package/cert/MuestrasImpresas.js +831 -131
- package/cert/comunaOficina.js +458 -458
- package/cert/index.js +122 -122
- package/cert/types.js +328 -328
- package/package.json +2 -3
- package/test-muestras.js +180 -0
- package/test-qdetestlibro.js +174 -0
- package/utils/progress.js +4 -0
- package/utils/browser.js +0 -79
package/cert/CertRunner.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
2
|
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
3
|
/**
|
|
4
4
|
* CertRunner - Orquestador del proceso de certificación SII
|
|
@@ -399,8 +399,19 @@ class CertRunner {
|
|
|
399
399
|
if (noProcessedError && intento < maxIntentos) {
|
|
400
400
|
console.log(` [...] SII aún procesando, reintentando en ${intervalo / 1000}s...`);
|
|
401
401
|
await sleep(intervalo);
|
|
402
|
+
} else if (result.conErrores) {
|
|
403
|
+
// SII reporta errores de contenido.
|
|
404
|
+
// Si verificado === false (campos vacíos), los TrackIds aún no están en el portal → reintentar.
|
|
405
|
+
// Si los campos sí aparecieron, es un error real de contenido → no reintentar.
|
|
406
|
+
if (result.verificado === false && intento < maxIntentos) {
|
|
407
|
+
console.log(` [!] ENVIO CON ERRORES pero campos vacíos — TrackIds aún no disponibles. Reintentando en ${intervalo / 1000}s...`);
|
|
408
|
+
await sleep(intervalo);
|
|
409
|
+
} else {
|
|
410
|
+
console.log(` [ERR] Contenido rechazado por SII (ENVIO CON ERRORES O REPAROS): ${(result.nombresConError || []).join(', ')}`);
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
402
413
|
} else if (result.allRejected) {
|
|
403
|
-
// SII rechazó todos los sets/libros —
|
|
414
|
+
// SII rechazó todos los sets/libros — período incorrecto, no tiene sentido reintentar
|
|
404
415
|
if (retryOnAllRejected && intento < maxIntentos) {
|
|
405
416
|
console.log(` [!] S21 — SII aún procesando TrackID, reintentando en ${intervalo / 1000}s...`);
|
|
406
417
|
await sleep(intervalo);
|
|
@@ -517,10 +528,97 @@ class CertRunner {
|
|
|
517
528
|
const resultados = {};
|
|
518
529
|
const errores = [];
|
|
519
530
|
|
|
520
|
-
//
|
|
521
|
-
|
|
531
|
+
// Consultar QEstLibro hacia atrás hasta encontrar 2 años consecutivos sin entradas.
|
|
532
|
+
// En entornos con historial extenso (ej: pruebas desde 2003) esto cubre todo el rango.
|
|
533
|
+
const _nowFase4 = new Date();
|
|
534
|
+
const _añoActual = _nowFase4.getFullYear();
|
|
535
|
+
const _ocupados = new Set();
|
|
536
|
+
let _librosPortal = {};
|
|
537
|
+
// QEstLibro siempre devuelve "Recibido" como estado — no hay distinción entre LTC/LRH.
|
|
538
|
+
// Cualquier período con entrada existente se considera ocupado (hay un libro previo enviado).
|
|
539
|
+
const _esConforme = (_estadoStr) => true; // toda entrada en QEstLibro = período ocupado
|
|
540
|
+
{
|
|
541
|
+
let _yq = _añoActual;
|
|
542
|
+
let _añosSinEntradas = 0;
|
|
543
|
+
while (_yq >= 1990) {
|
|
544
|
+
const _yStr = String(_yq);
|
|
545
|
+
let _nPeriodos = 0;
|
|
546
|
+
try {
|
|
547
|
+
// Timeout de 12s por petición para evitar colgarse en años sin datos del SII.
|
|
548
|
+
const _timeoutProm = new Promise((_, r) => setTimeout(() => r(new Error('timeout QEstLibro')), 12000));
|
|
549
|
+
const _data = await Promise.race([this.siiCert.consultarLibrosExistentes(_yStr), _timeoutProm]);
|
|
550
|
+
_nPeriodos = Object.keys(_data).length;
|
|
551
|
+
let _conformes = 0;
|
|
552
|
+
for (const [_p, _libros] of Object.entries(_data)) {
|
|
553
|
+
if (Object.values(_libros).some(l => _esConforme(l.estado))) {
|
|
554
|
+
_ocupados.add(_p);
|
|
555
|
+
_conformes++;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (_yq === _añoActual) _librosPortal = _data;
|
|
559
|
+
if (_nPeriodos > 0) console.log(`[INFO] QEstLibro ${_yStr}: ${_nPeriodos} períodos (${_conformes} bloqueados)`);
|
|
560
|
+
} catch (_e) {
|
|
561
|
+
console.warn(`[!] No se pudo consultar QEstLibro ${_yStr}: ${_e.message}`);
|
|
562
|
+
}
|
|
563
|
+
// Early exit: 2 años consecutivos sin entradas → no hay más historial que leer.
|
|
564
|
+
if (_nPeriodos === 0) {
|
|
565
|
+
_añosSinEntradas++;
|
|
566
|
+
if (_añosSinEntradas >= 2) {
|
|
567
|
+
console.log(`[INFO] QEstLibro: 2 años consecutivos sin datos (${_yStr}) — fin de escaneo.`);
|
|
568
|
+
break;
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
_añosSinEntradas = 0;
|
|
572
|
+
}
|
|
573
|
+
_yq--;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
console.log(`[INFO] Períodos bloqueados (QEstLibro): ${_ocupados.size} — rango ${[..._ocupados].sort()[0] ?? '-'} a ${[..._ocupados].sort().at(-1) ?? '-'}`);
|
|
577
|
+
|
|
578
|
+
// Período = derivado del FchDoc real de los documentos del SET.
|
|
579
|
+
// El SII genera el SET con fecha = hoy, por lo que PeriodoTributario debe coincidir
|
|
580
|
+
// con el mes de esas fechas. Usar currentMonth-1 causa "ENVIO CON ERRORES O REPAROS".
|
|
581
|
+
let _periodoBase = null;
|
|
582
|
+
const _fchDocMuestra =
|
|
583
|
+
this._estructuras?.libroCompras?.detalle?.[0]?.FchDoc ||
|
|
584
|
+
this._estructuras?.libroComprasExentos?.detalle?.[0]?.FchDoc;
|
|
585
|
+
if (_fchDocMuestra) {
|
|
586
|
+
const _dt = new Date(_fchDocMuestra);
|
|
587
|
+
if (!isNaN(_dt.getTime())) {
|
|
588
|
+
// Usar UTC para evitar que el offset de zona horaria local (ej: UTC-3 en Chile)
|
|
589
|
+
// desplace la fecha al mes anterior (ej: '2026-06-01' → 2026-05-31 local → mes 5).
|
|
590
|
+
_periodoBase = `${_dt.getUTCFullYear()}-${String(_dt.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
591
|
+
console.log(`[INFO] Período derivado de FchDoc en estructuras (${_fchDocMuestra}): ${_periodoBase}`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (!_periodoBase) {
|
|
595
|
+
// Fallback: mes actual UTC
|
|
596
|
+
_periodoBase = `${_nowFase4.getUTCFullYear()}-${String(_nowFase4.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
597
|
+
console.log(`[INFO] Período por fallback (mes actual UTC): ${_periodoBase}`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Helper: salta períodos conocidos en QEstLibro (ocupados) al buscar uno libre.
|
|
601
|
+
// Usa el set _ocupados ya construido. Siempre llama a _decrementarPeriodoLibros al menos una vez
|
|
602
|
+
// antes de entrar, así que solo sirve para saltar DESPUÉS de haber decrementado.
|
|
603
|
+
const _saltarOcupados = (tag = '') => {
|
|
604
|
+
let _saltos = 0;
|
|
605
|
+
while (_ocupados.has(this._getPeriodoLibros()) && _saltos < 400) {
|
|
606
|
+
console.log(` [skip${tag}] ${this._getPeriodoLibros()} está en QEstLibro — saltando...`);
|
|
607
|
+
this._decrementarPeriodoLibros();
|
|
608
|
+
_saltos++;
|
|
609
|
+
}
|
|
610
|
+
return _saltos;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// Usar siempre el período derivado de FchDoc como punto de partida.
|
|
614
|
+
// En entornos de certificación con historial de años, QEstLibro está lleno de
|
|
615
|
+
// entradas antiguas y saltar sobre ellas lleva a períodos LTC inesperados.
|
|
616
|
+
// Con _ocupados completo (todos los años hasta 1990), el skip inicial es seguro:
|
|
617
|
+
// salta directamente al primer período genuinamente libre.
|
|
618
|
+
this.resetPeriodoLibros(_periodoBase);
|
|
619
|
+
const _busquedaHuecos = _saltarOcupados();
|
|
522
620
|
const _periodoComunLibros = this._getPeriodoLibros();
|
|
523
|
-
console.log(` Período
|
|
621
|
+
console.log(` Período inicial: ${_periodoComunLibros}${_busquedaHuecos > 0 ? ` (saltados ${_busquedaHuecos} períodos ocupados desde ${_periodoBase})` : ''}`);
|
|
524
622
|
|
|
525
623
|
// Verificar cuáles libros ya están REVISADO CONFORME en el portal (no re-enviar)
|
|
526
624
|
let _estadoActual = {};
|
|
@@ -564,8 +662,19 @@ class CertRunner {
|
|
|
564
662
|
console.log('\nEnviando Libro de Compras...');
|
|
565
663
|
resultados.libroCompras = await this.ejecutarLibroCompras({ ...options, periodo: _periodoComunLibros });
|
|
566
664
|
if (!resultados.libroCompras.success) {
|
|
567
|
-
|
|
568
|
-
|
|
665
|
+
const _errLC = resultados.libroCompras.error || '';
|
|
666
|
+
if (/duplicado|cvc-/i.test(_errLC)) {
|
|
667
|
+
console.log('\n[!] LibroCompras: SII ya tiene envío TOTAL para este período — reintentando con AJUSTE...');
|
|
668
|
+
emitProgress(STEPS.BOOK_SENDING, { book: 'libroCompras' });
|
|
669
|
+
resultados.libroCompras = await this.ejecutarLibroCompras({ ...options, periodo: _periodoComunLibros, tipoEnvio: 'AJUSTE' });
|
|
670
|
+
}
|
|
671
|
+
if (!resultados.libroCompras.success) {
|
|
672
|
+
emitProgress(STEPS.BOOK_ERROR, { book: 'libroCompras', error: resultados.libroCompras.error });
|
|
673
|
+
errores.push(`Libro Compras: ${resultados.libroCompras.error}`);
|
|
674
|
+
} else {
|
|
675
|
+
emitProgress(STEPS.BOOK_OK, { book: 'libroCompras', trackId: resultados.libroCompras.trackId });
|
|
676
|
+
_guardarResultadosParciales();
|
|
677
|
+
}
|
|
569
678
|
} else {
|
|
570
679
|
emitProgress(STEPS.BOOK_OK, { book: 'libroCompras', trackId: resultados.libroCompras.trackId });
|
|
571
680
|
_guardarResultadosParciales();
|
|
@@ -679,19 +788,20 @@ class CertRunner {
|
|
|
679
788
|
|
|
680
789
|
// Helper para re-enviar los libros no conformes con un nuevo período
|
|
681
790
|
// keysAReenviar: Set opcional — si se pasa, solo re-envía las claves del Set
|
|
682
|
-
|
|
791
|
+
// tipoEnvio: 'TOTAL' (default) o 'AJUSTE' (fallback cuando todos los períodos están agotados)
|
|
792
|
+
const _reenviarLibros = async (nuevoPeriodo, keysAReenviar, tipoEnvio) => {
|
|
683
793
|
const _orden = [
|
|
684
|
-
{ key: 'libroCompras', fn: (p) => this.ejecutarLibroCompras({ ...options, periodo: p }) },
|
|
685
|
-
{ key: 'libroVentas', fn: (p) => this.ejecutarLibroVentas({ ...options, periodo: p }) },
|
|
686
|
-
{ key: 'libroGuias', fn: (p) => this.ejecutarLibroGuias({ ...options, periodo: p }) },
|
|
687
|
-
{ key: 'libroComprasExentos', fn: (p) => this.ejecutarLibroComprasExentos({ ...options, periodo: p }) },
|
|
794
|
+
{ key: 'libroCompras', fn: (p, te) => this.ejecutarLibroCompras({ ...options, periodo: p, tipoEnvio: te }) },
|
|
795
|
+
{ key: 'libroVentas', fn: (p, te) => this.ejecutarLibroVentas({ ...options, periodo: p, tipoEnvio: te }) },
|
|
796
|
+
{ key: 'libroGuias', fn: (p, te) => this.ejecutarLibroGuias({ ...options, periodo: p, tipoEnvio: te }) },
|
|
797
|
+
{ key: 'libroComprasExentos', fn: (p, te) => this.ejecutarLibroComprasExentos({ ...options, periodo: p, tipoEnvio: te }) },
|
|
688
798
|
];
|
|
689
799
|
for (const { key, fn } of _orden) {
|
|
690
800
|
if (resultados[key]?.conforme) continue; // ya conforme en SII
|
|
691
801
|
if (keysAReenviar && !keysAReenviar.has(key)) continue; // filtro por S21
|
|
692
802
|
emitProgress(STEPS.BOOK_SENDING, { book: key });
|
|
693
803
|
try {
|
|
694
|
-
resultados[key] = await fn(nuevoPeriodo);
|
|
804
|
+
resultados[key] = await fn(nuevoPeriodo, tipoEnvio);
|
|
695
805
|
if (!resultados[key].success) {
|
|
696
806
|
emitProgress(STEPS.BOOK_ERROR, { book: key, error: resultados[key].error });
|
|
697
807
|
} else {
|
|
@@ -701,27 +811,74 @@ class CertRunner {
|
|
|
701
811
|
resultados[key] = { success: false, error: e.message };
|
|
702
812
|
}
|
|
703
813
|
}
|
|
814
|
+
|
|
815
|
+
// Polling SOAP para detectar LNC antes de intentar declarar.
|
|
816
|
+
// Si el SII rechaza el envío (LNC), limpiamos el trackId para que declararLibros
|
|
817
|
+
// no incluya ese libro, y marcamos el período como ocupado para no reintentarlo.
|
|
818
|
+
const _reenvTids = _orden.filter(({ key }) =>
|
|
819
|
+
!resultados[key]?.conforme &&
|
|
820
|
+
(!keysAReenviar || keysAReenviar.has(key)) &&
|
|
821
|
+
resultados[key]?.trackId
|
|
822
|
+
);
|
|
823
|
+
if (_reenvTids.length > 0) {
|
|
824
|
+
const _nombresReenv = _reenvTids.map(({ key }) => _KEY_A_SII_NOMBRE[key] || key);
|
|
825
|
+
console.log(`\n[SOAP-retry] Verificando estado de libros re-enviados (${_nombresReenv.join(', ')})...`);
|
|
826
|
+
await sleep(15000);
|
|
827
|
+
const _sRPend = new Set(_reenvTids.map(b => b.key));
|
|
828
|
+
const _sRErr = {};
|
|
829
|
+
for (let _ri = 0; _ri < 20 && _sRPend.size > 0; _ri++) {
|
|
830
|
+
for (const { key } of _reenvTids) {
|
|
831
|
+
if (!_sRPend.has(key)) continue;
|
|
832
|
+
const _tid = resultados[key].trackId;
|
|
833
|
+
const _nom = _KEY_A_SII_NOMBRE[key] || key;
|
|
834
|
+
try {
|
|
835
|
+
const _est = await _enviadorSoap.consultarEstadoSoap(_tid, _rutEmisor);
|
|
836
|
+
if (_est.ok === false) {
|
|
837
|
+
_sRErr[key] = (_sRErr[key] || 0) + 1;
|
|
838
|
+
if (_sRErr[key] >= 5) _sRPend.delete(key);
|
|
839
|
+
} else {
|
|
840
|
+
_sRErr[key] = 0;
|
|
841
|
+
if (!_est.esIntermedio) {
|
|
842
|
+
_sRPend.delete(key);
|
|
843
|
+
const _glosa = _est.glosa || _est.estado || '?';
|
|
844
|
+
if (_est.esRechazado) {
|
|
845
|
+
console.log(` [SOAP-retry] ${_nom} (${_tid}): [LNC] ${_glosa} — período ${nuevoPeriodo} ocupado`);
|
|
846
|
+
_ocupados.add(nuevoPeriodo);
|
|
847
|
+
resultados[key] = { success: false, error: `LNC: ${_glosa}`, periodo: nuevoPeriodo };
|
|
848
|
+
} else {
|
|
849
|
+
console.log(` [SOAP-retry] ${_nom} (${_tid}): [LOK] ${_glosa}`);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
} catch (_e) {
|
|
854
|
+
_sRErr[key] = (_sRErr[key] || 0) + 1;
|
|
855
|
+
if (_sRErr[key] >= 5) _sRPend.delete(key);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
if (_sRPend.size > 0) await sleep(15000);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
704
861
|
};
|
|
705
862
|
|
|
706
863
|
// Polling de aprobación (reutilizable)
|
|
707
864
|
// librosAVerificar: array de nombres SII a esperar. Si se omite, usa todos los no-conformes.
|
|
708
865
|
// Devuelve { ok, estadosFinal }
|
|
709
|
-
//
|
|
866
|
+
// S21 = libro enviado al portal, PENDIENTE DE REVISIÓN — estado normal de espera.
|
|
867
|
+
// NO es un error de período. Solo LNC/LRH son errores reales que requieren acción.
|
|
710
868
|
const _esperarAprobacion = async (librosAVerificar) => {
|
|
711
869
|
const _todosCandidatos = ['LIBRO DE VENTAS', 'LIBRO DE COMPRAS', 'LIBRO DE GUIAS'];
|
|
712
870
|
if (this._estructuras?.libroComprasExentos) _todosCandidatos.push('LIBRO DE COMPRAS PARA EXENTOS');
|
|
713
871
|
const _librosAVerif = librosAVerificar || _todosCandidatos.filter(n => !_estaConforme(n));
|
|
714
872
|
console.log(`\nEsperando aprobacion del SII para: ${_librosAVerif.join(', ')}`);
|
|
715
873
|
let _ss = {};
|
|
716
|
-
let
|
|
717
|
-
for (let _i = 0; _i < 20; _i++) {
|
|
874
|
+
for (let _i = 0; _i < 40; _i++) {
|
|
718
875
|
await sleep(15000);
|
|
719
|
-
emitProgress(STEPS.POLLING, { intento: _i + 1, max:
|
|
876
|
+
emitProgress(STEPS.POLLING, { intento: _i + 1, max: 40, label: 'libros' });
|
|
720
877
|
const _poll = await this.siiCert.consultarEstadoSets();
|
|
721
878
|
if (!_poll.success) continue;
|
|
722
879
|
_ss = _poll.estadoSets || {};
|
|
723
880
|
const _info = Object.entries(_ss).filter(([k]) => k.toUpperCase().includes('LIBRO')).map(([k, v]) => `${k.trim()}: ${v}`);
|
|
724
|
-
if (_info.length) console.log(` [...] Intento ${_i + 1}/
|
|
881
|
+
if (_info.length) console.log(` [...] Intento ${_i + 1}/40: ${_info.join(' | ')}`);
|
|
725
882
|
const _librosObs = ['LIBRO DE VENTAS', 'LIBRO DE COMPRAS', 'LIBRO DE GUIAS'];
|
|
726
883
|
const _todosObligatoriosOk = _librosObs.every(n => {
|
|
727
884
|
const e = _findEntry(_ss, n);
|
|
@@ -736,6 +893,11 @@ class CertRunner {
|
|
|
736
893
|
return e && (e[1] === 'LNC' || e[1] === 'LRH' || e[1].includes('RECHAZADO') || e[1].includes('ERROR'));
|
|
737
894
|
});
|
|
738
895
|
if (_todosOk) {
|
|
896
|
+
// Marcar conforme en resultados para que el RESUMEN final sea correcto.
|
|
897
|
+
for (const n of _librosAVerif) {
|
|
898
|
+
const _cKey = _SII_NOMBRE_A_KEY[n];
|
|
899
|
+
if (_cKey && resultados[_cKey]) resultados[_cKey].conforme = true;
|
|
900
|
+
}
|
|
739
901
|
emitProgress(STEPS.BOOKS_DONE);
|
|
740
902
|
console.log('\n[OK] LIBROS APROBADOS POR EL SII!');
|
|
741
903
|
return { ok: true, estadosFinal: _ss };
|
|
@@ -744,46 +906,136 @@ class CertRunner {
|
|
|
744
906
|
console.log('\n[ERR] Hay libros rechazados. Revisar emails del SII.');
|
|
745
907
|
return { ok: false, estadosFinal: _ss };
|
|
746
908
|
}
|
|
747
|
-
//
|
|
909
|
+
// S21 = procesando. Informar pero seguir esperando (no bail-out).
|
|
748
910
|
const _pendientesAun = _librosAVerif.filter(n => {
|
|
749
911
|
const e = _findEntry(_ss, n);
|
|
750
912
|
return !e || (e[1] !== 'REVISADO CONFORME' && e[1] !== 'S25');
|
|
751
913
|
});
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
return e && e[1] === 'S21';
|
|
755
|
-
});
|
|
756
|
-
if (_todosS21) {
|
|
757
|
-
_consecutivosS21++;
|
|
758
|
-
if (_consecutivosS21 >= 5) {
|
|
759
|
-
console.log(`\n[!] ${_pendientesAun.join(', ')} llevan ${_consecutivosS21} polls en S21 — período incorrecto.`);
|
|
760
|
-
return { ok: false, estadosFinal: _ss, stuckS21: true };
|
|
761
|
-
}
|
|
762
|
-
} else {
|
|
763
|
-
_consecutivosS21 = 0;
|
|
914
|
+
if (_pendientesAun.length > 0 && (_i + 1) % 4 === 0) {
|
|
915
|
+
console.log(` [...] Aún esperando (${Math.round((_i + 1) * 15 / 60)} min): ${_pendientesAun.join(', ')}`);
|
|
764
916
|
}
|
|
765
917
|
}
|
|
766
|
-
console.log('\n[!] Timeout (
|
|
918
|
+
console.log('\n[!] Timeout (10 min). El SII aún no responde. Verifica con --avance más tarde.');
|
|
767
919
|
return { ok: false, estadosFinal: _ss };
|
|
768
920
|
};
|
|
769
921
|
|
|
922
|
+
// Polling SOAP: esperar que el SII procese los XMLs antes de declarar.
|
|
923
|
+
// Solo se sigue esperando mientras esIntermedio === true (REC, SOK, FOK, PRD, CRT, DNK...).
|
|
924
|
+
// Estados como LSO (no catalogado) no tienen esIntermedio = true → se consideran "listos".
|
|
925
|
+
const _librosParaConsultar = [
|
|
926
|
+
{ key: 'libroCompras', nombre: 'Libro Compras' },
|
|
927
|
+
{ key: 'libroVentas', nombre: 'Libro Ventas' },
|
|
928
|
+
{ key: 'libroGuias', nombre: 'Libro Guias' },
|
|
929
|
+
{ key: 'libroComprasExentos', nombre: 'Libro Compras Exentos' },
|
|
930
|
+
].filter(({ key }) => resultados[key]?.trackId);
|
|
931
|
+
|
|
932
|
+
const _enviadorSoap = this._createLibroEnviador();
|
|
933
|
+
const _rutEmisor = this.config.emisor.rut;
|
|
934
|
+
|
|
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;
|
|
938
|
+
const _SOAP_INTERVAL_MS = 15000;
|
|
939
|
+
let _soapPendientes = new Set(_librosParaConsultar.map(l => l.key));
|
|
940
|
+
const _soapFinalStates = {}; // key → resultado SOAP terminal (LOK/LNC/LRH/etc.)
|
|
941
|
+
const _soapErrCount = {}; // key → contador de errores SOAP consecutivos (ok:false o throw)
|
|
942
|
+
const _MAX_SOAP_ERR = 10; // reintentos antes de desistir por errores transitorios
|
|
943
|
+
|
|
944
|
+
console.log('\n[SOAP] Consultando estado de envíos (espera hasta 15 min)...');
|
|
945
|
+
await sleep(15000); // espera inicial — SII tarda al menos 15s en validar schema
|
|
946
|
+
for (let _pi = 0; _pi < _MAX_SOAP_POLLS && _soapPendientes.size > 0; _pi++) {
|
|
947
|
+
for (const { key, nombre } of _librosParaConsultar) {
|
|
948
|
+
if (!_soapPendientes.has(key)) continue;
|
|
949
|
+
const _tid = resultados[key].trackId;
|
|
950
|
+
try {
|
|
951
|
+
const _est = await _enviadorSoap.consultarEstadoSoap(_tid, _rutEmisor);
|
|
952
|
+
const _estado = _est.estado || '?';
|
|
953
|
+
const _detalle = _est.glosa || _est.mensaje || _est.error || 'sin detalle';
|
|
954
|
+
if (_est.ok === false) {
|
|
955
|
+
// Sin tag <ESTADO> en respuesta — error de parseo/red, no un rechazo real.
|
|
956
|
+
// Reintentar hasta _MAX_SOAP_ERR veces consecutivas antes de desistir.
|
|
957
|
+
_soapErrCount[key] = (_soapErrCount[key] || 0) + 1;
|
|
958
|
+
if (_soapErrCount[key] >= _MAX_SOAP_ERR) {
|
|
959
|
+
_soapPendientes.delete(key);
|
|
960
|
+
console.log(` [SOAP] ${nombre} (${_tid}): [?] demasiados errores — desistiendo`);
|
|
961
|
+
} else {
|
|
962
|
+
console.log(` [SOAP] ${nombre} (${_tid}): [?] error transitorio (${_soapErrCount[key]}/${_MAX_SOAP_ERR}) — reintentando...`);
|
|
963
|
+
}
|
|
964
|
+
} else {
|
|
965
|
+
_soapErrCount[key] = 0; // reset en cualquier respuesta válida
|
|
966
|
+
if (!_est.esIntermedio) {
|
|
967
|
+
// Estado terminal: LOK (exitoso), LNC/LRH (rechazado), u otro no catalogado
|
|
968
|
+
_soapPendientes.delete(key);
|
|
969
|
+
_soapFinalStates[key] = _est;
|
|
970
|
+
const _tag = _est.esExitoso ? '[OK]' : _est.esRechazado ? '[ERR]' : '[->]';
|
|
971
|
+
console.log(` [SOAP] ${nombre} (${_tid}): ${_tag} ${_estado} — ${_detalle}`);
|
|
972
|
+
} else {
|
|
973
|
+
const _elapsed = Math.round((_pi * _SOAP_INTERVAL_MS + 15000) / 1000);
|
|
974
|
+
console.log(` [SOAP] ${nombre} (${_tid}): [...] ${_estado} — aún procesando (${_elapsed}s / ${_pi + 1}/${_MAX_SOAP_POLLS})`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
} catch (_e) {
|
|
978
|
+
_soapErrCount[key] = (_soapErrCount[key] || 0) + 1;
|
|
979
|
+
if (_soapErrCount[key] >= _MAX_SOAP_ERR) {
|
|
980
|
+
_soapPendientes.delete(key);
|
|
981
|
+
console.log(` [SOAP] ${nombre} (${_tid}): error — máx reintentos: ${_e.message}`);
|
|
982
|
+
} else {
|
|
983
|
+
console.log(` [SOAP] ${nombre} (${_tid}): error (${_soapErrCount[key]}/${_MAX_SOAP_ERR}) — reintentando: ${_e.message}`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (_soapPendientes.size > 0) await sleep(_SOAP_INTERVAL_MS);
|
|
988
|
+
}
|
|
989
|
+
if (_soapPendientes.size > 0) {
|
|
990
|
+
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.`);
|
|
992
|
+
}
|
|
993
|
+
|
|
770
994
|
// 4. Declarar + retry automático:
|
|
771
|
-
// a) si allRejected al declarar → decrementar período y re-enviar
|
|
995
|
+
// a) si allRejected al declarar → decrementar período y re-enviar TOTAL
|
|
996
|
+
// (el período ya fue procesado en otra sesión → buscar uno libre)
|
|
772
997
|
// b) si libros quedan en S21 tras polling → decrementar y re-enviar solo los S21
|
|
773
998
|
emitProgress(STEPS.BOOKS_DECLARING);
|
|
774
999
|
console.log('\nDeclarando libros...');
|
|
775
|
-
|
|
1000
|
+
// 24 = ~2 años de meses. En entorno cert con histórico extenso puede haber
|
|
1001
|
+
// muchos períodos LTC consecutivos antes de encontrar uno libre.
|
|
1002
|
+
const MAX_PERIOD_RETRIES = 24;
|
|
1003
|
+
|
|
1004
|
+
// _librosPortal ya fue consultado arriba (antes de fijar período inicial)
|
|
1005
|
+
|
|
776
1006
|
try {
|
|
777
1007
|
let declaracion = await this.declararLibros({ ...resultados, ...(options.setsResultados || {}) });
|
|
778
1008
|
resultados.declaracion = declaracion;
|
|
779
1009
|
|
|
780
|
-
// Fase a: allRejected
|
|
1010
|
+
// Fase a: allRejected → el período ya tiene data en el portal.
|
|
1011
|
+
// Estrategia: decrementar uno a uno desde el período actual.
|
|
1012
|
+
// - Si tenemos LTC guardado localmente → AJUSTE una vez antes de decrementar.
|
|
1013
|
+
// - Si no hay LTC o AJUSTE también falló → decrementar y probar TOTAL.
|
|
1014
|
+
// No hacemos saltos basados en el portal: QEstLibro solo cubre el año actual
|
|
1015
|
+
// y puede haber entradas en años anteriores que no conocemos.
|
|
1016
|
+
const _intentadoAjuste = new Set();
|
|
1017
|
+
|
|
781
1018
|
for (let _pRetry = 0; _pRetry < MAX_PERIOD_RETRIES && declaracion.allRejected; _pRetry++) {
|
|
782
|
-
this.
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
1019
|
+
const _periodoActual = this._getPeriodoLibros();
|
|
1020
|
+
const _tenemoLtc = !!this._leerLtcTotales(_periodoActual, 'COMPRA') || !!this._leerLtcTotales(_periodoActual, 'VENTA');
|
|
1021
|
+
const _yaIntentadoAjuste = _intentadoAjuste.has(_periodoActual);
|
|
1022
|
+
|
|
1023
|
+
if (_tenemoLtc && !_yaIntentadoAjuste) {
|
|
1024
|
+
console.log(`\n[!] ${_periodoActual}: LTC guardado — re-enviando con AJUSTE (intento ${_pRetry + 1})...`);
|
|
1025
|
+
emitProgress(STEPS.BOOK_PERIOD_RETRY, { periodo: _periodoActual, intento: String(_pRetry + 1) });
|
|
1026
|
+
_intentadoAjuste.add(_periodoActual);
|
|
1027
|
+
await _reenviarLibros(_periodoActual, undefined, 'AJUSTE');
|
|
1028
|
+
} else {
|
|
1029
|
+
// Marcar este período como ocupado dinámicamente y saltar al siguiente libre.
|
|
1030
|
+
_ocupados.add(_periodoActual);
|
|
1031
|
+
this._decrementarPeriodoLibros();
|
|
1032
|
+
_saltarOcupados('a'); // salta períodos conocidos (QEstLibro + descubiertos en tiempo real)
|
|
1033
|
+
const _nuevoPeriodo = this._getPeriodoLibros();
|
|
1034
|
+
const _razon = _yaIntentadoAjuste ? 'AJUSTE falló' : 'sin LTC local';
|
|
1035
|
+
console.log(`\n[!] ${_periodoActual} (${_razon}) — probando TOTAL con ${_nuevoPeriodo} (intento ${_pRetry + 1})...`);
|
|
1036
|
+
emitProgress(STEPS.BOOK_PERIOD_RETRY, { periodo: _nuevoPeriodo, intento: String(_pRetry + 1) });
|
|
1037
|
+
await _reenviarLibros(_nuevoPeriodo, undefined, undefined);
|
|
1038
|
+
}
|
|
787
1039
|
declaracion = await this.declararLibros({ ...resultados, ...(options.setsResultados || {}) });
|
|
788
1040
|
resultados.declaracion = declaracion;
|
|
789
1041
|
}
|
|
@@ -796,33 +1048,103 @@ class CertRunner {
|
|
|
796
1048
|
if (this._estructuras?.libroComprasExentos) _todosLibrosNombres.push('LIBRO DE COMPRAS PARA EXENTOS');
|
|
797
1049
|
let _librosAVerificar = _todosLibrosNombres.filter(n => !_estaConforme(n));
|
|
798
1050
|
|
|
799
|
-
|
|
1051
|
+
// Si SOAP detectó LNC/LRH para algún libro → excluirlos de la espera portal.
|
|
1052
|
+
// El portal mostrará S21 para ellos (nunca resolverá) → phase b los maneja directamente.
|
|
1053
|
+
const _soapLncNombres = Object.entries(_soapFinalStates)
|
|
1054
|
+
.filter(([, est]) => est?.esRechazado)
|
|
1055
|
+
.map(([k]) => _KEY_A_SII_NOMBRE[k])
|
|
1056
|
+
.filter(Boolean);
|
|
1057
|
+
// Si la declaración reportó errores de contenido para algún libro → también excluirlos.
|
|
1058
|
+
// Esos libros tienen S21 en portal pero nunca resolverán a REVISADO CONFORME desde
|
|
1059
|
+
// esta declaración → phase b los maneja directamente con nuevo período.
|
|
1060
|
+
const _declaracionConErrorNombres = (declaracion.nombresConError || []);
|
|
1061
|
+
const _excluirDeEspera = [...new Set([..._soapLncNombres, ..._declaracionConErrorNombres])];
|
|
1062
|
+
const _librosAEsperar = _librosAVerificar.filter(n => !_excluirDeEspera.includes(n));
|
|
1063
|
+
if (_soapLncNombres.length > 0) {
|
|
1064
|
+
console.log(`\n[!] LNC vía SOAP en: ${_soapLncNombres.join(', ')} — esperando portal solo para: ${_librosAEsperar.join(', ') || 'ninguno'}`);
|
|
1065
|
+
}
|
|
1066
|
+
if (_declaracionConErrorNombres.length > 0) {
|
|
1067
|
+
console.log(`\n[!] Errores de contenido en declaración: ${_declaracionConErrorNombres.join(', ')} — no esperando portal para ellos`);
|
|
1068
|
+
}
|
|
800
1069
|
|
|
801
|
-
|
|
1070
|
+
let ok, estadosFinal;
|
|
1071
|
+
if (_librosAEsperar.length > 0) {
|
|
1072
|
+
({ ok, estadosFinal } = await _esperarAprobacion(_librosAEsperar));
|
|
1073
|
+
} else {
|
|
1074
|
+
ok = false;
|
|
1075
|
+
estadosFinal = {};
|
|
1076
|
+
}
|
|
1077
|
+
// Inyectar LNC de SOAP y errores de contenido de declaración en estadosFinal
|
|
1078
|
+
for (const n of _soapLncNombres) estadosFinal[n] = 'LNC';
|
|
1079
|
+
// Libros con errores de contenido en declaración = mismo tratamiento que LNC: period incorrecto
|
|
1080
|
+
for (const n of _declaracionConErrorNombres) if (!estadosFinal[n]) estadosFinal[n] = 'LNC';
|
|
1081
|
+
if (_excluirDeEspera.length > 0) ok = false;
|
|
1082
|
+
|
|
1083
|
+
// Fase b: libros con LNC/LRH/S21-timeout → reintentar con AJUSTE (si tenemos LTC) o nuevo período.
|
|
1084
|
+
// - LNC/LRH = período tiene LTC → intentar AJUSTE si hay datos, sino decrementar.
|
|
1085
|
+
// - S21 después de timeout = upload probablemente rechazado (SOAP LNC no reflejado en pe_avance2)
|
|
1086
|
+
// → mismo tratamiento que LNC.
|
|
1087
|
+
const _intentadoAjusteB = new Set();
|
|
802
1088
|
for (let _pRetry = 0; !ok && _pRetry < MAX_PERIOD_RETRIES; _pRetry++) {
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
1089
|
+
// Libros con LNC/LRH o S21-timeout (no resuelto → asumimos error de período)
|
|
1090
|
+
const _fallidos = Object.entries(estadosFinal)
|
|
1091
|
+
.filter(([k, v]) => {
|
|
1092
|
+
const ku = k.toUpperCase();
|
|
1093
|
+
const esLibro = ku.includes('LIBRO');
|
|
1094
|
+
const esFallido = (v === 'LNC' || v === 'LRH' || String(v).includes('RECHAZADO') || v === 'S21');
|
|
1095
|
+
return esLibro && esFallido;
|
|
1096
|
+
});
|
|
1097
|
+
const _fallidosKeys = _fallidos.map(([k]) => _SII_NOMBRE_A_KEY[k]).filter(Boolean);
|
|
1098
|
+
if (_fallidosKeys.length === 0) break; // todos conformes → salir
|
|
1099
|
+
|
|
1100
|
+
const _periodoFase = this._getPeriodoLibros();
|
|
1101
|
+
// Solo AJUSTE para libros que tienen su propio LTC guardado.
|
|
1102
|
+
// LibroGuias y LibroComprasExentos nunca tienen LTC → van siempre a TOTAL en período nuevo.
|
|
1103
|
+
const _LTC_TIPO_POR_LIBRO_B = { libroCompras: 'COMPRA', libroVentas: 'VENTA' };
|
|
1104
|
+
const _hayLtcB = _fallidosKeys.some(k => {
|
|
1105
|
+
const tipo = _LTC_TIPO_POR_LIBRO_B[k];
|
|
1106
|
+
return tipo && !!this._leerLtcTotales(_periodoFase, tipo);
|
|
1107
|
+
});
|
|
1108
|
+
const _yaAjusteB = _intentadoAjusteB.has(_periodoFase);
|
|
1109
|
+
const _fallidosNombres = _fallidosKeys.map(k => _KEY_A_SII_NOMBRE[k]).filter(Boolean);
|
|
1110
|
+
|
|
1111
|
+
if (_hayLtcB && !_yaAjusteB) {
|
|
1112
|
+
// Tenemos LTC guardado para este período → intentar AJUSTE antes de cambiar período
|
|
1113
|
+
console.log(`\n[!] ${_fallidosNombres.join(', ')} fallidos en ${_periodoFase} — LTC local existe, probando AJUSTE (intento ${_pRetry + 1})...`);
|
|
1114
|
+
emitProgress(STEPS.BOOK_PERIOD_RETRY, { periodo: _periodoFase, intento: String(_pRetry + 1) });
|
|
1115
|
+
_intentadoAjusteB.add(_periodoFase);
|
|
1116
|
+
await _reenviarLibros(_periodoFase, new Set(_fallidosKeys), 'AJUSTE');
|
|
1117
|
+
} else {
|
|
1118
|
+
// Sin LTC o AJUSTE ya intentado → marcar como ocupado y buscar período libre
|
|
1119
|
+
_ocupados.add(_periodoFase);
|
|
1120
|
+
this._decrementarPeriodoLibros();
|
|
1121
|
+
_saltarOcupados('b');
|
|
1122
|
+
const _nuevoPeriodo = this._getPeriodoLibros();
|
|
1123
|
+
const _razonB = _yaAjusteB ? 'AJUSTE falló' : 'sin LTC local';
|
|
1124
|
+
console.log(`\n[!] ${_fallidosNombres.join(', ')} (${_razonB}) — probando TOTAL en ${_nuevoPeriodo} (intento ${_pRetry + 1})...`);
|
|
1125
|
+
emitProgress(STEPS.BOOK_PERIOD_RETRY, { periodo: _nuevoPeriodo, intento: String(_pRetry + 1) });
|
|
1126
|
+
await _reenviarLibros(_nuevoPeriodo, new Set(_fallidosKeys), undefined);
|
|
1127
|
+
}
|
|
813
1128
|
declaracion = await this.declararLibros({ ...resultados, ...(options.setsResultados || {}) });
|
|
814
1129
|
resultados.declaracion = declaracion;
|
|
815
1130
|
|
|
816
1131
|
if (!declaracion.success && !declaracion.allRejected) {
|
|
817
|
-
|
|
818
|
-
|
|
1132
|
+
// Casos en que NO se debe cortar el loop:
|
|
1133
|
+
// 1. Tras intentar AJUSTE → la siguiente iteración buscará período libre.
|
|
1134
|
+
// 2. 'No hay libros para declarar' → todos los re-enviados tuvieron LNC en SOAP → continuar.
|
|
1135
|
+
const _todosSoapLnc = (declaracion.error || '').includes('No hay libros para declarar');
|
|
1136
|
+
if (_intentadoAjusteB.size > 0 || _todosSoapLnc) {
|
|
1137
|
+
const _msg = _todosSoapLnc ? 'todos LNC en SOAP' : `AJUSTE falló (${declaracion.error})`;
|
|
1138
|
+
console.log(`\n[!] Declaración fallida (${_msg}) — buscando período libre...`);
|
|
1139
|
+
} else {
|
|
1140
|
+
console.log(`\n[ERR] Declaración fallida: ${declaracion.error}`);
|
|
1141
|
+
break;
|
|
1142
|
+
}
|
|
819
1143
|
}
|
|
820
1144
|
if (declaracion.success) {
|
|
821
|
-
|
|
822
|
-
_librosAVerificar = _s21Nombres;
|
|
1145
|
+
_librosAVerificar = _fallidosNombres;
|
|
823
1146
|
;({ ok, estadosFinal } = await _esperarAprobacion(_librosAVerificar));
|
|
824
1147
|
}
|
|
825
|
-
// si allRejected → continuar loop (decrementar de nuevo)
|
|
826
1148
|
}
|
|
827
1149
|
|
|
828
1150
|
if (!ok) {
|
|
@@ -984,6 +1306,47 @@ class CertRunner {
|
|
|
984
1306
|
}
|
|
985
1307
|
}
|
|
986
1308
|
|
|
1309
|
+
/**
|
|
1310
|
+
* Guarda los totales del LTC (envío TOTAL/MENSUAL) para usar en futuros AJUSTE.
|
|
1311
|
+
* @param {string} periodo - Período YYYY-MM
|
|
1312
|
+
* @param {string} tipo - 'COMPRA' | 'VENTA'
|
|
1313
|
+
* @param {Array} resumen - Array de totales por TpoDoc (igual estructura que setResumen)
|
|
1314
|
+
* @private
|
|
1315
|
+
*/
|
|
1316
|
+
_guardarLtcTotales(periodo, tipo, resumen) {
|
|
1317
|
+
const ltcFile = path.join(this.debugDir, 'ltc-totales.json');
|
|
1318
|
+
let data = {};
|
|
1319
|
+
try {
|
|
1320
|
+
if (fs.existsSync(ltcFile)) data = JSON.parse(fs.readFileSync(ltcFile, 'utf8'));
|
|
1321
|
+
} catch (e) { /* usar vacío */ }
|
|
1322
|
+
if (!data[periodo]) data[periodo] = {};
|
|
1323
|
+
data[periodo][tipo] = resumen;
|
|
1324
|
+
try {
|
|
1325
|
+
fs.writeFileSync(ltcFile, JSON.stringify(data, null, 2));
|
|
1326
|
+
console.log(` [LTC] Totales guardados: ${periodo}/${tipo} (${resumen.length} tipos doc)`);
|
|
1327
|
+
} catch (e) {
|
|
1328
|
+
console.warn(` [!] No se pudo guardar ltcTotales: ${e.message}`);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* Lee los totales LTC guardados para un período y tipo.
|
|
1334
|
+
* @param {string} periodo - Período YYYY-MM
|
|
1335
|
+
* @param {string} tipo - 'COMPRA' | 'VENTA'
|
|
1336
|
+
* @returns {Array|null} Array de totales o null si no hay datos
|
|
1337
|
+
* @private
|
|
1338
|
+
*/
|
|
1339
|
+
_leerLtcTotales(periodo, tipo) {
|
|
1340
|
+
const ltcFile = path.join(this.debugDir, 'ltc-totales.json');
|
|
1341
|
+
try {
|
|
1342
|
+
if (fs.existsSync(ltcFile)) {
|
|
1343
|
+
const data = JSON.parse(fs.readFileSync(ltcFile, 'utf8'));
|
|
1344
|
+
return data[periodo]?.[tipo] || null;
|
|
1345
|
+
}
|
|
1346
|
+
} catch (e) { /* ignorar */ }
|
|
1347
|
+
return null;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
987
1350
|
/**
|
|
988
1351
|
* Crea un enviador de libros
|
|
989
1352
|
* @private
|
|
@@ -1016,9 +1379,21 @@ class CertRunner {
|
|
|
1016
1379
|
periodo,
|
|
1017
1380
|
certificado: this.certificado,
|
|
1018
1381
|
signoNC: options.signoNC || 'POSITIVO',
|
|
1382
|
+
tipoEnvio: options.tipoEnvio || 'TOTAL',
|
|
1019
1383
|
});
|
|
1020
1384
|
|
|
1021
|
-
const { libro, xml, detalle, resumen } = libroVentas.generar(setBasicoResult);
|
|
1385
|
+
const { libro, xml: _xmlVentas, detalle, resumen } = libroVentas.generar(setBasicoResult);
|
|
1386
|
+
|
|
1387
|
+
// Si es AJUSTE, inyectar LTC para que TotalesPeriodo sea acumulado correcto
|
|
1388
|
+
const _tipoEnvioVentas = options.tipoEnvio || 'TOTAL';
|
|
1389
|
+
if (_tipoEnvioVentas === 'AJUSTE') {
|
|
1390
|
+
const _ltcVentas = this._leerLtcTotales(periodo, 'VENTA');
|
|
1391
|
+
if (_ltcVentas) {
|
|
1392
|
+
libro.setLtcTotales(_ltcVentas);
|
|
1393
|
+
libro.generar(); // re-firma con TotalesPeriodo correcto
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
const xml = libro.getXML() || _xmlVentas;
|
|
1022
1397
|
|
|
1023
1398
|
// Guardar XML de debug
|
|
1024
1399
|
const outPath = path.join(this.debugDir, 'libro-ventas.xml');
|
|
@@ -1037,9 +1412,13 @@ class CertRunner {
|
|
|
1037
1412
|
};
|
|
1038
1413
|
|
|
1039
1414
|
this.resultados.libroVentas = result;
|
|
1040
|
-
|
|
1415
|
+
|
|
1041
1416
|
if (result.success) {
|
|
1042
1417
|
console.log(` [OK] Libro de Ventas enviado - TrackId: ${result.trackId}`);
|
|
1418
|
+
// Persistir totales LTC para futuros AJUSTE de este período
|
|
1419
|
+
if (_tipoEnvioVentas === 'TOTAL') {
|
|
1420
|
+
this._guardarLtcTotales(periodo, 'VENTA', resumen);
|
|
1421
|
+
}
|
|
1043
1422
|
} else {
|
|
1044
1423
|
console.log(` [ERR] Error enviando Libro de Ventas: ${result.error}`);
|
|
1045
1424
|
}
|
|
@@ -1062,6 +1441,7 @@ class CertRunner {
|
|
|
1062
1441
|
emisor: this.config.emisor,
|
|
1063
1442
|
periodo,
|
|
1064
1443
|
certificado: this.certificado,
|
|
1444
|
+
tipoEnvio: options.tipoEnvio || 'TOTAL',
|
|
1065
1445
|
});
|
|
1066
1446
|
|
|
1067
1447
|
if (!libroComprasData?.detalle) {
|
|
@@ -1069,7 +1449,18 @@ class CertRunner {
|
|
|
1069
1449
|
}
|
|
1070
1450
|
|
|
1071
1451
|
console.log(` Generando Libro de Compras para período ${periodo} (${libroComprasData.detalle.length} documentos del SII)...`);
|
|
1072
|
-
const { libro, xml, detalle, resumen } = libroCompras.generarDesdeEstructuras(libroComprasData, periodo);
|
|
1452
|
+
const { libro, xml: _xmlCompras, detalle, resumen } = libroCompras.generarDesdeEstructuras(libroComprasData, periodo);
|
|
1453
|
+
|
|
1454
|
+
// Si es AJUSTE, inyectar LTC para que TotalesPeriodo sea acumulado correcto
|
|
1455
|
+
const _tipoEnvioCompras = options.tipoEnvio || 'TOTAL';
|
|
1456
|
+
if (_tipoEnvioCompras === 'AJUSTE') {
|
|
1457
|
+
const _ltcCompras = this._leerLtcTotales(periodo, 'COMPRA');
|
|
1458
|
+
if (_ltcCompras) {
|
|
1459
|
+
libro.setLtcTotales(_ltcCompras);
|
|
1460
|
+
libro.generar(); // re-firma con TotalesPeriodo correcto
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
const xml = libro.getXML() || _xmlCompras;
|
|
1073
1464
|
|
|
1074
1465
|
// Guardar XML de debug
|
|
1075
1466
|
const outPath = path.join(this.debugDir, 'libro-compras.xml');
|
|
@@ -1088,9 +1479,13 @@ class CertRunner {
|
|
|
1088
1479
|
};
|
|
1089
1480
|
|
|
1090
1481
|
this.resultados.libroCompras = result;
|
|
1091
|
-
|
|
1482
|
+
|
|
1092
1483
|
if (result.success) {
|
|
1093
1484
|
console.log(` [OK] Libro de Compras enviado - TrackId: ${result.trackId}`);
|
|
1485
|
+
// Persistir totales LTC para futuros AJUSTE de este período
|
|
1486
|
+
if (_tipoEnvioCompras === 'TOTAL') {
|
|
1487
|
+
this._guardarLtcTotales(periodo, 'COMPRA', resumen);
|
|
1488
|
+
}
|
|
1094
1489
|
} else {
|
|
1095
1490
|
console.log(` [ERR] Error enviando Libro de Compras: ${result.error}`);
|
|
1096
1491
|
}
|
|
@@ -1115,6 +1510,7 @@ class CertRunner {
|
|
|
1115
1510
|
emisor: this.config.emisor,
|
|
1116
1511
|
periodo,
|
|
1117
1512
|
certificado: this.certificado,
|
|
1513
|
+
tipoEnvio: options.tipoEnvio || 'TOTAL',
|
|
1118
1514
|
});
|
|
1119
1515
|
|
|
1120
1516
|
console.log(` Generando Libro de Compras para Exentos para período ${periodo} (${libroData.detalle.length} documentos del SII)...`);
|
|
@@ -1168,6 +1564,7 @@ class CertRunner {
|
|
|
1168
1564
|
periodo,
|
|
1169
1565
|
certificado: this.certificado,
|
|
1170
1566
|
folioNotificacion: options.folioNotificacion || 3,
|
|
1567
|
+
tipoEnvio: options.tipoEnvio || 'TOTAL',
|
|
1171
1568
|
});
|
|
1172
1569
|
|
|
1173
1570
|
const { libro, xml, detalle } = libroGuias.generar(setGuiaResult, {
|
|
@@ -1894,292 +2291,341 @@ class CertRunner {
|
|
|
1894
2291
|
}
|
|
1895
2292
|
|
|
1896
2293
|
/**
|
|
1897
|
-
* Sube las 3 respuestas de intercambio a www4.sii.cl/pfeInternet
|
|
1898
|
-
*
|
|
1899
|
-
*
|
|
2294
|
+
* Sube las 3 respuestas de intercambio a www4.sii.cl/pfeInternet vía HTTP puro (sin Puppeteer).
|
|
2295
|
+
* Flujo: warm-up → validarUsuario (GWT RPC) → uploadFile1/2/3 (multipart POST).
|
|
2296
|
+
* Hashes GWT capturados 2026-06-01 desde INTERCAMBIO_SET.har.
|
|
1900
2297
|
* @private
|
|
1901
2298
|
*/
|
|
1902
2299
|
async _subirRespuestasPfeInternet({ recepcionXml, aprobacionXml, recibosXml, debugDir }) {
|
|
1903
|
-
const
|
|
1904
|
-
const
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
const
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
2300
|
+
const PFE_BASE = 'https://www4.sii.cl/pfeInternet/';
|
|
2301
|
+
const PFE_PERM = 'E487C7488217509D4EDCE9D341782C20'; // pfe.nocache.js 2026-06-01
|
|
2302
|
+
const PFE_POLICY = '4EB230A83E74980F353E4FCC209543CB'; // capturado 2026-06-01
|
|
2303
|
+
const PFE_SVC = 'cl.sii.sdi.dim.pfe.web.client.service.Facade';
|
|
2304
|
+
|
|
2305
|
+
const { cookies, makeReq } = await this._autenticarPfeInternet();
|
|
2306
|
+
|
|
2307
|
+
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'];
|
|
2311
|
+
|
|
2312
|
+
const gwtHeaders = {
|
|
2313
|
+
'Content-Type': 'text/x-gwt-rpc; charset=UTF-8',
|
|
2314
|
+
'X-GWT-Module-Base': PFE_BASE,
|
|
2315
|
+
'X-GWT-Permutation': PFE_PERM,
|
|
2316
|
+
'Origin': 'https://www4.sii.cl',
|
|
2317
|
+
'Referer': PFE_BASE,
|
|
2318
|
+
};
|
|
1918
2319
|
|
|
1919
|
-
//
|
|
1920
|
-
const
|
|
2320
|
+
// Parsear string table GWT (igual que pdfdteInternet)
|
|
2321
|
+
const parseGwtTable = (body) => {
|
|
2322
|
+
const stIdx = body.lastIndexOf(',["');
|
|
2323
|
+
const stEnd = body.lastIndexOf('],');
|
|
2324
|
+
if (stIdx === -1 || stEnd <= stIdx) return null;
|
|
2325
|
+
try {
|
|
2326
|
+
const raw = body.substring(stIdx + 1, stEnd + 1);
|
|
2327
|
+
return JSON.parse(raw.replace(/\\x([0-9a-fA-F]{2})/g, (_, h) => `\\u00${h}`));
|
|
2328
|
+
} catch (_) { return null; }
|
|
2329
|
+
};
|
|
1921
2330
|
|
|
1922
|
-
//
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2331
|
+
// === PASO 1: validarUsuario — verifica que empresa esté en estado P06 ===
|
|
2332
|
+
// Payload GWT RPC de validarUsuario(UsuarioTo{empRut,empDv,certRut,certDv})
|
|
2333
|
+
// String table: [base, policy, svc, method, UsuarioTo type, certDv, empDv, Integer type]
|
|
2334
|
+
const validPayload = [
|
|
2335
|
+
'7|0|8',
|
|
2336
|
+
PFE_BASE, PFE_POLICY, PFE_SVC, 'validarUsuario',
|
|
2337
|
+
'cl.sii.sdi.dim.pfe.to.UsuarioTo/3723336533',
|
|
2338
|
+
certDv, empDv, 'java.lang.Integer/3438268394',
|
|
2339
|
+
'1|2|3|4|1|5|5|0|0|0|0|6|7|8', empRut,
|
|
2340
|
+
'0|0|0|0|0|8', certRut, '',
|
|
2341
|
+
].join('|');
|
|
2342
|
+
|
|
2343
|
+
console.log(' → pfeInternet: validarUsuario...');
|
|
2344
|
+
const validResp = await makeReq(`${PFE_BASE}facade`, {
|
|
2345
|
+
method: 'POST', body: validPayload, headers: gwtHeaders, cookies,
|
|
2346
|
+
});
|
|
1926
2347
|
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
2348
|
+
if (debugDir) fs.writeFileSync(path.join(debugDir, 'pfe-validar-resp.txt'), validResp.body.substring(0, 1000), 'utf8');
|
|
2349
|
+
|
|
2350
|
+
const validTable = parseGwtTable(validResp.body);
|
|
2351
|
+
// MensajeTo con mensaje de error → empresa no está en P06
|
|
2352
|
+
const errorMsg = validTable?.find(s => typeof s === 'string' && s.length > 5 && !/^(cl\.|java\.)/.test(s));
|
|
2353
|
+
if (errorMsg && /no esta en estado|error/i.test(errorMsg)) {
|
|
2354
|
+
const estadoMatch = errorMsg.match(/estado:\s*(\w+)/i);
|
|
2355
|
+
const estadoActual = estadoMatch ? estadoMatch[1] : 'desconocido';
|
|
2356
|
+
console.log(` → pfeInternet: empresa NO en P06 (${estadoActual}) — ${errorMsg}`);
|
|
2357
|
+
return {
|
|
2358
|
+
success: false,
|
|
2359
|
+
estadoPortal: estadoActual,
|
|
2360
|
+
error: errorMsg,
|
|
2361
|
+
hint: estadoActual === 'P90'
|
|
2362
|
+
? 'Fase de intercambio ya completada (empresa en P90)'
|
|
2363
|
+
: `Estado SII pfeInternet: ${estadoActual}`,
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
console.log(' ✓ pfeInternet: empresa en P06, subiendo archivos XML...');
|
|
1930
2367
|
|
|
1931
|
-
|
|
1932
|
-
|
|
2368
|
+
// === PASO 2: Upload de los 3 XMLs vía multipart/form-data ===
|
|
2369
|
+
const archivos = [
|
|
2370
|
+
{ filename: 'respuesta-recepcion-envio.xml', content: recepcionXml, uploadN: 1 },
|
|
2371
|
+
{ filename: 'envio-recibos.xml', content: recibosXml, uploadN: 2 },
|
|
2372
|
+
{ filename: 'respuesta-aprobacion-comercial.xml', content: aprobacionXml, uploadN: 3 },
|
|
2373
|
+
];
|
|
1933
2374
|
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2375
|
+
for (const archivo of archivos) {
|
|
2376
|
+
const boundary = `----WebKitFormBoundary${Date.now().toString(16)}`;
|
|
2377
|
+
const CRLF = '\r\n';
|
|
2378
|
+
const multipartBody = [
|
|
2379
|
+
`--${boundary}`,
|
|
2380
|
+
`Content-Disposition: form-data; name="uploadFormElement"; filename="${archivo.filename}"`,
|
|
2381
|
+
'Content-Type: text/xml',
|
|
2382
|
+
'',
|
|
2383
|
+
archivo.content,
|
|
2384
|
+
`--${boundary}--`,
|
|
2385
|
+
'',
|
|
2386
|
+
].join(CRLF);
|
|
2387
|
+
|
|
2388
|
+
console.log(` → Subiendo ${archivo.filename}...`);
|
|
2389
|
+
const uploadResp = await makeReq(`${PFE_BASE}uploadFile${archivo.uploadN}`, {
|
|
2390
|
+
method: 'POST',
|
|
2391
|
+
body: multipartBody,
|
|
2392
|
+
headers: {
|
|
2393
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
2394
|
+
'Origin': 'https://www4.sii.cl',
|
|
2395
|
+
'Referer': PFE_BASE,
|
|
2396
|
+
},
|
|
2397
|
+
cookies,
|
|
1939
2398
|
});
|
|
1940
2399
|
|
|
1941
|
-
|
|
1942
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
1943
|
-
|
|
1944
|
-
// Hacer click en el enlace "Subir archivos XML de respuesta de Intercambio"
|
|
1945
|
-
// El href es javascript:openForm('opt-ingresoEmpresaUp') — necesita click real para GWT
|
|
1946
|
-
console.log(' → Clickeando "Subir archivos XML de respuesta de Intercambio"...');
|
|
1947
|
-
const linkClicked = await page.click('a[href*="ingresoEmpresaUp"]').then(() => true).catch(() => false);
|
|
1948
|
-
if (!linkClicked) {
|
|
1949
|
-
// Fallback: evaluar click con dispatchEvent
|
|
1950
|
-
await page.evaluate(() => {
|
|
1951
|
-
const link = document.querySelector('a[href*="ingresoEmpresaUp"]');
|
|
1952
|
-
if (link) link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
1953
|
-
});
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
// Esperar a que GWT complete el RPC y re-renderice la vista de upload
|
|
1957
|
-
await page.waitForNetworkIdle({ timeout: 15000, idleTime: 1000 }).catch(() => {});
|
|
1958
|
-
|
|
1959
|
-
// === PASO INTERMEDIO: ingresar RUT y confirmar empresa ===
|
|
1960
|
-
// GWT muestra "Ingrese el RUT de la empresa" antes de mostrar el formulario de upload
|
|
1961
|
-
const rutInput = await page.$('input.gwt-TextBox[maxlength="10"]');
|
|
1962
|
-
if (rutInput) {
|
|
1963
|
-
const [rutNum, dv] = this.config.emisor.rut.split('-');
|
|
1964
|
-
const rutConDv = `${rutNum}-${dv}`;
|
|
1965
|
-
console.log(` → Ingresando RUT empresa: ${rutConDv}`);
|
|
1966
|
-
await rutInput.click({ clickCount: 3 }); // seleccionar todo
|
|
1967
|
-
await rutInput.type(rutConDv);
|
|
1968
|
-
|
|
1969
|
-
// Click en "Confirmar Empresa"
|
|
1970
|
-
const confirmBtn = await page.evaluateHandle(() => {
|
|
1971
|
-
return Array.from(document.querySelectorAll('button.gwt-Button'))
|
|
1972
|
-
.find(b => b.textContent.trim() === 'Confirmar Empresa');
|
|
1973
|
-
});
|
|
1974
|
-
if (confirmBtn) {
|
|
1975
|
-
await confirmBtn.asElement().click();
|
|
1976
|
-
console.log(' → Click "Confirmar Empresa", esperando formulario de upload...');
|
|
1977
|
-
await page.waitForNetworkIdle({ timeout: 15000, idleTime: 1000 }).catch(() => {});
|
|
1978
|
-
}
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
// Esperar que GWT renderice el formulario con los 3 inputs de upload
|
|
1982
|
-
const inputFound = await page.waitForSelector('input[name="uploadFormElement"]', { timeout: 30000 }).catch(() => null);
|
|
1983
|
-
if (!inputFound) {
|
|
1984
|
-
if (debugDir) {
|
|
1985
|
-
await page.screenshot({ path: path.join(debugDir, 'pfeInternet-error.png'), fullPage: true }).catch(() => {});
|
|
1986
|
-
fs.writeFileSync(path.join(debugDir, 'pfeInternet-error.html'), await page.content().catch(() => ''), 'utf8');
|
|
1987
|
-
}
|
|
1988
|
-
const pageText = await page.$eval('body', el => el.textContent).catch(() => '');
|
|
1989
|
-
if (pageText.includes('DOCUMENTOS IMPRESOS') || pageText.includes('fue cargado exitosamente')) {
|
|
1990
|
-
throw new Error('PASO_YA_COMPLETADO');
|
|
1991
|
-
}
|
|
1992
|
-
throw new Error('pfeInternet no mostró formulario de upload tras openForm — ver pfeInternet-error.png/.html');
|
|
1993
|
-
}
|
|
1994
|
-
console.log(' → Formulario de upload listo');
|
|
1995
|
-
|
|
1996
|
-
// ── DEBUG: screenshot del formulario con los inputs listos ──
|
|
2400
|
+
const respBody = uploadResp.body || '';
|
|
1997
2401
|
if (debugDir) {
|
|
1998
|
-
|
|
1999
|
-
fs.writeFileSync(path.join(debugDir, 'pfeInternet-form-listo.html'), await page.content().catch(() => ''), 'utf8');
|
|
2402
|
+
fs.writeFileSync(path.join(debugDir, `pfe-upload${archivo.uploadN}-resp.txt`), respBody.substring(0, 2000), 'utf8');
|
|
2000
2403
|
}
|
|
2001
2404
|
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
// Asegurarse de que no haya diálogo abierto del upload anterior
|
|
2009
|
-
await page.waitForFunction(
|
|
2010
|
-
() => !document.querySelector('.gwt-DialogBox'),
|
|
2011
|
-
{ timeout: 10000 }
|
|
2012
|
-
).catch(() => {});
|
|
2013
|
-
|
|
2014
|
-
// Verificar si este archivo ya fue procesado (GWT reemplaza el input con texto)
|
|
2015
|
-
// Estructura DOM: <td class="filter-label">Archivo N: Label</td> en una <tr>
|
|
2016
|
-
// La siguiente <tr> tiene <td class="filter-widget"> con el input O el texto "procesado"
|
|
2017
|
-
const yaProcessado = await page.evaluate((labelText) => {
|
|
2018
|
-
const allTds = Array.from(document.querySelectorAll('td.filter-label'));
|
|
2019
|
-
const labelTd = allTds.find(el => el.textContent.includes(labelText));
|
|
2020
|
-
if (!labelTd) return false;
|
|
2021
|
-
// Subir al <tr> padre y tomar el siguiente <tr>
|
|
2022
|
-
const tr = labelTd.closest('tr');
|
|
2023
|
-
if (!tr) return false;
|
|
2024
|
-
const nextTr = tr.nextElementSibling;
|
|
2025
|
-
if (!nextTr) return false;
|
|
2026
|
-
return nextTr.textContent.includes('procesado con exito anteriormente');
|
|
2027
|
-
}, archivo.label);
|
|
2028
|
-
|
|
2029
|
-
if (yaProcessado) {
|
|
2030
|
-
console.log(` → ${archivo.filename}: ya procesado anteriormente, saltando...`);
|
|
2031
|
-
continue;
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
console.log(` → Subiendo ${archivo.filename}...`);
|
|
2405
|
+
const respLow = respBody.toLowerCase();
|
|
2406
|
+
const hasError = uploadResp.status >= 400
|
|
2407
|
+
|| (respLow.includes('error') && !respLow.includes('procesado'))
|
|
2408
|
+
|| respLow.includes('rechaz');
|
|
2409
|
+
const hasSuccess = respLow.includes('procesado') || respLow.includes('exitosamente')
|
|
2410
|
+
|| respLow.includes('cargado') || uploadResp.status === 200;
|
|
2035
2411
|
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2412
|
+
if (hasError && !hasSuccess) {
|
|
2413
|
+
throw new Error(`Error al subir ${archivo.filename}: HTTP ${uploadResp.status} — ${respBody.substring(0, 200)}`);
|
|
2414
|
+
}
|
|
2415
|
+
console.log(` ✓ ${archivo.filename} subido (HTTP ${uploadResp.status})`);
|
|
2416
|
+
}
|
|
2041
2417
|
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
await input.uploadFile(filePath);
|
|
2418
|
+
return { success: true, resultado: 'Los 3 archivos XML de intercambio subidos correctamente' };
|
|
2419
|
+
}
|
|
2045
2420
|
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2421
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2422
|
+
// FASE 8: MUESTRAS IMPRESAS — subir PDFs a pe_avance5
|
|
2423
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2049
2424
|
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2425
|
+
/**
|
|
2426
|
+
* Autentica contra pdfdteInternet reutilizando la sesión SII cacheada.
|
|
2427
|
+
* Hace warm-up GET, obtiene el hash de política GWT desde nocache.js + cache.html
|
|
2428
|
+
* y retorna los utilitarios HTTP listos para hacer llamadas GWT RPC.
|
|
2429
|
+
* @private
|
|
2430
|
+
* @returns {Promise<{cookies: string, makeReq: Function, permHash: string, policyHash: string}>}
|
|
2431
|
+
*/
|
|
2432
|
+
async _autenticarPdfDteInternet() {
|
|
2433
|
+
const https = require('https');
|
|
2434
|
+
const crypto = require('crypto');
|
|
2435
|
+
const { URL } = require('url');
|
|
2054
2436
|
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2437
|
+
const tlsOpts = {
|
|
2438
|
+
rejectUnauthorized: false,
|
|
2439
|
+
maxVersion: 'TLSv1.2',
|
|
2440
|
+
secureOptions: crypto.constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
|
|
2441
|
+
| crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
|
2442
|
+
};
|
|
2061
2443
|
|
|
2062
|
-
|
|
2063
|
-
throw new Error(`Error en archivo ${archivo.filename}: ${msgText}`);
|
|
2064
|
-
}
|
|
2444
|
+
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36';
|
|
2065
2445
|
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2446
|
+
const makeReq = (urlStr, { method = 'GET', body = null, headers = {}, cookies: reqCookies = '' }) =>
|
|
2447
|
+
new Promise((resolve, reject) => {
|
|
2448
|
+
const u = new URL(urlStr);
|
|
2449
|
+
const agent = new https.Agent(tlsOpts);
|
|
2450
|
+
const bodyBuf = body ? (Buffer.isBuffer(body) ? body : Buffer.from(body, 'utf8')) : null;
|
|
2451
|
+
const opts = {
|
|
2452
|
+
hostname: u.hostname, port: u.port || 443,
|
|
2453
|
+
path: u.pathname + u.search, method, agent,
|
|
2454
|
+
headers: {
|
|
2455
|
+
'User-Agent': UA,
|
|
2456
|
+
'Accept': '*/*',
|
|
2457
|
+
'Connection': 'keep-alive',
|
|
2458
|
+
'Origin': 'https://www4.sii.cl',
|
|
2459
|
+
'Referer': 'https://www4.sii.cl/pdfdteInternet/',
|
|
2460
|
+
...(reqCookies ? { 'Cookie': reqCookies } : {}),
|
|
2461
|
+
...headers,
|
|
2462
|
+
},
|
|
2463
|
+
};
|
|
2464
|
+
if (bodyBuf) opts.headers['Content-Length'] = bodyBuf.length;
|
|
2465
|
+
const req = https.request(opts, (res) => {
|
|
2466
|
+
const chunks = [];
|
|
2467
|
+
res.on('data', c => chunks.push(Buffer.isBuffer(c) ? c : Buffer.from(c)));
|
|
2468
|
+
res.on('end', () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString('utf8'), headers: res.headers }));
|
|
2074
2469
|
});
|
|
2470
|
+
req.on('error', reject);
|
|
2471
|
+
if (bodyBuf) req.write(bodyBuf);
|
|
2472
|
+
req.end();
|
|
2473
|
+
});
|
|
2075
2474
|
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
await page.waitForNetworkIdle({ timeout: 10000, idleTime: 500 }).catch(() => {});
|
|
2475
|
+
const collectNewCookies = (headers, existing) => {
|
|
2476
|
+
const merged = {};
|
|
2477
|
+
existing.split(';').forEach(c => { const [k, v] = c.trim().split('='); if (k) merged[k.trim()] = (v || '').trim(); });
|
|
2478
|
+
for (const c of (headers['set-cookie'] || [])) {
|
|
2479
|
+
const [kv] = c.split(';');
|
|
2480
|
+
const eq = kv.indexOf('=');
|
|
2481
|
+
if (eq > 0) merged[kv.slice(0, eq).trim()] = kv.slice(eq + 1).trim();
|
|
2084
2482
|
}
|
|
2483
|
+
return Object.entries(merged).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
2484
|
+
};
|
|
2085
2485
|
|
|
2086
|
-
|
|
2486
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
2487
|
+
let cookies = Object.entries(cookieJar).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
2488
|
+
|
|
2489
|
+
// Hashes GWT conocidos del portal pdfdteInternet (capturados 2026-06-01 vía HAR).
|
|
2490
|
+
// permHash = nombre del .cache.html cargado por el browser (permutation del GWT module)
|
|
2491
|
+
// policyHash = hash de política de serialización GWT embebido dentro del cache.html
|
|
2492
|
+
// Si el SII redespliega el portal se deben actualizar estos valores.
|
|
2493
|
+
const KNOWN_PERM_HASH = 'D86ACF99AE5C17F0B0F673A9872EF6CB';
|
|
2494
|
+
const KNOWN_POLICY_HASH = '5459B93B9D030A67564300FBD346270F';
|
|
2495
|
+
|
|
2496
|
+
// Warm-up: GET /pdfdteInternet/ para inicializar contexto del portal en el servidor.
|
|
2497
|
+
// Aprovechamos para intentar descubrir el permHash dinámicamente desde el HTML.
|
|
2498
|
+
let permHash = KNOWN_PERM_HASH;
|
|
2499
|
+
let policyHash = KNOWN_POLICY_HASH;
|
|
2500
|
+
try {
|
|
2501
|
+
const warmup = await makeReq('https://www4.sii.cl/pdfdteInternet/', { cookies });
|
|
2502
|
+
cookies = collectNewCookies(warmup.headers, cookies);
|
|
2503
|
+
if ((warmup.status === 301 || warmup.status === 302) && warmup.headers?.location) {
|
|
2504
|
+
const loc = warmup.headers.location;
|
|
2505
|
+
const absLoc = loc.startsWith('http') ? loc : `https://www4.sii.cl${loc}`;
|
|
2506
|
+
const warmup2 = await makeReq(absLoc, { cookies });
|
|
2507
|
+
cookies = collectNewCookies(warmup2.headers, cookies);
|
|
2508
|
+
}
|
|
2509
|
+
console.log(`[pdfdteInternet Auth] warm-up → HTTP ${warmup.status}`);
|
|
2087
2510
|
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2511
|
+
// Descubrir permHash dinámicamente desde pdfdte.nocache.js
|
|
2512
|
+
// (El módulo GWT se llama 'pdfdte', no 'pdfdteInternet' como podría esperarse)
|
|
2513
|
+
try {
|
|
2514
|
+
const ncResp = await makeReq('https://www4.sii.cl/pdfdteInternet/pdfdte.nocache.js', { cookies });
|
|
2515
|
+
if (ncResp.status === 200 && ncResp.body) {
|
|
2516
|
+
const ncHashes = [...new Set(
|
|
2517
|
+
[...ncResp.body.matchAll(/[=',]([0-9A-Fa-f]{32})[',]/g)].map(m => m[1].toUpperCase())
|
|
2518
|
+
)];
|
|
2519
|
+
if (ncHashes.includes(KNOWN_PERM_HASH)) {
|
|
2520
|
+
// Hash conocido sigue vigente
|
|
2521
|
+
console.log(`[pdfdteInternet Auth] pdfdte.nocache.js confirma hash conocido (${ncHashes.length} permutaciones)`);
|
|
2522
|
+
} else if (ncHashes.length > 0) {
|
|
2523
|
+
// Portal redesplegado — actualizar hashes
|
|
2524
|
+
const newPerm = ncHashes[0];
|
|
2525
|
+
console.log(`[pdfdteInternet Auth] Nuevo permHash detectado: ${newPerm.substring(0, 8)}... (portal redesplegado)`);
|
|
2526
|
+
try {
|
|
2527
|
+
const cacheResp = await makeReq(`https://www4.sii.cl/pdfdteInternet/${newPerm}.cache.html`, {
|
|
2528
|
+
cookies, headers: { 'Referer': 'https://www4.sii.cl/pdfdteInternet/' },
|
|
2529
|
+
});
|
|
2530
|
+
const hexInCache = [...new Set([...cacheResp.body.matchAll(/["']([0-9A-Fa-f]{32})["']/g)].map(m => m[1].toUpperCase()))];
|
|
2531
|
+
const newPolicy = hexInCache.find(h => h !== newPerm);
|
|
2532
|
+
if (newPolicy) {
|
|
2533
|
+
permHash = newPerm;
|
|
2534
|
+
policyHash = newPolicy;
|
|
2535
|
+
console.log(`[pdfdteInternet Auth] Hashes actualizados correctamente.`);
|
|
2536
|
+
}
|
|
2537
|
+
} catch (e2) {
|
|
2538
|
+
console.log(`[pdfdteInternet Auth] No se pudo obtener nuevo cache.html: ${e2.message}. Usando hashes conocidos.`);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
}
|
|
2542
|
+
} catch (eNc) {
|
|
2543
|
+
console.log(`[pdfdteInternet Auth] pdfdte.nocache.js no accesible: ${eNc.message}. Usando hashes conocidos.`);
|
|
2544
|
+
}
|
|
2545
|
+
} catch (e) {
|
|
2546
|
+
console.log(`[pdfdteInternet Auth] warm-up falló (no crítico): ${e.message}`);
|
|
2092
2547
|
}
|
|
2093
|
-
}
|
|
2094
2548
|
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2549
|
+
console.log(`[pdfdteInternet Auth] permHash=${permHash.substring(0, 8)}... policyHash=${policyHash.substring(0, 8)}...`);
|
|
2550
|
+
return { cookies, makeReq, permHash, policyHash };
|
|
2551
|
+
}
|
|
2098
2552
|
|
|
2099
2553
|
/**
|
|
2100
2554
|
* Consulta el estado actual del portal pdfdteInternet sin subir nada.
|
|
2101
2555
|
* Útil para saltarse la generación de PDFs si ya están enviados.
|
|
2556
|
+
* Implementación HTTP pura — llama a leeImpreso() via GWT RPC.
|
|
2102
2557
|
* @returns {Promise<{estado: string|null, error?: string}>}
|
|
2103
2558
|
*/
|
|
2104
2559
|
async verificarEstadoPortalMuestras() {
|
|
2105
|
-
const puppeteer = require('puppeteer');
|
|
2106
|
-
const cookieJar = await this._obtenerCookiesSII();
|
|
2107
|
-
const puppeteerCookies = Object.entries(cookieJar).map(([name, value]) => ({
|
|
2108
|
-
name, value, domain: '.sii.cl', path: '/', httpOnly: false, secure: true,
|
|
2109
|
-
}));
|
|
2110
|
-
const [rutNum, dvChar] = this.config.emisor.rut.split('-');
|
|
2111
|
-
let browser;
|
|
2112
2560
|
try {
|
|
2113
|
-
|
|
2114
|
-
const
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2561
|
+
const { cookies, makeReq, permHash, policyHash } = await this._autenticarPdfDteInternet();
|
|
2562
|
+
const [rutNum, dvChar] = this.config.emisor.rut.split('-');
|
|
2563
|
+
|
|
2564
|
+
const body =
|
|
2565
|
+
`7|0|6|https://www4.sii.cl/pdfdteInternet/|${policyHash}|` +
|
|
2566
|
+
`cl.sii.sdi.dim.validaPdfDte.web.client.service.ServicePdfDte|leeImpreso|` +
|
|
2567
|
+
`java.lang.String/2004016611|${rutNum}-${dvChar}|1|2|3|4|1|5|6|`;
|
|
2568
|
+
|
|
2569
|
+
const resp = await makeReq('https://www4.sii.cl/pdfdteInternet/ServicePdfDte', {
|
|
2570
|
+
method: 'POST',
|
|
2571
|
+
body,
|
|
2572
|
+
headers: {
|
|
2573
|
+
'Content-Type': 'text/x-gwt-rpc; charset=UTF-8',
|
|
2574
|
+
'X-GWT-Module-Base': 'https://www4.sii.cl/pdfdteInternet/',
|
|
2575
|
+
'X-GWT-Permutation': permHash,
|
|
2576
|
+
'Cache-Control': 'no-cache',
|
|
2577
|
+
'Pragma': 'no-cache',
|
|
2578
|
+
},
|
|
2579
|
+
cookies,
|
|
2580
|
+
});
|
|
2581
|
+
|
|
2582
|
+
if (resp.status !== 200 || !resp.body.startsWith('//OK')) {
|
|
2583
|
+
return { estado: null, error: `GWT leeImpreso HTTP ${resp.status}: ${resp.body.substring(0, 150)}` };
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// Parsear string table GWT: siempre el penúltimo elemento (antes de flags,version)
|
|
2587
|
+
// GWT usa \x27 etc. (no válido en JSON) → normalizar antes de parsear
|
|
2588
|
+
const stIdx2 = resp.body.lastIndexOf(',["');
|
|
2589
|
+
const stEnd = stIdx2 !== -1 ? resp.body.lastIndexOf('],') : -1;
|
|
2590
|
+
let table = null;
|
|
2591
|
+
if (stIdx2 !== -1 && stEnd > stIdx2) {
|
|
2592
|
+
try {
|
|
2593
|
+
const raw = resp.body.substring(stIdx2 + 1, stEnd + 1);
|
|
2594
|
+
const normalized = raw.replace(/\\x([0-9a-fA-F]{2})/g, (_, h) => `\\u00${h}`);
|
|
2595
|
+
table = JSON.parse(normalized);
|
|
2596
|
+
} catch (_) {}
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
let estado = null, numImpresos = null, fechaEnvio = null, revId = null;
|
|
2600
|
+
if (table) {
|
|
2601
|
+
const estadoStr = table.find(s => typeof s === 'string' && /APROBADO|POR REVISAR|RECHAZADO|INGRESO|EN REVISI/i.test(s));
|
|
2602
|
+
const errorStr = table.find(s => typeof s === 'string' && /El estado de la postulacion/i.test(s));
|
|
2603
|
+
const fechaStr = table.find(s => typeof s === 'string' && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(s));
|
|
2604
|
+
const nStr = table.find(s => typeof s === 'string' && /^\d+$/.test(s) && +s > 0 && +s < 500);
|
|
2605
|
+
estado = estadoStr ? estadoStr.toUpperCase() : (errorStr ? 'BLOQUEADO' : null);
|
|
2606
|
+
fechaEnvio = fechaStr || null;
|
|
2607
|
+
numImpresos = nStr ? parseInt(nStr, 10) : null;
|
|
2608
|
+
}
|
|
2609
|
+
// Fallback: regex directo en el body
|
|
2610
|
+
if (!estado) {
|
|
2611
|
+
const m = resp.body.match(/"(APROBADO|POR REVISAR|EN REVISI[OÓ]N|RECHAZADO|INGRESO|ENVIADO AL SII)"/i);
|
|
2612
|
+
if (m) estado = m[1].toUpperCase();
|
|
2613
|
+
else if (/El estado de la postulacion/i.test(resp.body)) estado = 'BLOQUEADO';
|
|
2614
|
+
}
|
|
2615
|
+
// Extraer revId (Long GWT-base64 en rango plausible de IDs de revisión)
|
|
2616
|
+
const GWT_B64_V = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$_';
|
|
2617
|
+
for (const m of resp.body.matchAll(/'([A-Za-z0-9$_]{3,7})'/g)) {
|
|
2618
|
+
let n = 0; for (const c of m[1]) n = n * 64 + GWT_B64_V.indexOf(c);
|
|
2619
|
+
if (n >= 100000 && n <= 9999999) { revId = n; break; }
|
|
2620
|
+
}
|
|
2621
|
+
return { estado, revId, numImpresos, fechaEnvio };
|
|
2168
2622
|
} catch (err) {
|
|
2169
2623
|
return { estado: null, error: err.message };
|
|
2170
|
-
} finally {
|
|
2171
|
-
if (browser) {
|
|
2172
|
-
await Promise.race([
|
|
2173
|
-
browser.close().catch(() => {}),
|
|
2174
|
-
new Promise(r => setTimeout(r, 8000)),
|
|
2175
|
-
]);
|
|
2176
|
-
try { browser.process()?.kill('SIGKILL'); } catch { /* ignorar */ }
|
|
2177
|
-
}
|
|
2178
2624
|
}
|
|
2179
2625
|
}
|
|
2180
2626
|
|
|
2181
2627
|
/**
|
|
2182
|
-
* Sube los PDFs de muestras impresas al portal pe_avance5 via
|
|
2628
|
+
* Sube los PDFs de muestras impresas al portal pe_avance5 via HTTP puro.
|
|
2183
2629
|
* @param {Object} opts
|
|
2184
2630
|
* @param {string} opts.pdfDir - Directorio con los PDFs generados
|
|
2185
2631
|
* @returns {Promise<Object>} { success, error? }
|
|
@@ -2204,443 +2650,193 @@ class CertRunner {
|
|
|
2204
2650
|
}
|
|
2205
2651
|
|
|
2206
2652
|
/**
|
|
2207
|
-
* Sube PDFs al portal https://www4.sii.cl/pdfdteInternet/ via Puppeteer.
|
|
2208
|
-
*
|
|
2653
|
+
* Sube PDFs al portal https://www4.sii.cl/pdfdteInternet/ via HTTP puro (sin Puppeteer).
|
|
2654
|
+
* Flujo GWT RPC: leeImpreso → creaLista → upload×N → solicitaRevisionSII.
|
|
2209
2655
|
* @private
|
|
2210
2656
|
*/
|
|
2211
2657
|
async _subirMuestrasImpresasPortal({ pdfPaths, debugDir }) {
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
const
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2658
|
+
// ── Helpers GWT base64 para codificar/decodificar java.lang.Long ──────────
|
|
2659
|
+
const GWT_B64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$_';
|
|
2660
|
+
const decodeGwtLong = (s) => { let n = 0; for (const c of s) n = n * 64 + GWT_B64.indexOf(c); return n; };
|
|
2661
|
+
const encodeGwtLong = (n) => {
|
|
2662
|
+
const chars = []; let r = n;
|
|
2663
|
+
while (r > 0) { chars.unshift(GWT_B64[r & 63]); r = Math.floor(r / 64); }
|
|
2664
|
+
while (chars.length < 4) chars.unshift('A');
|
|
2665
|
+
return chars.join('');
|
|
2666
|
+
};
|
|
2219
2667
|
|
|
2668
|
+
// ── Auth + obtención de hashes GWT ────────────────────────────────────────
|
|
2669
|
+
const { cookies, makeReq, permHash, policyHash } = await this._autenticarPdfDteInternet();
|
|
2220
2670
|
const [rutNum, dvChar] = this.config.emisor.rut.split('-');
|
|
2221
2671
|
|
|
2222
|
-
|
|
2223
|
-
const
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
await page.goto('https://www4.sii.cl/pdfdteInternet/', {
|
|
2239
|
-
waitUntil: 'networkidle2', timeout: 60000,
|
|
2240
|
-
});
|
|
2241
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
2242
|
-
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-01-loaded.png'), fullPage: true }).catch(() => {});
|
|
2243
|
-
|
|
2244
|
-
// Hookear SWFUpload para capturar post_params.id cuando GWT inicialice el uploader
|
|
2245
|
-
await page.evaluate(() => {
|
|
2246
|
-
if (window.SWFUpload) {
|
|
2247
|
-
const _Orig = window.SWFUpload;
|
|
2248
|
-
window.SWFUpload = function(settings) {
|
|
2249
|
-
const pp = (settings && settings.post_params) || {};
|
|
2250
|
-
if (pp.id) window.__swfCapturedRevId = String(pp.id);
|
|
2251
|
-
const inst = new _Orig(settings);
|
|
2252
|
-
// Copiar propiedades estáticas
|
|
2253
|
-
Object.assign(window.SWFUpload, _Orig);
|
|
2254
|
-
return inst;
|
|
2255
|
-
};
|
|
2256
|
-
window.SWFUpload.prototype = _Orig.prototype;
|
|
2257
|
-
// Copiar constantes estáticas del prototipo original
|
|
2258
|
-
for (const k of Object.getOwnPropertyNames(_Orig)) {
|
|
2259
|
-
try { window.SWFUpload[k] = _Orig[k]; } catch { }
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
}).catch(() => {});
|
|
2263
|
-
|
|
2264
|
-
// Paso 1: RUT Empresa
|
|
2265
|
-
const rutInputs = await page.$$('input[name="rut"]');
|
|
2266
|
-
const dvInputs = await page.$$('input[name="dv"]');
|
|
2267
|
-
if (!rutInputs.length) {
|
|
2268
|
-
if (debugDir) {
|
|
2269
|
-
await page.screenshot({ path: path.join(debugDir, 'pdfte-error-no-rut.png'), fullPage: true }).catch(() => {});
|
|
2270
|
-
fs.writeFileSync(path.join(debugDir, 'pdfte-error.html'), await page.content(), 'utf8');
|
|
2271
|
-
}
|
|
2272
|
-
throw new Error('pdfdteInternet: no se encontraron campos de RUT (¿sesión expirada?)');
|
|
2273
|
-
}
|
|
2274
|
-
console.log(` → Ingresando RUT empresa: ${rutNum}-${dvChar}`);
|
|
2275
|
-
await rutInputs[0].click({ clickCount: 3 }); await rutInputs[0].type(rutNum);
|
|
2276
|
-
await dvInputs[0].click({ clickCount: 3 }); await dvInputs[0].type(dvChar);
|
|
2277
|
-
await clickBoton(page, 'Rut');
|
|
2278
|
-
await new Promise(r => setTimeout(r, 2500));
|
|
2279
|
-
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-02-after-rut.png'), fullPage: true }).catch(() => {});
|
|
2280
|
-
|
|
2281
|
-
// Paso 2: Diálogo "ya existe revisión" → click "Sí"
|
|
2282
|
-
// nuevaRevisionCreada=true cuando el usuario acepta crear una nueva revisión;
|
|
2283
|
-
// en ese caso NO se hace el early-exit por estado previo (el texto "EN REVISIÓN"
|
|
2284
|
-
// que queda visible corresponde a la revisión antigua, no a la nueva vacía).
|
|
2285
|
-
let nuevaRevisionCreada = false;
|
|
2286
|
-
const hayDialog = await page.evaluate(() => {
|
|
2287
|
-
const dlg = document.querySelector('.x-window');
|
|
2288
|
-
return !!(dlg && dlg.offsetParent !== null);
|
|
2289
|
-
});
|
|
2290
|
-
if (hayDialog) {
|
|
2291
|
-
console.log(' → Diálogo "Ya existe revisión" detectado → click "Sí"');
|
|
2292
|
-
if (debugDir) {
|
|
2293
|
-
await page.screenshot({ path: path.join(debugDir, 'pdfte-dialog-ya-existe.png'), fullPage: true }).catch(() => {});
|
|
2294
|
-
fs.writeFileSync(path.join(debugDir, 'pdfte-dialog-ya-existe.html'), await page.content().catch(() => ''), 'utf8');
|
|
2295
|
-
// Log texto del diálogo para debug
|
|
2296
|
-
const dlgText = await page.evaluate(() => {
|
|
2297
|
-
const dlg = document.querySelector('.x-window');
|
|
2298
|
-
return dlg ? dlg.textContent.trim().replace(/\s+/g, ' ') : '';
|
|
2299
|
-
}).catch(() => '');
|
|
2300
|
-
console.log(` [DEBUG] Texto del diálogo: "${dlgText}"`);
|
|
2301
|
-
}
|
|
2302
|
-
const clicked = await page.evaluate(() => {
|
|
2303
|
-
const si = Array.from(document.querySelectorAll('button.x-btn-text'))
|
|
2304
|
-
.find(b => /^s[ií]$/i.test(b.textContent.trim()));
|
|
2305
|
-
if (si) { si.click(); return true; }
|
|
2306
|
-
return false;
|
|
2307
|
-
});
|
|
2308
|
-
if (!clicked) await page.evaluate(() => { const b = document.querySelector('.x-window button'); if (b) b.click(); });
|
|
2309
|
-
await new Promise(r => setTimeout(r, 2500));
|
|
2310
|
-
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-dialog-despues-si.png'), fullPage: true }).catch(() => {});
|
|
2311
|
-
nuevaRevisionCreada = true;
|
|
2312
|
-
}
|
|
2672
|
+
const GWT_SVC = 'cl.sii.sdi.dim.validaPdfDte.web.client.service.ServicePdfDte';
|
|
2673
|
+
const GWT_BASE = 'https://www4.sii.cl/pdfdteInternet/';
|
|
2674
|
+
const SVC_URL = `${GWT_BASE}ServicePdfDte`;
|
|
2675
|
+
|
|
2676
|
+
const gwtPost = (body) => makeReq(SVC_URL, {
|
|
2677
|
+
method: 'POST',
|
|
2678
|
+
body,
|
|
2679
|
+
headers: {
|
|
2680
|
+
'Content-Type': 'text/x-gwt-rpc; charset=UTF-8',
|
|
2681
|
+
'X-GWT-Module-Base': GWT_BASE,
|
|
2682
|
+
'X-GWT-Permutation': permHash,
|
|
2683
|
+
'Cache-Control': 'no-cache',
|
|
2684
|
+
'Pragma': 'no-cache',
|
|
2685
|
+
},
|
|
2686
|
+
cookies,
|
|
2687
|
+
});
|
|
2313
2688
|
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
const
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
const body = await resp.text();
|
|
2335
|
-
const matches = body.match(/\b(\d{5,7})\b/g);
|
|
2336
|
-
if (matches) {
|
|
2337
|
-
for (const m of matches) {
|
|
2338
|
-
const n = +m;
|
|
2339
|
-
if (n > 10000 && n < 9999999 && !_capturedRevId) {
|
|
2340
|
-
_capturedRevId = m;
|
|
2341
|
-
console.log(` [DEBUG] Posible nroRevision capturado de red: ${m} (url: ${url.split('?')[0]})`);
|
|
2342
|
-
}
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
} catch { /* skip */ }
|
|
2346
|
-
};
|
|
2347
|
-
page.on('response', _onPortalResponse);
|
|
2348
|
-
await clickBoton(page, 'Consultar');
|
|
2349
|
-
await new Promise(r => setTimeout(r, 2500));
|
|
2350
|
-
page.off('response', _onPortalResponse);
|
|
2351
|
-
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-03-after-consultar.png'), fullPage: true }).catch(() => {});
|
|
2352
|
-
|
|
2353
|
-
// ── Re-ejecución: detectar estado terminal antes de proceder ──
|
|
2354
|
-
// Solo aplicable cuando NO se creó una nueva revisión.
|
|
2355
|
-
// NOTA: el tab de navegación "Muestras Impresas en revisión" siempre contiene
|
|
2356
|
-
// el texto "en revisión", por lo que NO se puede usar textContent global para
|
|
2357
|
-
// detectar ese estado. Solo hacer early-exit si el formulario de upload
|
|
2358
|
-
// (input.gwt-FileUpload o botón "Crear") NO está visible, lo que indica que
|
|
2359
|
-
// la revisión está en un estado terminal irreversible.
|
|
2360
|
-
const _estadoYaSubido = nuevaRevisionCreada ? null : await page.evaluate(() => {
|
|
2361
|
-
// Si el formulario de carga está presente, proceder siempre con el upload
|
|
2362
|
-
const tieneFormulario = !!document.querySelector('input.gwt-FileUpload') ||
|
|
2363
|
-
Array.from(document.querySelectorAll('button.x-btn-text'))
|
|
2364
|
-
.some(b => ['Crear', 'Limpiar', 'Eliminar'].includes(b.textContent.trim()) && !b.disabled);
|
|
2365
|
-
if (tieneFormulario) return null;
|
|
2366
|
-
// Solo si NO hay formulario, verificar estado terminal
|
|
2367
|
-
const t = (document.body.textContent || '').toUpperCase();
|
|
2368
|
-
if (t.includes('APROBADO')) return 'APROBADO';
|
|
2369
|
-
if (t.includes('ENVIADO AL SII')) return 'ENVIADO AL SII';
|
|
2370
|
-
if (t.includes('RECHAZADO')) return 'RECHAZADO';
|
|
2371
|
-
return null;
|
|
2372
|
-
}).catch(() => null);
|
|
2373
|
-
if (_estadoYaSubido) {
|
|
2374
|
-
console.log(` [OK] Portal ya muestra estado "${_estadoYaSubido}" — muestras subidas previamente. Proceso completado.`);
|
|
2375
|
-
return { success: true, alreadyCompleted: true, estado: _estadoYaSubido };
|
|
2376
|
-
}
|
|
2689
|
+
// ── Paso 1: verificar si existe revisión previa (leeImpreso) ─────────────
|
|
2690
|
+
const leeBody =
|
|
2691
|
+
`7|0|6|${GWT_BASE}|${policyHash}|${GWT_SVC}|leeImpreso|` +
|
|
2692
|
+
`java.lang.String/2004016611|${rutNum}-${dvChar}|1|2|3|4|1|5|6|`;
|
|
2693
|
+
const leeResp = await gwtPost(leeBody);
|
|
2694
|
+
if (leeResp.status !== 200) throw new Error(`pdfdteInternet leeImpreso HTTP ${leeResp.status}`);
|
|
2695
|
+
|
|
2696
|
+
// ── Parsear string table GWT (siempre antes de flags/version al final) ─────
|
|
2697
|
+
const _parseGwtStringTable = (body) => {
|
|
2698
|
+
const stIdx = body.lastIndexOf(',["');
|
|
2699
|
+
if (stIdx === -1) return null;
|
|
2700
|
+
const stEnd = body.lastIndexOf('],'); // ÚLTIMO ], = cierre del string table
|
|
2701
|
+
if (stEnd <= stIdx) return null;
|
|
2702
|
+
try {
|
|
2703
|
+
// GWT usa \x27 etc. (no válido en JSON) → convertir a \uXXXX
|
|
2704
|
+
const raw = body.substring(stIdx + 1, stEnd + 1);
|
|
2705
|
+
const normalized = raw.replace(/\\x([0-9a-fA-F]{2})/g, (_, h) => `\\u00${h}`);
|
|
2706
|
+
return JSON.parse(normalized);
|
|
2707
|
+
} catch (_) { return null; }
|
|
2708
|
+
};
|
|
2377
2709
|
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
await new Promise(r => setTimeout(r, 2500));
|
|
2382
|
-
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-04-after-crear.png'), fullPage: true }).catch(() => {});
|
|
2383
|
-
|
|
2384
|
-
// Paso 5: Verificar que el input de archivo existe antes de empezar
|
|
2385
|
-
const inputCheck = await page.waitForSelector('input.gwt-FileUpload', { timeout: 30000 }).catch(() => null);
|
|
2386
|
-
if (!inputCheck) {
|
|
2387
|
-
if (debugDir) {
|
|
2388
|
-
await page.screenshot({ path: path.join(debugDir, 'pdfte-error-no-fileinput.png'), fullPage: true }).catch(() => {});
|
|
2389
|
-
fs.writeFileSync(path.join(debugDir, 'pdfte-error.html'), await page.content(), 'utf8');
|
|
2390
|
-
}
|
|
2391
|
-
throw new Error('pdfdteInternet: no apareció el input de archivo tras "Crear"');
|
|
2392
|
-
}
|
|
2710
|
+
const leeTable = _parseGwtStringTable(leeResp.body);
|
|
2711
|
+
const estadoMatch = leeResp.body.match(/"(APROBADO|POR REVISAR|EN REVISI[OÓ]N|RECHAZADO|INGRESO|ENVIADO AL SII)"/i);
|
|
2712
|
+
const estadoActual = estadoMatch ? estadoMatch[1].toUpperCase() : null;
|
|
2393
2713
|
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
await _dvs[0].click({ clickCount: 3 }); await _dvs[0].type(dvChar);
|
|
2406
|
-
await clickBoton(page, 'Rut');
|
|
2407
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
2408
|
-
// Diálogo "ya existe revisión" → click "Sí" para abrirla
|
|
2409
|
-
const _dlg = await page.evaluate(() => { const d = document.querySelector('.x-window'); return !!(d && d.offsetParent !== null); });
|
|
2410
|
-
if (_dlg) {
|
|
2411
|
-
const _ok = await page.evaluate(() => {
|
|
2412
|
-
const si = Array.from(document.querySelectorAll('button.x-btn-text')).find(b => /^s[ií]$/i.test(b.textContent.trim()));
|
|
2413
|
-
if (si) { si.click(); return true; }
|
|
2414
|
-
return false;
|
|
2415
|
-
});
|
|
2416
|
-
if (!_ok) await page.evaluate(() => { const b = document.querySelector('.x-window button'); if (b) b.click(); });
|
|
2417
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
2418
|
-
}
|
|
2419
|
-
// RUT proveedor (mismo que empresa)
|
|
2420
|
-
await page.waitForFunction(() => { const ins = document.querySelectorAll('input[name="rut"]'); return ins.length >= 2 && !ins[1].disabled; }, { timeout: 10000 }).catch(() => {});
|
|
2421
|
-
const _rutsN = await page.$$('input[name="rut"]');
|
|
2422
|
-
const _dvsN = await page.$$('input[name="dv"]');
|
|
2423
|
-
const _pRut = _rutsN.length >= 2 ? _rutsN[1] : _rutsN[0];
|
|
2424
|
-
const _pDv = _dvsN.length >= 2 ? _dvsN[1] : _dvsN[0];
|
|
2425
|
-
await _pRut.click({ clickCount: 3 }); await _pRut.type(rutNum);
|
|
2426
|
-
await _pDv.click({ clickCount: 3 }); await _pDv.type(dvChar);
|
|
2427
|
-
await clickBoton(page, 'Consultar');
|
|
2428
|
-
await new Promise(r => setTimeout(r, 2500));
|
|
2429
|
-
// Si aún no hay formulario de subida (primera vez), crear revisión
|
|
2430
|
-
const _hayInp = await page.$('input.gwt-FileUpload').catch(() => null);
|
|
2431
|
-
if (!_hayInp) { await clickBoton(page, 'Crear'); await new Promise(r => setTimeout(r, 2500)); }
|
|
2432
|
-
};
|
|
2433
|
-
|
|
2434
|
-
const fileInput = await page.$('input.gwt-FileUpload');
|
|
2435
|
-
if (!fileInput) throw new Error('pdfdteInternet: input.gwt-FileUpload no encontrado');
|
|
2436
|
-
|
|
2437
|
-
// ── Obtener nroRevision y subir todos los archivos vía fetch() ──────────────────
|
|
2438
|
-
// ── Primer upload para obtener nroRevision ─────────────────────────────────────
|
|
2439
|
-
// Puppeteer 22+ usa waitForFileChooser() para file uploads.
|
|
2440
|
-
// Triggear el input via click + page.waitForFileChooser() es el API nativo.
|
|
2441
|
-
// GWT procesa el onChange, somete el FormPanel a /upload con id de revisión.
|
|
2442
|
-
// Capturamos nroRevision desde la respuesta del servidor.
|
|
2443
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
2444
|
-
|
|
2445
|
-
let nroRevision = null;
|
|
2446
|
-
|
|
2447
|
-
// Listener de respuesta para capturar nroRevision del primer upload GWT
|
|
2448
|
-
const _onFirstUploadResp = async (resp) => {
|
|
2449
|
-
if (resp.url().includes('/pdfdteInternet/upload')) {
|
|
2450
|
-
const txt = await resp.text().catch(() => '');
|
|
2451
|
-
const m = txt.trim().match(/^(\d+),(\d+)$/);
|
|
2452
|
-
if (m && !nroRevision) {
|
|
2453
|
-
nroRevision = m[2];
|
|
2454
|
-
console.log(` ✓ ID de revisión obtenido: ${nroRevision} (1/${pdfPaths.length} subido)`);
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2714
|
+
// Detectar mensaje de error del SII: tabla parseada o regex directo en body (por si \x27 falla)
|
|
2715
|
+
const errorEstadoMsg = !estadoActual && (
|
|
2716
|
+
(leeTable && leeTable.find(s => typeof s === 'string' && /El estado de la postulacion/i.test(s))) ||
|
|
2717
|
+
(() => { const m = leeResp.body.match(/"(El estado de la postulacion[^"\\]*(?:\\.[^"\\]*)*)"/i); return m ? m[1].replace(/\\x27/g, "'").replace(/\\x22/g, '"') : null; })()
|
|
2718
|
+
);
|
|
2719
|
+
if (errorEstadoMsg) {
|
|
2720
|
+
console.log(` → Portal SII bloqueado: ${errorEstadoMsg}`);
|
|
2721
|
+
return {
|
|
2722
|
+
success: false, blocked: true, estado: 'BLOQUEADO',
|
|
2723
|
+
error: errorEstadoMsg,
|
|
2724
|
+
hint: 'Espere a que el SII procese la revisión en curso (APROBADO/RECHAZADO) antes de re-subir.',
|
|
2457
2725
|
};
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
console.log(` → Disparando primer upload vía fileChooser para obtener ID de revisión...`);
|
|
2461
|
-
try {
|
|
2462
|
-
const _chooserPromise = page.waitForFileChooser({ timeout: 15000 });
|
|
2463
|
-
// Triggear el file input: click en el wrapper visible O directamente en el input
|
|
2464
|
-
await page.evaluate(() => {
|
|
2465
|
-
// a) intentar click en el botón visible (wrapper div con overflow: hidden)
|
|
2466
|
-
const wrapper = document.querySelector('div[style*="position: relative"][style*="overflow: hidden"] div.gwt-Label');
|
|
2467
|
-
if (wrapper) { wrapper.click(); return; }
|
|
2468
|
-
// b) click directo en el input oculto (funciona en headless Chrome)
|
|
2469
|
-
const inp = document.querySelector('input.gwt-FileUpload');
|
|
2470
|
-
if (inp) inp.click();
|
|
2471
|
-
});
|
|
2472
|
-
const _chooser = await _chooserPromise;
|
|
2473
|
-
await _chooser.accept([path.resolve(pdfPaths[0])]);
|
|
2474
|
-
console.log(` [DEBUG] fileChooser.accept → OK (${path.basename(pdfPaths[0])})`);
|
|
2475
|
-
} catch (e) {
|
|
2476
|
-
console.warn(` [!] waitForFileChooser falló (${e.message}), intentando CDP DOM.setFileInputFiles...`);
|
|
2477
|
-
// Fallback: CDP DOM.setFileInputFiles
|
|
2478
|
-
const _cdp = await page.createCDPSession();
|
|
2479
|
-
try {
|
|
2480
|
-
const { root } = await _cdp.send('DOM.getDocument', { depth: 0 });
|
|
2481
|
-
const { nodeId } = await _cdp.send('DOM.querySelector', {
|
|
2482
|
-
nodeId: root.nodeId, selector: 'input.gwt-FileUpload',
|
|
2483
|
-
});
|
|
2484
|
-
if (nodeId) {
|
|
2485
|
-
await _cdp.send('DOM.setFileInputFiles', { files: [path.resolve(pdfPaths[0])], nodeId });
|
|
2486
|
-
console.log(` [DEBUG] CDP setFileInputFiles → OK`);
|
|
2487
|
-
}
|
|
2488
|
-
} catch (cdpErr) {
|
|
2489
|
-
console.warn(` [!] CDP también falló: ${cdpErr.message}`);
|
|
2490
|
-
} finally {
|
|
2491
|
-
await _cdp.detach().catch(() => {});
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
|
|
2495
|
-
// Esperar hasta 60s para que el FormPanel iframe complete el upload
|
|
2496
|
-
const _t0 = Date.now();
|
|
2497
|
-
while (!nroRevision && Date.now() - _t0 < 60000) {
|
|
2498
|
-
await new Promise(r => setTimeout(r, 500));
|
|
2499
|
-
}
|
|
2500
|
-
page.off('response', _onFirstUploadResp);
|
|
2726
|
+
}
|
|
2501
2727
|
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2728
|
+
// Si ya hay una revisión activa (no rechazada ni aprobada), no se puede re-subir.
|
|
2729
|
+
// El SII sólo permite crear nueva revisión cuando el estado es RECHAZADO o APROBADO.
|
|
2730
|
+
if (estadoActual && estadoActual !== 'RECHAZADO' && estadoActual !== 'APROBADO') {
|
|
2731
|
+
// Extraer detalles adicionales de la respuesta leeImpreso para diagnóstico
|
|
2732
|
+
let numImpresos = null, fechaEnvio = null, revId = null;
|
|
2733
|
+
if (leeTable) {
|
|
2734
|
+
const fechaStr = leeTable.find(s => typeof s === 'string' && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}/.test(s));
|
|
2735
|
+
const nStr = leeTable.find(s => typeof s === 'string' && /^\d+$/.test(s) && +s > 0 && +s < 500);
|
|
2736
|
+
fechaEnvio = fechaStr || null;
|
|
2737
|
+
numImpresos = nStr ? parseInt(nStr, 10) : null;
|
|
2508
2738
|
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
// ── Subir archivos restantes en paralelo vía fetch directo ──
|
|
2513
|
-
const CONCURRENCIA = 5;
|
|
2514
|
-
const _remaining = pdfPaths.slice(1);
|
|
2515
|
-
let _procesados = 1;
|
|
2516
|
-
|
|
2517
|
-
for (let i = 0; i < _remaining.length; i += CONCURRENCIA) {
|
|
2518
|
-
const _chunk = _remaining.slice(i, i + CONCURRENCIA);
|
|
2519
|
-
const _fileData = _chunk.map(p => ({
|
|
2520
|
-
name: path.basename(p),
|
|
2521
|
-
b64: fs.readFileSync(p).toString('base64'),
|
|
2522
|
-
}));
|
|
2523
|
-
const _responses = await page.evaluate(async (files, revId) => {
|
|
2524
|
-
return Promise.all(files.map(async ({ name, b64 }) => {
|
|
2525
|
-
const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
|
|
2526
|
-
const blob = new Blob([bytes], { type: 'application/pdf' });
|
|
2527
|
-
const fd = new FormData();
|
|
2528
|
-
fd.append('Filedata', blob, name);
|
|
2529
|
-
fd.append('id', String(revId));
|
|
2530
|
-
fd.append('ambiente', 'false');
|
|
2531
|
-
const resp = await fetch('/pdfdteInternet/upload', { method: 'POST', body: fd });
|
|
2532
|
-
const text = await resp.text();
|
|
2533
|
-
return { name, ok: resp.ok, status: resp.status, text };
|
|
2534
|
-
}));
|
|
2535
|
-
}, _fileData, nroRevision);
|
|
2536
|
-
|
|
2537
|
-
_procesados += _chunk.length;
|
|
2538
|
-
console.log(` → Procesados ${_procesados}/${pdfPaths.length}`);
|
|
2539
|
-
for (const r of _responses) {
|
|
2540
|
-
if (!r.ok || !r.text.match(/^\d+,\d+$/)) {
|
|
2541
|
-
console.warn(` [!] ${r.name}: HTTP ${r.status} → ${r.text.substring(0, 120)}`);
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2739
|
+
for (const m of leeResp.body.matchAll(/'([A-Za-z0-9$_]{3,7})'/g)) {
|
|
2740
|
+
let n = 0; for (const c of m[1]) n = n * 64 + GWT_B64.indexOf(c);
|
|
2741
|
+
if (n >= 100000 && n <= 9999999) { revId = n; break; }
|
|
2544
2742
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2743
|
+
console.log(` → Estado: ${estadoActual}${revId ? ` | Revisión #${revId}` : ''}${fechaEnvio ? ` | Enviado: ${fechaEnvio}` : ''}${numImpresos ? ` | ${numImpresos} documentos` : ''} — ya enviadas, no se requiere re-subida`);
|
|
2744
|
+
return { success: true, alreadyCompleted: true, estado: estadoActual, revId, numImpresos, fechaEnvio };
|
|
2745
|
+
}
|
|
2746
|
+
if (estadoActual) {
|
|
2747
|
+
console.log(` → Estado previo: ${estadoActual} — creando nueva revisión`);
|
|
2748
|
+
} else {
|
|
2749
|
+
console.log(` → No hay revisión previa — creando primera revisión`);
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// ── Paso 2: crear nueva lista/revisión (creaLista) ────────────────────────
|
|
2753
|
+
// Params (6): amb=Z, rutNum, dv, proveedor=rutNum, provDv=dv, provNomre=""
|
|
2754
|
+
// String table (9): module, policy, svc, method, String type, Z, rutNum, dv, ""
|
|
2755
|
+
const creaBody =
|
|
2756
|
+
`7|0|9|${GWT_BASE}|${policyHash}|${GWT_SVC}|creaLista|` +
|
|
2757
|
+
`java.lang.String/2004016611|Z|${rutNum}|${dvChar}||` +
|
|
2758
|
+
`1|2|3|4|6|5|5|5|5|5|6|7|8|9|7|8|0|`;
|
|
2759
|
+
const creaResp = await gwtPost(creaBody);
|
|
2760
|
+
if (creaResp.status !== 200 || !creaResp.body.startsWith('//OK')) {
|
|
2761
|
+
throw new Error(`pdfdteInternet creaLista falló HTTP ${creaResp.status}: ${creaResp.body.substring(0, 200)}`);
|
|
2762
|
+
}
|
|
2763
|
+
// Detectar error de SII dentro del //OK (e.g. revisión en curso bloqueando)
|
|
2764
|
+
const creaTable = _parseGwtStringTable(creaResp.body);
|
|
2765
|
+
const creaErrorMsg = creaTable && creaTable.find(s => typeof s === 'string' && /El estado de la postulacion/i.test(s));
|
|
2766
|
+
if (creaErrorMsg) {
|
|
2767
|
+
throw new Error(`pdfdteInternet creaLista: ${creaErrorMsg} — Espere a que el SII procese la revisión en curso.`);
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
// Extraer ID de revisión (Long codificado en GWT base64, ej: 'BAqo' = 264872)
|
|
2771
|
+
let revId = null;
|
|
2772
|
+
let revIdEncoded = null;
|
|
2773
|
+
for (const m of creaResp.body.matchAll(/'([A-Za-z0-9$_]{3,7})'/g)) {
|
|
2774
|
+
const n = decodeGwtLong(m[1]);
|
|
2775
|
+
if (n >= 10000 && n <= 9999999) { revId = n; revIdEncoded = m[1]; break; }
|
|
2776
|
+
}
|
|
2777
|
+
if (!revId) throw new Error(`pdfdteInternet: no se pudo extraer ID de revisión de: ${creaResp.body.substring(0, 300)}`);
|
|
2778
|
+
console.log(` → ID de revisión: ${revId} (GWT: ${revIdEncoded})`);
|
|
2779
|
+
|
|
2780
|
+
if (debugDir) {
|
|
2781
|
+
fs.writeFileSync(path.join(debugDir, `pdfte-crea-lista-resp.txt`), creaResp.body, 'utf8');
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// ── Paso 3: subir cada PDF ────────────────────────────────────────────────
|
|
2785
|
+
const uploadUrl = `${GWT_BASE}upload`;
|
|
2786
|
+
let uploadedCount = 0;
|
|
2787
|
+
for (const pdfPath of pdfPaths) {
|
|
2788
|
+
const filename = path.basename(pdfPath);
|
|
2789
|
+
const pdfData = fs.readFileSync(pdfPath);
|
|
2790
|
+
const boundary = `----WebKitFormBoundary${Date.now().toString(16)}`;
|
|
2791
|
+
|
|
2792
|
+
// Construir multipart/form-data manualmente para soportar datos binarios
|
|
2793
|
+
const partFile = Buffer.concat([
|
|
2794
|
+
Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="Filedata"; filename="${filename}"\r\nContent-Type: application/octet-stream\r\n\r\n`),
|
|
2795
|
+
pdfData,
|
|
2796
|
+
Buffer.from('\r\n'),
|
|
2797
|
+
]);
|
|
2798
|
+
const partId = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="id"\r\n\r\n${revId}\r\n`);
|
|
2799
|
+
const partAmb = Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="ambiente"\r\n\r\nfalse\r\n`);
|
|
2800
|
+
const closing = Buffer.from(`--${boundary}--\r\n`);
|
|
2801
|
+
const formBody = Buffer.concat([partFile, partId, partAmb, closing]);
|
|
2802
|
+
|
|
2803
|
+
const upResp = await makeReq(uploadUrl, {
|
|
2804
|
+
method: 'POST',
|
|
2805
|
+
body: formBody,
|
|
2806
|
+
headers: {
|
|
2807
|
+
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
|
2808
|
+
'Referer': `${GWT_BASE}${permHash}.cache.html`,
|
|
2809
|
+
'x-dtreferer': GWT_BASE,
|
|
2810
|
+
'Cache-Control': 'no-cache',
|
|
2811
|
+
'Pragma': 'no-cache',
|
|
2812
|
+
},
|
|
2813
|
+
cookies,
|
|
2596
2814
|
});
|
|
2597
2815
|
|
|
2598
|
-
if (
|
|
2599
|
-
|
|
2600
|
-
await new Promise(r => setTimeout(r, 500));
|
|
2601
|
-
const _menuClicked = await page.evaluate(() => {
|
|
2602
|
-
for (const el of document.querySelectorAll('.x-menu-item-text')) {
|
|
2603
|
-
if (el.textContent.trim() === 'Enviar al SII') {
|
|
2604
|
-
el.click();
|
|
2605
|
-
return true;
|
|
2606
|
-
}
|
|
2607
|
-
}
|
|
2608
|
-
return false;
|
|
2609
|
-
});
|
|
2610
|
-
if (!_menuClicked) throw new Error('pdfdteInternet: botón "Enviar al SII" no encontrado en overflow menu');
|
|
2611
|
-
} else if (!_clickedEnviar) {
|
|
2612
|
-
throw new Error('pdfdteInternet: botón "Enviar al SII" no disponible o deshabilitado');
|
|
2613
|
-
}
|
|
2614
|
-
|
|
2615
|
-
await page.waitForNetworkIdle({ timeout: 30000, idleTime: 1000 }).catch(() => {});
|
|
2616
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
2617
|
-
if (debugDir) {
|
|
2618
|
-
await page.screenshot({ path: path.join(debugDir, 'pdfte-06-enviado.png'), fullPage: true }).catch(() => {});
|
|
2619
|
-
fs.writeFileSync(path.join(debugDir, 'pdfte-06-enviado.html'), await page.content(), 'utf8');
|
|
2816
|
+
if (upResp.status !== 200) {
|
|
2817
|
+
throw new Error(`pdfdteInternet upload ${filename} → HTTP ${upResp.status}: ${upResp.body.substring(0, 150)}`);
|
|
2620
2818
|
}
|
|
2819
|
+
uploadedCount++;
|
|
2820
|
+
console.log(` ✓ [${uploadedCount}/${pdfPaths.length}] ${filename} → ${upResp.body.trim()}`);
|
|
2821
|
+
}
|
|
2621
2822
|
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2823
|
+
// ── Paso 4: enviar al SII (solicitaRevisionSII) ────────────────────────
|
|
2824
|
+
// Params: Long id (revIdEncoded), String amb=Z
|
|
2825
|
+
const enviarBody =
|
|
2826
|
+
`7|0|6|${GWT_BASE}|${policyHash}|${GWT_SVC}|solicitaRevisionSII|` +
|
|
2827
|
+
`java.lang.Long/4227064769|Z|1|2|3|4|2|5|6|5|${revIdEncoded}|0|`;
|
|
2828
|
+
const enviarResp = await gwtPost(enviarBody);
|
|
2829
|
+
if (enviarResp.status !== 200 || !enviarResp.body.startsWith('//OK')) {
|
|
2830
|
+
throw new Error(`pdfdteInternet solicitaRevisionSII falló HTTP ${enviarResp.status}: ${enviarResp.body.substring(0, 200)}`);
|
|
2831
|
+
}
|
|
2629
2832
|
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
} finally {
|
|
2633
|
-
if (browser) {
|
|
2634
|
-
// browser.close() puede colgarse indefinidamente si Chromium no responde.
|
|
2635
|
-
// Limitar a 8 segundos; si no cierra, matar el proceso del browser directamente.
|
|
2636
|
-
await Promise.race([
|
|
2637
|
-
browser.close().catch(() => {}),
|
|
2638
|
-
new Promise(r => setTimeout(r, 8000)),
|
|
2639
|
-
]);
|
|
2640
|
-
// Forzar cierre del proceso de Chromium si quedó colgado
|
|
2641
|
-
try { browser.process()?.kill('SIGKILL'); } catch { /* ignorar */ }
|
|
2642
|
-
}
|
|
2833
|
+
if (debugDir) {
|
|
2834
|
+
fs.writeFileSync(path.join(debugDir, `pdfte-enviar-sii-resp.txt`), enviarResp.body, 'utf8');
|
|
2643
2835
|
}
|
|
2836
|
+
|
|
2837
|
+
console.log(` ✓ Solicitud enviada al SII correctamente (${pdfPaths.length} PDFs, revisión ${revId})`);
|
|
2838
|
+
process.stdout.write('\nMUESTRAS SUBIDAS EXITOSAMENTE\n');
|
|
2839
|
+
return { success: true, revId };
|
|
2644
2840
|
}
|
|
2645
2841
|
|
|
2646
2842
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -2660,179 +2856,85 @@ class CertRunner {
|
|
|
2660
2856
|
* @returns {Promise<{success: boolean, setText?: string, error?: string}>}
|
|
2661
2857
|
*/
|
|
2662
2858
|
async obtenerSetBoletaPortal({ setPath, correoSet = '' } = {}) {
|
|
2663
|
-
const
|
|
2664
|
-
const
|
|
2665
|
-
const
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
const
|
|
2669
|
-
|
|
2670
|
-
let browser;
|
|
2671
|
-
try {
|
|
2672
|
-
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions());
|
|
2673
|
-
const page = await browser.newPage();
|
|
2674
|
-
await page.setCookie(...puppeteerCookies);
|
|
2675
|
-
page.on('dialog', async dlg => { console.log(` → [dialog SET=1] ${dlg.message()}`); await dlg.accept(); });
|
|
2676
|
-
|
|
2677
|
-
// Helper debug: guarda screenshot + HTML con timestamp
|
|
2678
|
-
const dbgDirSet1 = this.config.debugDir ? path.join(this.config.debugDir, 'boleta-set1') : null;
|
|
2679
|
-
if (dbgDirSet1) fs.mkdirSync(dbgDirSet1, { recursive: true });
|
|
2680
|
-
const saveDebugSet1 = async (label) => {
|
|
2681
|
-
if (!dbgDirSet1) return;
|
|
2682
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
2683
|
-
const base = path.join(dbgDirSet1, `${label}-${ts}`);
|
|
2684
|
-
try { await page.screenshot({ path: `${base}.png`, fullPage: true }); } catch {}
|
|
2685
|
-
try { fs.writeFileSync(`${base}.html`, await page.content(), 'utf-8'); } catch {}
|
|
2686
|
-
console.log(` [debug] Captura guardada: ${base}.png`);
|
|
2687
|
-
};
|
|
2688
|
-
|
|
2689
|
-
console.log(' → Cargando portal certBolElectDteInternet (SET=1)...');
|
|
2690
|
-
await page.goto('https://www4.sii.cl/certBolElectDteInternet/?SET=1', {
|
|
2691
|
-
waitUntil: 'networkidle2', timeout: 60000,
|
|
2692
|
-
});
|
|
2693
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
2694
|
-
await saveDebugSet1('01-carga');
|
|
2695
|
-
|
|
2696
|
-
// Paso 1: RUT empresa → "Confirmar Empresa"
|
|
2697
|
-
const rutInput = await page.waitForSelector('input[maxlength="8"]', { timeout: 15000 }).catch(() => null);
|
|
2698
|
-
const dvInput = await page.waitForSelector('input[maxlength="1"]', { timeout: 5000 }).catch(() => null);
|
|
2699
|
-
if (!rutInput) {
|
|
2700
|
-
await saveDebugSet1('ERROR-norut');
|
|
2701
|
-
throw new Error('certBolElectDteInternet/?SET=1: no se encontró campo RUT');
|
|
2702
|
-
}
|
|
2703
|
-
console.log(` → Ingresando RUT empresa: ${rutNum}-${dvChar}`);
|
|
2704
|
-
await rutInput.click({ clickCount: 3 }); await rutInput.type(rutNum);
|
|
2705
|
-
await dvInput.click({ clickCount: 3 }); await dvInput.type(dvChar);
|
|
2706
|
-
await page.evaluate(() => {
|
|
2707
|
-
const btn = Array.from(document.querySelectorAll('button'))
|
|
2708
|
-
.find(b => /confirmar/i.test(b.textContent));
|
|
2709
|
-
if (btn) btn.click();
|
|
2710
|
-
});
|
|
2711
|
-
console.log(' → Click "Confirmar Empresa"');
|
|
2712
|
-
|
|
2713
|
-
// Esperar checkboxes — GWT dispara ~8-10 POST /facade en paralelo
|
|
2714
|
-
await page.waitForNetworkIdle({ idleTime: 800, timeout: 40000 }).catch(() => {});
|
|
2715
|
-
await page.waitForFunction(() => {
|
|
2716
|
-
return document.querySelector('input[type="checkbox"]') !== null;
|
|
2717
|
-
}, { timeout: 40000, polling: 500 }).catch(() => {});
|
|
2718
|
-
await new Promise(r => setTimeout(r, 500));
|
|
2719
|
-
await saveDebugSet1('02-post-confirm');
|
|
2720
|
-
|
|
2721
|
-
// Paso 2: Marcar todos los checkboxes
|
|
2722
|
-
const nCbs = await page.evaluate(() => {
|
|
2723
|
-
const cbs = Array.from(document.querySelectorAll('input[type="checkbox"]'));
|
|
2724
|
-
cbs.forEach(cb => { if (!cb.checked) cb.click(); });
|
|
2725
|
-
return cbs.length;
|
|
2726
|
-
});
|
|
2727
|
-
console.log(` → ${nCbs} checkbox(es) marcados`);
|
|
2728
|
-
await new Promise(r => setTimeout(r, 300));
|
|
2729
|
-
|
|
2730
|
-
// Paso 3: Rellenar correo proveedor
|
|
2731
|
-
// El campo de email es el input visible que NO tiene maxlength pequeño
|
|
2732
|
-
const allInputs = await page.$$('input[type="text"].form-control');
|
|
2733
|
-
let emailInput = null;
|
|
2734
|
-
for (const inp of allInputs) {
|
|
2735
|
-
const ml = await page.evaluate(el => el.maxLength, inp);
|
|
2736
|
-
const visible = await page.evaluate(el => el.offsetParent !== null, inp);
|
|
2737
|
-
if (visible && (ml <= 0 || ml > 8)) { emailInput = inp; break; }
|
|
2738
|
-
}
|
|
2739
|
-
if (emailInput) {
|
|
2740
|
-
await emailInput.click({ clickCount: 3 });
|
|
2741
|
-
await emailInput.type(correoSet);
|
|
2742
|
-
console.log(` → Correo proveedor: ${correoSet}`);
|
|
2743
|
-
} else {
|
|
2744
|
-
console.log(' [!] No se encontró campo de correo — continuando sin él');
|
|
2745
|
-
}
|
|
2746
|
-
|
|
2747
|
-
// Paso 4: Click "Bajar Nuevo Set" — esperar POST /facade (GWT RPC) y luego
|
|
2748
|
-
// construir la URL de DownloadFileServlet directamente con los parámetros conocidos.
|
|
2749
|
-
// La descarga real es un GET:
|
|
2750
|
-
// DownloadFileServlet?rutEmpresa=X&dvEmpresa=X&rutRepre=X&dvRepre=X&mailProvSw=X
|
|
2751
|
-
// donde rutRepre/dvRepre vienen de las cookies NETSCAPE_LIVEWIRE.rut / .dv
|
|
2752
|
-
|
|
2753
|
-
const rutRepreNum = cookieJar['NETSCAPE_LIVEWIRE.rut'] || cookieJar['RUT_NS'] || '';
|
|
2754
|
-
const dvRepreChar = cookieJar['NETSCAPE_LIVEWIRE.dv'] || cookieJar['DV_NS'] || '';
|
|
2755
|
-
if (!rutRepreNum) throw new Error('No se pudo obtener rutRepre de las cookies SII (NETSCAPE_LIVEWIRE.rut)');
|
|
2756
|
-
|
|
2757
|
-
// Registrar listener de facade ANTES de hacer click
|
|
2758
|
-
const facadePromise = page.waitForResponse(
|
|
2759
|
-
resp => resp.url().includes('/certBolElectDteInternet/facade'),
|
|
2760
|
-
{ timeout: 20000 }
|
|
2761
|
-
).catch(() => null);
|
|
2762
|
-
|
|
2763
|
-
console.log(' → Click "Bajar Nuevo Set" — esperando GWT facade...');
|
|
2764
|
-
await page.evaluate(() => {
|
|
2765
|
-
const btn = Array.from(document.querySelectorAll('button'))
|
|
2766
|
-
.find(b => /bajar/i.test(b.textContent));
|
|
2767
|
-
if (btn) btn.click();
|
|
2768
|
-
});
|
|
2859
|
+
const https = require('https');
|
|
2860
|
+
const crypto = require('crypto');
|
|
2861
|
+
const CBE_BASE = 'https://www4.sii.cl/certBolElectDteInternet/';
|
|
2862
|
+
const CBE_PERM = '0FC3D987613537E6E13E9BB93A406F13';
|
|
2863
|
+
const CBE_POLICY = '082D0AC4BC4D75A5DF38F116C53877D4';
|
|
2864
|
+
const CBE_SVC = 'cl.sii.sdi.diii.certBolElectDte.web.client.service.Facade';
|
|
2769
2865
|
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2866
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
2867
|
+
const cookieStr = Object.entries(cookieJar).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
2868
|
+
const [rutNum, dvChar] = this.config.emisor.rut.replace(/\./g, '').split('-');
|
|
2869
|
+
const dvUp = dvChar.toUpperCase();
|
|
2773
2870
|
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2871
|
+
const tlsOpts = {
|
|
2872
|
+
rejectUnauthorized: false, maxVersion: 'TLSv1.2',
|
|
2873
|
+
secureOptions: crypto.constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
|
|
2874
|
+
| crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
|
2875
|
+
};
|
|
2876
|
+
const gwtPost = (bodyStr) => new Promise((resolve, reject) => {
|
|
2877
|
+
const buf = Buffer.from(bodyStr, 'utf-8');
|
|
2878
|
+
const req = https.request({
|
|
2879
|
+
hostname: 'www4.sii.cl', port: 443, path: '/certBolElectDteInternet/facade', method: 'POST',
|
|
2880
|
+
headers: {
|
|
2881
|
+
'Content-Type': 'text/x-gwt-rpc; charset=UTF-8',
|
|
2882
|
+
'X-GWT-Permutation': CBE_PERM,
|
|
2883
|
+
'X-GWT-Module-Base': CBE_BASE,
|
|
2884
|
+
'Cookie': cookieStr,
|
|
2885
|
+
'Content-Length': buf.length,
|
|
2886
|
+
},
|
|
2887
|
+
...tlsOpts,
|
|
2888
|
+
}, (res) => { const ch = []; res.on('data', c => ch.push(c)); res.on('end', () => resolve(Buffer.concat(ch).toString('utf-8'))); });
|
|
2889
|
+
req.on('error', reject);
|
|
2890
|
+
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout gwtPost CBE')); });
|
|
2891
|
+
req.write(buf); req.end();
|
|
2892
|
+
});
|
|
2781
2893
|
|
|
2782
|
-
|
|
2894
|
+
// recuperarRepresentantesVigentesUsuariosAutorizados -> obtener rutRepre/dvRepre
|
|
2895
|
+
const reprResp = await gwtPost(
|
|
2896
|
+
`7|0|7|${CBE_BASE}|${CBE_POLICY}|${CBE_SVC}|recuperarRepresentantesVigentesUsuariosAutorizados|java.lang.Integer/3438268394|java.lang.String/2004016611|${dvUp}|1|2|3|4|2|5|6|5|${rutNum}|7|`
|
|
2897
|
+
);
|
|
2898
|
+
// Response: //OK[...,["...","...RepreTo/...","<dv>","<rut>"],0,7]
|
|
2899
|
+
const reprMatch = reprResp.match(/"(d{1,2})","(d{7,8})"],0/);
|
|
2900
|
+
const dvRepreChar = reprMatch ? reprMatch[1] : (cookieJar['NETSCAPE_LIVEWIRE.dv'] || cookieJar['DV_NS'] || '');
|
|
2901
|
+
const rutRepreNum = reprMatch ? reprMatch[2] : (cookieJar['NETSCAPE_LIVEWIRE.rut'] || cookieJar['RUT_NS'] || '');
|
|
2902
|
+
if (!rutRepreNum) throw new Error('No se pudo obtener rutRepre (facade + cookies fallaron)');
|
|
2903
|
+
|
|
2904
|
+
// obtenerPostulacionSeg -> requerido por portal antes de descarga
|
|
2905
|
+
await gwtPost(
|
|
2906
|
+
`7|0|9|${CBE_BASE}|${CBE_POLICY}|${CBE_SVC}|obtenerPostulacionSeg|java.lang.Integer/3438268394|java.lang.String/2004016611|${dvUp}|90|P90|1|2|3|4|4|5|6|6|6|5|${rutNum}|7|8|9|`
|
|
2907
|
+
);
|
|
2783
2908
|
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
'Cookie': cookieStr,
|
|
2788
|
-
'Referer': 'https://www4.sii.cl/certBolElectDteInternet/?SET=1',
|
|
2789
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
2790
|
-
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
2791
|
-
},
|
|
2792
|
-
rejectUnauthorized: false,
|
|
2793
|
-
}, (res) => {
|
|
2794
|
-
const chunks = [];
|
|
2795
|
-
res.on('data', chunk => chunks.push(chunk));
|
|
2796
|
-
res.on('end', () => {
|
|
2797
|
-
const body = Buffer.concat(chunks).toString('utf-8');
|
|
2798
|
-
resolve(body);
|
|
2799
|
-
});
|
|
2800
|
-
});
|
|
2801
|
-
req.on('error', reject);
|
|
2802
|
-
req.setTimeout(30000, () => { req.destroy(); reject(new Error('Timeout descargando DownloadFileServlet')); });
|
|
2803
|
-
});
|
|
2909
|
+
// DownloadFileServlet GET
|
|
2910
|
+
const dlUrl = `${CBE_BASE}DownloadFileServlet?rutEmpresa=${rutNum}&dvEmpresa=${dvUp}&rutRepre=${rutRepreNum}&dvRepre=${dvRepreChar}&mailProvSw=${encodeURIComponent(correoSet)}`;
|
|
2911
|
+
console.log(` -> Descargando set boleta: DownloadFileServlet?rutEmpresa=${rutNum}&dvEmpresa=${dvUp}...`);
|
|
2804
2912
|
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2913
|
+
const setText = await new Promise((resolve, reject) => {
|
|
2914
|
+
const req = https.get(dlUrl, {
|
|
2915
|
+
headers: {
|
|
2916
|
+
'Cookie': cookieStr,
|
|
2917
|
+
'Referer': `${CBE_BASE}?SET=1`,
|
|
2918
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
2919
|
+
'Accept': 'text/plain,text/html,*/*',
|
|
2920
|
+
},
|
|
2921
|
+
...tlsOpts,
|
|
2922
|
+
}, (res) => { const ch = []; res.on('data', c => ch.push(c)); res.on('end', () => resolve(Buffer.concat(ch).toString('utf-8'))); });
|
|
2923
|
+
req.on('error', reject);
|
|
2924
|
+
req.setTimeout(30000, () => { req.destroy(); reject(new Error('Timeout DownloadFileServlet boleta')); });
|
|
2925
|
+
});
|
|
2812
2926
|
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
fs.mkdirSync(nodePath.dirname(setPath), { recursive: true });
|
|
2816
|
-
fs.writeFileSync(setPath, setText, 'utf-8');
|
|
2817
|
-
console.log(` ✓ Set guardado en: ${setPath}`);
|
|
2818
|
-
}
|
|
2927
|
+
if (!setText || setText.trim().length < 10)
|
|
2928
|
+
throw new Error(`DownloadFileServlet boleta vacio (${setText?.length ?? 0} chars). Verificar sesion SII.`);
|
|
2819
2929
|
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
2826
|
-
const base = path.join(dbgDirSet1, `ERROR-catch-${ts}`);
|
|
2827
|
-
await page.screenshot({ path: `${base}.png`, fullPage: true }).catch(() => {});
|
|
2828
|
-
fs.writeFileSync(`${base}.html`, await page.content().catch(() => ''), 'utf-8');
|
|
2829
|
-
console.log(` [debug] Captura de error guardada: ${base}.png`);
|
|
2830
|
-
}
|
|
2831
|
-
} catch {}
|
|
2832
|
-
return { success: false, error: err.message };
|
|
2833
|
-
} finally {
|
|
2834
|
-
if (browser) await browser.close().catch(() => {});
|
|
2930
|
+
if (setPath) {
|
|
2931
|
+
const nodePath = require('path');
|
|
2932
|
+
fs.mkdirSync(nodePath.dirname(setPath), { recursive: true });
|
|
2933
|
+
fs.writeFileSync(setPath, setText, 'utf-8');
|
|
2934
|
+
console.log(` OK Set boleta guardado en: ${setPath}`);
|
|
2835
2935
|
}
|
|
2936
|
+
console.log(` OK Set de pruebas boleta obtenido (${setText.length} chars)`);
|
|
2937
|
+
return { success: true, setText };
|
|
2836
2938
|
}
|
|
2837
2939
|
|
|
2838
2940
|
/**
|
|
@@ -2848,95 +2950,53 @@ class CertRunner {
|
|
|
2848
2950
|
*/
|
|
2849
2951
|
async solicitarValidacionBoletaPortal({ trackId } = {}) {
|
|
2850
2952
|
if (!trackId) throw new Error('solicitarValidacionBoletaPortal: trackId es obligatorio');
|
|
2851
|
-
const
|
|
2852
|
-
const
|
|
2853
|
-
const
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
const
|
|
2953
|
+
const https = require('https');
|
|
2954
|
+
const crypto = require('crypto');
|
|
2955
|
+
const CBE_BASE = 'https://www4.sii.cl/certBolElectDteInternet/';
|
|
2956
|
+
const CBE_PERM = '0FC3D987613537E6E13E9BB93A406F13';
|
|
2957
|
+
const CBE_POLICY = '082D0AC4BC4D75A5DF38F116C53877D4';
|
|
2958
|
+
const CBE_SVC = 'cl.sii.sdi.diii.certBolElectDte.web.client.service.Facade';
|
|
2857
2959
|
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
page.on('dialog', async dlg => { console.log(` → [dialog SET=2] ${dlg.message()}`); await dlg.accept(); });
|
|
2864
|
-
|
|
2865
|
-
const debugSet2Dir = this.config.debugDir ? require('path').join(this.config.debugDir, 'boleta-set2') : null;
|
|
2866
|
-
if (debugSet2Dir) require('fs').mkdirSync(debugSet2Dir, { recursive: true });
|
|
2867
|
-
const snapSet2 = async (nombre) => {
|
|
2868
|
-
if (!debugSet2Dir) return;
|
|
2869
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
2870
|
-
const p = require('path').join(debugSet2Dir, `${nombre}-${ts}.png`);
|
|
2871
|
-
await page.screenshot({ path: p, fullPage: true }).catch(() => {});
|
|
2872
|
-
console.log(` [debug] Captura guardada: ${p}`);
|
|
2873
|
-
};
|
|
2960
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
2961
|
+
const cookieStr = Object.entries(cookieJar).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
2962
|
+
const [rutNum, dvChar] = this.config.emisor.rut.replace(/\./g, '').split('-');
|
|
2963
|
+
const dvUp = dvChar.toUpperCase();
|
|
2964
|
+
const trackIdStr = String(trackId);
|
|
2874
2965
|
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
await page.waitForNetworkIdle({ idleTime: 800, timeout: 40000 }).catch(() => {});
|
|
2898
|
-
await page.waitForFunction(() => {
|
|
2899
|
-
return document.querySelector('input[maxlength="15"]') !== null;
|
|
2900
|
-
}, { timeout: 40000, polling: 500 }).catch(() => {});
|
|
2901
|
-
await new Promise(r => setTimeout(r, 500));
|
|
2902
|
-
await snapSet2('02-post-confirm');
|
|
2903
|
-
|
|
2904
|
-
// Paso 2: Ingresar TrackId
|
|
2905
|
-
const trackInput = await page.$('input[maxlength="15"]');
|
|
2906
|
-
if (!trackInput) {
|
|
2907
|
-
const pageText = await page.evaluate(() => (document.body.innerText || '').trim().substring(0, 600));
|
|
2908
|
-
console.log(` [!] Texto del portal al fallar:\n${pageText}`);
|
|
2909
|
-
throw new Error('No se encontró campo "Identificador de Envio" en certBolElectDteInternet/?SET=2');
|
|
2910
|
-
}
|
|
2911
|
-
console.log(` → Ingresando TrackId: ${trackId}`);
|
|
2912
|
-
await trackInput.click({ clickCount: 3 });
|
|
2913
|
-
await trackInput.type(String(trackId));
|
|
2914
|
-
|
|
2915
|
-
// Paso 3: Click "Solicitar validación"
|
|
2916
|
-
await page.evaluate(() => {
|
|
2917
|
-
const btn = Array.from(document.querySelectorAll('button'))
|
|
2918
|
-
.find(b => /solicitar/i.test(b.textContent));
|
|
2919
|
-
if (btn) btn.click();
|
|
2920
|
-
});
|
|
2921
|
-
console.log(' → Click "Solicitar validación" — esperando respuesta...');
|
|
2966
|
+
const tlsOpts = {
|
|
2967
|
+
rejectUnauthorized: false, maxVersion: 'TLSv1.2',
|
|
2968
|
+
secureOptions: crypto.constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
|
|
2969
|
+
| crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
|
2970
|
+
};
|
|
2971
|
+
const gwtPost = (bodyStr) => new Promise((resolve, reject) => {
|
|
2972
|
+
const buf = Buffer.from(bodyStr, 'utf-8');
|
|
2973
|
+
const req = https.request({
|
|
2974
|
+
hostname: 'www4.sii.cl', port: 443, path: '/certBolElectDteInternet/facade', method: 'POST',
|
|
2975
|
+
headers: {
|
|
2976
|
+
'Content-Type': 'text/x-gwt-rpc; charset=UTF-8',
|
|
2977
|
+
'X-GWT-Permutation': CBE_PERM,
|
|
2978
|
+
'X-GWT-Module-Base': CBE_BASE,
|
|
2979
|
+
'Cookie': cookieStr,
|
|
2980
|
+
'Content-Length': buf.length,
|
|
2981
|
+
},
|
|
2982
|
+
...tlsOpts,
|
|
2983
|
+
}, (res) => { const ch = []; res.on('data', c => ch.push(c)); res.on('end', () => resolve(Buffer.concat(ch).toString('utf-8'))); });
|
|
2984
|
+
req.on('error', reject);
|
|
2985
|
+
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout gwtPost CBE')); });
|
|
2986
|
+
req.write(buf); req.end();
|
|
2987
|
+
});
|
|
2922
2988
|
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
await snapSet2('03-respuesta');
|
|
2989
|
+
// ingresarTrackId — GWT body format from BOLETA_SET2.har
|
|
2990
|
+
// 7 params: (Integer rutNum, String dvChar, String null, String "90", Integer 0, String null, String trackId)
|
|
2991
|
+
const body = `7|0|9|${CBE_BASE}|${CBE_POLICY}|${CBE_SVC}|ingresarTrackId|java.lang.Integer/3438268394|java.lang.String/2004016611|${dvUp}|90|${trackIdStr}|1|2|3|4|7|5|6|6|6|5|6|6|5|${rutNum}|7|0|8|0|0|9|`;
|
|
2992
|
+
console.log(` -> certBolElectDteInternet/?SET=2: ingresarTrackId trackId=${trackIdStr}...`);
|
|
2993
|
+
const resp = await gwtPost(body);
|
|
2994
|
+
console.log(` OK ingresarTrackId respuesta: ${resp.substring(0, 120)}`);
|
|
2930
2995
|
|
|
2931
|
-
|
|
2932
|
-
|
|
2996
|
+
if (!resp.startsWith('//OK'))
|
|
2997
|
+
return { success: false, error: `ingresarTrackId fallo: ${resp.substring(0, 200)}` };
|
|
2933
2998
|
|
|
2934
|
-
|
|
2935
|
-
} catch (err) {
|
|
2936
|
-
return { success: false, error: err.message };
|
|
2937
|
-
} finally {
|
|
2938
|
-
if (browser) await browser.close().catch(() => {});
|
|
2939
|
-
}
|
|
2999
|
+
return { success: true, respuesta: resp };
|
|
2940
3000
|
}
|
|
2941
3001
|
|
|
2942
3002
|
/**
|
|
@@ -2955,220 +3015,101 @@ class CertRunner {
|
|
|
2955
3015
|
nombreProveedor = '',
|
|
2956
3016
|
correoProveedor = '',
|
|
2957
3017
|
} = {}) {
|
|
2958
|
-
const
|
|
2959
|
-
const
|
|
3018
|
+
const https = require('https');
|
|
3019
|
+
const crypto = require('crypto');
|
|
3020
|
+
const CBE_BASE = 'https://www4.sii.cl/certBolElectDteInternet/';
|
|
3021
|
+
const CBE_PERM = '0FC3D987613537E6E13E9BB93A406F13';
|
|
3022
|
+
const CBE_POLICY = '082D0AC4BC4D75A5DF38F116C53877D4';
|
|
3023
|
+
const CBE_SVC = 'cl.sii.sdi.diii.certBolElectDte.web.client.service.Facade';
|
|
2960
3024
|
|
|
2961
|
-
|
|
2962
|
-
const
|
|
2963
|
-
const [
|
|
2964
|
-
const
|
|
3025
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
3026
|
+
const cookieStr = Object.entries(cookieJar).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
3027
|
+
const [rutNum, dvChar] = this.config.emisor.rut.replace(/\./g, '').split('-');
|
|
3028
|
+
const dvUp = dvChar.toUpperCase();
|
|
3029
|
+
const razonSocial = this.config.emisor.razon_social || this.config.emisor.razonSocial || '';
|
|
2965
3030
|
|
|
2966
|
-
const
|
|
2967
|
-
|
|
2968
|
-
|
|
3031
|
+
const tlsOpts = {
|
|
3032
|
+
rejectUnauthorized: false,
|
|
3033
|
+
maxVersion: 'TLSv1.2',
|
|
3034
|
+
secureOptions: crypto.constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION
|
|
3035
|
+
| crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
|
3036
|
+
};
|
|
3037
|
+
const gwtPost = (bodyStr) => new Promise((resolve, reject) => {
|
|
3038
|
+
const buf = Buffer.from(bodyStr, 'utf-8');
|
|
3039
|
+
const req = https.request({
|
|
3040
|
+
hostname: 'www4.sii.cl', port: 443, path: '/certBolElectDteInternet/facade', method: 'POST',
|
|
3041
|
+
headers: {
|
|
3042
|
+
'Content-Type': 'text/x-gwt-rpc; charset=UTF-8',
|
|
3043
|
+
'X-GWT-Permutation': CBE_PERM,
|
|
3044
|
+
'X-GWT-Module-Base': CBE_BASE,
|
|
3045
|
+
'Cookie': cookieStr,
|
|
3046
|
+
'Content-Length': buf.length,
|
|
3047
|
+
},
|
|
3048
|
+
...tlsOpts,
|
|
3049
|
+
}, (res) => { const ch = []; res.on('data', c => ch.push(c)); res.on('end', () => resolve(Buffer.concat(ch).toString('utf-8'))); });
|
|
3050
|
+
req.on('error', reject);
|
|
3051
|
+
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout gwtPost CBE declaracion')); });
|
|
3052
|
+
req.write(buf); req.end();
|
|
3053
|
+
});
|
|
2969
3054
|
|
|
2970
|
-
|
|
3055
|
+
// 1. Obtener representante legal vigente
|
|
3056
|
+
const reprResp = await gwtPost(
|
|
3057
|
+
`7|0|7|${CBE_BASE}|${CBE_POLICY}|${CBE_SVC}|recuperarRepresentantesVigentesUsuariosAutorizados|java.lang.Integer/3438268394|java.lang.String/2004016611|${dvUp}|1|2|3|4|2|5|6|5|${rutNum}|7|`
|
|
3058
|
+
);
|
|
3059
|
+
const reprTableStr = reprResp.substring(reprResp.lastIndexOf(',[') + 1, reprResp.lastIndexOf('],0,7]') + 1);
|
|
3060
|
+
let rutRepreNum = '';
|
|
2971
3061
|
try {
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
const dbgDirDecl = this.config.debugDir ? path.join(this.config.debugDir, 'boleta-declaracion') : null;
|
|
2986
|
-
if (dbgDirDecl) fs.mkdirSync(dbgDirDecl, { recursive: true });
|
|
2987
|
-
const saveDebugDecl = async (label) => {
|
|
2988
|
-
if (!dbgDirDecl) return;
|
|
2989
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
2990
|
-
const base = path.join(dbgDirDecl, `${label}-${ts}`);
|
|
2991
|
-
try { await page.screenshot({ path: `${base}.png`, fullPage: true }); } catch {}
|
|
2992
|
-
try { fs.writeFileSync(`${base}.html`, await page.content(), 'utf-8'); } catch {}
|
|
2993
|
-
console.log(` [debug] Captura guardada: ${base}.png`);
|
|
2994
|
-
};
|
|
2995
|
-
|
|
2996
|
-
// ── PASOS 1+2: Confirmar Empresa → esperar formulario (con retry) ──
|
|
2997
|
-
// El portal GWT a veces responde con error transitorio ("empresa no autorizada")
|
|
2998
|
-
// que se resuelve recargando la página y reintentando.
|
|
2999
|
-
const MAX_INTENTOS = 3;
|
|
3000
|
-
let checkboxOk = false;
|
|
3001
|
-
let lastDialogMsg = null;
|
|
3002
|
-
for (let intento = 1; intento <= MAX_INTENTOS; intento++) {
|
|
3003
|
-
dialogMsg = null; // resetear entre intentos
|
|
3004
|
-
|
|
3005
|
-
console.log(` → Navegando a certBolElectDteInternet (declaración) [intento ${intento}/${MAX_INTENTOS}]...`);
|
|
3006
|
-
await page.goto('https://www4.sii.cl/certBolElectDteInternet/', {
|
|
3007
|
-
waitUntil: 'networkidle2', timeout: 60000,
|
|
3008
|
-
});
|
|
3009
|
-
await new Promise(r => setTimeout(r, 2500));
|
|
3010
|
-
await saveDebugDecl(`intento${intento}-01-carga`);
|
|
3011
|
-
|
|
3012
|
-
// GWT requiere eventos de teclado reales — NO funciona con .value = ...
|
|
3013
|
-
const rutInput = await page.waitForSelector('input[maxlength="8"]', { timeout: 15000 }).catch(() => null);
|
|
3014
|
-
const dvInput = await page.waitForSelector('input[maxlength="1"]', { timeout: 5000 }).catch(() => null);
|
|
3015
|
-
if (!rutInput) {
|
|
3016
|
-
await saveDebugDecl(`intento${intento}-ERROR-norut`);
|
|
3017
|
-
throw new Error('certBolElectDteInternet/: no se encontró campo RUT empresa');
|
|
3018
|
-
}
|
|
3019
|
-
console.log(` → Ingresando RUT empresa: ${rutEmpresaRaw}`);
|
|
3020
|
-
await rutInput.click({ clickCount: 3 }); await rutInput.type(rutEmpNum);
|
|
3021
|
-
await dvInput.click({ clickCount: 3 }); await dvInput.type(rutEmpDv);
|
|
3022
|
-
|
|
3023
|
-
await page.evaluate(() => {
|
|
3024
|
-
const btn = Array.from(document.querySelectorAll('button'))
|
|
3025
|
-
.find(b => /confirmar empresa/i.test(b.textContent));
|
|
3026
|
-
if (btn) btn.click();
|
|
3027
|
-
});
|
|
3028
|
-
console.log(' → Click "Confirmar Empresa"...');
|
|
3029
|
-
|
|
3030
|
-
// GWT dispara ~8-10 POST /facade EN PARALELO al confirmar.
|
|
3031
|
-
// Esperar red inactiva y luego DOM con checkboxes.
|
|
3032
|
-
await page.waitForNetworkIdle({ idleTime: 800, timeout: 40000 }).catch(() => {});
|
|
3033
|
-
await page.waitForFunction(() => {
|
|
3034
|
-
return document.querySelector('input[type="checkbox"]') !== null;
|
|
3035
|
-
}, { timeout: 40000, polling: 500 }).catch(() => {});
|
|
3036
|
-
await saveDebugDecl(`intento${intento}-02-post-confirm`);
|
|
3037
|
-
|
|
3038
|
-
if (dialogMsg) {
|
|
3039
|
-
// Portal lanzó alert — puede ser transitorio. Guardar y reintentar.
|
|
3040
|
-
lastDialogMsg = dialogMsg;
|
|
3041
|
-
await saveDebugDecl(`intento${intento}-DIALOG`);
|
|
3042
|
-
console.log(` [!] Portal respondió con alerta en intento ${intento}: ${dialogMsg.substring(0, 100)}`);
|
|
3043
|
-
if (intento < MAX_INTENTOS) {
|
|
3044
|
-
console.log(' → Recargando y reintentando en 3s...');
|
|
3045
|
-
await new Promise(r => setTimeout(r, 3000));
|
|
3046
|
-
continue;
|
|
3047
|
-
}
|
|
3048
|
-
// Agotados los intentos con alerta — el SII puede requerir esperar SOK
|
|
3049
|
-
await saveDebugDecl('PENDINGSOK-final');
|
|
3050
|
-
return { success: false, pendingSok: true, error: lastDialogMsg };
|
|
3051
|
-
}
|
|
3052
|
-
|
|
3053
|
-
if (await page.$('input[type="checkbox"]')) {
|
|
3054
|
-
checkboxOk = true;
|
|
3055
|
-
break;
|
|
3056
|
-
}
|
|
3057
|
-
|
|
3058
|
-
console.log(` [!] Formulario no cargó en intento ${intento}${intento < MAX_INTENTOS ? ` — reintentando...` : ''}`);
|
|
3059
|
-
await new Promise(r => setTimeout(r, 2000));
|
|
3060
|
-
}
|
|
3061
|
-
|
|
3062
|
-
if (!checkboxOk) {
|
|
3063
|
-
throw new Error('Formulario de declaración no cargó tras confirmar empresa (3 intentos)');
|
|
3064
|
-
}
|
|
3065
|
-
|
|
3066
|
-
const totalCbs = await page.evaluate(() => {
|
|
3067
|
-
const todos = Array.from(document.querySelectorAll('input[type="checkbox"]'));
|
|
3068
|
-
todos.forEach(cb => { if (!cb.checked) cb.click(); });
|
|
3069
|
-
return todos.length;
|
|
3070
|
-
});
|
|
3071
|
-
console.log(` → ${totalCbs} checkbox(es) marcados`);
|
|
3072
|
-
await new Promise(r => setTimeout(r, 500));
|
|
3073
|
-
|
|
3074
|
-
// ── PASO 3: Rellenar campos proveedor software ────────────────
|
|
3075
|
-
// GWT requiere page.type() real — NOT funciona con .value + dispatchEvent
|
|
3076
|
-
const fillByLabel = async (labelFragment, value) => {
|
|
3077
|
-
const handle = await page.evaluateHandle((frag) => {
|
|
3078
|
-
for (const td of document.querySelectorAll('td')) {
|
|
3079
|
-
if (!td.textContent.includes(frag)) continue;
|
|
3080
|
-
const next = td.nextElementSibling;
|
|
3081
|
-
if (!next) continue;
|
|
3082
|
-
const inp = next.querySelector('input[type="text"]');
|
|
3083
|
-
if (inp) return inp;
|
|
3084
|
-
}
|
|
3085
|
-
return null;
|
|
3086
|
-
}, labelFragment);
|
|
3087
|
-
const elem = handle && await handle.asElement();
|
|
3088
|
-
if (!elem) return false;
|
|
3089
|
-
await elem.click({ clickCount: 3 });
|
|
3090
|
-
await elem.type(value);
|
|
3091
|
-
return true;
|
|
3092
|
-
};
|
|
3093
|
-
|
|
3094
|
-
// Link de Consulta (maxlength=100)
|
|
3095
|
-
const linkOk = await fillByLabel('Link de Consulta', linkConsulta);
|
|
3096
|
-
if (!linkOk) {
|
|
3097
|
-
const inp = await page.$('input[type="text"][maxlength="100"]');
|
|
3098
|
-
if (inp) { await inp.click({ clickCount: 3 }); await inp.type(linkConsulta); }
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
// RUT Proveedor — dos inputs (num + DV) en la fila "Rut Proveedor"
|
|
3102
|
-
const [rutProvNum, rutProvDvRaw] = rutProveedor.replace(/\./g, '').split('-');
|
|
3103
|
-
const rutProvDv = (rutProvDvRaw || 'K').toUpperCase();
|
|
3104
|
-
const rutProvH = await page.evaluateHandle(() => {
|
|
3105
|
-
for (const td of document.querySelectorAll('td')) {
|
|
3106
|
-
if (!td.textContent.includes('Rut Proveedor')) continue;
|
|
3107
|
-
const next = td.nextElementSibling;
|
|
3108
|
-
if (next) { const ins = next.querySelectorAll('input[type="text"]'); if (ins[0]) return ins[0]; }
|
|
3109
|
-
}
|
|
3110
|
-
return null;
|
|
3111
|
-
});
|
|
3112
|
-
const dvProvH = await page.evaluateHandle(() => {
|
|
3113
|
-
for (const td of document.querySelectorAll('td')) {
|
|
3114
|
-
if (!td.textContent.includes('Rut Proveedor')) continue;
|
|
3115
|
-
const next = td.nextElementSibling;
|
|
3116
|
-
if (next) { const ins = next.querySelectorAll('input[type="text"]'); if (ins[1]) return ins[1]; }
|
|
3117
|
-
}
|
|
3118
|
-
return null;
|
|
3119
|
-
});
|
|
3120
|
-
if (rutProvH && await rutProvH.asElement()) { const e = rutProvH.asElement(); await e.click({ clickCount: 3 }); await e.type(rutProvNum); }
|
|
3121
|
-
if (dvProvH && await dvProvH.asElement()) { const e = dvProvH.asElement(); await e.click({ clickCount: 3 }); await e.type(rutProvDv); }
|
|
3122
|
-
|
|
3123
|
-
// Nombre Proveedor
|
|
3124
|
-
await fillByLabel('Nombre Proveedor', nombreProveedor);
|
|
3125
|
-
|
|
3126
|
-
// Correo Proveedor Software
|
|
3127
|
-
await fillByLabel('Correo electrónico Proveedor', correoProveedor);
|
|
3128
|
-
|
|
3129
|
-
// Captura pre-submit
|
|
3130
|
-
await saveDebugDecl('03-pre-submit');
|
|
3131
|
-
|
|
3132
|
-
// ── PASO 4: Click "Grabar Declaración" ───────────────────────
|
|
3133
|
-
const submitOk = await page.evaluate(() => {
|
|
3134
|
-
const btn = Array.from(document.querySelectorAll('button'))
|
|
3135
|
-
.find(b => /grabar declaraci/i.test(b.textContent));
|
|
3136
|
-
if (btn) { btn.click(); return true; }
|
|
3137
|
-
return false;
|
|
3138
|
-
});
|
|
3139
|
-
if (!submitOk) {
|
|
3140
|
-
await saveDebugDecl('ERROR-no-boton-grabar');
|
|
3141
|
-
throw new Error('No se encontró botón "Grabar Declaración" en certBolElectDteInternet/');
|
|
3142
|
-
}
|
|
3143
|
-
console.log(' → Click "Grabar Declaración"...');
|
|
3144
|
-
|
|
3145
|
-
// Esperar confirmación del SII
|
|
3146
|
-
await page.waitForFunction(() => {
|
|
3147
|
-
const t = (document.body.textContent || '').toUpperCase();
|
|
3148
|
-
return t.includes('GRABADA') || t.includes('GRABADO') || t.includes('COMPLETADA') ||
|
|
3149
|
-
t.includes('EXITOSA') || t.includes('GUARDADA') || t.includes('REGISTRADA');
|
|
3150
|
-
}, { timeout: 20000, polling: 1000 }).catch(() => {});
|
|
3151
|
-
|
|
3152
|
-
const msgFinal = await page.evaluate(() => (document.body.textContent || '').trim().substring(0, 300));
|
|
3153
|
-
console.log(` ✓ Declaración completada. Respuesta: ${msgFinal.substring(0, 150)}`);
|
|
3154
|
-
await saveDebugDecl('04-post-submit-OK');
|
|
3155
|
-
|
|
3156
|
-
return { success: true, mensaje: msgFinal };
|
|
3157
|
-
} catch (err) {
|
|
3158
|
-
// Guardar captura del estado de error si page sigue abierta
|
|
3159
|
-
try {
|
|
3160
|
-
if (dbgDirDecl) {
|
|
3161
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
3162
|
-
const base = path.join(dbgDirDecl, `ERROR-catch-${ts}`);
|
|
3163
|
-
await page.screenshot({ path: `${base}.png`, fullPage: true }).catch(() => {});
|
|
3164
|
-
fs.writeFileSync(`${base}.html`, await page.content().catch(() => ''), 'utf-8');
|
|
3165
|
-
console.log(` [debug] Captura de error guardada: ${base}.png`);
|
|
3166
|
-
}
|
|
3167
|
-
} catch {}
|
|
3168
|
-
return { success: false, error: err.message };
|
|
3169
|
-
} finally {
|
|
3170
|
-
if (browser) await browser.close().catch(() => {});
|
|
3062
|
+
const reprTable = JSON.parse(reprTableStr);
|
|
3063
|
+
rutRepreNum = reprTable.filter(s => /^\d{7,8}$/.test(s)).pop() || '';
|
|
3064
|
+
} catch {}
|
|
3065
|
+
if (!rutRepreNum) return { success: false, error: 'No se pudo obtener representante vigente (facade CBE)' };
|
|
3066
|
+
|
|
3067
|
+
// 2. Verificar estado portal — debe ser P90 (SOK recibido, listo para declarar)
|
|
3068
|
+
const estadoResp = await gwtPost(
|
|
3069
|
+
`7|0|7|${CBE_BASE}|${CBE_POLICY}|${CBE_SVC}|obtenerEstadoAutorizaEmp|java.lang.Integer/3438268394|java.lang.String/2004016611|${dvUp}|1|2|3|4|4|5|6|5|6|5|${rutNum}|7|5|90|0|`
|
|
3070
|
+
);
|
|
3071
|
+
if (!estadoResp.includes('P90')) {
|
|
3072
|
+
const estadoMatch = estadoResp.match(/"(P\d+)"/);
|
|
3073
|
+
const estado = estadoMatch ? estadoMatch[1] : '(desconocido)';
|
|
3074
|
+
return { success: false, pendingSok: true, error: `Estado portal no es P90 (es ${estado}) — espere SOK del SII` };
|
|
3171
3075
|
}
|
|
3076
|
+
|
|
3077
|
+
// 3. Obtener datos de postulacion: fchAutorizacion y longCharValue
|
|
3078
|
+
const postulResp = await gwtPost(
|
|
3079
|
+
`7|0|9|${CBE_BASE}|${CBE_POLICY}|${CBE_SVC}|obtenerPostulacionSeg|java.lang.Integer/3438268394|java.lang.String/2004016611|${dvUp}|90|P90|1|2|3|4|4|5|6|6|6|5|${rutNum}|7|8|9|`
|
|
3080
|
+
);
|
|
3081
|
+
const postulTableStr = postulResp.substring(postulResp.lastIndexOf(',[') + 1, postulResp.lastIndexOf('],0,7]') + 1);
|
|
3082
|
+
let fchAutorizacion = '';
|
|
3083
|
+
try {
|
|
3084
|
+
const postulTable = JSON.parse(postulTableStr);
|
|
3085
|
+
fchAutorizacion = postulTable.find(s => /^\d{2}\/\d{2}\/\d{4}$/.test(s)) || '';
|
|
3086
|
+
} catch {}
|
|
3087
|
+
const longMatch = /'([^']+)'/.exec(postulResp);
|
|
3088
|
+
const longCharValue = longMatch ? longMatch[1] : '0';
|
|
3089
|
+
|
|
3090
|
+
// 4. Autorizar empresa boleta produccion
|
|
3091
|
+
const fechaHoy = this._getFechaHoy();
|
|
3092
|
+
const authBody =
|
|
3093
|
+
`7|0|23|${CBE_BASE}|${CBE_POLICY}|${CBE_SVC}|autorizarEmpresaBolProd|` +
|
|
3094
|
+
`cl.sii.sdi.diii.certBolElectDte.to.PostulSegHistInsUpdTo/138688689|P91|BVE|` +
|
|
3095
|
+
`java.lang.Integer/3438268394|${dvUp}|` +
|
|
3096
|
+
`cl.sii.sdi.diii.certBolElectDte.to.TdtEmpresaAutorizadaTo/2240026086|8|SII|` +
|
|
3097
|
+
`${fechaHoy}|${correoProveedor}|${nombreProveedor}|19|S|${linkConsulta}|` +
|
|
3098
|
+
`java.util.ArrayList/4159755760|cl.sii.sdi.diii.certBolElectDte.to.DocumentoAutorizadoTo/493967287|` +
|
|
3099
|
+
`${fchAutorizacion}|java.lang.Long/4227064769|${razonSocial}|` +
|
|
3100
|
+
`1|2|3|4|1|5|5|6|0|7|8|90|0|0|0|0|9|0|10|0|7|9|9|11|12|13|13|13|14|15|16|17|8|` +
|
|
3101
|
+
`${rutNum}|8|${rutNum}|8|${rutRepreNum}|18|13|13|0|19|2|20|0|0|9|11|21|0|0|0|0|0|0|8|` +
|
|
3102
|
+
`${rutNum}|8|${rutRepreNum}|22|${longCharValue}|-11|8|39|0|20|0|0|9|11|21|0|0|0|0|0|0|8|` +
|
|
3103
|
+
`${rutNum}|8|${rutRepreNum}|-11|-11|8|41|0|0|23|-11|0|8|${rutNum}|0|0|`;
|
|
3104
|
+
|
|
3105
|
+
console.log(` -> autorizarEmpresaBolProd rutNum=${rutNum} rutRepreNum=${rutRepreNum} fchAutorizacion=${fchAutorizacion} longCharValue=${longCharValue}`);
|
|
3106
|
+
const authResp = await gwtPost(authBody);
|
|
3107
|
+
console.log(` OK autorizarEmpresaBolProd respuesta: ${authResp.substring(0, 200)}`);
|
|
3108
|
+
|
|
3109
|
+
if (!authResp.startsWith('//OK') || !authResp.includes('DECLARACION EFECTUADA')) {
|
|
3110
|
+
return { success: false, error: `autorizarEmpresaBolProd respuesta inesperada: ${authResp.substring(0, 300)}` };
|
|
3111
|
+
}
|
|
3112
|
+
return { success: true, mensaje: 'DECLARACION EFECTUADA' };
|
|
3172
3113
|
}
|
|
3173
3114
|
|
|
3174
3115
|
/**
|
|
@@ -3272,33 +3213,6 @@ class CertRunner {
|
|
|
3272
3213
|
// Helpers privados
|
|
3273
3214
|
// ═══════════════════════════════════════════════════════════════
|
|
3274
3215
|
|
|
3275
|
-
/**
|
|
3276
|
-
* Opciones base para puppeteer.launch.
|
|
3277
|
-
* Usa Chrome del sistema (executablePath) para evitar crashes del Chromium
|
|
3278
|
-
* bundled en Windows (0xC0000409). Respeta this.config.puppeteerHeadless.
|
|
3279
|
-
*/
|
|
3280
|
-
_getPuppeteerLaunchOptions(extraArgs = [], extraOpts = {}) {
|
|
3281
|
-
const fs = require('fs');
|
|
3282
|
-
const chromePaths = [
|
|
3283
|
-
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
3284
|
-
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
3285
|
-
process.env.LOCALAPPDATA ? process.env.LOCALAPPDATA + '\\Google\\Chrome\\Application\\chrome.exe' : null,
|
|
3286
|
-
].filter(Boolean);
|
|
3287
|
-
const systemChrome = chromePaths.find(p => fs.existsSync(p));
|
|
3288
|
-
|
|
3289
|
-
const opts = {
|
|
3290
|
-
headless: this.config.puppeteerHeadless !== false,
|
|
3291
|
-
ignoreHTTPSErrors: true,
|
|
3292
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors', ...extraArgs],
|
|
3293
|
-
...extraOpts,
|
|
3294
|
-
};
|
|
3295
|
-
if (systemChrome) {
|
|
3296
|
-
opts.executablePath = systemChrome;
|
|
3297
|
-
console.log(` [browser] Usando Chrome del sistema: ${systemChrome}`);
|
|
3298
|
-
}
|
|
3299
|
-
return opts;
|
|
3300
|
-
}
|
|
3301
|
-
|
|
3302
3216
|
_getFechaHoy() {
|
|
3303
3217
|
const now = new Date();
|
|
3304
3218
|
return `${String(now.getDate()).padStart(2, '0')}-${String(now.getMonth() + 1).padStart(2, '0')}-${now.getFullYear()}`;
|