@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.
- package/CafSolicitor.js +7 -7
- package/EnviadorSII.js +1 -1
- package/FolioService.js +2 -2
- package/README.md +146 -25
- package/SiiPortalAuth.js +394 -32
- package/SiiSession.js +2 -2
- package/cert/BoletaCert.js +5 -1
- package/cert/CertRunner.js +79 -14
- package/cert/SetBase.js +1 -1
- package/cert/SetParser.js +15 -1
- package/index.js +1 -1
- package/package.json +2 -2
- package/utils/c14n.js +1 -1
- package/utils/pfx.js +1 -1
- package/test-qdetestlibro.js +0 -174
package/cert/CertRunner.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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',
|
|
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
|
-
|
|
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',
|
|
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',
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.
|
|
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
|
|
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
|
|
186
|
+
* contra Acepta, IDOK y Signapis).
|
|
187
187
|
* @param {forge.pki.Certificate} cert - Certificado
|
|
188
188
|
* @returns {string|null} RUT o null
|
|
189
189
|
*/
|
package/test-qdetestlibro.js
DELETED
|
@@ -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
|
-
});
|