@devlas/dte-sii 2.14.2 → 2.15.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.
@@ -447,7 +447,11 @@ class BoletaCert {
447
447
  // 4. Enviar EnvioBOLETA
448
448
  console.log('\nEnviando EnvioBOLETA al SII...');
449
449
  const enviador = new EnviadorSII(this.certificado, this.ambiente);
450
- const resultadoBoleta = await enviador.enviarBoletaSoap(envioBoleta);
450
+ // `let`, no `const`: la rama de duplicado más abajo lo reasigna. Con `const` eso lanzaba
451
+ // `TypeError: Assignment to constant variable` justo en el path que pretende recuperarse.
452
+ // Sin disparador confirmado: el duplicado real de EnvioBOLETA vuelve STATUS 99 (no 7), y
453
+ // los STATUS 7 observados son cvc-* de Libros, que EnviadorSII rutea a duplicado:false.
454
+ let resultadoBoleta = await enviador.enviarBoletaSoap(envioBoleta);
451
455
 
452
456
  if (!resultadoBoleta.ok) {
453
457
  if (resultadoBoleta.duplicado) {
@@ -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
@@ -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
  *
@@ -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;
@@ -3944,12 +4009,43 @@ class CertRunner {
3944
4009
  const listaParaDeclarar = estadoResp.includes('P90');
3945
4010
  const estadoMatch = estadoResp.match(/"(P\d+)"/);
3946
4011
  const listaVacia = /\/\/OK\[0,\[\]/.test(estadoResp);
3947
- const estado = listaParaDeclarar ? 'P90'
3948
- : estadoMatch ? estadoMatch[1]
3949
- : listaVacia ? 'sin_estado_sok_pendiente'
3950
- : 'desconocido';
3951
4012
 
3952
- return { success: true, inscrita: true, listaParaDeclarar, estado };
4013
+ // ⚠️ La consulta pregunta puntualmente por el estado 90, así que una lista vacía
4014
+ // significa "no está en P90" — y eso pasa en DOS situaciones opuestas:
4015
+ // a) todavía no llegó (el SII no emitió el SOK del set), o
4016
+ // b) YA lo pasó: declaró cumplimiento y avanzó a P91.
4017
+ //
4018
+ // Darlas por (a) siempre hacía que, después de una declaración exitosa, se
4019
+ // informara "esperando aprobación del SII (SOK)". La pantalla le pedía al usuario
4020
+ // esperar una etapa que ya había superado (visto el 17/08/2026, RUT 78441936-3:
4021
+ // declaró a las 14:01 con DECLARACION EFECTUADA y a las 14:5x seguía diciendo que
4022
+ // faltaba el SOK).
4023
+ //
4024
+ // Se desempata preguntando por el 91, que es el estado al que mueve
4025
+ // `autorizarEmpresaBolProd`.
4026
+ let estado = listaParaDeclarar ? 'P90'
4027
+ : estadoMatch ? estadoMatch[1]
4028
+ : listaVacia ? 'sin_estado_sok_pendiente'
4029
+ : 'desconocido';
4030
+ let yaDeclarada = false;
4031
+
4032
+ if (estado === 'sin_estado_sok_pendiente') {
4033
+ try {
4034
+ const resp91 = await gwtPost(
4035
+ `7|0|7|${CBE_BASE}|${CBE_POLICY}|${CBE_SVC}|obtenerEstadoAutorizaEmp|java.lang.Integer/3438268394|java.lang.String/2004016611|${dvUp}|1|2|3|4|4|5|6|5|6|5|${rutNum}|7|5|91|0|`
4036
+ );
4037
+ if (resp91.includes('P91')) {
4038
+ estado = 'P91';
4039
+ yaDeclarada = true;
4040
+ }
4041
+ } catch (e) {
4042
+ // Si la segunda consulta falla se conserva la lectura original: es un dato de
4043
+ // más, no una dependencia.
4044
+ console.log(` [!] No se pudo desempatar el estado con P91: ${e.message}`);
4045
+ }
4046
+ }
4047
+
4048
+ return { success: true, inscrita: true, listaParaDeclarar, estado, yaDeclarada };
3953
4049
  }
3954
4050
 
3955
4051
  async completarDeclaracionBoletaPortal({
@@ -3973,6 +4069,13 @@ class CertRunner {
3973
4069
  if (!estadoActual.inscrita) {
3974
4070
  return { success: false, error: 'No se pudo obtener representante vigente (facade CBE)' };
3975
4071
  }
4072
+ // Ya declaró y avanzó a P91: la etapa está cumplida, no pendiente. Sin este caso,
4073
+ // reintentar la declaración sobre una empresa ya declarada devolvía `pendingSok` y
4074
+ // la etapa quedaba "esperando" para siempre, esperando un SOK que ya había llegado
4075
+ // y ya se había usado.
4076
+ if (estadoActual.yaDeclarada) {
4077
+ return { success: true, yaDeclarada: true, mensaje: 'La declaración de cumplimiento ya estaba efectuada (P91)' };
4078
+ }
3976
4079
  if (!estadoActual.listaParaDeclarar) {
3977
4080
  const legible = estadoActual.estado === 'sin_estado_sok_pendiente'
3978
4081
  ? 'sin estado registrado (SOK aún no emitido)'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devlas/dte-sii",
3
- "version": "2.14.2",
3
+ "version": "2.15.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"
12
12
  },
13
13
  "repository": {
14
14
  "type": "git",