@devlas/dte-sii 2.5.12 → 2.5.14

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.
@@ -16,9 +16,10 @@
16
16
  */
17
17
 
18
18
  const SiiSession = require('./SiiSession.js');
19
+ const { STEPS, emitProgress } = require('./utils/progress');
19
20
 
20
21
  /**
21
- * Etapas de certificación DTE
22
+ * Etapas de certificacion DTE
22
23
  */
23
24
  const ETAPAS_CERTIFICACION = {
24
25
  SET_BASICO: 'SET_BASICO',
@@ -71,6 +72,20 @@ class SiiCertificacion {
71
72
  pfxPassword: options.pfxPassword,
72
73
  ambiente: 'certificacion',
73
74
  });
75
+
76
+ // Reutilización de sesión: cargar desde archivo y guardar automáticamente tras cada login
77
+ if (options.sessionPath) {
78
+ this._sessionPath = options.sessionPath;
79
+ this.session.loadSession(options.sessionPath);
80
+ const _origEnsure = this.session.ensureSession.bind(this.session);
81
+ const _sess = this.session;
82
+ const _sp = options.sessionPath;
83
+ this.session.ensureSession = async function(targetPath) {
84
+ const result = await _origEnsure(targetPath);
85
+ _sess.saveSession(_sp);
86
+ return result;
87
+ };
88
+ }
74
89
  }
75
90
 
