@devlas/dte-sii 2.5.16 → 2.7.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 +260 -64
- package/cert/MuestrasImpresas.js +31 -3
- package/package.json +7 -1
- package/utils/browser.js +73 -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
|
@@ -1926,11 +1926,7 @@ class CertRunner {
|
|
|
1926
1926
|
|
|
1927
1927
|
let browser;
|
|
1928
1928
|
try {
|
|
1929
|
-
browser = await puppeteer.launch(
|
|
1930
|
-
headless: true,
|
|
1931
|
-
ignoreHTTPSErrors: true,
|
|
1932
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
1933
|
-
});
|
|
1929
|
+
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions());
|
|
1934
1930
|
|
|
1935
1931
|
const page = await browser.newPage();
|
|
1936
1932
|
await page.setCookie(...puppeteerCookies);
|
|
@@ -2114,11 +2110,7 @@ class CertRunner {
|
|
|
2114
2110
|
const [rutNum, dvChar] = this.config.emisor.rut.split('-');
|
|
2115
2111
|
let browser;
|
|
2116
2112
|
try {
|
|
2117
|
-
browser = await puppeteer.launch(
|
|
2118
|
-
headless: true,
|
|
2119
|
-
ignoreHTTPSErrors: true,
|
|
2120
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2121
|
-
});
|
|
2113
|
+
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions());
|
|
2122
2114
|
const page = await browser.newPage();
|
|
2123
2115
|
await page.setCookie(...puppeteerCookies);
|
|
2124
2116
|
await page.goto('https://www4.sii.cl/pdfdteInternet/', { waitUntil: 'networkidle2', timeout: 60000 });
|
|
@@ -2137,23 +2129,32 @@ class CertRunner {
|
|
|
2137
2129
|
if (btn) btn.click();
|
|
2138
2130
|
}, 'Rut');
|
|
2139
2131
|
|
|
2140
|
-
//
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
.find(b => /^s[ií]$/i.test(b.textContent.trim()));
|
|
2145
|
-
if (si) si.click();
|
|
2146
|
-
}).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));
|
|
2147
2136
|
|
|
2148
|
-
// Esperar hasta
|
|
2137
|
+
// Esperar hasta 10s a que aparezca el estado en el DOM (detrás del diálogo si aplica)
|
|
2149
2138
|
await page.waitForFunction(() => {
|
|
2150
2139
|
const t = (document.body.textContent || '').toUpperCase();
|
|
2151
|
-
return t.includes('
|
|
2152
|
-
t.includes('
|
|
2153
|
-
t.includes('
|
|
2154
|
-
}, { 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(() => {});
|
|
2155
2144
|
|
|
2145
|
+
// Leer estado directamente del label del formulario en el DOM
|
|
2156
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
|
|
2157
2158
|
const t = (document.body.textContent || '').toUpperCase();
|
|
2158
2159
|
if (t.includes('APROBADO')) return 'APROBADO';
|
|
2159
2160
|
if (t.includes('POR REVISAR')) return 'POR REVISAR';
|
|
@@ -2167,7 +2168,13 @@ class CertRunner {
|
|
|
2167
2168
|
} catch (err) {
|
|
2168
2169
|
return { estado: null, error: err.message };
|
|
2169
2170
|
} finally {
|
|
2170
|
-
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
|
+
}
|
|
2171
2178
|
}
|
|
2172
2179
|
}
|
|
2173
2180
|
|
|
@@ -2222,12 +2229,7 @@ class CertRunner {
|
|
|
2222
2229
|
|
|
2223
2230
|
let browser;
|
|
2224
2231
|
try {
|
|
2225
|
-
browser = await puppeteer.launch({
|
|
2226
|
-
headless: true,
|
|
2227
|
-
ignoreHTTPSErrors: true,
|
|
2228
|
-
protocolTimeout: 300000, // 5 min — DOM.setFileInputFiles con 256 archivos supera el default de 30s
|
|
2229
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2230
|
-
});
|
|
2232
|
+
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions([], { protocolTimeout: 300000 }));
|
|
2231
2233
|
const page = await browser.newPage();
|
|
2232
2234
|
await page.setCookie(...puppeteerCookies);
|
|
2233
2235
|
|
|
@@ -2618,14 +2620,26 @@ class CertRunner {
|
|
|
2618
2620
|
}
|
|
2619
2621
|
|
|
2620
2622
|
const pageText = await page.$eval('body', el => el.textContent).catch(() => '');
|
|
2621
|
-
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);
|
|
2622
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');
|
|
2623
2628
|
return { success: exitoso, pageText: pageText.substring(0, 800) };
|
|
2624
2629
|
|
|
2625
2630
|
} catch (err) {
|
|
2626
2631
|
return { success: false, error: err.message };
|
|
2627
2632
|
} finally {
|
|
2628
|
-
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
|
+
}
|
|
2629
2643
|
}
|
|
2630
2644
|
}
|
|
2631
2645
|
|
|
@@ -2655,25 +2669,37 @@ class CertRunner {
|
|
|
2655
2669
|
|
|
2656
2670
|
let browser;
|
|
2657
2671
|
try {
|
|
2658
|
-
browser = await puppeteer.launch(
|
|
2659
|
-
headless: true,
|
|
2660
|
-
ignoreHTTPSErrors: true,
|
|
2661
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2662
|
-
});
|
|
2672
|
+
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions());
|
|
2663
2673
|
const page = await browser.newPage();
|
|
2664
2674
|
await page.setCookie(...puppeteerCookies);
|
|
2665
2675
|
page.on('dialog', async dlg => { console.log(` → [dialog SET=1] ${dlg.message()}`); await dlg.accept(); });
|
|
2666
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
|
+
|
|
2667
2689
|
console.log(' → Cargando portal certBolElectDteInternet (SET=1)...');
|
|
2668
2690
|
await page.goto('https://www4.sii.cl/certBolElectDteInternet/?SET=1', {
|
|
2669
2691
|
waitUntil: 'networkidle2', timeout: 60000,
|
|
2670
2692
|
});
|
|
2671
2693
|
await new Promise(r => setTimeout(r, 2000));
|
|
2694
|
+
await saveDebugSet1('01-carga');
|
|
2672
2695
|
|
|
2673
2696
|
// Paso 1: RUT empresa → "Confirmar Empresa"
|
|
2674
|
-
const rutInput = await page
|
|
2675
|
-
const dvInput = await page
|
|
2676
|
-
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
|
+
}
|
|
2677
2703
|
console.log(` → Ingresando RUT empresa: ${rutNum}-${dvChar}`);
|
|
2678
2704
|
await rutInput.click({ clickCount: 3 }); await rutInput.type(rutNum);
|
|
2679
2705
|
await dvInput.click({ clickCount: 3 }); await dvInput.type(dvChar);
|
|
@@ -2690,6 +2716,7 @@ class CertRunner {
|
|
|
2690
2716
|
return document.querySelector('input[type="checkbox"]') !== null;
|
|
2691
2717
|
}, { timeout: 40000, polling: 500 }).catch(() => {});
|
|
2692
2718
|
await new Promise(r => setTimeout(r, 500));
|
|
2719
|
+
await saveDebugSet1('02-post-confirm');
|
|
2693
2720
|
|
|
2694
2721
|
// Paso 2: Marcar todos los checkboxes
|
|
2695
2722
|
const nCbs = await page.evaluate(() => {
|
|
@@ -2776,9 +2803,9 @@ class CertRunner {
|
|
|
2776
2803
|
});
|
|
2777
2804
|
|
|
2778
2805
|
if (!setText || setText.trim().length < 10) {
|
|
2779
|
-
if (
|
|
2780
|
-
fs.
|
|
2781
|
-
|
|
2806
|
+
if (dbgDirSet1) {
|
|
2807
|
+
fs.writeFileSync(path.join(dbgDirSet1, 'boleta-set-contenido-vacio.txt'), setText || '', 'utf-8');
|
|
2808
|
+
await saveDebugSet1('ERROR-descarga-vacia');
|
|
2782
2809
|
}
|
|
2783
2810
|
throw new Error(`DownloadFileServlet devolvió contenido vacío (${setText?.length ?? 0} chars). Verificar cookies.`);
|
|
2784
2811
|
}
|
|
@@ -2793,6 +2820,15 @@ class CertRunner {
|
|
|
2793
2820
|
console.log(` ✓ Set de pruebas obtenido (${setText.length} chars)`);
|
|
2794
2821
|
return { success: true, setText };
|
|
2795
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 {}
|
|
2796
2832
|
return { success: false, error: err.message };
|
|
2797
2833
|
} finally {
|
|
2798
2834
|
if (browser) await browser.close().catch(() => {});
|
|
@@ -2821,20 +2857,27 @@ class CertRunner {
|
|
|
2821
2857
|
|
|
2822
2858
|
let browser;
|
|
2823
2859
|
try {
|
|
2824
|
-
browser = await puppeteer.launch(
|
|
2825
|
-
headless: true,
|
|
2826
|
-
ignoreHTTPSErrors: true,
|
|
2827
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2828
|
-
});
|
|
2860
|
+
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions());
|
|
2829
2861
|
const page = await browser.newPage();
|
|
2830
2862
|
await page.setCookie(...puppeteerCookies);
|
|
2831
2863
|
page.on('dialog', async dlg => { console.log(` → [dialog SET=2] ${dlg.message()}`); await dlg.accept(); });
|
|
2832
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
|
+
|
|
2833
2875
|
console.log(' → Cargando portal certBolElectDteInternet (SET=2)...');
|
|
2834
2876
|
await page.goto('https://www4.sii.cl/certBolElectDteInternet/?SET=2', {
|
|
2835
2877
|
waitUntil: 'networkidle2', timeout: 60000,
|
|
2836
2878
|
});
|
|
2837
2879
|
await new Promise(r => setTimeout(r, 2000));
|
|
2880
|
+
await snapSet2('01-carga');
|
|
2838
2881
|
|
|
2839
2882
|
// Paso 1: RUT empresa → "Confirmar Empresa"
|
|
2840
2883
|
const rutInput = await page.$('input[maxlength="8"]');
|
|
@@ -2856,10 +2899,15 @@ class CertRunner {
|
|
|
2856
2899
|
return document.querySelector('input[maxlength="15"]') !== null;
|
|
2857
2900
|
}, { timeout: 40000, polling: 500 }).catch(() => {});
|
|
2858
2901
|
await new Promise(r => setTimeout(r, 500));
|
|
2902
|
+
await snapSet2('02-post-confirm');
|
|
2859
2903
|
|
|
2860
2904
|
// Paso 2: Ingresar TrackId
|
|
2861
2905
|
const trackInput = await page.$('input[maxlength="15"]');
|
|
2862
|
-
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
|
+
}
|
|
2863
2911
|
console.log(` → Ingresando TrackId: ${trackId}`);
|
|
2864
2912
|
await trackInput.click({ clickCount: 3 });
|
|
2865
2913
|
await trackInput.type(String(trackId));
|
|
@@ -2878,6 +2926,7 @@ class CertRunner {
|
|
|
2878
2926
|
return t.includes('ENVI') || t.includes('CORREO') || t.includes('ERROR') ||
|
|
2879
2927
|
t.includes('VALIDACI') || t.includes('SOLICITUD');
|
|
2880
2928
|
}, { timeout: 15000, polling: 500 }).catch(() => {});
|
|
2929
|
+
await snapSet2('03-respuesta');
|
|
2881
2930
|
|
|
2882
2931
|
const respuesta = await page.evaluate(() => (document.body.innerText || '').trim().substring(0, 500));
|
|
2883
2932
|
console.log(` ✓ Validación solicitada. Respuesta: ${respuesta.substring(0, 120)}`);
|
|
@@ -2920,11 +2969,7 @@ class CertRunner {
|
|
|
2920
2969
|
|
|
2921
2970
|
let browser;
|
|
2922
2971
|
try {
|
|
2923
|
-
browser = await puppeteer.launch(
|
|
2924
|
-
headless: true,
|
|
2925
|
-
ignoreHTTPSErrors: true,
|
|
2926
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
|
|
2927
|
-
});
|
|
2972
|
+
browser = await puppeteer.launch(this._getPuppeteerLaunchOptions());
|
|
2928
2973
|
const page = await browser.newPage();
|
|
2929
2974
|
await page.setCookie(...puppeteerCookies);
|
|
2930
2975
|
|
|
@@ -2936,6 +2981,18 @@ class CertRunner {
|
|
|
2936
2981
|
await dlg.accept();
|
|
2937
2982
|
});
|
|
2938
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
|
+
|
|
2939
2996
|
// ── PASOS 1+2: Confirmar Empresa → esperar formulario (con retry) ──
|
|
2940
2997
|
// El portal GWT a veces responde con error transitorio ("empresa no autorizada")
|
|
2941
2998
|
// que se resuelve recargando la página y reintentando.
|
|
@@ -2950,11 +3007,15 @@ class CertRunner {
|
|
|
2950
3007
|
waitUntil: 'networkidle2', timeout: 60000,
|
|
2951
3008
|
});
|
|
2952
3009
|
await new Promise(r => setTimeout(r, 2500));
|
|
3010
|
+
await saveDebugDecl(`intento${intento}-01-carga`);
|
|
2953
3011
|
|
|
2954
3012
|
// GWT requiere eventos de teclado reales — NO funciona con .value = ...
|
|
2955
|
-
const rutInput = await page
|
|
2956
|
-
const dvInput = await page
|
|
2957
|
-
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
|
+
}
|
|
2958
3019
|
console.log(` → Ingresando RUT empresa: ${rutEmpresaRaw}`);
|
|
2959
3020
|
await rutInput.click({ clickCount: 3 }); await rutInput.type(rutEmpNum);
|
|
2960
3021
|
await dvInput.click({ clickCount: 3 }); await dvInput.type(rutEmpDv);
|
|
@@ -2972,10 +3033,12 @@ class CertRunner {
|
|
|
2972
3033
|
await page.waitForFunction(() => {
|
|
2973
3034
|
return document.querySelector('input[type="checkbox"]') !== null;
|
|
2974
3035
|
}, { timeout: 40000, polling: 500 }).catch(() => {});
|
|
3036
|
+
await saveDebugDecl(`intento${intento}-02-post-confirm`);
|
|
2975
3037
|
|
|
2976
3038
|
if (dialogMsg) {
|
|
2977
3039
|
// Portal lanzó alert — puede ser transitorio. Guardar y reintentar.
|
|
2978
3040
|
lastDialogMsg = dialogMsg;
|
|
3041
|
+
await saveDebugDecl(`intento${intento}-DIALOG`);
|
|
2979
3042
|
console.log(` [!] Portal respondió con alerta en intento ${intento}: ${dialogMsg.substring(0, 100)}`);
|
|
2980
3043
|
if (intento < MAX_INTENTOS) {
|
|
2981
3044
|
console.log(' → Recargando y reintentando en 3s...');
|
|
@@ -2983,6 +3046,7 @@ class CertRunner {
|
|
|
2983
3046
|
continue;
|
|
2984
3047
|
}
|
|
2985
3048
|
// Agotados los intentos con alerta — el SII puede requerir esperar SOK
|
|
3049
|
+
await saveDebugDecl('PENDINGSOK-final');
|
|
2986
3050
|
return { success: false, pendingSok: true, error: lastDialogMsg };
|
|
2987
3051
|
}
|
|
2988
3052
|
|
|
@@ -3063,9 +3127,7 @@ class CertRunner {
|
|
|
3063
3127
|
await fillByLabel('Correo electrónico Proveedor', correoProveedor);
|
|
3064
3128
|
|
|
3065
3129
|
// Captura pre-submit
|
|
3066
|
-
|
|
3067
|
-
await page.screenshot({ path: path.join(this.config.debugDir, 'boleta-declaracion-pre-submit.png'), fullPage: true }).catch(() => {});
|
|
3068
|
-
}
|
|
3130
|
+
await saveDebugDecl('03-pre-submit');
|
|
3069
3131
|
|
|
3070
3132
|
// ── PASO 4: Click "Grabar Declaración" ───────────────────────
|
|
3071
3133
|
const submitOk = await page.evaluate(() => {
|
|
@@ -3074,7 +3136,10 @@ class CertRunner {
|
|
|
3074
3136
|
if (btn) { btn.click(); return true; }
|
|
3075
3137
|
return false;
|
|
3076
3138
|
});
|
|
3077
|
-
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
|
+
}
|
|
3078
3143
|
console.log(' → Click "Grabar Declaración"...');
|
|
3079
3144
|
|
|
3080
3145
|
// Esperar confirmación del SII
|
|
@@ -3086,23 +3151,154 @@ class CertRunner {
|
|
|
3086
3151
|
|
|
3087
3152
|
const msgFinal = await page.evaluate(() => (document.body.textContent || '').trim().substring(0, 300));
|
|
3088
3153
|
console.log(` ✓ Declaración completada. Respuesta: ${msgFinal.substring(0, 150)}`);
|
|
3089
|
-
|
|
3090
|
-
if (this.config.debugDir) {
|
|
3091
|
-
await page.screenshot({ path: path.join(this.config.debugDir, 'boleta-declaracion-post-submit.png'), fullPage: true }).catch(() => {});
|
|
3092
|
-
}
|
|
3154
|
+
await saveDebugDecl('04-post-submit-OK');
|
|
3093
3155
|
|
|
3094
3156
|
return { success: true, mensaje: msgFinal };
|
|
3095
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 {}
|
|
3096
3168
|
return { success: false, error: err.message };
|
|
3097
3169
|
} finally {
|
|
3098
3170
|
if (browser) await browser.close().catch(() => {});
|
|
3099
3171
|
}
|
|
3100
3172
|
}
|
|
3101
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
|
+
|
|
3102
3271
|
// ═══════════════════════════════════════════════════════════════
|
|
3103
3272
|
// Helpers privados
|
|
3104
3273
|
// ═══════════════════════════════════════════════════════════════
|
|
3105
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
|
+
|
|
3106
3302
|
_getFechaHoy() {
|
|
3107
3303
|
const now = new Date();
|
|
3108
3304
|
return `${String(now.getDate()).padStart(2, '0')}-${String(now.getMonth() + 1).padStart(2, '0')}-${now.getFullYear()}`;
|
package/cert/MuestrasImpresas.js
CHANGED
|
@@ -20,7 +20,7 @@ const fs = require('fs');
|
|
|
20
20
|
const path = require('path');
|
|
21
21
|
const { XMLParser, XMLBuilder } = require('fast-xml-parser');
|
|
22
22
|
const bwipjs = require('bwip-js');
|
|
23
|
-
const
|
|
23
|
+
const { launchBrowser } = require('../utils/browser');
|
|
24
24
|
|
|
25
25
|
// Usar constantes del core
|
|
26
26
|
const {
|
|
@@ -485,7 +485,7 @@ class MuestrasImpresas {
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
/**
|
|
488
|
-
* Genera PDF desde HTML
|
|
488
|
+
* Genera PDF desde HTML y lo escribe a disco.
|
|
489
489
|
* @private
|
|
490
490
|
*/
|
|
491
491
|
async _generarPdf({ html, outputPath, browser }) {
|
|
@@ -501,6 +501,34 @@ class MuestrasImpresas {
|
|
|
501
501
|
await page.close();
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Genera un PDF desde HTML y devuelve el contenido como Buffer (sin escribir a disco).
|
|
506
|
+
* Útil para servir el PDF directamente desde una respuesta HTTP.
|
|
507
|
+
*
|
|
508
|
+
* @param {string} html - HTML a convertir a PDF
|
|
509
|
+
* @param {object} [opts]
|
|
510
|
+
* @param {string} [opts.width='215mm']
|
|
511
|
+
* @param {string} [opts.height='280mm']
|
|
512
|
+
* @returns {Promise<Buffer>}
|
|
513
|
+
*/
|
|
514
|
+
async generarPdfBuffer(html, opts = {}) {
|
|
515
|
+
const browser = await launchBrowser();
|
|
516
|
+
try {
|
|
517
|
+
const page = await browser.newPage();
|
|
518
|
+
await page.setContent(html, { waitUntil: 'networkidle0' });
|
|
519
|
+
const pdfBuffer = await page.pdf({
|
|
520
|
+
printBackground: true,
|
|
521
|
+
width: opts.width ?? '215mm',
|
|
522
|
+
height: opts.height ?? '280mm',
|
|
523
|
+
margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
|
|
524
|
+
});
|
|
525
|
+
await page.close();
|
|
526
|
+
return Buffer.from(pdfBuffer);
|
|
527
|
+
} finally {
|
|
528
|
+
await browser.close();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
504
532
|
/**
|
|
505
533
|
* Genera muestras impresas desde archivos XML
|
|
506
534
|
* @param {Object} options
|
|
@@ -524,7 +552,7 @@ class MuestrasImpresas {
|
|
|
524
552
|
console.log(` SET-PRUEBAS: ${pruebasDir}`);
|
|
525
553
|
console.log(` SET-SIMULACION: ${simulacionDir}`);
|
|
526
554
|
|
|
527
|
-
const browser = await
|
|
555
|
+
const browser = await launchBrowser();
|
|
528
556
|
const resultado = {
|
|
529
557
|
success: true,
|
|
530
558
|
totalDocs: 0,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.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",
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
"engines": {
|
|
29
29
|
"node": ">=18.0.0"
|
|
30
30
|
},
|
|
31
|
+
"optionalDependencies": {
|
|
32
|
+
"@sparticuz/chromium": "^133.0.0"
|
|
33
|
+
},
|
|
31
34
|
"dependencies": {
|
|
32
35
|
"@xmldom/xmldom": "^0.8.11",
|
|
33
36
|
"bwip-js": "^4.8.0",
|
|
@@ -40,6 +43,9 @@
|
|
|
40
43
|
"xml-c14n": "^0.0.6",
|
|
41
44
|
"xml-crypto": "^6.1.2"
|
|
42
45
|
},
|
|
46
|
+
"optionalDependencies": {
|
|
47
|
+
"@sparticuz/chromium": "^133.0.0"
|
|
48
|
+
},
|
|
43
49
|
"files": [
|
|
44
50
|
"index.js",
|
|
45
51
|
"dte-sii.d.ts",
|
package/utils/browser.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* utils/browser.js
|
|
5
|
+
*
|
|
6
|
+
* Helper para lanzar Puppeteer con el Chromium correcto según el entorno:
|
|
7
|
+
* - Producción / serverless (Railway, Lambda, etc.): usa @sparticuz/chromium
|
|
8
|
+
* - Desarrollo local: usa el Chrome del sistema si @sparticuz/chromium no está disponible
|
|
9
|
+
*
|
|
10
|
+
* Uso:
|
|
11
|
+
* const { launchBrowser } = require('./utils/browser')
|
|
12
|
+
* const browser = await launchBrowser()
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const puppeteer = require('puppeteer');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Devuelve las opciones de lanzamiento para puppeteer.launch() según el entorno.
|
|
21
|
+
* @returns {Promise<import('puppeteer').LaunchOptions>}
|
|
22
|
+
*/
|
|
23
|
+
async function getLaunchOptions() {
|
|
24
|
+
// 1. Intentar @sparticuz/chromium (viene en el paquete, funciona en serverless)
|
|
25
|
+
try {
|
|
26
|
+
const chromium = require('@sparticuz/chromium');
|
|
27
|
+
return {
|
|
28
|
+
args: chromium.args,
|
|
29
|
+
defaultViewport: chromium.defaultViewport,
|
|
30
|
+
executablePath: await chromium.executablePath(),
|
|
31
|
+
headless: chromium.headless ?? true,
|
|
32
|
+
};
|
|
33
|
+
} catch {
|
|
34
|
+
// No está disponible, continuar con fallback
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 2. Ruta explícita por variable de entorno
|
|
38
|
+
if (process.env.PUPPETEER_EXECUTABLE_PATH) {
|
|
39
|
+
return {
|
|
40
|
+
headless: true,
|
|
41
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
42
|
+
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 3. Chrome del sistema (rutas comunes Linux / Windows)
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
const SYSTEM_PATHS = [
|
|
49
|
+
'/usr/bin/google-chrome-stable',
|
|
50
|
+
'/usr/bin/google-chrome',
|
|
51
|
+
'/usr/bin/chromium-browser',
|
|
52
|
+
'/usr/bin/chromium',
|
|
53
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
54
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
|
|
55
|
+
];
|
|
56
|
+
const executablePath = SYSTEM_PATHS.find(p => { try { return fs.existsSync(p) } catch { return false } });
|
|
57
|
+
return {
|
|
58
|
+
headless: true,
|
|
59
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
60
|
+
...(executablePath ? { executablePath } : {}),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Lanza un browser Puppeteer con el Chromium correcto para el entorno actual.
|
|
66
|
+
* @returns {Promise<import('puppeteer').Browser>}
|
|
67
|
+
*/
|
|
68
|
+
async function launchBrowser() {
|
|
69
|
+
const opts = await getLaunchOptions();
|
|
70
|
+
return puppeteer.launch(opts);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { launchBrowser, getLaunchOptions };
|