@devlas/dte-sii 2.14.3 → 2.16.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.
@@ -58,6 +58,32 @@ const { STEPS, emitProgress } = require('../utils/progress');
58
58
  * @property {string} [sessionPath]
59
59
  */
60
60
 
61
+ /**
62
+ * CAF que consume cada set de certificación. **Fuente única de verdad**: la usan tanto los
63
+ * `ejecutarSet*` (como fallback cuando el set no trae `cafRequired`) como
64
+ * `precargarPlanDeCorrida()`. Antes estos números vivían inline en cada `ejecutarSet*` y los
65
+ * consumidores los duplicaban por su cuenta, así que un cambio acá los dejaba desincronizados
66
+ * y timbrando mal.
67
+ *
68
+ * El valor de `guia` es una función porque depende de cuántos casos traiga el set.
69
+ * El default de 3 sale de tráfico real: en 17 corridas registradas el set de guía vino
70
+ * siempre con `cafRequired: { 52: 3 }` y 3 casos. Antes la lib usaba 1, que timbraba de menos.
71
+ */
72
+ const CAF_POR_SET = {
73
+ basico: { 33: 4, 56: 1, 61: 3 },
74
+ guia: (setData) => ({ 52: setData?.casos?.length || 3 }),
75
+ exenta: { 34: 3, 56: 1, 61: 4 },
76
+ compra: { 46: 1, 56: 1, 61: 1 },
77
+ };
78
+
79
+ /** Nombre de la estructura y su plan de CAF, en el orden en que se ejecutan los sets. */
80
+ const SETS_DE_CORRIDA = [
81
+ ['basico', 'setBasico', CAF_POR_SET.basico],
82
+ ['guia', 'setGuiaDespacho', CAF_POR_SET.guia],
83
+ ['exenta', 'setFacturaExenta', CAF_POR_SET.exenta],
84
+ ['compra', 'setFacturaCompra', CAF_POR_SET.compra],
85
+ ];
86
+
61
87
  class CertRunner {
62
88
  /**
63
89
  * @param {CertConfig} config - Configuración del runner
@@ -270,7 +296,7 @@ class CertRunner {
270
296
  * Timbra de una sola vez TODOS los folios que la corrida va a necesitar, sumados por
271
297
  * tipo entre los cuatro sets. Llamar antes de ejecutar el primer set.
272
298
  *
273
- * El problema que resuelve (medido el 13/08/2026, RUT 78480527-1): cada set pedía sus
299
+ * El problema que resuelve (medido el 13/08/2026, RUT 79888999-0): cada set pedía sus
274
300
  * propios folios cuando le tocaba, así que del tipo 56 se pedían tres tandas separadas
275
301
  * —básico 1, exenta 2, compra 1— en la misma corrida. El SII raciona el timbraje según
276
302
  * cuántos folios tengas autorizados sin usar, así que la primera tanda le bajaba el tope
@@ -302,6 +328,45 @@ class CertRunner {
302
328
  return this._cafsPrecargados;
303
329
  }
304
330
 
331
+ /**
332
+ * Timbra por adelantado el total de folios de la corrida, armando el plan desde los sets ya
333
+ * cargados en `_estructuras`. Azúcar sobre `precargarCafsDeSets()` para que el consumidor no
334
+ * tenga que duplicar los `cafRequired` de cada set (ver `CAF_POR_SET`).
335
+ *
336
+ * Llamar después de `obtenerSets()` y antes del primer `ejecutarSet*`.
337
+ *
338
+ * ⚠️ **Hay que pasar los sets que realmente se van a ejecutar.** `_estructuras` puede traer
339
+ * los cuatro aunque la corrida ejecute uno solo, porque se rehidrata desde disco. Precargar
340
+ * todo en ese caso timbraría folios de sets que nadie va a emitir, y quedarían autorizados
341
+ * sin usar: exactamente el gatillo del racionamiento del SII que este método viene a evitar.
342
+ *
343
+ * @param {Array<'basico'|'guia'|'exenta'|'compra'>} [sets] - Sets de esta corrida.
344
+ * @returns {Promise<Object>} { 33: cafPath, 56: cafPath, ... }
345
+ */
346
+ async precargarPlanDeCorrida(sets = ['basico', 'guia', 'exenta', 'compra']) {
347
+ if (!this._estructuras) {
348
+ throw new Error('Ejecutar obtenerSets() antes de precargarPlanDeCorrida().');
349
+ }
350
+
351
+ const pedidos = new Set(sets);
352
+ const planes = [];
353
+ for (const [nombre, claveEstructura, fallback] of SETS_DE_CORRIDA) {
354
+ if (!pedidos.has(nombre)) continue;
355
+ const setData = this._estructuras[claveEstructura];
356
+ if (!setData) continue; // set opcional ausente en esta corrida
357
+ planes.push(
358
+ setData.cafRequired || (typeof fallback === 'function' ? fallback(setData) : fallback)
359
+ );
360
+ }
361
+
362
+ if (!planes.length) {
363
+ console.log('\n Nada que precargar: ninguno de los sets pedidos está presente.');
364
+ return {};
365
+ }
366
+ return this.precargarCafsDeSets(planes);
367
+ }
368
+
369
+
305
370
  /**
306
371
  * Devuelve los CAF precargados que le tocan a un set.
307
372
  *
@@ -534,7 +599,7 @@ class CertRunner {
534
599
  //
535
600
  // Con tope publicado alcanza con que el cupo CUBRA lo necesario. Antes se exigía
536
601
  // un margen de 3x para "actuar antes de quedar contra la pared", pero medido el
537
- // 13/08/2026 (RUT 78480527-1) ese margen disparaba la limpieza teniendo cupo de
602
+ // 13/08/2026 (RUT 79888999-0) ese margen disparaba la limpieza teniendo cupo de
538
603
  // sobra —tipo 61 con MAX_AUTOR=6 para 3 folios, tipo 33 con 4 para 4— y en los
539
604
  // tres tipos el resultado fue `0 anulados`: el SII rechazó cada anulación.
540
605
  //
@@ -583,7 +648,7 @@ class CertRunner {
583
648
  // bloquean el timbraje suelen ser de semanas atrás, el filtro los
584
649
  // descartaba antes de intentarlos, y la corrida se quedaba
585
650
  // reintentando para siempre contra algo que nunca iba a cambiar.
586
- // Caso real (14/08/2026, RUT 77967443-6): tipo 56 bloqueado por
651
+ // Caso real (14/08/2026, RUT 76543210-K): tipo 56 bloqueado por
587
652
  // folios del 22-07, seis intentos idénticos, cero anulados.
588
653
  //
589
654
  // Ojo: esto es el ÚLTIMO recurso. Antes de llegar acá se intenta reusar el CAF
@@ -619,7 +684,7 @@ class CertRunner {
619
684
  // para negar el timbraje: "usted tiene disponible una cantidad de folios suficiente".
620
685
  //
621
686
  // O sea que cada reintento empeoraba el bloqueo que intentaba superar. Medido el
622
- // 14/08/2026 (RUT 77967443-6): 6 reintentos de ENVIAR_SETS quemaron 66 folios
687
+ // 14/08/2026 (RUT 76543210-K): 6 reintentos de ENVIAR_SETS quemaron 66 folios
623
688
  // —tipos 33, 34, 46 y 52, seis rangos cada uno— sin emitir un solo documento,
624
689
  // mientras el tipo 56 seguía bloqueado.
625
690
  //
@@ -803,7 +868,7 @@ class CertRunner {
803
868
 
804
869
  async ejecutarSetBasico(casos) {
805
870
  emitProgress(STEPS.SET_START, { set: 'basico' });
806
- const r = await this._ejecutarSet(SetBasico, 'setBasico', 'basico', { 33: 4, 56: 1, 61: 3 }, 'basico', casos);
871
+ const r = await this._ejecutarSet(SetBasico, 'setBasico', 'basico', CAF_POR_SET.basico, 'basico', casos);
807
872
  if (r.success) emitProgress(STEPS.SET_OK, { set: 'basico', trackId: r.trackId });
808
873
  else emitProgress(STEPS.SET_ERROR, { set: 'basico', error: r.error });
809
874
  return r;
@@ -812,7 +877,7 @@ class CertRunner {
812
877
  async ejecutarSetGuia(casos) {
813
878
  emitProgress(STEPS.SET_START, { set: 'guia' });
814
879
  const r = await this._ejecutarSet(SetGuia, 'setGuiaDespacho', 'guia',
815
- (setData) => ({ 52: setData.casos?.length || 1 }), 'guia', casos);
880
+ CAF_POR_SET.guia, 'guia', casos);
816
881
  if (r.success) emitProgress(STEPS.SET_OK, { set: 'guia', trackId: r.trackId });
817
882
  else emitProgress(STEPS.SET_ERROR, { set: 'guia', error: r.error });
818
883
  return r;
@@ -820,7 +885,7 @@ class CertRunner {
820
885
 
821
886
  async ejecutarSetExenta(casos) {
822
887
  emitProgress(STEPS.SET_START, { set: 'exenta' });
823
- const r = await this._ejecutarSet(SetExenta, 'setFacturaExenta', 'exenta', { 34: 3, 56: 1, 61: 4 }, 'exenta', casos);
888
+ const r = await this._ejecutarSet(SetExenta, 'setFacturaExenta', 'exenta', CAF_POR_SET.exenta, 'exenta', casos);
824
889
  if (r.success) emitProgress(STEPS.SET_OK, { set: 'exenta', trackId: r.trackId });
825
890
  else emitProgress(STEPS.SET_ERROR, { set: 'exenta', error: r.error });
826
891
  return r;
@@ -828,7 +893,7 @@ class CertRunner {
828
893
 
829
894
  async ejecutarSetCompra(casos) {
830
895
  emitProgress(STEPS.SET_START, { set: 'compra' });
831
- const r = await this._ejecutarSet(SetCompra, 'setFacturaCompra', 'compra', { 46: 1, 56: 1, 61: 1 }, 'compra', casos);
896
+ const r = await this._ejecutarSet(SetCompra, 'setFacturaCompra', 'compra', CAF_POR_SET.compra, 'compra', casos);
832
897
  if (r.success) emitProgress(STEPS.SET_OK, { set: 'compra', trackId: r.trackId });
833
898
  else emitProgress(STEPS.SET_ERROR, { set: 'compra', error: r.error });
834
899
  return r;
@@ -2628,7 +2693,7 @@ class CertRunner {
2628
2693
  // avance es imposible — en ambos la etapa se queda quieta. La única fuente de
2629
2694
  // verdad es el estado del envío, así que se le pregunta al SII por el trackId.
2630
2695
  //
2631
- // Caso real (14/08/2026, RUT 78206276-K): la simulación quedó `RFR - Rechazado por
2696
+ // Caso real (14/08/2026, RUT 77111222-3): la simulación quedó `RFR - Rechazado por
2632
2697
  // Error en Firma` y este método devolvió "Timeout esperando aprobación". Quien lo
2633
2698
  // llamaba lo leyó como "sigue en revisión", siguió adelante y marcó la etapa como
2634
2699
  // completada sobre un envío que el SII había rechazado.
@@ -3508,7 +3573,7 @@ class CertRunner {
3508
3573
  // buscaba UNA sola frase ("El estado de la postulacion"), así que cualquier otro
3509
3574
  // rechazo pasaba de largo, no se encontraba el ID de revisión y el usuario recibía
3510
3575
  // "no se pudo extraer ID de revisión de: //OK[0,0,2,...".
3511
- // Caso real (11/08/2026): "El contribuyente 78206276-K no ha solicitado Set de
3576
+ // Caso real (11/08/2026): "El contribuyente 77111222-3 no ha solicitado Set de
3512
3577
  // pruebas." quedó completamente oculto detrás de ese mensaje.
3513
3578
  //
3514
3579
  // Ahora se reconoce cualquier frase con pinta de mensaje al usuario. Es deliberado
@@ -3909,7 +3974,7 @@ class CertRunner {
3909
3974
  * - recuperar el estado de boleta cuando las banderas locales se resetearon (ese
3910
3975
  * portal NO aparece en pe_avance, así que consultarEstadoAvance no lo cubre)
3911
3976
  *
3912
- * Verificado 2026-07-24 contra 78441936-3 con curl crudo (independiente de este
3977
+ * Verificado 2026-07-24 contra 79555666-7 con curl crudo (independiente de este
3913
3978
  * código) — misma respuesta `//OK[0,[],0,7]` cuando no hay P90 todavía.
3914
3979
  *
3915
3980
  * @returns {Promise<{success: boolean, inscrita: boolean, listaParaDeclarar: boolean, estado: string|null, error?: string}>}
@@ -3952,7 +4017,7 @@ class CertRunner {
3952
4017
  //
3953
4018
  // Darlas por (a) siempre hacía que, después de una declaración exitosa, se
3954
4019
  // informara "esperando aprobación del SII (SOK)". La pantalla le pedía al usuario
3955
- // esperar una etapa que ya había superado (visto el 17/08/2026, RUT 78441936-3:
4020
+ // esperar una etapa que ya había superado (visto el 17/08/2026, RUT 79555666-7:
3956
4021
  // declaró a las 14:01 con DECLARACION EFECTUADA y a las 14:5x seguía diciendo que
3957
4022
  // faltaba el SOK).
3958
4023
  //
@@ -4174,7 +4239,7 @@ class CertRunner {
4174
4239
  // autorizada". Es la secuencia que ya usa CafSolicitor._processMultiStepFlow() para
4175
4240
  // pedir folios de verdad, y por eso ESE camino sí funciona.
4176
4241
  //
4177
- // Caso real (17/08/2026, RUT 78441936-3): el flujo reportaba "Boleta NO autorizada"
4242
+ // Caso real (17/08/2026, RUT 79555666-7): el flujo reportaba "Boleta NO autorizada"
4178
4243
  // mientras el SII tenía a la empresa como FACTURADOR ELECTRONICO con BOLETA
4179
4244
  // ELECTRONICA autorizada hasta el folio 103009 desde el 12/07/2026, y palena le
4180
4245
  // entregaba folios reales de factura por el camino bien armado. Un comercio ya
@@ -4206,7 +4271,7 @@ class CertRunner {
4206
4271
  // veía cero tipos y concluía "no autorizada" — un dato inventado, indistinguible
4207
4272
  // de la respuesta real.
4208
4273
  //
4209
- // Visto el 17/08/2026 (RUT 78441936-3): palena devolvió 3377 bytes con
4274
+ // Visto el 17/08/2026 (RUT 79555666-7): palena devolvió 3377 bytes con
4210
4275
  // "No ha sido posible completar su solicitud... código LIBRUD-OFSF-DTE-3-1-02" y
4211
4276
  // el flujo venía reportando "certificación incompleta" desde hacía días.
4212
4277
  //
package/cert/SetBase.js CHANGED
@@ -224,7 +224,7 @@ class SetBase {
224
224
  * `cafRef` acepta una ruta (comportamiento de siempre) o una lista de rutas. La lista
225
225
  * hace falta porque el SII no siempre entrega un rango contiguo: al reobtener folios ya
226
226
  * autorizados los devuelve de a uno (folios 1-1 y 3-3 como CAF separados, visto el
227
- * 14/08/2026 en el RUT 77967443-6).
227
+ * 14/08/2026 en el RUT 76543210-K).
228
228
  *
229
229
  * Y no alcanza con concatenar numeraciones: **cada CAF trae su propia llave privada RSA**
230
230
  * (`CAF.js` → `RSASK`) y el timbre de cada documento se firma con la llave del CAF que
package/cert/SetParser.js CHANGED
@@ -1016,7 +1016,21 @@ function generarEstructuraLibroCompras(set, opts = {}) {
1016
1016
  NroDoc: doc.folio,
1017
1017
  TasaImp: tasaIva,
1018
1018
  FchDoc: new Date().toISOString().split('T')[0], // Se debe ajustar
1019
- RUTDoc: '17096073-4', // RUT por defecto para certificación
1019
+ // Contraparte de relleno del libro de COMPRAS de certificación: los documentos del set
1020
+ // son ficticios y va emparejada con `RznSoc: 'Razon Social'`, también un marcador.
1021
+ //
1022
+ // ⚠️ Este valor SÍ llega al SII: `LibroCompras.generarDesdeEstructuras()` hace
1023
+ // `{ ...doc }` y el RUTDoc entra tal cual al XML que se declara. No es código muerto.
1024
+ //
1025
+ // Se usa el RUT del propio SII porque cumple las tres cosas que hacen falta: existe como
1026
+ // contribuyente (si el SII validara existencia, pasa), es público y no es dato personal
1027
+ // de nadie, y ya se usa en este repo como contraparte (`RutReceptor` de los envíos).
1028
+ //
1029
+ // ⚠️ Antes acá había un RUT tomado de la documentación de otro sistema de facturación,
1030
+ // que resultaba ser el de una persona real. NO poner un RUT de un ejemplo ajeno, y
1031
+ // evitar `11111111-1`: tiene DV válido pero es el dummy más conocido de Chile y varios
1032
+ // sistemas lo rechazan por eso.
1033
+ RUTDoc: '60803000-K',
1020
1034
  RznSoc: 'Razon Social',
1021
1035
  };
1022
1036
 
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * Facturación y boletas electrónicas para el SII de Chile.
7
7
  *
8
8
  * @version 2.5.2
9
- * @author Devlas SpA <hola@devlas.cl>
9
+ * @author Devlas SpA <ti@devlas.cl>
10
10
  * @license MIT
11
11
  */
12
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devlas/dte-sii",
3
- "version": "2.14.3",
3
+ "version": "2.16.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",
@@ -8,7 +8,7 @@
8
8
  "author": "Devlas SpA <ti@devlas.cl> (https://devlas.cl)",
9
9
  "homepage": "https://github.com/devlas-cl/dte-sii",
10
10
  "scripts": {
11
- "test": "node test/pfx.test.js && node test/folios-consolidados.test.js && node test/c14n-apostrofe.test.js"
11
+ "test": "node test/pfx.test.js && node test/folios-consolidados.test.js && node test/c14n-apostrofe.test.js && node test/precargar-plan-corrida.test.js && node test/respaldo-mipyme.test.js"
12
12
  },
13
13
  "repository": {
14
14
  "type": "git",
package/utils/c14n.js CHANGED
@@ -51,7 +51,7 @@ function escapeText(text) {
51
51
  * por qué. Y solo pasaba cuando algún texto traía un apóstrofe, que es lo que lo hacía
52
52
  * tan difícil de ver: los mismos sets pasaban o fallaban según el contenido del set.
53
53
  *
54
- * Medido el 14/08/2026 (RUT 78206276-K, maullin). Un solo ítem, "CAPACITACION USO PLC'S
54
+ * Medido el 14/08/2026 (RUT 77111222-3, maullin). Un solo ítem, "CAPACITACION USO PLC'S
55
55
  * CNC", en un único documento del Set Factura Exenta:
56
56
  *
57
57
  * Set Básico → EPR 0 documentos con digest divergente
package/utils/pfx.js CHANGED
@@ -183,7 +183,7 @@ function extractSubjectFields(cert) {
183
183
  * 1.3.6.1.4.1.8321.1 — reservado bajo la normativa chilena de firma
184
184
  * electrónica (Ley 19.799). A diferencia de serialNumber/OU/CN, que son
185
185
  * convención de cada CA, este OID es consistente entre CAs (verificado
186
- * contra Acepta, IDOK y Signapis — ver docs/EXTRACCION_RUT_CERTIFICADO.md).
186
+ * contra Acepta, IDOK y Signapis).
187
187
  * @param {forge.pki.Certificate} cert - Certificado
188
188
  * @returns {string|null} RUT o null
189
189
  */
@@ -1,174 +0,0 @@
1
- 'use strict';
2
- /**
3
- * test-qdetestlibro.js
4
- *
5
- * Prueba los endpoints del portal SII para consultar libros electrónicos:
6
- * 1. QEstLibro — lista todos los libros del año y extrae los Códigos
7
- * 2. QDetEstLibro — detalle de cada envío (TrackId, estado, etc.)
8
- *
9
- * Uso:
10
- * node test-qdetestlibro.js [year=2026] [periodo=2026-04]
11
- *
12
- * Requiere que la sesión exista o la crea automáticamente.
13
- */
14
-
15
- const path = require('path');
16
- const fs = require('fs');
17
- const SiiCertificacion = require('./SiiCertificacion.js');
18
-
19
- // ─── Configuración ────────────────────────────────────────────────────────────
20
- const PFX_PATH = path.resolve(__dirname, '../devlas-cloud-api-node/secret/19925444-8.pfx');
21
- const PFX_PASS = 'Lsr12345';
22
- const RUT_EMPRESA = '78206276';
23
- const DV_EMPRESA = 'K';
24
- const SESSION_PATH = path.resolve(__dirname, '../devlas-cloud-api-node/debug/cert-v2/session.json');
25
-
26
- const YEAR = process.argv.find(a => a.startsWith('year='))?.split('=')[1] || '2026';
27
- const PERIODO = process.argv.find(a => a.startsWith('periodo='))?.split('=')[1] || null; // null = todos
28
-
29
- // ─── Main ─────────────────────────────────────────────────────────────────────
30
- async function main() {
31
- console.log('=== test-qdetestlibro.js ===');
32
- console.log('PFX:', PFX_PATH);
33
- console.log('RUT:', `${RUT_EMPRESA}-${DV_EMPRESA}`);
34
- console.log('Year:', YEAR, '| Filtro período:', PERIODO || '(todos)');
35
-
36
- const cert = new SiiCertificacion({
37
- pfxPath: PFX_PATH,
38
- pfxPassword: PFX_PASS,
39
- rutEmpresa: RUT_EMPRESA,
40
- dvEmpresa: DV_EMPRESA,
41
- sessionPath: SESSION_PATH,
42
- });
43
-
44
- // ── 1. Asegurar sesión portal en subsistema /cgi_dte/UPL/ ─────────────────
45
- // DTEauth?7 es la página del formulario de búsqueda de libros.
46
- // CSESSIONID es el token de sesión del portal SII. Lo necesitamos válido.
47
- console.log('\n[1] Autenticando en /cgi_dte/UPL/DTEauth?7...');
48
- // Parchar request para capturar Set-Cookie crudos de DTEauth?7
49
- const origRequest = cert.session.request.bind(cert.session);
50
- let lastRawHeaders = null;
51
- cert.session.request = async function(url, opts) {
52
- const resp = await origRequest(url, opts);
53
- if (url.includes('DTEauth')) {
54
- lastRawHeaders = resp.headers;
55
- }
56
- return resp;
57
- };
58
- const authResp = await cert.session.ensureSession('/cgi_dte/UPL/DTEauth?7');
59
- cert.session.request = origRequest; // restaurar
60
- console.log(' Status DTEauth:', authResp?.status);
61
- if (lastRawHeaders) {
62
- const sc = lastRawHeaders['set-cookie'];
63
- console.log(' Set-Cookie DTEauth?7:', JSON.stringify(sc));
64
- }
65
- console.log(' CSESSIONID en jar:', cert.session.cookieJar?.match(/CSESSIONID=[^;]+/)?.[0] || '(none)');
66
- fs.writeFileSync(path.join(__dirname, 'test-output', 'dteauth7.html'), authResp?.body || '', 'latin1');
67
-
68
- // ── TEST DIRECTO: llamar QDetEstLibro sin QEstLibro de por medio ──────────
69
- console.log('\n[TEST DIRECTO] QDetEstLibro sin pasar por QEstLibro...');
70
- const urlDetDirect = `https://maullin.sii.cl/cgi_dte/UPL/QDetEstLibro` +
71
- `?Codigo=COMPRA-772220&rutC=78206276&dvC=K&periodo=2026-04`;
72
- const directResp = await cert.session.request(urlDetDirect);
73
- console.log(' Status:', directResp.status);
74
- const directBody = directResp.body;
75
- if (directBody.includes('SESION HA EXPIRADO')) {
76
- console.warn(' [WARN] TAMBIÉN falla sin QEstLibro — es el CSESSIONID o la sesión');
77
- } else if (directBody.includes('AUTORIZADO')) {
78
- console.warn(' [WARN] Error de autorización');
79
- console.log(directBody.slice(0, 500));
80
- } else {
81
- console.log(' OK! Funciona directamente');
82
- console.log(directBody.slice(0, 1000));
83
- }
84
- fs.writeFileSync(path.join(__dirname, 'test-output', 'qdetestlibro-direct.html'), directBody, 'latin1');
85
-
86
- // ── 2. Llamar QEstLibro ────────────────────────────────────────────────────
87
- const urlLista = `https://maullin.sii.cl/cgi_dte/UPL/QEstLibro` +
88
- `?rutCompany=${RUT_EMPRESA}&dvCompany=${DV_EMPRESA}&TrackId=&year=${YEAR}&month=00&tipo=TODOS`;
89
-
90
- console.log('\n[2] GET', urlLista);
91
- const listaResp = await cert.session.request(urlLista);
92
- console.log(' Status:', listaResp.status);
93
- console.log(' Set-Cookie headers:', listaResp.headers?.['set-cookie'] || '(none)');
94
- console.log(' Cookies post-QEstLibro:', cert.session.cookieJar?.slice(0, 250) + '...');
95
-
96
- if (listaResp.body.includes('SESION HA EXPIRADO')) {
97
- console.error(' [ERR] Sesión expirada en QEstLibro — revisar ensureSession');
98
- process.exit(1);
99
- }
100
-
101
- // Guardar HTML para inspección
102
- const htmlListaPath = path.join(__dirname, 'test-output', 'qestlibro.html');
103
- fs.mkdirSync(path.dirname(htmlListaPath), { recursive: true });
104
- fs.writeFileSync(htmlListaPath, listaResp.body, 'latin1');
105
- console.log(' HTML guardado en:', htmlListaPath);
106
-
107
- // ── 3. Parsear la tabla: extraer Código → periodo → operación ──────────────
108
- // El HTML tiene links tipo: QDetEstLibro?Codigo=VENTA-772219&rutC=...&periodo=2026-04
109
- // href sin comillas: href=QDetEstLibro?Codigo=X&rutC=Y&dvC=Z&periodo=PPPP>Ver
110
- // el > cierra el atributo, por eso lo excluimos del grupo de captura
111
- const linkRegex = /QDetEstLibro\?Codigo=([^&"'\s>]+)&rutC=[^&"'\s>]+&dvC=[^&"'\s>]+&periodo=([^&"'\s>]+)/gi;
112
- const codigos = {}; // { '2026-04': { VENTA: 'VENTA-772219', COMPRA: 'COMPRA-XXXXX' } }
113
- let m;
114
- while ((m = linkRegex.exec(listaResp.body)) !== null) {
115
- const codigo = m[1];
116
- const periodo = m[2];
117
- if (PERIODO && periodo !== PERIODO) continue;
118
- codigos[periodo] = codigos[periodo] || {};
119
- const tipoMatch = /^(VENTA|COMPRA|GUIAS?)/i.exec(codigo);
120
- const tipo = tipoMatch ? tipoMatch[1].toUpperCase() : codigo;
121
- codigos[periodo][tipo] = codigo;
122
- }
123
-
124
- console.log('\n Códigos encontrados:');
125
- if (Object.keys(codigos).length === 0) {
126
- console.log(' (ninguno — revisar HTML en test-output/qestlibro.html)');
127
- }
128
- for (const [p, ops] of Object.entries(codigos)) {
129
- for (const [op, cod] of Object.entries(ops)) {
130
- console.log(` ${p} / ${op} → ${cod}`);
131
- }
132
- }
133
-
134
- // ── 4. Llamar QDetEstLibro para cada código ────────────────────────────────
135
- // Usamos las MISMAS cookies que obtuvimos de DTEauth?7 (paso 1).
136
- // NO re-autenticamos: si DTEauth?7 usa tokens one-shot, un segundo call
137
- // lo consumiría sin beneficio. Las cookies NETSCAPE_LIVEWIRE persisten.
138
- for (const [periodo, ops] of Object.entries(codigos)) {
139
- for (const [operacion, codigo] of Object.entries(ops)) {
140
- const urlDet = `https://maullin.sii.cl/cgi_dte/UPL/QDetEstLibro` +
141
- `?Codigo=${encodeURIComponent(codigo)}&rutC=${RUT_EMPRESA}&dvC=${DV_EMPRESA}&periodo=${periodo}`;
142
-
143
- const refererQEstLibro = `https://maullin.sii.cl/cgi_dte/UPL/QEstLibro` +
144
- `?rutCompany=${RUT_EMPRESA}&dvCompany=${DV_EMPRESA}&TrackId=&year=${YEAR}&month=00&tipo=TODOS`;
145
-
146
- console.log(`\n[3] QDetEstLibro ${periodo} ${operacion} (${codigo})`);
147
- console.log(' GET', urlDet);
148
-
149
- const detResp = await cert.session.request(urlDet, {
150
- headers: { Referer: refererQEstLibro },
151
- });
152
- console.log(' Status:', detResp.status);
153
-
154
- const outPath = path.join(__dirname, 'test-output', `qdetestlibro-${periodo}-${operacion}.html`);
155
- fs.writeFileSync(outPath, detResp.body, 'latin1');
156
-
157
- if (detResp.body.includes('SESION HA EXPIRADO')) {
158
- console.warn(' [WARN] Sesión expirada — revisar cookies / DTEauth');
159
- console.log('\n--- BODY (500 chars) ---\n', detResp.body.slice(0, 500));
160
- } else {
161
- console.log(' HTML guardado en:', outPath);
162
- console.log('\n--- BODY ---\n', detResp.body);
163
- }
164
- }
165
- }
166
-
167
- console.log('\n=== Fin ===');
168
- }
169
-
170
- main().catch(e => {
171
- console.error('[FATAL]', e.message);
172
- console.error(e.stack);
173
- process.exit(1);
174
- });