76
91
  /**
@@ -378,14 +393,22 @@ class SiiCertificacion {
378
393
  // Parsear estado de cada set/libro
379
394
  const estadoSets = {};
380
395
 
381
- // Buscar filas de la tabla con formato:
382
- // <td>SET/LIBRO nombre</td><td><b>ESTADO</b></td>
383
- const rowRegex = /<tr[^>]*>[\s\S]*?<td[^>]*>[\s\S]*?(SET[^<]*|LIBRO[^<]*)<\/font><\/td>[\s\S]*?<td[^>]*>[\s\S]*?<b>([^<]+)<\/b>/gi;
384
- let rowMatch;
385
- while ((rowMatch = rowRegex.exec(html)) !== null) {
386
- const nombre = rowMatch[1].trim();
387
- const estado = rowMatch[2].trim().toUpperCase();
388
- estadoSets[nombre] = estado;
396
+ // Parsear estado de cada set/libro fila por fila para evitar falsos positivos
397
+ // (la regex global cruzaba filas y asignaba REVISADO CONFORME de SET CASO GENERAL a LIBRO DE VENTAS)
398
+ const rows = html.split(/<\/tr>/i);
399
+ for (const row of rows) {
400
+ const nameMatch = /(SET[^<\n\r]*|LIBRO[^<\n\r]*)<\/font><\/td>/i.exec(row);
401
+ if (!nameMatch) continue;
402
+ const nombre = nameMatch[1].trim();
403
+ // Estado explícito en <b>...</b>
404
+ const stateMatch = /<b>([^<]+)<\/b>/i.exec(row);
405
+ if (stateMatch) {
406
+ estadoSets[nombre] = stateMatch[1].trim().toUpperCase();
407
+ } else {
408
+ // Fila con campos input: leer valor EST oculto (S01 = sin declarar, S21 = declarado)
409
+ const estMatch = /name="EST\d+"[^>]*value="([^"]+)"/i.exec(row);
410
+ estadoSets[nombre] = estMatch ? estMatch[1] : 'S01';
411
+ }
389
412
  }
390
413
 
391
414
  // Verificar si todos los sets requeridos están REVISADO CONFORME
@@ -510,7 +533,7 @@ class SiiCertificacion {
510
533
  }
511
534
 
512
535
  if (process.env.DEBUG_SII) {
513
- console.log(' [DEBUG] Campos hidden encontrados:', Object.keys(hiddenFields).join(', '));
536
+ console.log(' [DEBUG] Campos hidden encontrados:', Object.keys(hiddenFields).join(', '));
514
537
  }
515
538
 
516
539
  // Si no hay formulario para declarar (página de estado/reparos)
@@ -541,16 +564,17 @@ class SiiCertificacion {
541
564
  // El SII cambia el orden según qué sets están aprobados
542
565
  const fieldMapping = {};
543
566
 
544
- if (process.env.DEBUG_SII) {
567
+ // Guardar pe_avance2 para debug (siempre)
568
+ {
545
569
  const fs = require('fs');
546
570
  const path = require('path');
547
- const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug');
571
+ const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug/cert-v2');
548
572
  if (!fs.existsSync(debugDir)) {
549
573
  fs.mkdirSync(debugDir, { recursive: true });
550
574
  }
551
- const debugPath = path.join(debugDir, 'pe_avance2.html');
575
+ const debugPath = path.join(debugDir, 'pe_avance2_form.html');
552
576
  fs.writeFileSync(debugPath, formHtml, 'utf8');
553
- console.log(' [DEBUG] HTML guardado en:', debugPath);
577
+
554
578
  }
555
579
 
556
580
  // Extraer todas las filas <tr>...</tr> del formulario
@@ -587,7 +611,7 @@ class SiiCertificacion {
587
611
  // Solo agregar si tiene NUM_ENV (no si tiene REVISADO CONFORME o EN REVISION)
588
612
  fieldMapping[pattern.name] = parseInt(numEnvMatch[1]);
589
613
  if (process.env.DEBUG_SII) {
590
- console.log(` [DEBUG] ${pattern.name} → NUM_ENV${numEnvMatch[1]}`);
614
+ console.log(` [DEBUG] ${pattern.name} → NUM_ENV${numEnvMatch[1]}`);
591
615
  }
592
616
  }
593
617
  break; // Ya encontramos la fila para este pattern
@@ -596,7 +620,7 @@ class SiiCertificacion {
596
620
  }
597
621
 
598
622
  if (process.env.DEBUG_SII) {
599
- console.log(' [DEBUG] Mapeo dinámico de índices:', fieldMapping);
623
+ console.log(' [DEBUG] Mapeo dinámico de índices:', fieldMapping);
600
624
  }
601
625
 
602
626
  if (sets.setSimulacion && !fieldMapping.setSimulacion) {
@@ -637,7 +661,7 @@ class SiiCertificacion {
637
661
  numEnvValues[index] = cleanTrackId;
638
662
  fecEnvValues[index] = formatDate(fecha);
639
663
  if (String(trackId) !== cleanTrackId) {
640
- console.log(` [TrackID] Normalizado: ${trackId} → ${cleanTrackId}`);
664
+
641
665
  }
642
666
  }
643
667
  }
@@ -668,16 +692,16 @@ class SiiCertificacion {
668
692
 
669
693
  // Debug: mostrar datos del formulario
670
694
  if (process.env.DEBUG_SII) {
671
- console.log(' [DEBUG] Datos del formulario a enviar:');
695
+ console.log(' [DEBUG] Datos del formulario a enviar:');
672
696
  for (const [k, v] of Object.entries(formData)) {
673
- console.log(` ${k}: ${v || '(vacío)'}`);
697
+ console.log(` ${k}: ${v || '(vacío)'}`);
674
698
  }
675
699
 
676
700
  // Mostrar el body codificado
677
701
  const SiiSession = require('./SiiSession');
678
702
  const encodedBody = SiiSession.formEncode(formData);
679
- console.log(' [DEBUG] Body codificado (primeros 500 chars):');
680
- console.log(` ${encodedBody.substring(0, 500)}`);
703
+ console.log(' [DEBUG] Body codificado (primeros 500 chars):');
704
+ console.log(` ${encodedBody.substring(0, 500)}`);
681
705
  }
682
706
 
683
707
  // 5. Enviar formulario de declaración
@@ -697,7 +721,7 @@ class SiiCertificacion {
697
721
 
698
722
  // Debug - guardar respuesta
699
723
  if (process.env.DEBUG_SII) {
700
- console.log(` [DEBUG] Respuesta: ${body.length} bytes, status: ${declareResponse.status}`);
724
+ console.log(` [DEBUG] Respuesta: ${body.length} bytes, status: ${declareResponse.status}`);
701
725
  // Guardar respuesta de pe_avance3 para debug
702
726
  const fs = require('fs');
703
727
  const path = require('path');
@@ -707,7 +731,7 @@ class SiiCertificacion {
707
731
  }
708
732
  const debugPath = path.join(debugDir, 'pe_avance3_response.html');
709
733
  fs.writeFileSync(debugPath, body);
710
- console.log(` [DEBUG] Respuesta guardada en: ${debugPath}`);
734
+ console.log(` [DEBUG] Respuesta guardada en: ${debugPath}`);
711
735
  }
712
736
 
713
737
  const bodyLower = body.toLowerCase();
@@ -737,13 +761,117 @@ class SiiCertificacion {
737
761
  // Éxito si el form se envió sin errores de sesión/contenido.
738
762
  // El estado del envío (ERRORES O REPAROS / EN REVISION / REVISADO CONFORME)
739
763
  // NO determina el éxito de la declaración — eso se resuelve vía polling.
740
- const success = (!hasError || hasSuccess) && !sesionExpirada && !contenidoNoCorresponde;
764
+ const postOk = (!hasError || hasSuccess) && !sesionExpirada && !contenidoNoCorresponde;
765
+
766
+ if (!postOk) {
767
+ return {
768
+ success: false,
769
+ error: errorMsg || undefined,
770
+ status: declareResponse.status,
771
+ rawHtml: body,
772
+ formHtml,
773
+ setsDeclarados: Object.keys(sets),
774
+ formDataSent: formData,
775
+ };
776
+ }
777
+
778
+ // Guardar HTML de pe_avance3 (respuesta de declaración) siempre para debug
779
+ {
780
+ const fs = require('fs');
781
+ const path = require('path');
782
+ const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug/cert-v2');
783
+ if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
784
+ fs.writeFileSync(path.join(debugDir, 'pe_avance3_response.html'), body, 'utf8');
785
+
786
+ }
787
+
788
+ // 7. VERIFICACIÓN POST-DECLARACIÓN: Re-leer pe_avance2 y confirmar que los TrackIDs se guardaron
789
+ // Si aparece "EN REVISION" → la declaración fue aceptada y el SII está procesando
790
+ // Si reaparecen inputs vacíos → la declaración no se guardó
791
+ let verificado = true;
792
+ let verificacionError = '';
793
+ let enRevision = false;
794
+ let camposVacios = [];
795
+ try {
796
+ const verifyResponse = await this.session.submitForm(
797
+ '/cvc_cgi/dte/pe_avance2',
798
+ { RUT_EMP: this.rutEmpresa, DV_EMP: this.dvEmpresa, ACEPTAR: 'Continuar' },
799
+ 'https://maullin.sii.cl/cvc_cgi/dte/pe_avance1'
800
+ );
801
+ const verifyHtml = verifyResponse.body || '';
802
+
803
+ // Guardar HTML de verificación pe_avance2 siempre
804
+ {
805
+ const fs = require('fs');
806
+ const path = require('path');
807
+ const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug/cert-v2');
808
+ if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
809
+ fs.writeFileSync(path.join(debugDir, 'pe_avance2_verify.html'), verifyHtml, 'utf8');
810
+
811
+ }
812
+
813
+ // Para cada set que declaramos, verificar estado
814
+ const verifyRows = verifyHtml.split(/<\/tr>/i);
815
+ const camposEnRevision = [];
816
+ for (const [setName, index] of Object.entries(fieldMapping)) {
817
+ if (!sets[setName]) continue;
818
+ for (const row of verifyRows) {
819
+ const numEnvMatch = row.match(new RegExp(`NAME="NUM_ENV${index}"`, 'i'));
820
+ if (!numEnvMatch) {
821
+ // Si no hay input NUM_ENV, verificar si aparece REVISADO CONFORME o EN REVISION
822
+ const labelPattern = patterns.find(p => p.name === setName);
823
+ if (labelPattern && labelPattern.label.test(row)) {
824
+ if (/<b>[^<]*REVISADO CONFORME[^<]*<\/b>/i.test(row)) break;
825
+ if (/EN REVISION/i.test(row)) {
826
+ camposEnRevision.push(setName);
827
+ break;
828
+ }
829
+ }
830
+ continue;
831
+ }
832
+ // Tiene input — verificar si tiene valor o está REVISADO CONFORME
833
+ const tieneConforme = /<b>[^<]*REVISADO CONFORME[^<]*<\/b>/i.test(row);
834
+ if (tieneConforme) break;
835
+ if (/EN REVISION/i.test(row)) {
836
+ camposEnRevision.push(setName);
837
+ break;
838
+ }
839
+ const valueMatch = row.match(new RegExp(`NAME="NUM_ENV${index}"[^>]*value="([^"]*)"`, 'i'));
840
+ const tieneValor = valueMatch && valueMatch[1] && valueMatch[1].trim() !== '';
841
+ if (!tieneValor) {
842
+ camposVacios.push(setName);
843
+ }
844
+ break;
845
+ }
846
+ }
847
+
848
+ if (camposEnRevision.length > 0) {
849
+ enRevision = true;
850
+ console.log(` [...] EN REVISION: ${camposEnRevision.join(', ')} — declaración aceptada, SII procesando`);
851
+ }
852
+
853
+ if (camposVacios.length > 0 && !enRevision) {
854
+ verificado = false;
855
+ verificacionError = `Declaración NO se guardó en el portal SII. Campos vacíos para: ${camposVacios.join(', ')}. Posible error de sesión o TrackID no reconocido.`;
856
+ console.log(` [!] ${verificacionError}`);
857
+ }
858
+ } catch (verifyErr) {
859
+ console.log(` [!] No se pudo verificar la declaración: ${verifyErr.message}`);
860
+ }
861
+
862
+ // allRejected: SII rechazó todos los sets declarados — período incorrecto, no tiene sentido reintentar
863
+ const _declaredCount = Object.keys(fieldMapping).filter(k => sets[k]).length;
864
+ const allRejected = !enRevision && camposVacios.length > 0 && camposVacios.length >= _declaredCount && _declaredCount > 0;
741
865
 
742
866
  return {
743
- success,
744
- error: errorMsg || undefined,
867
+ success: verificado || enRevision,
868
+ error: verificacionError || errorMsg || undefined,
869
+ verificado,
870
+ enRevision,
871
+ allRejected,
745
872
  status: declareResponse.status,
746
873
  rawHtml: body,
874
+ formHtml,
747
875
  setsDeclarados: Object.keys(sets),
748
876
  formDataSent: formData,
749
877
  };
@@ -798,7 +926,7 @@ class SiiCertificacion {
798
926
  }
799
927
  const debugPath = path.join(debugDir, 'pe_avance2_avanzar.html');
800
928
  fs.writeFileSync(debugPath, formHtml, 'utf8');
801
- console.log(' [DEBUG] HTML avanzar guardado en:', debugPath);
929
+ console.log(' [DEBUG] HTML avanzar guardado en:', debugPath);
802
930
  }
803
931
 
804
932
  let avanceResponse = await this.session.submitForm(
@@ -824,7 +952,7 @@ class SiiCertificacion {
824
952
  }
825
953
  const debugPath = path.join(debugDir, 'pe_avance3_avanzar_response.html');
826
954
  fs.writeFileSync(debugPath, body);
827
- console.log(' [DEBUG] Respuesta avanzar guardada en:', debugPath);
955
+ console.log(' [DEBUG] Respuesta avanzar guardada en:', debugPath);
828
956
  }
829
957
 
830
958
  const hasError = body.toLowerCase().includes('error') && !body.includes('Error de Sesión');
@@ -1161,6 +1289,7 @@ class SiiCertificacion {
1161
1289
  if (onProgress) {
1162
1290
  onProgress({ intento, maxIntentos, estado: 'polling' });
1163
1291
  }
1292
+ emitProgress(STEPS.POLLING, { intento, max: maxIntentos, label: 'sets' });
1164
1293
 
1165
1294
  const result = await this.verAvanceParsed();
1166
1295
  if (!result.success) continue;
@@ -1180,10 +1309,12 @@ class SiiCertificacion {
1180
1309
  }
1181
1310
 
1182
1311
  if (todosConformes) {
1312
+ emitProgress(STEPS.SETS_APPROVED);
1183
1313
  return { success: true, estados: estadosRelevantes };
1184
1314
  }
1185
1315
 
1186
1316
  if (algunoRechazado) {
1317
+ emitProgress(STEPS.SETS_REJECTED);
1187
1318
  return { success: false, error: 'Sets rechazados', estados: estadosRelevantes };
1188
1319
  }
1189
1320
  }
package/SiiPortalAuth.js CHANGED
@@ -173,10 +173,10 @@ class SiiPortalAuth {
173
173
  if (cached) {
174
174
  const valida = await this._validarSesion(cached);
175
175
  if (valida) {
176
- console.log('[SiiPortalAuth] ♻️ Reutilizando sesión SII cacheada');
176
+ console.log('[SiiPortalAuth] Reutilizando sesión SII cacheada');
177
177
  return cached;
178
178
  }
179
- console.log('[SiiPortalAuth] ⚠️ Sesión cacheada expirada, re-autenticando...');
179
+ console.warn('[SiiPortalAuth] Sesión cacheada expirada, re-autenticando...');
180
180
  }
181
181
 
182
182
  // ── 2. Nueva autenticación ────────────────────────────────────────────────
@@ -187,6 +187,7 @@ class SiiPortalAuth {
187
187
  { cookieJar }
188
188
  );
189
189
 
190
+ console.log('[SiiPortalAuth] Autenticando con certificado en herculesr.sii.cl...');
190
191
  const r2 = await this._request(
191
192
  `https://herculesr.sii.cl/cgi_AUT2000/CAutInicio.cgi?${TARGET}`,
192
193
  {
@@ -196,6 +197,7 @@ class SiiPortalAuth {
196
197
  usarCert: true,
197
198
  }
198
199
  );
200
+ console.log(`[SiiPortalAuth] Respuesta herculesr: status=${r2.status}`);
199
201
 
200
202
  // Verificar mensaje de límite de sesiones
201
203
  if (r2.body.includes('m\u00e1ximo de sesiones') || r2.body.includes('maximo de sesiones') ||
@@ -222,19 +224,25 @@ class SiiPortalAuth {
222
224
  */
