@devlas/dte-sii 2.5.15 → 2.6.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/ConsumoFolio.js +2 -1
- package/EnviadorSII.js +24 -1
- package/cert/BoletaCert.js +129 -45
- package/cert/CertRunner.js +480 -144
- package/cert/ConfigLoader.js +1 -1
- package/package.json +1 -1
- package/utils/progress.js +1 -0
package/ConsumoFolio.js
CHANGED
|
@@ -223,8 +223,9 @@ class ConsumoFolio {
|
|
|
223
223
|
}
|
|
224
224
|
|
|
225
225
|
// Construir XML SIN indentación para C14N consistente
|
|
226
|
+
// NOTA: SII DTEUpload requiere ISO-8859-1 (endpoint SOAP/CGI antiguo)
|
|
226
227
|
const schemaLoc = 'http://www.sii.cl/SiiDte ConsumoFolio_v10.xsd';
|
|
227
|
-
const xmlSinFirma = `<?xml version="1.0" encoding="
|
|
228
|
+
const xmlSinFirma = `<?xml version="1.0" encoding="ISO-8859-1"?>
|
|
228
229
|
<ConsumoFolios xmlns="http://www.sii.cl/SiiDte" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="${schemaLoc}" version="1.0"><DocumentoConsumoFolios ID="${this.id}"><Caratula version="1.0"><RutEmisor>${this.caratula.RutEmisor}</RutEmisor><RutEnvia>${this.caratula.RutEnvia}</RutEnvia><FchResol>${this.caratula.FchResol}</FchResol><NroResol>${this.caratula.NroResol}</NroResol><FchInicio>${this.caratula.FchInicio}</FchInicio><FchFinal>${this.caratula.FchFinal}</FchFinal><SecEnvio>${this.caratula.SecEnvio}</SecEnvio><TmstFirmaEnv>${this.caratula.TmstFirmaEnv}</TmstFirmaEnv></Caratula>${resumenXml}</DocumentoConsumoFolios></ConsumoFolios>`;
|
|
229
230
|
|
|
230
231
|
// Firmar el documento
|
package/EnviadorSII.js
CHANGED
|
@@ -1132,7 +1132,9 @@ class EnviadorSII {
|
|
|
1132
1132
|
body += xml;
|
|
1133
1133
|
body += `\r\n--${boundary}--\r\n`;
|
|
1134
1134
|
|
|
1135
|
-
|
|
1135
|
+
// SII DTEUpload requiere ISO-8859-1 (igual que EnvioBOLETA y EnvioDTE SOAP)
|
|
1136
|
+
const bodyBuffer = Buffer.from(body, 'latin1');
|
|
1137
|
+
return await this._enviarMultipart(url, bodyBuffer, boundary, xml, 'RCOF');
|
|
1136
1138
|
}
|
|
1137
1139
|
|
|
1138
1140
|
/**
|
|
@@ -1337,9 +1339,30 @@ class EnviadorSII {
|
|
|
1337
1339
|
3: 'Error en XML',
|
|
1338
1340
|
4: 'Error de firma',
|
|
1339
1341
|
5: 'Error de sistema',
|
|
1342
|
+
7: 'Envío duplicado — el SII ya recibió este set anteriormente',
|
|
1340
1343
|
99: 'Error desconocido',
|
|
1341
1344
|
};
|
|
1342
1345
|
|
|
1346
|
+
// STATUS 7 = Envío duplicado (para EnvioBOLETA/EnvioDTE).
|
|
1347
|
+
// EXCEPCIÓN: CHR-00001 en <DETAIL> = error de charset, NO es duplicado.
|
|
1348
|
+
if (status === 7) {
|
|
1349
|
+
const detailMatch = responseText.match(/<ERROR>([^<]*)<\/ERROR>/i);
|
|
1350
|
+
const detailMsg = detailMatch ? detailMatch[1] : '';
|
|
1351
|
+
if (detailMsg.includes('CHR-00001')) {
|
|
1352
|
+
return {
|
|
1353
|
+
ok: false,
|
|
1354
|
+
status,
|
|
1355
|
+
error: `Error de charset en XML enviado (${detailMsg}). Verifique encoding="ISO-8859-1" en la declaración XML.`,
|
|
1356
|
+
respuesta: responseText,
|
|
1357
|
+
duplicado: false,
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
if (trackId) {
|
|
1361
|
+
return { ok: true, status, trackId, archivo: fileName, mensaje: `[DUPLICADO] TrackID recuperado: ${trackId}`, respuesta: responseText };
|
|
1362
|
+
}
|
|
1363
|
+
return { ok: false, status, error: errorMessages[7], respuesta: responseText, duplicado: true };
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1343
1366
|
return {
|
|
1344
1367
|
ok: false,
|
|
1345
1368
|
status: status,
|
package/cert/BoletaCert.js
CHANGED
|
@@ -247,31 +247,41 @@ class BoletaCert {
|
|
|
247
247
|
*/
|
|
248
248
|
generarConsumoFolio(envioBoleta, options = {}) {
|
|
249
249
|
const { ConsumoFolio } = this._lib();
|
|
250
|
+
const { DOMParser } = require('@xmldom/xmldom');
|
|
250
251
|
|
|
251
252
|
const consumoFolio = new ConsumoFolio(this.certificado);
|
|
252
253
|
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
254
|
+
// Parsear el XML generado para obtener los totales exactos que quedaron firmados.
|
|
255
|
+
// NO leer de dte.datos.Encabezado.Totales: son valores pre-cálculo (input del constructor)
|
|
256
|
+
// y no coinciden con los totales calculados que el SII valida contra el RCOF.
|
|
257
|
+
const parser = new DOMParser();
|
|
258
|
+
const doc = parser.parseFromString(envioBoleta.xml, 'text/xml');
|
|
259
|
+
const dteEls = doc.getElementsByTagName('Documento');
|
|
260
|
+
|
|
261
|
+
for (let i = 0; i < dteEls.length; i++) {
|
|
262
|
+
const dteEl = dteEls[i];
|
|
263
|
+
const tipo = parseInt(dteEl.getElementsByTagName('TipoDTE')[0]?.textContent || '39', 10);
|
|
264
|
+
const folio = parseInt(dteEl.getElementsByTagName('Folio')[0]?.textContent || '0', 10);
|
|
265
|
+
const fchEmis = dteEl.getElementsByTagName('FchEmis')[0]?.textContent || '';
|
|
266
|
+
|
|
267
|
+
const totalesEl = dteEl.getElementsByTagName('Totales')[0];
|
|
268
|
+
const mntNeto = parseInt(totalesEl?.getElementsByTagName('MntNeto')[0]?.textContent || '0', 10);
|
|
269
|
+
const mntExe = parseInt(totalesEl?.getElementsByTagName('MntExe')[0]?.textContent || '0', 10);
|
|
270
|
+
const iva = parseInt(totalesEl?.getElementsByTagName('IVA')[0]?.textContent || '0', 10);
|
|
271
|
+
const mntTotal = parseInt(totalesEl?.getElementsByTagName('MntTotal')[0]?.textContent || '0', 10);
|
|
261
272
|
|
|
262
|
-
// Estructura que espera ConsumoFolio: Totales como hijo de Encabezado
|
|
263
273
|
consumoFolio.agregar(tipo, {
|
|
264
274
|
Encabezado: {
|
|
265
275
|
IdDoc: {
|
|
266
276
|
TipoDTE: tipo,
|
|
267
277
|
Folio: folio,
|
|
268
|
-
FchEmis:
|
|
278
|
+
FchEmis: fchEmis,
|
|
269
279
|
},
|
|
270
280
|
Totales: {
|
|
271
|
-
MntNeto:
|
|
272
|
-
MntExe:
|
|
273
|
-
IVA:
|
|
274
|
-
MntTotal:
|
|
281
|
+
MntNeto: mntNeto,
|
|
282
|
+
MntExe: mntExe,
|
|
283
|
+
IVA: iva,
|
|
284
|
+
MntTotal: mntTotal,
|
|
275
285
|
}
|
|
276
286
|
},
|
|
277
287
|
});
|
|
@@ -384,6 +394,7 @@ class BoletaCert {
|
|
|
384
394
|
* @param {string} options.setPath - Ruta al archivo del set de pruebas
|
|
385
395
|
* @param {Object} options.cafBoleta - CAF para boletas tipo 39
|
|
386
396
|
* @param {number} options.folioInicial - Folio inicial a usar
|
|
397
|
+
* @param {number} [options.secEnvio=1] - Número de secuencia del RCOF (auto-incrementar por run)
|
|
387
398
|
* @returns {Promise<Object>} Resultado con trackIds
|
|
388
399
|
*/
|
|
389
400
|
async ejecutarCertificacion(options) {
|
|
@@ -431,52 +442,125 @@ class BoletaCert {
|
|
|
431
442
|
const resultadoBoleta = await enviador.enviarBoletaSoap(envioBoleta);
|
|
432
443
|
|
|
433
444
|
if (!resultadoBoleta.ok) {
|
|
434
|
-
|
|
435
|
-
|
|
445
|
+
if (resultadoBoleta.duplicado) {
|
|
446
|
+
// STATUS 7 = Envío duplicado — el mismo set ya fue enviado al SII con anterioridad.
|
|
447
|
+
// Continuamos con el RCOF y la declaración; el SII ya tiene el set.
|
|
448
|
+
console.log(` [⚠️ DUPLICADO] EnvioBOLETA ya enviado anteriormente — continuando con RCOF y declaración...`);
|
|
449
|
+
resultadoBoleta = { ok: false, duplicado: true, trackId: null };
|
|
450
|
+
} else {
|
|
451
|
+
console.log(` [ERR] Error: ${resultadoBoleta.error}`);
|
|
452
|
+
return { success: false, error: resultadoBoleta.error, fase: 'EnvioBOLETA' };
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
console.log(` [OK] Enviado - TrackId: ${resultadoBoleta.trackId}`);
|
|
436
456
|
}
|
|
437
|
-
console.log(` [OK] Enviado - TrackId: ${resultadoBoleta.trackId}`);
|
|
438
457
|
|
|
439
|
-
// 5. Generar RCOF
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
console.log(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
458
|
+
// 5 & 6. Generar y enviar RCOF — loop hasta que SII lo acepte o se agoten intentos.
|
|
459
|
+
// Estrategia: enviar → si ok, esperar 30s → consultar estado (EPR o RPR = éxito).
|
|
460
|
+
// Si DUPLICADO: el SII ya tiene un RCOF para este RUT/período. El entorno de certificación
|
|
461
|
+
// acepta solo 1 RCOF por RUT por día — tras 3 duplicados consecutivos se asume ya enviado.
|
|
462
|
+
console.log('\nGenerando y enviando RCOF (ConsumoFolio)...');
|
|
463
|
+
const secInicio = options.secEnvio || 100;
|
|
464
|
+
const MAX_INTENTOS_RCOF = 10;
|
|
465
|
+
const MAX_DUPLICADOS_CONSECUTIVOS = 3;
|
|
466
|
+
let secUsado = secInicio;
|
|
467
|
+
let resultadoRCOF = { ok: false };
|
|
468
|
+
let trackIdRCOFloop = null;
|
|
469
|
+
let duplicadosConsecutivos = 0;
|
|
470
|
+
|
|
471
|
+
for (let intento = 0; intento < MAX_INTENTOS_RCOF; intento++) {
|
|
472
|
+
const secActual = secInicio + intento;
|
|
473
|
+
const cf = this.generarConsumoFolio(envioBoleta, { secEnvio: secActual });
|
|
474
|
+
cf.generar();
|
|
475
|
+
|
|
476
|
+
if (this.debugDir) {
|
|
477
|
+
const debugPath = path.join(this.debugDir, 'boleta-cert');
|
|
478
|
+
fs.mkdirSync(debugPath, { recursive: true });
|
|
479
|
+
const fname = intento === 0 ? 'ConsumoFolio.xml' : `ConsumoFolio-sec${secActual}.xml`;
|
|
480
|
+
fs.writeFileSync(path.join(debugPath, fname), cf.xml, 'utf-8');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
console.log(` → Enviando RCOF SecEnvio=${secActual} (intento ${intento + 1}/${MAX_INTENTOS_RCOF})...`);
|
|
484
|
+
const res = await enviador.enviarConsumoFolios(cf);
|
|
485
|
+
secUsado = secActual;
|
|
486
|
+
|
|
487
|
+
if (!res.ok && res.duplicado) {
|
|
488
|
+
duplicadosConsecutivos++;
|
|
489
|
+
console.log(` [⚠️ DUPLICADO ${duplicadosConsecutivos}/${MAX_DUPLICADOS_CONSECUTIVOS}] SecEnvio=${secActual} ya existe en SII.`);
|
|
490
|
+
if (duplicadosConsecutivos >= MAX_DUPLICADOS_CONSECUTIVOS) {
|
|
491
|
+
// El entorno SII solo permite 1 RCOF por RUT/día → ya hay uno registrado. Continuar.
|
|
492
|
+
console.log(` [ℹ️] SII ya tiene un RCOF para hoy (${MAX_DUPLICADOS_CONSECUTIVOS} duplicados consecutivos). Continuando sin nuevo trackId RCOF.`);
|
|
493
|
+
resultadoRCOF = { ok: true, trackId: null, rcofYaEnviado: true };
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
console.log(` → Probando sec=${secActual + 1}...`);
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
duplicadosConsecutivos = 0;
|
|
501
|
+
|
|
502
|
+
if (!res.ok) {
|
|
503
|
+
console.log(` [ERR] Error al enviar RCOF (sec=${secActual}): ${res.error}`);
|
|
504
|
+
resultadoRCOF = res;
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Enviado correctamente — esperar 30s y verificar que SII lo aceptó
|
|
509
|
+
// Nota: EPR (Envío Procesado) Y RPR (Aceptado con Reparos) son ambos válidos para RCOF.
|
|
510
|
+
trackIdRCOFloop = res.trackId;
|
|
511
|
+
console.log(` ✓ RCOF recibido por SII — TrackId: ${trackIdRCOFloop} (SecEnvio=${secActual})`);
|
|
512
|
+
console.log(` ⏳ Esperando 30s para verificar estado RCOF en SII...`);
|
|
513
|
+
await new Promise(r => setTimeout(r, 30000));
|
|
514
|
+
|
|
515
|
+
let estadoRcof;
|
|
516
|
+
try {
|
|
517
|
+
estadoRcof = await enviador.consultarEstadoSoap(trackIdRCOFloop, this.emisor.rut);
|
|
518
|
+
console.log(` [Estado RCOF] ${estadoRcof.estado} — ${estadoRcof.mensaje}`);
|
|
519
|
+
} catch (e) {
|
|
520
|
+
console.log(` [!] No se pudo consultar estado RCOF: ${e.message} — asumiendo aceptado`);
|
|
521
|
+
resultadoRCOF = { ok: true, trackId: trackIdRCOFloop };
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// EPR = procesado, RPR = aceptado con reparos (ambos válidos para RCOF), estados intermedios = OK para continuar
|
|
526
|
+
if (estadoRcof.esExitoso || estadoRcof.esIntermedio) {
|
|
527
|
+
resultadoRCOF = { ok: true, trackId: trackIdRCOFloop, estado: estadoRcof.estado };
|
|
528
|
+
break;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// SII lo rechazó explícitamente — probar con el siguiente sec
|
|
532
|
+
console.log(` [⚠️ RECHAZADO] RCOF sec=${secActual} rechazado (${estadoRcof.estado}: ${estadoRcof.glosa || estadoRcof.mensaje}). Probando sec=${secActual + 1}...`);
|
|
533
|
+
trackIdRCOFloop = null;
|
|
450
534
|
}
|
|
451
|
-
|
|
452
|
-
// 6. Enviar RCOF
|
|
453
|
-
console.log('\nEnviando RCOF al SII...');
|
|
454
|
-
const resultadoRCOF = await enviador.enviarConsumoFolios(consumoFolio);
|
|
455
|
-
|
|
535
|
+
|
|
456
536
|
if (!resultadoRCOF.ok) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
537
|
+
if (!resultadoRCOF.error) {
|
|
538
|
+
console.log(` [⚠️] RCOF no aceptado tras ${MAX_INTENTOS_RCOF} intentos. Continuando sin trackId RCOF.`);
|
|
539
|
+
} else {
|
|
540
|
+
console.log(` [ERR] Error RCOF: ${resultadoRCOF.error}`);
|
|
541
|
+
return {
|
|
542
|
+
success: false,
|
|
543
|
+
error: resultadoRCOF.error,
|
|
544
|
+
fase: 'RCOF',
|
|
545
|
+
trackIdBoleta: resultadoBoleta.trackId
|
|
546
|
+
};
|
|
547
|
+
}
|
|
464
548
|
}
|
|
465
|
-
console.log(` [OK] Enviado - TrackId: ${resultadoRCOF.trackId}`);
|
|
466
549
|
|
|
467
550
|
// Resumen
|
|
468
551
|
console.log('\n' + '═'.repeat(60));
|
|
469
552
|
console.log('[OK] CERTIFICACIÓN BOLETAS COMPLETADA');
|
|
470
553
|
console.log('═'.repeat(60));
|
|
471
|
-
console.log(` EnvioBOLETA: ${resultadoBoleta.trackId}`);
|
|
472
|
-
console.log(` RCOF: ${resultadoRCOF.trackId}`);
|
|
554
|
+
console.log(` EnvioBOLETA: ${resultadoBoleta.trackId ?? '(enviado previamente)'}`);
|
|
555
|
+
console.log(` RCOF: ${resultadoRCOF.trackId ?? '(enviado previamente)'}`);
|
|
473
556
|
console.log(` Boletas: ${boletas.length}`);
|
|
474
557
|
console.log(` Folios: ${folioInicial} - ${folioFinal}`);
|
|
475
558
|
|
|
476
559
|
return {
|
|
477
560
|
success: true,
|
|
478
|
-
trackIdBoleta: resultadoBoleta.trackId,
|
|
479
|
-
trackIdRCOF: resultadoRCOF.trackId,
|
|
561
|
+
trackIdBoleta: resultadoBoleta.trackId ?? null,
|
|
562
|
+
trackIdRCOF: resultadoRCOF.trackId ?? null,
|
|
563
|
+
secEnvioRCOF: secUsado,
|
|
480
564
|
boletas: boletas.length,
|
|
481
565
|
folioInicial,
|
|
482
566
|
folioFinal,
|
package/cert/CertRunner.js
CHANGED
|
@@ -782,6 +782,7 @@ class CertRunner {
|
|
|
782
782
|
this._decrementarPeriodoLibros();
|
|
783
783
|
const _nuevoPeriodo = this._getPeriodoLibros();
|
|
784
784
|
console.log(`\n[!] Período rechazado por SII. Reintentando con ${_nuevoPeriodo} (${_pRetry + 1}/${MAX_PERIOD_RETRIES})...`);
|
|
785
|
+
emitProgress(STEPS.BOOK_PERIOD_RETRY, { periodo: _nuevoPeriodo, intento: String(_pRetry + 1) });
|
|
785
786
|
await _reenviarLibros(_nuevoPeriodo);
|
|
786
787
|
declaracion = await this.declararLibros({ ...resultados, ...(options.setsResultados || {}) });
|
|
787
788
|
resultados.declaracion = declaracion;
|
|
@@ -806,6 +807,7 @@ class CertRunner {
|
|
|
806
807
|
const _nuevoPeriodo = this._getPeriodoLibros();
|
|
807
808
|
const _s21Nombres = _s21Keys.map(k => _KEY_A_SII_NOMBRE[k]).filter(Boolean);
|
|
808
809
|
console.log(`\n[!] ${_s21Nombres.join(', ')} bloqueados en S21. Reintentando con período ${_nuevoPeriodo} (${_pRetry + 1}/${MAX_PERIOD_RETRIES})...`);
|
|
810
|
+
emitProgress(STEPS.BOOK_PERIOD_RETRY, { periodo: _nuevoPeriodo, intento: String(_pRetry + 1) });
|
|
809
811
|
|
|
810
812
|
await _reenviarLibros(_nuevoPeriodo, new Set(_s21Keys));
|
|
811
813
|
declaracion = await this.declararLibros({ ...resultados, ...(options.setsResultados || {}) });
|
|
@@ -1550,9 +1552,16 @@ class CertRunner {
|
|
|
1550
1552
|
const yaIntercambio = Boolean(verificacion?.etapaActual?.includes('INTERCAMBIO'));
|
|
1551
1553
|
const simConforme = Boolean(estadoSim?.esConforme || estadoSim?.estado?.toUpperCase()?.includes('REVISADO CONFORME'));
|
|
1552
1554
|
|
|
1553
|
-
if (yaIntercambio
|
|
1554
|
-
console.log('\n ¡SIMULACIÓN CONFIRMADA!
|
|
1555
|
-
return { success: true, confirmada: true };
|
|
1555
|
+
if (yaIntercambio) {
|
|
1556
|
+
console.log('\n ¡SIMULACIÓN CONFIRMADA! Empresa ya en etapa INTERCAMBIO.');
|
|
1557
|
+
return { success: true, confirmada: true, etapa: 'INTERCAMBIO' };
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
if (simConforme || !sigueFormulario) {
|
|
1561
|
+
// La empresa pasó a la siguiente etapa automáticamente (INTERCAMBIO → DOCUMENTOS IMPRESOS → DECLARAR CUMPLIMIENTO)
|
|
1562
|
+
const etapaActual = verificacion?.etapaActual || 'INTERCAMBIO';
|
|
1563
|
+
console.log(`\n ¡SIMULACIÓN APROBADA! Etapa actual: ${etapaActual}`);
|
|
1564
|
+
return { success: true, confirmada: true, etapa: etapaActual };
|
|
1556
1565
|
}
|
|
1557
1566
|
|
|
1558
1567
|
console.log(' [!] SII aún mantiene formulario de simulación pendiente; se reintentará...');
|
|
@@ -1585,8 +1594,11 @@ class CertRunner {
|
|
|
1585
1594
|
|
|
1586
1595
|
if (esAprobado) {
|
|
1587
1596
|
console.log(` [OK] SIMULACIÓN: REVISADO CONFORME`);
|
|
1588
|
-
|
|
1589
|
-
|
|
1597
|
+
// Consultar etapa actual (INTERCAMBIO es el siguiente paso tras simulación)
|
|
1598
|
+
const postSimAvance = await this.siiCert.verAvanceParsed().catch(() => null);
|
|
1599
|
+
const etapaActual = postSimAvance?.etapaActual || 'INTERCAMBIO';
|
|
1600
|
+
console.log(`\n ¡SIMULACIÓN APROBADA! Etapa actual: ${etapaActual}`);
|
|
1601
|
+
return { success: true, etapa: etapaActual };
|
|
1590
1602
|
} else if (esRechazado) {
|
|
1591
1603
|
console.log(` [ERR] SIMULACIÓN: ${simEstado.estado}`);
|
|
1592
1604
|
return { success: false, error: 'Simulación rechazada' };
|
|
@@ -1914,11 +1926,7 @@ class CertRunner {
|
|
|
1914
1926
|
|
|
1915
1927
|
let browser;
|
|
1916
1928
|
try {
|
|
1917
|
-
browser = await puppeteer.launch(
|
|
1918
|
-
headless: true,
|
|
1919
|
-
ignoreHTTPSErrors: true,
|
|
1920
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
1921
|
-
});
|
|
1929
|
+
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions());
|
|
1922
1930
|
|
|
1923
1931
|
const page = await browser.newPage();
|
|
1924
1932
|
await page.setCookie(...puppeteerCookies);
|
|
@@ -2102,11 +2110,7 @@ class CertRunner {
|
|
|
2102
2110
|
const [rutNum, dvChar] = this.config.emisor.rut.split('-');
|
|
2103
2111
|
let browser;
|
|
2104
2112
|
try {
|
|
2105
|
-
browser = await puppeteer.launch(
|
|
2106
|
-
headless: true,
|
|
2107
|
-
ignoreHTTPSErrors: true,
|
|
2108
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2109
|
-
});
|
|
2113
|
+
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions());
|
|
2110
2114
|
const page = await browser.newPage();
|
|
2111
2115
|
await page.setCookie(...puppeteerCookies);
|
|
2112
2116
|
await page.goto('https://www4.sii.cl/pdfdteInternet/', { waitUntil: 'networkidle2', timeout: 60000 });
|
|
@@ -2125,23 +2129,32 @@ class CertRunner {
|
|
|
2125
2129
|
if (btn) btn.click();
|
|
2126
2130
|
}, 'Rut');
|
|
2127
2131
|
|
|
2128
|
-
//
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
.find(b => /^s[ií]$/i.test(b.textContent.trim()));
|
|
2133
|
-
if (si) si.click();
|
|
2134
|
-
}).catch(() => {});
|
|
2132
|
+
// IMPORTANTE: NO hacer click en "Sí" del diálogo "ya existe revisión".
|
|
2133
|
+
// Hacerlo crea una revisión nueva vacía y borra el estado "POR REVISAR" del DOM.
|
|
2134
|
+
// El estado de la revisión existente es visible en el body aunque el diálogo esté abierto.
|
|
2135
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
2135
2136
|
|
|
2136
|
-
// Esperar hasta
|
|
2137
|
+
// Esperar hasta 10s a que aparezca el estado en el DOM (detrás del diálogo si aplica)
|
|
2137
2138
|
await page.waitForFunction(() => {
|
|
2138
2139
|
const t = (document.body.textContent || '').toUpperCase();
|
|
2139
|
-
return t.includes('
|
|
2140
|
-
t.includes('
|
|
2141
|
-
t.includes('
|
|
2142
|
-
}, { timeout:
|
|
2140
|
+
return t.includes('POR REVISAR') || t.includes('APROBADO') ||
|
|
2141
|
+
t.includes('EN REVISI') || t.includes('RECHAZADO') ||
|
|
2142
|
+
t.includes('ENVIADO AL SII');
|
|
2143
|
+
}, { timeout: 10000, polling: 500 }).catch(() => {});
|
|
2143
2144
|
|
|
2145
|
+
// Leer estado directamente del label del formulario en el DOM
|
|
2144
2146
|
const estado = await page.evaluate(() => {
|
|
2147
|
+
// Primero intentar leer el label específico de estado (más preciso)
|
|
2148
|
+
const labels = Array.from(document.querySelectorAll('.x-form-label'));
|
|
2149
|
+
for (const lbl of labels) {
|
|
2150
|
+
const txt = (lbl.textContent || '').trim().toUpperCase();
|
|
2151
|
+
if (txt === 'POR REVISAR') return 'POR REVISAR';
|
|
2152
|
+
if (txt === 'APROBADO') return 'APROBADO';
|
|
2153
|
+
if (txt.startsWith('EN REVISI')) return 'EN REVISIÓN';
|
|
2154
|
+
if (txt === 'RECHAZADO') return 'RECHAZADO';
|
|
2155
|
+
if (txt.includes('ENVIADO AL SII')) return 'ENVIADO AL SII';
|
|
2156
|
+
}
|
|
2157
|
+
// Fallback: buscar en todo el body
|
|
2145
2158
|
const t = (document.body.textContent || '').toUpperCase();
|
|
2146
2159
|
if (t.includes('APROBADO')) return 'APROBADO';
|
|
2147
2160
|
if (t.includes('POR REVISAR')) return 'POR REVISAR';
|
|
@@ -2155,7 +2168,13 @@ class CertRunner {
|
|
|
2155
2168
|
} catch (err) {
|
|
2156
2169
|
return { estado: null, error: err.message };
|
|
2157
2170
|
} finally {
|
|
2158
|
-
if (browser)
|
|
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
|
+
}
|
|
2159
2178
|
}
|
|
2160
2179
|
}
|
|
2161
2180
|
|
|
@@ -2210,12 +2229,7 @@ class CertRunner {
|
|
|
2210
2229
|
|
|
2211
2230
|
let browser;
|
|
2212
2231
|
try {
|
|
2213
|
-
browser = await puppeteer.launch({
|
|
2214
|
-
headless: true,
|
|
2215
|
-
ignoreHTTPSErrors: true,
|
|
2216
|
-
protocolTimeout: 300000, // 5 min — DOM.setFileInputFiles con 256 archivos supera el default de 30s
|
|
2217
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2218
|
-
});
|
|
2232
|
+
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions([], { protocolTimeout: 300000 }));
|
|
2219
2233
|
const page = await browser.newPage();
|
|
2220
2234
|
await page.setCookie(...puppeteerCookies);
|
|
2221
2235
|
|
|
@@ -2227,6 +2241,26 @@ class CertRunner {
|
|
|
2227
2241
|
await new Promise(r => setTimeout(r, 3000));
|
|
2228
2242
|
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-01-loaded.png'), fullPage: true }).catch(() => {});
|
|
2229
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
|
+
|
|
2230
2264
|
// Paso 1: RUT Empresa
|
|
2231
2265
|
const rutInputs = await page.$$('input[name="rut"]');
|
|
2232
2266
|
const dvInputs = await page.$$('input[name="dv"]');
|
|
@@ -2290,21 +2324,50 @@ class CertRunner {
|
|
|
2290
2324
|
console.log(` → Ingresando RUT proveedor: ${rutNum}-${dvChar}`);
|
|
2291
2325
|
await pRut.click({ clickCount: 3 }); await pRut.type(rutNum);
|
|
2292
2326
|
await pDv.click({ clickCount: 3 }); await pDv.type(dvChar);
|
|
2327
|
+
|
|
2328
|
+
// Interceptar respuestas del portal durante "Consultar" para capturar el ID de revisión
|
|
2329
|
+
let _capturedRevId = null;
|
|
2330
|
+
const _onPortalResponse = async (resp) => {
|
|
2331
|
+
const url = resp.url();
|
|
2332
|
+
if (!url.includes('sii.cl/pdfdteInternet')) return;
|
|
2333
|
+
try {
|
|
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);
|
|
2293
2348
|
await clickBoton(page, 'Consultar');
|
|
2294
2349
|
await new Promise(r => setTimeout(r, 2500));
|
|
2350
|
+
page.off('response', _onPortalResponse);
|
|
2295
2351
|
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-03-after-consultar.png'), fullPage: true }).catch(() => {});
|
|
2296
2352
|
|
|
2297
2353
|
// ── Re-ejecución: detectar estado terminal antes de proceder ──
|
|
2298
2354
|
// Solo aplicable cuando NO se creó una nueva revisión.
|
|
2299
|
-
//
|
|
2300
|
-
//
|
|
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.
|
|
2301
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
|
|
2302
2367
|
const t = (document.body.textContent || '').toUpperCase();
|
|
2303
2368
|
if (t.includes('APROBADO')) return 'APROBADO';
|
|
2304
|
-
if (t.includes('POR REVISAR')) return 'POR REVISAR';
|
|
2305
|
-
if (t.includes('EN REVISI')) return 'EN REVISIÓN';
|
|
2306
|
-
if (t.includes('RECHAZADO')) return 'RECHAZADO';
|
|
2307
2369
|
if (t.includes('ENVIADO AL SII')) return 'ENVIADO AL SII';
|
|
2370
|
+
if (t.includes('RECHAZADO')) return 'RECHAZADO';
|
|
2308
2371
|
return null;
|
|
2309
2372
|
}).catch(() => null);
|
|
2310
2373
|
if (_estadoYaSubido) {
|
|
@@ -2368,86 +2431,121 @@ class CertRunner {
|
|
|
2368
2431
|
if (!_hayInp) { await clickBoton(page, 'Crear'); await new Promise(r => setTimeout(r, 2500)); }
|
|
2369
2432
|
};
|
|
2370
2433
|
|
|
2371
|
-
// Cargar todos los PDFs a la vez en el input de archivo.
|
|
2372
|
-
// GWT no tiene atributo "multiple" por defecto — se fuerza vía DOM.
|
|
2373
|
-
// Puppeteer dispara el evento "change" internamente tras uploadFile(),
|
|
2374
|
-
// lo que dispara el handler GWT que encola todos los archivos para
|
|
2375
|
-
// procesarlos en batch (una POST por archivo vía iframe, sin re-navegación).
|
|
2376
|
-
console.log(` → Cargando ${pdfPaths.length} PDFs de una vez en el input...`);
|
|
2377
2434
|
const fileInput = await page.$('input.gwt-FileUpload');
|
|
2378
2435
|
if (!fileInput) throw new Error('pdfdteInternet: input.gwt-FileUpload no encontrado');
|
|
2379
2436
|
|
|
2380
|
-
//
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
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
|
+
}
|
|
2457
|
+
};
|
|
2458
|
+
page.on('response', _onFirstUploadResp);
|
|
2385
2459
|
|
|
2386
|
-
|
|
2387
|
-
|
|
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
|
+
}
|
|
2388
2494
|
|
|
2389
|
-
//
|
|
2390
|
-
|
|
2391
|
-
|
|
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);
|
|
2392
2501
|
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
for (const el of document.querySelectorAll('.x-progress-text')) {
|
|
2398
|
-
const m = el.textContent.match(/Procesados\s+(\d+)/);
|
|
2399
|
-
if (m && +m[1] > 0) return true;
|
|
2400
|
-
}
|
|
2401
|
-
const t = (document.body.textContent || '').toUpperCase();
|
|
2402
|
-
if (t.includes('APROBADO') || t.includes('POR REVISAR') || t.includes('EN REVISI') || t.includes('RECHAZADO')) return true;
|
|
2403
|
-
return false;
|
|
2404
|
-
}, { timeout: 45000, polling: 1000 }).catch(() => {});
|
|
2405
|
-
|
|
2406
|
-
// Leer estado real tras la fase 1
|
|
2407
|
-
const _fase1 = await page.evaluate(() => {
|
|
2408
|
-
let procesados = 0;
|
|
2409
|
-
for (const el of document.querySelectorAll('.x-progress-text')) {
|
|
2410
|
-
const m = el.textContent.match(/Procesados\s+(\d+)/);
|
|
2411
|
-
if (m) { procesados = +m[1]; break; }
|
|
2502
|
+
if (!nroRevision) {
|
|
2503
|
+
if (debugDir) {
|
|
2504
|
+
await page.screenshot({ path: path.join(debugDir, 'pdfte-error-no-revid.png'), fullPage: true }).catch(() => {});
|
|
2505
|
+
fs.writeFileSync(path.join(debugDir, 'pdfte-error-no-revid.html'), await page.content(), 'utf8');
|
|
2412
2506
|
}
|
|
2413
|
-
|
|
2414
|
-
let estado = null;
|
|
2415
|
-
if (t.includes('APROBADO')) estado = 'APROBADO';
|
|
2416
|
-
else if (t.includes('POR REVISAR')) estado = 'POR REVISAR';
|
|
2417
|
-
else if (t.includes('EN REVISI')) estado = 'EN REVISIÓN';
|
|
2418
|
-
else if (t.includes('RECHAZADO')) estado = 'RECHAZADO';
|
|
2419
|
-
return { procesados, estado };
|
|
2420
|
-
}).catch(() => ({ procesados: 0, estado: null }));
|
|
2421
|
-
|
|
2422
|
-
if (_fase1.estado) {
|
|
2423
|
-
console.log(` [OK] Portal en estado "${_fase1.estado}" — muestras ya procesadas previamente.`);
|
|
2424
|
-
return { success: true, alreadyCompleted: true, estado: _fase1.estado };
|
|
2507
|
+
throw new Error('pdfdteInternet: no se pudo obtener ID de revisión. El file chooser o CDP no disparó el handler GWT o el servidor rechazó la petición.');
|
|
2425
2508
|
}
|
|
2426
2509
|
|
|
2427
|
-
if (
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2510
|
+
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-04b-primer-upload.png'), fullPage: true }).catch(() => {});
|
|
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)}`);
|
|
2436
2542
|
}
|
|
2437
|
-
|
|
2438
|
-
}, { timeout: pdfPaths.length * 15000, polling: 1000 }, pdfPaths.length).catch(async () => {
|
|
2439
|
-
const procesados = await page.evaluate(() => {
|
|
2440
|
-
for (const el of document.querySelectorAll('.x-progress-text')) {
|
|
2441
|
-
const m = el.textContent.match(/Procesados\s+(\d+)/);
|
|
2442
|
-
if (m) return +m[1];
|
|
2443
|
-
}
|
|
2444
|
-
return 0;
|
|
2445
|
-
}).catch(() => 0);
|
|
2446
|
-
console.warn(` [!] Timeout: solo se procesaron ${procesados}/${pdfPaths.length} antes del timeout`);
|
|
2447
|
-
});
|
|
2543
|
+
}
|
|
2448
2544
|
}
|
|
2449
2545
|
|
|
2450
|
-
|
|
2546
|
+
console.log(` ✓ ${pdfPaths.length} PDFs subidos al portal`);
|
|
2547
|
+
|
|
2548
|
+
// Esperar que las validaciones del portal (leeImpresoById) terminen
|
|
2451
2549
|
await page.waitForNetworkIdle({ timeout: 60000, idleTime: 2000 }).catch(() => {});
|
|
2452
2550
|
await new Promise(r => setTimeout(r, 2000));
|
|
2453
2551
|
|
|
@@ -2466,9 +2564,53 @@ class CertRunner {
|
|
|
2466
2564
|
|
|
2467
2565
|
if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-05b-antes-enviar.png'), fullPage: true }).catch(() => {});
|
|
2468
2566
|
|
|
2567
|
+
// Debug: listar todos los botones visibles y sus estados
|
|
2568
|
+
const _btnsDebug = await page.evaluate(() =>
|
|
2569
|
+
Array.from(document.querySelectorAll('button.x-btn-text')).map(b => ({
|
|
2570
|
+
text: b.textContent.trim(),
|
|
2571
|
+
disabled: b.disabled,
|
|
2572
|
+
aria: b.getAttribute('aria-disabled'),
|
|
2573
|
+
visible: b.offsetParent !== null,
|
|
2574
|
+
}))
|
|
2575
|
+
).catch(() => []);
|
|
2576
|
+
console.log(` [DEBUG] Botones en página: ${JSON.stringify(_btnsDebug.filter(b => b.text))}`);
|
|
2577
|
+
|
|
2469
2578
|
console.log(' → Click "Enviar al SII"...');
|
|
2470
|
-
|
|
2471
|
-
|
|
2579
|
+
|
|
2580
|
+
// Intentar click en el botón directo (ignorar aria-disabled en esta etapa)
|
|
2581
|
+
const _clickedEnviar = await page.evaluate(() => {
|
|
2582
|
+
// a) Buscar button.x-btn-text exacto
|
|
2583
|
+
for (const btn of document.querySelectorAll('button.x-btn-text')) {
|
|
2584
|
+
if (btn.textContent.trim() === 'Enviar al SII') {
|
|
2585
|
+
btn.click();
|
|
2586
|
+
return 'direct';
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
// b) Abrir overflow ">" y buscar en el menú desplegable
|
|
2590
|
+
const overflow = document.querySelector('td.x-toolbar-overflow-region, .x-toolbar-more-icon, button[class*="overflow"]');
|
|
2591
|
+
if (overflow) {
|
|
2592
|
+
overflow.click();
|
|
2593
|
+
return 'overflow-click';
|
|
2594
|
+
}
|
|
2595
|
+
return false;
|
|
2596
|
+
});
|
|
2597
|
+
|
|
2598
|
+
if (_clickedEnviar === 'overflow-click') {
|
|
2599
|
+
// Esperar que aparezca el menú y hacer click en "Enviar al SII"
|
|
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
|
+
}
|
|
2472
2614
|
|
|
2473
2615
|
await page.waitForNetworkIdle({ timeout: 30000, idleTime: 1000 }).catch(() => {});
|
|
2474
2616
|
await new Promise(r => setTimeout(r, 3000));
|
|
@@ -2478,14 +2620,26 @@ class CertRunner {
|
|
|
2478
2620
|
}
|
|
2479
2621
|
|
|
2480
2622
|
const pageText = await page.$eval('body', el => el.textContent).catch(() => '');
|
|
2481
|
-
const exitoso = /revision.*creada|solicitud.*enviada|documentos.*enviados|fue.*enviado|[eé]xito/i.test(pageText);
|
|
2623
|
+
const exitoso = /revision.*creada|solicitud.*enviada|documentos.*enviados|fue.*enviado|[eé]xito|por revisar/i.test(pageText);
|
|
2482
2624
|
console.log(` → Resultado: ${exitoso ? '[OK] enviado correctamente' : '[!] sin confirmación explícita'}`);
|
|
2625
|
+
// Imprimir el marcador de éxito ANTES de cerrar el browser para que quede en stdout
|
|
2626
|
+
// aunque browser.close() cuelgue y el proceso sea forzado a terminar.
|
|
2627
|
+
if (exitoso) process.stdout.write('\nMUESTRAS SUBIDAS EXITOSAMENTE\n');
|
|
2483
2628
|
return { success: exitoso, pageText: pageText.substring(0, 800) };
|
|
2484
2629
|
|
|
2485
2630
|
} catch (err) {
|
|
2486
2631
|
return { success: false, error: err.message };
|
|
2487
2632
|
} finally {
|
|
2488
|
-
if (browser)
|
|
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
|
+
}
|
|
2489
2643
|
}
|
|
2490
2644
|
}
|
|
2491
2645
|
|
|
@@ -2515,25 +2669,37 @@ class CertRunner {
|
|
|
2515
2669
|
|
|
2516
2670
|
let browser;
|
|
2517
2671
|
try {
|
|
2518
|
-
browser = await puppeteer.launch(
|
|
2519
|
-
headless: true,
|
|
2520
|
-
ignoreHTTPSErrors: true,
|
|
2521
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2522
|
-
});
|
|
2672
|
+
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions());
|
|
2523
2673
|
const page = await browser.newPage();
|
|
2524
2674
|
await page.setCookie(...puppeteerCookies);
|
|
2525
2675
|
page.on('dialog', async dlg => { console.log(` → [dialog SET=1] ${dlg.message()}`); await dlg.accept(); });
|
|
2526
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
|
+
|
|
2527
2689
|
console.log(' → Cargando portal certBolElectDteInternet (SET=1)...');
|
|
2528
2690
|
await page.goto('https://www4.sii.cl/certBolElectDteInternet/?SET=1', {
|
|
2529
2691
|
waitUntil: 'networkidle2', timeout: 60000,
|
|
2530
2692
|
});
|
|
2531
2693
|
await new Promise(r => setTimeout(r, 2000));
|
|
2694
|
+
await saveDebugSet1('01-carga');
|
|
2532
2695
|
|
|
2533
2696
|
// Paso 1: RUT empresa → "Confirmar Empresa"
|
|
2534
|
-
const rutInput = await page
|
|
2535
|
-
const dvInput = await page
|
|
2536
|
-
if (!rutInput)
|
|
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
|
+
}
|
|
2537
2703
|
console.log(` → Ingresando RUT empresa: ${rutNum}-${dvChar}`);
|
|
2538
2704
|
await rutInput.click({ clickCount: 3 }); await rutInput.type(rutNum);
|
|
2539
2705
|
await dvInput.click({ clickCount: 3 }); await dvInput.type(dvChar);
|
|
@@ -2550,6 +2716,7 @@ class CertRunner {
|
|
|
2550
2716
|
return document.querySelector('input[type="checkbox"]') !== null;
|
|
2551
2717
|
}, { timeout: 40000, polling: 500 }).catch(() => {});
|
|
2552
2718
|
await new Promise(r => setTimeout(r, 500));
|
|
2719
|
+
await saveDebugSet1('02-post-confirm');
|
|
2553
2720
|
|
|
2554
2721
|
// Paso 2: Marcar todos los checkboxes
|
|
2555
2722
|
const nCbs = await page.evaluate(() => {
|
|
@@ -2636,9 +2803,9 @@ class CertRunner {
|
|
|
2636
2803
|
});
|
|
2637
2804
|
|
|
2638
2805
|
if (!setText || setText.trim().length < 10) {
|
|
2639
|
-
if (
|
|
2640
|
-
fs.
|
|
2641
|
-
|
|
2806
|
+
if (dbgDirSet1) {
|
|
2807
|
+
fs.writeFileSync(path.join(dbgDirSet1, 'boleta-set-contenido-vacio.txt'), setText || '', 'utf-8');
|
|
2808
|
+
await saveDebugSet1('ERROR-descarga-vacia');
|
|
2642
2809
|
}
|
|
2643
2810
|
throw new Error(`DownloadFileServlet devolvió contenido vacío (${setText?.length ?? 0} chars). Verificar cookies.`);
|
|
2644
2811
|
}
|
|
@@ -2653,6 +2820,15 @@ class CertRunner {
|
|
|
2653
2820
|
console.log(` ✓ Set de pruebas obtenido (${setText.length} chars)`);
|
|
2654
2821
|
return { success: true, setText };
|
|
2655
2822
|
} catch (err) {
|
|
2823
|
+
try {
|
|
2824
|
+
if (dbgDirSet1) {
|
|
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 {}
|
|
2656
2832
|
return { success: false, error: err.message };
|
|
2657
2833
|
} finally {
|
|
2658
2834
|
if (browser) await browser.close().catch(() => {});
|
|
@@ -2681,20 +2857,27 @@ class CertRunner {
|
|
|
2681
2857
|
|
|
2682
2858
|
let browser;
|
|
2683
2859
|
try {
|
|
2684
|
-
browser = await puppeteer.launch(
|
|
2685
|
-
headless: true,
|
|
2686
|
-
ignoreHTTPSErrors: true,
|
|
2687
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2688
|
-
});
|
|
2860
|
+
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions());
|
|
2689
2861
|
const page = await browser.newPage();
|
|
2690
2862
|
await page.setCookie(...puppeteerCookies);
|
|
2691
2863
|
page.on('dialog', async dlg => { console.log(` → [dialog SET=2] ${dlg.message()}`); await dlg.accept(); });
|
|
2692
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
|
+
};
|
|
2874
|
+
|
|
2693
2875
|
console.log(' → Cargando portal certBolElectDteInternet (SET=2)...');
|
|
2694
2876
|
await page.goto('https://www4.sii.cl/certBolElectDteInternet/?SET=2', {
|
|
2695
2877
|
waitUntil: 'networkidle2', timeout: 60000,
|
|
2696
2878
|
});
|
|
2697
2879
|
await new Promise(r => setTimeout(r, 2000));
|
|
2880
|
+
await snapSet2('01-carga');
|
|
2698
2881
|
|
|
2699
2882
|
// Paso 1: RUT empresa → "Confirmar Empresa"
|
|
2700
2883
|
const rutInput = await page.$('input[maxlength="8"]');
|
|
@@ -2716,10 +2899,15 @@ class CertRunner {
|
|
|
2716
2899
|
return document.querySelector('input[maxlength="15"]') !== null;
|
|
2717
2900
|
}, { timeout: 40000, polling: 500 }).catch(() => {});
|
|
2718
2901
|
await new Promise(r => setTimeout(r, 500));
|
|
2902
|
+
await snapSet2('02-post-confirm');
|
|
2719
2903
|
|
|
2720
2904
|
// Paso 2: Ingresar TrackId
|
|
2721
2905
|
const trackInput = await page.$('input[maxlength="15"]');
|
|
2722
|
-
if (!trackInput)
|
|
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
|
+
}
|
|
2723
2911
|
console.log(` → Ingresando TrackId: ${trackId}`);
|
|
2724
2912
|
await trackInput.click({ clickCount: 3 });
|
|
2725
2913
|
await trackInput.type(String(trackId));
|
|
@@ -2738,6 +2926,7 @@ class CertRunner {
|
|
|
2738
2926
|
return t.includes('ENVI') || t.includes('CORREO') || t.includes('ERROR') ||
|
|
2739
2927
|
t.includes('VALIDACI') || t.includes('SOLICITUD');
|
|
2740
2928
|
}, { timeout: 15000, polling: 500 }).catch(() => {});
|
|
2929
|
+
await snapSet2('03-respuesta');
|
|
2741
2930
|
|
|
2742
2931
|
const respuesta = await page.evaluate(() => (document.body.innerText || '').trim().substring(0, 500));
|
|
2743
2932
|
console.log(` ✓ Validación solicitada. Respuesta: ${respuesta.substring(0, 120)}`);
|
|
@@ -2780,11 +2969,7 @@ class CertRunner {
|
|
|
2780
2969
|
|
|
2781
2970
|
let browser;
|
|
2782
2971
|
try {
|
|
2783
|
-
browser = await puppeteer.launch(
|
|
2784
|
-
headless: true,
|
|
2785
|
-
ignoreHTTPSErrors: true,
|
|
2786
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2787
|
-
});
|
|
2972
|
+
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions());
|
|
2788
2973
|
const page = await browser.newPage();
|
|
2789
2974
|
await page.setCookie(...puppeteerCookies);
|
|
2790
2975
|
|
|
@@ -2796,6 +2981,18 @@ class CertRunner {
|
|
|
2796
2981
|
await dlg.accept();
|
|
2797
2982
|
});
|
|
2798
2983
|
|
|
2984
|
+
// Helper debug: guarda screenshot + HTML con timestamp
|
|
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
|
+
|
|
2799
2996
|
// ── PASOS 1+2: Confirmar Empresa → esperar formulario (con retry) ──
|
|
2800
2997
|
// El portal GWT a veces responde con error transitorio ("empresa no autorizada")
|
|
2801
2998
|
// que se resuelve recargando la página y reintentando.
|
|
@@ -2810,11 +3007,15 @@ class CertRunner {
|
|
|
2810
3007
|
waitUntil: 'networkidle2', timeout: 60000,
|
|
2811
3008
|
});
|
|
2812
3009
|
await new Promise(r => setTimeout(r, 2500));
|
|
3010
|
+
await saveDebugDecl(`intento${intento}-01-carga`);
|
|
2813
3011
|
|
|
2814
3012
|
// GWT requiere eventos de teclado reales — NO funciona con .value = ...
|
|
2815
|
-
const rutInput = await page
|
|
2816
|
-
const dvInput = await page
|
|
2817
|
-
if (!rutInput)
|
|
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
|
+
}
|
|
2818
3019
|
console.log(` → Ingresando RUT empresa: ${rutEmpresaRaw}`);
|
|
2819
3020
|
await rutInput.click({ clickCount: 3 }); await rutInput.type(rutEmpNum);
|
|
2820
3021
|
await dvInput.click({ clickCount: 3 }); await dvInput.type(rutEmpDv);
|
|
@@ -2832,10 +3033,12 @@ class CertRunner {
|
|
|
2832
3033
|
await page.waitForFunction(() => {
|
|
2833
3034
|
return document.querySelector('input[type="checkbox"]') !== null;
|
|
2834
3035
|
}, { timeout: 40000, polling: 500 }).catch(() => {});
|
|
3036
|
+
await saveDebugDecl(`intento${intento}-02-post-confirm`);
|
|
2835
3037
|
|
|
2836
3038
|
if (dialogMsg) {
|
|
2837
3039
|
// Portal lanzó alert — puede ser transitorio. Guardar y reintentar.
|
|
2838
3040
|
lastDialogMsg = dialogMsg;
|
|
3041
|
+
await saveDebugDecl(`intento${intento}-DIALOG`);
|
|
2839
3042
|
console.log(` [!] Portal respondió con alerta en intento ${intento}: ${dialogMsg.substring(0, 100)}`);
|
|
2840
3043
|
if (intento < MAX_INTENTOS) {
|
|
2841
3044
|
console.log(' → Recargando y reintentando en 3s...');
|
|
@@ -2843,6 +3046,7 @@ class CertRunner {
|
|
|
2843
3046
|
continue;
|
|
2844
3047
|
}
|
|
2845
3048
|
// Agotados los intentos con alerta — el SII puede requerir esperar SOK
|
|
3049
|
+
await saveDebugDecl('PENDINGSOK-final');
|
|
2846
3050
|
return { success: false, pendingSok: true, error: lastDialogMsg };
|
|
2847
3051
|
}
|
|
2848
3052
|
|
|
@@ -2923,9 +3127,7 @@ class CertRunner {
|
|
|
2923
3127
|
await fillByLabel('Correo electrónico Proveedor', correoProveedor);
|
|
2924
3128
|
|
|
2925
3129
|
// Captura pre-submit
|
|
2926
|
-
|
|
2927
|
-
await page.screenshot({ path: path.join(this.config.debugDir, 'boleta-declaracion-pre-submit.png'), fullPage: true }).catch(() => {});
|
|
2928
|
-
}
|
|
3130
|
+
await saveDebugDecl('03-pre-submit');
|
|
2929
3131
|
|
|
2930
3132
|
// ── PASO 4: Click "Grabar Declaración" ───────────────────────
|
|
2931
3133
|
const submitOk = await page.evaluate(() => {
|
|
@@ -2934,7 +3136,10 @@ class CertRunner {
|
|
|
2934
3136
|
if (btn) { btn.click(); return true; }
|
|
2935
3137
|
return false;
|
|
2936
3138
|
});
|
|
2937
|
-
if (!submitOk)
|
|
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
|
+
}
|
|
2938
3143
|
console.log(' → Click "Grabar Declaración"...');
|
|
2939
3144
|
|
|
2940
3145
|
// Esperar confirmación del SII
|
|
@@ -2946,23 +3151,154 @@ class CertRunner {
|
|
|
2946
3151
|
|
|
2947
3152
|
const msgFinal = await page.evaluate(() => (document.body.textContent || '').trim().substring(0, 300));
|
|
2948
3153
|
console.log(` ✓ Declaración completada. Respuesta: ${msgFinal.substring(0, 150)}`);
|
|
2949
|
-
|
|
2950
|
-
if (this.config.debugDir) {
|
|
2951
|
-
await page.screenshot({ path: path.join(this.config.debugDir, 'boleta-declaracion-post-submit.png'), fullPage: true }).catch(() => {});
|
|
2952
|
-
}
|
|
3154
|
+
await saveDebugDecl('04-post-submit-OK');
|
|
2953
3155
|
|
|
2954
3156
|
return { success: true, mensaje: msgFinal };
|
|
2955
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 {}
|
|
2956
3168
|
return { success: false, error: err.message };
|
|
2957
3169
|
} finally {
|
|
2958
3170
|
if (browser) await browser.close().catch(() => {});
|
|
2959
3171
|
}
|
|
2960
3172
|
}
|
|
2961
3173
|
|
|
3174
|
+
/**
|
|
3175
|
+
* Verifica si la empresa ya está autorizada para emitir Boletas Electrónicas tipo 39
|
|
3176
|
+
* en PRODUCCIÓN (palena.sii.cl). Si el tipo 39 aparece en el select de
|
|
3177
|
+
* of_solicita_folios_dcto significa que el SII ya certificó la empresa.
|
|
3178
|
+
*
|
|
3179
|
+
* También consulta el avance en certificación (maullin) como dato adicional.
|
|
3180
|
+
*
|
|
3181
|
+
* @returns {Promise<{
|
|
3182
|
+
* autorizadaProduccion: boolean,
|
|
3183
|
+
* autorizadaCertificacion: boolean,
|
|
3184
|
+
* mensaje: string,
|
|
3185
|
+
* tiposDisponiblesProduccion: number[],
|
|
3186
|
+
* estadoCertificacion: string|null
|
|
3187
|
+
* }>}
|
|
3188
|
+
*/
|
|
3189
|
+
async verificarAutorizacionBoleta() {
|
|
3190
|
+
const https = require('https');
|
|
3191
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
3192
|
+
const cookieStr = Object.entries(cookieJar).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
3193
|
+
|
|
3194
|
+
const fetchHtml = (url) => new Promise((resolve, reject) => {
|
|
3195
|
+
const req = https.get(url, {
|
|
3196
|
+
headers: {
|
|
3197
|
+
'Cookie': cookieStr,
|
|
3198
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
|
3199
|
+
'Accept': 'text/html,*/*',
|
|
3200
|
+
},
|
|
3201
|
+
rejectUnauthorized: false,
|
|
3202
|
+
}, (res) => {
|
|
3203
|
+
const chunks = [];
|
|
3204
|
+
res.on('data', c => chunks.push(c));
|
|
3205
|
+
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
3206
|
+
});
|
|
3207
|
+
req.on('error', reject);
|
|
3208
|
+
req.setTimeout(20000, () => { req.destroy(); reject(new Error('Timeout')); });
|
|
3209
|
+
});
|
|
3210
|
+
|
|
3211
|
+
const result = {
|
|
3212
|
+
autorizadaProduccion: false,
|
|
3213
|
+
autorizadaCertificacion: false,
|
|
3214
|
+
mensaje: '',
|
|
3215
|
+
tiposDisponiblesProduccion: [],
|
|
3216
|
+
estadoCertificacion: null,
|
|
3217
|
+
};
|
|
3218
|
+
|
|
3219
|
+
// ── 1. Verificar producción: of_solicita_folios_dcto en palena ─────────
|
|
3220
|
+
try {
|
|
3221
|
+
const htmlProd = await fetchHtml('https://palena.sii.cl/cvc_cgi/dte/of_solicita_folios_dcto');
|
|
3222
|
+
if (this.config.debugDir) {
|
|
3223
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
3224
|
+
fs.writeFileSync(
|
|
3225
|
+
path.join(this.config.debugDir, `verificar-boleta-produccion-${ts}.html`),
|
|
3226
|
+
htmlProd, 'utf-8'
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
3229
|
+
const matches = [...htmlProd.matchAll(/<option[^>]+value="(\d+)"[^>]*>/gi)];
|
|
3230
|
+
result.tiposDisponiblesProduccion = matches.map(m => parseInt(m[1], 10)).filter(n => n > 0);
|
|
3231
|
+
result.autorizadaProduccion = result.tiposDisponiblesProduccion.includes(39);
|
|
3232
|
+
console.log(` ✓ Producción — tipos disponibles: [${result.tiposDisponiblesProduccion.join(', ')}] | Boleta 39: ${result.autorizadaProduccion ? '✅ SÍ' : '❌ NO'}`);
|
|
3233
|
+
} catch (e) {
|
|
3234
|
+
console.log(` [!] No se pudo verificar producción: ${e.message}`);
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
// ── 2. Verificar certificación: pe_avance6 en maullin ─────────────────
|
|
3238
|
+
try {
|
|
3239
|
+
const htmlCert = await fetchHtml('https://maullin.sii.cl/cvc_cgi/dte/pe_avance6');
|
|
3240
|
+
if (this.config.debugDir) {
|
|
3241
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
3242
|
+
fs.writeFileSync(
|
|
3243
|
+
path.join(this.config.debugDir, `verificar-boleta-certificacion-${ts}.html`),
|
|
3244
|
+
htmlCert, 'utf-8'
|
|
3245
|
+
);
|
|
3246
|
+
}
|
|
3247
|
+
// Buscar fila de boleta en tabla de avance
|
|
3248
|
+
const boletaRow = htmlCert.match(/BOLETA[^<]*<\/[^>]+>\s*<[^>]+><b>([^<]+)<\/b>/i);
|
|
3249
|
+
if (boletaRow) {
|
|
3250
|
+
result.estadoCertificacion = boletaRow[1].trim();
|
|
3251
|
+
result.autorizadaCertificacion = /conform|aprobad|autoriz/i.test(result.estadoCertificacion);
|
|
3252
|
+
}
|
|
3253
|
+
if (!result.estadoCertificacion && /autorizada.*boleta|boleta.*autorizada/i.test(htmlCert)) {
|
|
3254
|
+
result.autorizadaCertificacion = true;
|
|
3255
|
+
result.estadoCertificacion = 'AUTORIZADA';
|
|
3256
|
+
}
|
|
3257
|
+
console.log(` ✓ Certificación — estado boleta: ${result.estadoCertificacion || '(no encontrado)'} | Autorizada: ${result.autorizadaCertificacion ? '✅ SÍ' : '❌ NO'}`);
|
|
3258
|
+
} catch (e) {
|
|
3259
|
+
console.log(` [!] No se pudo verificar certificación: ${e.message}`);
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
result.mensaje = result.autorizadaProduccion
|
|
3263
|
+
? '✅ Empresa autorizada para emitir Boleta Electrónica (tipo 39) en producción'
|
|
3264
|
+
: result.autorizadaCertificacion
|
|
3265
|
+
? '⏳ Boleta certificada en SII pero aún no disponible en producción'
|
|
3266
|
+
: '❌ Boleta NO autorizada — certificación incompleta';
|
|
3267
|
+
|
|
3268
|
+
return result;
|
|
3269
|
+
}
|
|
3270
|
+
|
|
2962
3271
|
// ═══════════════════════════════════════════════════════════════
|
|
2963
3272
|
// Helpers privados
|
|
2964
3273
|
// ═══════════════════════════════════════════════════════════════
|
|
2965
3274
|
|
|
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
|
+
|
|
2966
3302
|
_getFechaHoy() {
|
|
2967
3303
|
const now = new Date();
|
|
2968
3304
|
return `${String(now.getDate()).padStart(2, '0')}-${String(now.getMonth() + 1).padStart(2, '0')}-${now.getFullYear()}`;
|
package/cert/ConfigLoader.js
CHANGED
|
@@ -54,7 +54,7 @@ function loadConfig(options = {}) {
|
|
|
54
54
|
console.log(`[ConfigLoader] Resolución: NroResol=${nro_resol} FchResol=${fch_resol} (AMBIENTE=${AMBIENTE})`);
|
|
55
55
|
|
|
56
56
|
const EMISOR = {
|
|
57
|
-
rut: process.env.EMISOR_RUT,
|
|
57
|
+
rut: process.env.EMISOR_RUT || process.env.EMISOR_RUT_EMPRESA,
|
|
58
58
|
razon_social: process.env.EMISOR_RAZON_SOCIAL,
|
|
59
59
|
giro: process.env.EMISOR_GIRO || 'ACTIVIDADES DE PROGRAMACION INFORMATICA',
|
|
60
60
|
acteco: process.env.EMISOR_ACTECO || '620200',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Facturación y boletas electrónicas para el SII de Chile. Genera, timbra, firma y envía DTEs, libros electrónicos y automatiza la certificación.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "dte-sii.d.ts",
|
package/utils/progress.js
CHANGED
|
@@ -35,6 +35,7 @@ const STEPS = {
|
|
|
35
35
|
BOOK_ERROR: 'BOOK_ERROR', // data: { book, error }
|
|
36
36
|
BOOKS_DECLARING: 'BOOKS_DECLARING',
|
|
37
37
|
BOOKS_DONE: 'BOOKS_DONE',
|
|
38
|
+
BOOK_PERIOD_RETRY: 'BOOK_PERIOD_RETRY', // data: { periodo, intento }
|
|
38
39
|
// Avance (Fase 5)
|
|
39
40
|
ADVANCE_WAITING: 'ADVANCE_WAITING',
|
|
40
41
|
ADVANCE_DONE: 'ADVANCE_DONE',
|