@devlas/dte-sii 2.5.14 → 2.5.16

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.
@@ -354,7 +354,7 @@ class CertRunner {
354
354
  * @param {Object} [options] - { maxIntentos, intervalo, label }
355
355
  */
356
356
  async _declararConReintentos(sets, debugPrefix, options = {}) {
357
- const { maxIntentos = 10, intervalo = 5000, label = 'avance' } = options;
357
+ const { maxIntentos = 10, intervalo = 5000, label = 'avance', retryOnAllRejected = false } = options;
358
358
 
359
359
  console.log(` Esperando 10s para que SII procese los envios...`);
360
360
  await sleep(10000);
@@ -400,9 +400,14 @@ class CertRunner {
400
400
  console.log(` [...] SII aún procesando, reintentando en ${intervalo / 1000}s...`);
401
401
  await sleep(intervalo);
402
402
  } else if (result.allRejected) {
403
- // SII rechazó todos los sets/libros — período incorrecto, no tiene sentido reintentar
404
- console.log(` [ERR] SII rechazó todos los envíos (campos vacíos en portal) — período incorrecto. Corregir período y reenviar.`);
405
- break;
403
+ // SII rechazó todos los sets/libros — puede ser período incorrecto (libros) o TrackID no procesado aún (simulación)
404
+ if (retryOnAllRejected && intento < maxIntentos) {
405
+ console.log(` [!] S21 — SII aún procesando TrackID, reintentando en ${intervalo / 1000}s...`);
406
+ await sleep(intervalo);
407
+ } else {
408
+ console.log(` [ERR] SII rechazó todos los envíos (campos vacíos en portal) — período incorrecto. Corregir período y reenviar.`);
409
+ break;
410
+ }
406
411
  } else if (result.verificado === false && intento < maxIntentos) {
407
412
  // Verificación post-declaración falló: los campos quedaron vacíos en el portal
408
413
  console.log(` [!] Verificación fallida: ${result.error}`);
@@ -777,6 +782,7 @@ class CertRunner {
777
782
  this._decrementarPeriodoLibros();
778
783
  const _nuevoPeriodo = this._getPeriodoLibros();
779
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) });
780
786
  await _reenviarLibros(_nuevoPeriodo);
781
787
  declaracion = await this.declararLibros({ ...resultados, ...(options.setsResultados || {}) });
782
788
  resultados.declaracion = declaracion;
@@ -801,6 +807,7 @@ class CertRunner {
801
807
  const _nuevoPeriodo = this._getPeriodoLibros();
802
808
  const _s21Nombres = _s21Keys.map(k => _KEY_A_SII_NOMBRE[k]).filter(Boolean);
803
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) });
804
811
 
805
812
  await _reenviarLibros(_nuevoPeriodo, new Set(_s21Keys));
806
813
  declaracion = await this.declararLibros({ ...resultados, ...(options.setsResultados || {}) });
@@ -1476,7 +1483,7 @@ class CertRunner {
1476
1483
  },
1477
1484
  };
1478
1485
 
1479
- const result = await this._declararConReintentos(sets, 'declaracion-simulacion-response', { maxIntentos, intervalo, label: 'simulación' });
1486
+ const result = await this._declararConReintentos(sets, 'declaracion-simulacion-response', { maxIntentos, intervalo, label: 'simulación', retryOnAllRejected: true });
1480
1487
  if (result?.success) console.log(' [OK] Simulación declarada exitosamente');
1481
1488
  return result;
1482
1489
  }
@@ -1545,9 +1552,16 @@ class CertRunner {
1545
1552
  const yaIntercambio = Boolean(verificacion?.etapaActual?.includes('INTERCAMBIO'));
1546
1553
  const simConforme = Boolean(estadoSim?.esConforme || estadoSim?.estado?.toUpperCase()?.includes('REVISADO CONFORME'));
1547
1554
 
1548
- if (yaIntercambio || simConforme || !sigueFormulario) {
1549
- console.log('\n ¡SIMULACIÓN CONFIRMADA! Certificación completa.');
1550
- 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 };
1551
1565
  }
1552
1566
 