223
225
  async _validarSesion(cookieJar) {
224
226
  try {
227
+ console.log('[SiiPortalAuth] Validando sesión cacheada en SII...');
225
228
  const res = await this._request(
226
229
  'https://maullin.sii.cl/cvc_cgi/dte/ad_empresa1',
227
230
  { cookieJar: { ...cookieJar } } // copia para no contaminar
228
231
  );
229
232
  // Si redirige al login SII → expirada
230
233
  const loc = res.headers['location'] || '';
231
- if (loc.includes('InicioAutenticacion') || loc.includes('IngresoRutClave')) return false;
234
+ if (loc.includes('InicioAutenticacion') || loc.includes('IngresoRutClave')) {
235
+ console.warn('[SiiPortalAuth] Validación: SII redirigió al login → sesión expirada');
236
+ return false;
237
+ }
232
238
  // Si el body contiene formulario de empresa → válida
233
239
  const valida = res.status === 200 && (res.body.includes('RUT_EMP') || res.body.includes('ad_empresa'));
240
+ console.log(`[SiiPortalAuth] Validación: status=${res.status} → sesión ${valida ? 'VÁLIDA ✓' : 'INVÁLIDA ✗'}`);
234
241
  // Refrescar timestamp del caché para extender TTL mientras la sesión se usa activamente
235
242
  if (valida) SiiPortalAuth._guardarSesionCache(this._certHash, cookieJar);
236
243
  return valida;
237
- } catch {
244
+ } catch (err) {
245
+ console.warn('[SiiPortalAuth] Validación: error de red →', err.message);
238
246
  return false;
239
247
  }
240
248
  }
@@ -242,13 +250,25 @@ class SiiPortalAuth {
242
250
  /** Lee sesión cacheada del disco para el cert dado. @private */
243
251
  static _cargarSesionCache(certHash) {
244
252
  try {
245
- if (!fs.existsSync(SESSION_CACHE_PATH)) return null;
253
+ if (!fs.existsSync(SESSION_CACHE_PATH)) {
254
+ console.log('[SiiPortalAuth] Cache: archivo no existe →', SESSION_CACHE_PATH);
255
+ return null;
256
+ }
246
257
  const data = JSON.parse(fs.readFileSync(SESSION_CACHE_PATH, 'utf8'));
247
- if (data.certHash !== certHash) return null;
258
+ if (data.certHash !== certHash) {
259
+ console.log(`[SiiPortalAuth] Cache: cert no coincide (guardado=${data.certHash} actual=${certHash})`);
260
+ return null;
261
+ }
262
+ const edadMin = Math.round((Date.now() - data.ts) / 60000);
248
263
  // TTL: 90 minutos (SII permite ~2h de inactividad; se refresca en cada validación)
249
- if (Date.now() - data.ts > 90 * 60 * 1000) return null;
264
+ if (Date.now() - data.ts > 90 * 60 * 1000) {
265
+ console.log(`[SiiPortalAuth] Cache: sesión expirada (edad=${edadMin} min, TTL=90 min)`);
266
+ return null;
267
+ }
268
+ console.log(`[SiiPortalAuth] Cache: sesión encontrada`);
250
269
  return data.cookies;
251
- } catch {
270
+ } catch (err) {
271
+ console.warn('[SiiPortalAuth] Cache: error leyendo caché →', err.message);
252
272
  return null;
253
273
  }
254
274
  }
@@ -262,7 +282,11 @@ class SiiPortalAuth {
262
282
  ts: Date.now(),
263
283
  cookies: cookieJar,
264
284
  }), 'utf8');