1553
1567
  console.log(' [!] SII aún mantiene formulario de simulación pendiente; se reintentará...');
@@ -1580,8 +1594,11 @@ class CertRunner {
1580
1594
 
1581
1595
  if (esAprobado) {
1582
1596
  console.log(` [OK] SIMULACIÓN: REVISADO CONFORME`);
1583
- console.log('\n ¡SIMULACIÓN APROBADA! Certificación completa.');
1584
- return { success: true };
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 };
1585
1602
  } else if (esRechazado) {
1586
1603
  console.log(` [ERR] SIMULACIÓN: ${simEstado.estado}`);
1587
1604
  return { success: false, error: 'Simulación rechazada' };
@@ -2208,6 +2225,7 @@ class CertRunner {
2208
2225
  browser = await puppeteer.launch({
2209
2226
  headless: true,
2210
2227
  ignoreHTTPSErrors: true,
2228
+ protocolTimeout: 300000, // 5 min — DOM.setFileInputFiles con 256 archivos supera el default de 30s
2211
2229
  args: ['--no-sandbox', '--disable-setuid-sandbox', '--ignore-certificate-errors'],
2212
2230
  });
2213
2231
  const page = await browser.newPage();
@@ -2221,6 +2239,26 @@ class CertRunner {
2221
2239
  await new Promise(r => setTimeout(r, 3000));
2222
2240
  if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-01-loaded.png'), fullPage: true }).catch(() => {});
2223
2241
 
2242
+ // Hookear SWFUpload para capturar post_params.id cuando GWT inicialice el uploader
2243
+ await page.evaluate(() => {
2244
+ if (window.SWFUpload) {
2245
+ const _Orig = window.SWFUpload;
2246
+ window.SWFUpload = function(settings) {
2247
+ const pp = (settings && settings.post_params) || {};
2248
+ if (pp.id) window.__swfCapturedRevId = String(pp.id);
2249
+ const inst = new _Orig(settings);
2250
+ // Copiar propiedades estáticas
2251
+ Object.assign(window.SWFUpload, _Orig);
2252
+ return inst;
2253
+ };
2254
+ window.SWFUpload.prototype = _Orig.prototype;
2255
+ // Copiar constantes estáticas del prototipo original
2256
+ for (const k of Object.getOwnPropertyNames(_Orig)) {
2257
+ try { window.SWFUpload[k] = _Orig[k]; } catch { }
2258
+ }
2259
+ }
2260
+ }).catch(() => {});
2261
+
2224
2262
  // Paso 1: RUT Empresa
2225
2263
  const rutInputs = await page.$$('input[name="rut"]');
2226
2264
  const dvInputs = await page.$$('input[name="dv"]');
@@ -2239,12 +2277,26 @@ class CertRunner {
2239
2277
  if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-02-after-rut.png'), fullPage: true }).catch(() => {});
2240
2278
 
2241
2279
  // Paso 2: Diálogo "ya existe revisión" → click "Sí"
2280
+ // nuevaRevisionCreada=true cuando el usuario acepta crear una nueva revisión;
2281
+ // en ese caso NO se hace el early-exit por estado previo (el texto "EN REVISIÓN"
2282
+ // que queda visible corresponde a la revisión antigua, no a la nueva vacía).
2283
+ let nuevaRevisionCreada = false;
2242
2284
  const hayDialog = await page.evaluate(() => {
2243
2285
  const dlg = document.querySelector('.x-window');
2244
2286
  return !!(dlg && dlg.offsetParent !== null);
2245
2287
  });
2246
2288
  if (hayDialog) {
2247
- console.log(' → Diálogo de revisión existentehaciendo click en "Sí"');
2289
+ console.log(' → Diálogo "Ya existe revisión" detectado → click "Sí"');
2290
+ if (debugDir) {
2291
+ await page.screenshot({ path: path.join(debugDir, 'pdfte-dialog-ya-existe.png'), fullPage: true }).catch(() => {});
2292
+ fs.writeFileSync(path.join(debugDir, 'pdfte-dialog-ya-existe.html'), await page.content().catch(() => ''), 'utf8');
2293
+ // Log texto del diálogo para debug
2294
+ const dlgText = await page.evaluate(() => {
2295
+ const dlg = document.querySelector('.x-window');
2296
+ return dlg ? dlg.textContent.trim().replace(/\s+/g, ' ') : '';
2297
+ }).catch(() => '');
2298
+ console.log(` [DEBUG] Texto del diálogo: "${dlgText}"`);
2299
+ }
2248
2300
  const clicked = await page.evaluate(() => {
2249
2301
  const si = Array.from(document.querySelectorAll('button.x-btn-text'))
2250
2302
  .find(b => /^s[ií]$/i.test(b.textContent.trim()));
@@ -2253,6 +2305,8 @@ class CertRunner {
2253
2305
  });
2254
2306
  if (!clicked) await page.evaluate(() => { const b = document.querySelector('.x-window button'); if (b) b.click(); });
2255
2307
  await new Promise(r => setTimeout(r, 2500));
2308
+ if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-dialog-despues-si.png'), fullPage: true }).catch(() => {});
2309
+ nuevaRevisionCreada = true;
2256
2310
  }
2257
2311
 
2258
2312
  // Paso 3: RUT Proveedor (mismo RUT empresa)
@@ -2268,18 +2322,50 @@ class CertRunner {
2268
2322
  console.log(` → Ingresando RUT proveedor: ${rutNum}-${dvChar}`);
2269
2323
  await pRut.click({ clickCount: 3 }); await pRut.type(rutNum);
2270
2324
  await pDv.click({ clickCount: 3 }); await pDv.type(dvChar);
2325
+
2326
+ // Interceptar respuestas del portal durante "Consultar" para capturar el ID de revisión
2327
+ let _capturedRevId = null;
2328
+ const _onPortalResponse = async (resp) => {
2329
+ const url = resp.url();
2330
+ if (!url.includes('sii.cl/pdfdteInternet')) return;
2331
+ try {
2332
+ const body = await resp.text();
2333
+ const matches = body.match(/\b(\d{5,7})\b/g);
2334
+ if (matches) {
2335
+ for (const m of matches) {
2336
+ const n = +m;
2337
+ if (n > 10000 && n < 9999999 && !_capturedRevId) {
2338
+ _capturedRevId = m;
2339
+ console.log(` [DEBUG] Posible nroRevision capturado de red: ${m} (url: ${url.split('?')[0]})`);
2340
+ }
2341
+ }
2342
+ }
2343
+ } catch { /* skip */ }
2344
+ };
2345
+ page.on('response', _onPortalResponse);
2271
2346
  await clickBoton(page, 'Consultar');
2272
2347
  await new Promise(r => setTimeout(r, 2500));
2348
+ page.off('response', _onPortalResponse);
2273
2349
  if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-03-after-consultar.png'), fullPage: true }).catch(() => {});
2274
2350
 
2275
2351
  // ── Re-ejecución: detectar estado terminal antes de proceder ──
2276
- const _estadoYaSubido = await page.evaluate(() => {
2352
+ // Solo aplicable cuando NO se creó una nueva revisión.
2353
+ // NOTA: el tab de navegación "Muestras Impresas en revisión" siempre contiene
2354
+ // el texto "en revisión", por lo que NO se puede usar textContent global para
2355
+ // detectar ese estado. Solo hacer early-exit si el formulario de upload
2356
+ // (input.gwt-FileUpload o botón "Crear") NO está visible, lo que indica que
2357
+ // la revisión está en un estado terminal irreversible.
2358
+ const _estadoYaSubido = nuevaRevisionCreada ? null : await page.evaluate(() => {
2359
+ // Si el formulario de carga está presente, proceder siempre con el upload
2360
+ const tieneFormulario = !!document.querySelector('input.gwt-FileUpload') ||
2361
+ Array.from(document.querySelectorAll('button.x-btn-text'))
2362
+ .some(b => ['Crear', 'Limpiar', 'Eliminar'].includes(b.textContent.trim()) && !b.disabled);
2363
+ if (tieneFormulario) return null;
2364
+ // Solo si NO hay formulario, verificar estado terminal
2277
2365
  const t = (document.body.textContent || '').toUpperCase();
2278
2366
  if (t.includes('APROBADO')) return 'APROBADO';
2279
- if (t.includes('POR REVISAR')) return 'POR REVISAR';
2280
- if (t.includes('EN REVISI')) return 'EN REVISIÓN';
2281
- if (t.includes('RECHAZADO')) return 'RECHAZADO';
2282
2367
  if (t.includes('ENVIADO AL SII')) return 'ENVIADO AL SII';
2368
+ if (t.includes('RECHAZADO')) return 'RECHAZADO';
2283
2369
  return null;
2284
2370
  }).catch(() => null);
2285
2371
  if (_estadoYaSubido) {
@@ -2343,97 +2429,121 @@ class CertRunner {
2343
2429
  if (!_hayInp) { await clickBoton(page, 'Crear'); await new Promise(r => setTimeout(r, 2500)); }
2344
2430
  };
2345
2431
 
2346
- // Cargar todos los PDFs como base64 en Node.js y soltarlos en el drop zone de GWT
2347
- // de una sola vez via DataTransfer. GWT los procesa en secuencia internamente:
2348
- // drop → por cada file: submit form al iframe → respuesta → leeImpresoById → tick verde
2349
- // Esto evita la re-navegación entre archivos y garantiza que la validación
2350
- // (Timbre/CAF/TED) ocurra antes de salir de la página.
2351
- console.log(` Cargando ${pdfPaths.length} PDFs para drop en el portal...`);
2352
- const _fileDataList = pdfPaths.map(p => ({
2353
- name: path.basename(p),
2354
- b64: fs.readFileSync(p).toString('base64'),
2355
- }));
2356
-
2357
- if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-04b-antes-drop.png'), fullPage: true }).catch(() => {});
2358
-
2359
- console.log(` Ejecutando drop de ${pdfPaths.length} PDFs sobre el portal...`);
2360
- const _dropped = await page.evaluate((files) => {
2361
- const dt = new DataTransfer();
2362
- for (const f of files) {
2363
- const bin = atob(f.b64);
2364
- const arr = new Uint8Array(bin.length);
2365
- for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
2366
- dt.items.add(new File([arr], f.name, { type: 'application/pdf' }));
2432
+ const fileInput = await page.$('input.gwt-FileUpload');
2433
+ if (!fileInput) throw new Error('pdfdteInternet: input.gwt-FileUpload no encontrado');
2434
+
2435
+ // ── Obtener nroRevision y subir todos los archivos vía fetch() ──────────────────
2436
+ // ── Primer upload para obtener nroRevision ─────────────────────────────────────
2437
+ // Puppeteer 22+ usa waitForFileChooser() para file uploads.
2438
+ // Triggear el input via click + page.waitForFileChooser() es el API nativo.
2439
+ // GWT procesa el onChange, somete el FormPanel a /upload con id de revisión.
2440
+ // Capturamos nroRevision desde la respuesta del servidor.
2441
+ // ────────────────────────────────────────────────────────────────────────────
2442
+
2443
+ let nroRevision = null;
2444
+
2445
+ // Listener de respuesta para capturar nroRevision del primer upload GWT
2446
+ const _onFirstUploadResp = async (resp) => {
2447
+ if (resp.url().includes('/pdfdteInternet/upload')) {
2448
+ const txt = await resp.text().catch(() => '');
2449
+ const m = txt.trim().match(/^(\d+),(\d+)$/);
2450
+ if (m && !nroRevision) {
2451
+ nroRevision = m[2];
2452
+ console.log(` ID de revisión obtenido: ${nroRevision} (1/${pdfPaths.length} subido)`);
2453
+ }
2367
2454
  }
2368
- const dz = document.querySelector('.dropFilesLabel');
2369
- if (!dz) return 0;
2370
- dz.dispatchEvent(new DragEvent('dragenter', { dataTransfer: dt, bubbles: true, cancelable: true }));
2371
- dz.dispatchEvent(new DragEvent('dragover', { dataTransfer: dt, bubbles: true, cancelable: true }));
2372
- dz.dispatchEvent(new DragEvent('drop', { dataTransfer: dt, bubbles: true, cancelable: true }));
2373
- return dt.files.length;
2374
- }, _fileDataList);
2375
-
2376
- if (_dropped === 0) throw new Error('pdfdteInternet: drop zone no encontrado (.dropFilesLabel)');
2377
- console.log(` Drop ejecutado (${_dropped} archivos). Esperando procesamiento...`);
2378
-
2379
- // ── Fase 1: esperar hasta 45s por primera señal de progreso o estado terminal ──
2380
- // Si el portal ya está en "POR REVISAR" (re-ejecución), lo detectamos aquí inmediatamente.
2381
- // Si el drop inició normalmente, "Procesados 1" aparece en pocos segundos.
2382
- await page.waitForFunction(() => {
2383
- for (const el of document.querySelectorAll('.x-progress-text')) {
2384
- const m = el.textContent.match(/Procesados\s+(\d+)/);
2385
- if (m && +m[1] > 0) return true;
2455
+ };
2456
+ page.on('response', _onFirstUploadResp);
2457
+
2458
+ console.log(` Disparando primer upload vía fileChooser para obtener ID de revisión...`);
2459
+ try {
2460
+ const _chooserPromise = page.waitForFileChooser({ timeout: 15000 });
2461
+ // Triggear el file input: click en el wrapper visible O directamente en el input
2462
+ await page.evaluate(() => {
2463
+ // a) intentar click en el botón visible (wrapper div con overflow: hidden)
2464
+ const wrapper = document.querySelector('div[style*="position: relative"][style*="overflow: hidden"] div.gwt-Label');
2465
+ if (wrapper) { wrapper.click(); return; }
2466
+ // b) click directo en el input oculto (funciona en headless Chrome)
2467
+ const inp = document.querySelector('input.gwt-FileUpload');
2468
+ if (inp) inp.click();
2469
+ });
2470
+ const _chooser = await _chooserPromise;
2471
+ await _chooser.accept([path.resolve(pdfPaths[0])]);
2472
+ console.log(` [DEBUG] fileChooser.accept → OK (${path.basename(pdfPaths[0])})`);
2473
+ } catch (e) {
2474
+ console.warn(` [!] waitForFileChooser falló (${e.message}), intentando CDP DOM.setFileInputFiles...`);
2475
+ // Fallback: CDP DOM.setFileInputFiles
2476
+ const _cdp = await page.createCDPSession();
2477
+ try {
2478
+ const { root } = await _cdp.send('DOM.getDocument', { depth: 0 });
2479
+ const { nodeId } = await _cdp.send('DOM.querySelector', {
2480
+ nodeId: root.nodeId, selector: 'input.gwt-FileUpload',
2481
+ });
2482
+ if (nodeId) {
2483
+ await _cdp.send('DOM.setFileInputFiles', { files: [path.resolve(pdfPaths[0])], nodeId });
2484
+ console.log(` [DEBUG] CDP setFileInputFiles → OK`);
2485
+ }
2486
+ } catch (cdpErr) {
2487
+ console.warn(` [!] CDP también falló: ${cdpErr.message}`);
2488
+ } finally {
2489
+ await _cdp.detach().catch(() => {});
2386
2490
  }
2387
- const t = (document.body.textContent || '').toUpperCase();
2388
- if (t.includes('APROBADO') || t.includes('POR REVISAR') || t.includes('EN REVISI') || t.includes('RECHAZADO')) return true;
2389
- return false;
2390
- }, { timeout: 45000, polling: 1000 }).catch(() => {});
2391
-
2392
- // Leer estado real tras la fase 1
2393
- const _fase1 = await page.evaluate(() => {
2394
- let procesados = 0;
2395
- for (const el of document.querySelectorAll('.x-progress-text')) {
2396
- const m = el.textContent.match(/Procesados\s+(\d+)/);
2397
- if (m) { procesados = +m[1]; break; }
2491
+ }
2492
+
2493
+ // Esperar hasta 60s para que el FormPanel iframe complete el upload
2494
+ const _t0 = Date.now();
2495
+ while (!nroRevision && Date.now() - _t0 < 60000) {
2496
+ await new Promise(r => setTimeout(r, 500));
2497
+ }
2498
+ page.off('response', _onFirstUploadResp);
2499
+
2500
+ if (!nroRevision) {
2501
+ if (debugDir) {
2502
+ await page.screenshot({ path: path.join(debugDir, 'pdfte-error-no-revid.png'), fullPage: true }).catch(() => {});
2503
+ fs.writeFileSync(path.join(debugDir, 'pdfte-error-no-revid.html'), await page.content(), 'utf8');
2398
2504
  }
2399
- const t = (document.body.textContent || '').toUpperCase();
2400
- let estado = null;
2401
- if (t.includes('APROBADO')) estado = 'APROBADO';
2402
- else if (t.includes('POR REVISAR')) estado = 'POR REVISAR';
2403
- else if (t.includes('EN REVISI')) estado = 'EN REVISIÓN';
2404
- else if (t.includes('RECHAZADO')) estado = 'RECHAZADO';
2405
- return { procesados, estado };
2406
- }).catch(() => ({ procesados: 0, estado: null }));
2407
-
2408
- if (_fase1.estado) {
2409
- console.log(` [OK] Portal en estado "${_fase1.estado}" — muestras ya procesadas previamente.`);
2410
- return { success: true, alreadyCompleted: true, estado: _fase1.estado };
2505
+ 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.');
2411
2506
  }
2412
2507
 
2413
- if (_fase1.procesados === 0) {
2414
- // Sin progreso y sin estado terminal: el portal puede no estar procesando
2415
- console.warn(' [!] Sin progreso en 45s y sin estado terminal. Continuando al paso siguiente...');
2416
- } else {
2417
- // ── Fase 2: progreso iniciado — esperar al total ──
2418
- await page.waitForFunction((total) => {
2419
- for (const el of document.querySelectorAll('.x-progress-text')) {
2420
- const m = el.textContent.match(/Procesados\s+(\d+)/);
2421
- if (m && +m[1] >= total) return true;
2508
+ if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-04b-primer-upload.png'), fullPage: true }).catch(() => {});
2509
+
2510
+ // ── Subir archivos restantes en paralelo vía fetch directo ──
2511
+ const CONCURRENCIA = 5;
2512
+ const _remaining = pdfPaths.slice(1);
2513
+ let _procesados = 1;
2514
+
2515
+ for (let i = 0; i < _remaining.length; i += CONCURRENCIA) {
2516
+ const _chunk = _remaining.slice(i, i + CONCURRENCIA);
2517
+ const _fileData = _chunk.map(p => ({
2518
+ name: path.basename(p),
2519
+ b64: fs.readFileSync(p).toString('base64'),
2520
+ }));
2521
+ const _responses = await page.evaluate(async (files, revId) => {
2522
+ return Promise.all(files.map(async ({ name, b64 }) => {
2523
+ const bytes = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
2524
+ const blob = new Blob([bytes], { type: 'application/pdf' });
2525
+ const fd = new FormData();
2526
+ fd.append('Filedata', blob, name);
2527
+ fd.append('id', String(revId));
2528
+ fd.append('ambiente', 'false');
2529
+ const resp = await fetch('/pdfdteInternet/upload', { method: 'POST', body: fd });
2530
+ const text = await resp.text();
2531
+ return { name, ok: resp.ok, status: resp.status, text };
2532
+ }));
2533
+ }, _fileData, nroRevision);
2534
+
2535
+ _procesados += _chunk.length;
2536
+ console.log(` → Procesados ${_procesados}/${pdfPaths.length}`);
2537
+ for (const r of _responses) {
2538
+ if (!r.ok || !r.text.match(/^\d+,\d+$/)) {
2539
+ console.warn(` [!] ${r.name}: HTTP ${r.status} → ${r.text.substring(0, 120)}`);
2422
2540
  }
2423
- return false;
2424
- }, { timeout: pdfPaths.length * 15000, polling: 1000 }, pdfPaths.length).catch(async () => {
2425
- const procesados = await page.evaluate(() => {
2426
- for (const el of document.querySelectorAll('.x-progress-text')) {
2427
- const m = el.textContent.match(/Procesados\s+(\d+)/);
2428
- if (m) return +m[1];
2429
- }
2430
- return 0;
2431
- }).catch(() => 0);
2432
- console.warn(` [!] Timeout: solo se procesaron ${procesados}/${pdfPaths.length} antes del timeout`);
2433
- });
2541
+ }
2434
2542
  }
2435
2543
 
2436
- // Esperar que todos los requests de leeImpresoById (validación) terminen
2544
+ console.log(` ${pdfPaths.length} PDFs subidos al portal`);
2545
+
2546
+ // Esperar que las validaciones del portal (leeImpresoById) terminen
2437
2547
  await page.waitForNetworkIdle({ timeout: 60000, idleTime: 2000 }).catch(() => {});
2438
2548
  await new Promise(r => setTimeout(r, 2000));
2439
2549
 
@@ -2452,9 +2562,53 @@ class CertRunner {
2452
2562
 
2453
2563
  if (debugDir) await page.screenshot({ path: path.join(debugDir, 'pdfte-05b-antes-enviar.png'), fullPage: true }).catch(() => {});
2454
2564
 
2565
+ // Debug: listar todos los botones visibles y sus estados
2566
+ const _btnsDebug = await page.evaluate(() =>
2567
+ Array.from(document.querySelectorAll('button.x-btn-text')).map(b => ({
2568
+ text: b.textContent.trim(),
2569
+ disabled: b.disabled,
2570
+ aria: b.getAttribute('aria-disabled'),
2571
+ visible: b.offsetParent !== null,
2572
+ }))
2573
+ ).catch(() => []);
2574
+ console.log(` [DEBUG] Botones en página: ${JSON.stringify(_btnsDebug.filter(b => b.text))}`);
2575
+
2455
2576
  console.log(' → Click "Enviar al SII"...');
2456
- const enviado = await clickBoton(page, 'Enviar al SII');
2457
- if (!enviado) throw new Error('pdfdteInternet: botón "Enviar al SII" no disponible o deshabilitado');
2577
+
2578
+ // Intentar click en el botón directo (ignorar aria-disabled en esta etapa)
2579
+ const _clickedEnviar = await page.evaluate(() => {
2580
+ // a) Buscar button.x-btn-text exacto
2581
+ for (const btn of document.querySelectorAll('button.x-btn-text')) {
2582
+ if (btn.textContent.trim() === 'Enviar al SII') {
2583
+ btn.click();
2584
+ return 'direct';
2585
+ }
2586
+ }
2587
+ // b) Abrir overflow ">" y buscar en el menú desplegable
2588
+ const overflow = document.querySelector('td.x-toolbar-overflow-region, .x-toolbar-more-icon, button[class*="overflow"]');
2589
+ if (overflow) {
2590
+ overflow.click();
2591
+ return 'overflow-click';
2592
+ }
2593
+ return false;
2594
+ });
2595
+
2596
+ if (_clickedEnviar === 'overflow-click') {
2597
+ // Esperar que aparezca el menú y hacer click en "Enviar al SII"
2598
+ await new Promise(r => setTimeout(r, 500));
2599
+ const _menuClicked = await page.evaluate(() => {
2600
+ for (const el of document.querySelectorAll('.x-menu-item-text')) {
2601
+ if (el.textContent.trim() === 'Enviar al SII') {
2602
+ el.click();
2603
+ return true;
2604
+ }
2605
+ }
2606
+ return false;
2607
+ });
2608
+ if (!_menuClicked) throw new Error('pdfdteInternet: botón "Enviar al SII" no encontrado en overflow menu');
2609
+ } else if (!_clickedEnviar) {
2610
+ throw new Error('pdfdteInternet: botón "Enviar al SII" no disponible o deshabilitado');
2611
+ }
2458
2612
 
2459
2613
  await page.waitForNetworkIdle({ timeout: 30000, idleTime: 1000 }).catch(() => {});
2460
2614
  await new Promise(r => setTimeout(r, 3000));
@@ -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.5.14",
3
+ "version": "2.5.16",
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',