265
- } catch { /* no crítico */ }
285
+ const cookieKeys = Object.keys(cookieJar);
286
+ console.log(`[SiiPortalAuth] Cache: sesión guardada`);
287
+ } catch (err) {
288
+ console.warn('[SiiPortalAuth] Cache: error guardando caché →', err.message);
289
+ }
266
290
  }
267
291
 
268
292
  /** Borra la sesión cacheada (útil para forzar re-login). */
package/SiiSession.js CHANGED
@@ -279,7 +279,7 @@ class SiiSession {
279
279
  // Detectar bloqueo por demasiadas sesiones
280
280
  if (response.body && response.body.includes('superado el m')) {
281
281
  const errorMsg = 'SII: Demasiadas sesiones abiertas. Espera ~30 min o cierra sesión manualmente en el portal SII.';
282
- console.error(`\n ${errorMsg}\n`);
282
+ console.error(`\n[ERR] ${errorMsg}\n`);
283
283
  throw new Error(errorMsg);
284
284
  }
285
285
 
@@ -292,7 +292,7 @@ class SiiSession {
292
292
  // Verificar si el login resultó en bloqueo por sesiones
293
293
  if (response.body && response.body.includes('superado el m')) {
294
294
  const errorMsg = 'SII: Demasiadas sesiones abiertas. Espera ~30 min o cierra sesión manualmente en el portal SII.';
295
- console.error(`\n ${errorMsg}\n`);
295
+ console.error(`\n[ERR] ${errorMsg}\n`);
296
296
  throw new Error(errorMsg);
297
297
  }
298
298
  }
@@ -386,10 +386,10 @@ class SiiSession {
386
386
  }
387
387
 
388
388
  this.cookieJar = data.cookieJar || '';
389
- console.log('Sesión SII cargada desde archivo');
389
+ console.log('[SessionSii] Sesión SII cargada desde archivo');
390
390
  return true;
391
391
  } catch (err) {
392
- console.log('Error cargando sesión SII:', err.message);
392
+ console.log('[SessionSii] Error cargando sesión SII:', err.message);
393
393
  return false;
394
394
  }
395
395
  }