@devlas/dte-sii 2.12.26 → 2.13.1

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.
@@ -17,6 +17,7 @@
17
17
 
18
18
  const SiiSession = require('./SiiSession.js');
19
19
  const { STEPS, emitProgress } = require('./utils/progress');
20
+ const { resolveArtifactDir } = require('./utils/paths');
20
21
 
21
22
  /** Decodifica entidades HTML latinas (el portal SII las usa en vez de UTF-8 crudo) y limpia
22
23
  * tags/whitespace, para poder aplicar regex de texto sobre las respuestas de forma confiable. */
@@ -83,11 +84,18 @@ class SiiCertificacion {
83
84
 
84
85
  this.rutEmpresa = options.rutEmpresa.replace(/\./g, '');
85
86
  this.dvEmpresa = options.dvEmpresa.toUpperCase();
86
-
87
+
88
+ // Un solo lugar donde se decide dónde escribir. Antes había 7 call sites que
89
+ // recalculaban la ruta a mano, y no coincidían entre sí: cuatro usaban
90
+ // `../../debug/cert-v2` y tres `../../debug`, así que archivos de la misma
91
+ // operación quedaban repartidos en dos carpetas distintas.
92
+ this.debugDir = resolveArtifactDir(options.debugDir, options.debugDir ? '' : 'cert-v2');
93
+
87
94
  this.session = new SiiSession({
88
95
  pfxPath: options.pfxPath,
89
96
  pfxPassword: options.pfxPassword,
90
97
  ambiente: 'certificacion',
98
+ debugDir: this.debugDir,
91
99
  });
92
100
 
93
101
  // Reutilización de sesión: cargar desde archivo y guardar automáticamente tras cada login
@@ -632,7 +640,7 @@ class SiiCertificacion {
632
640
  {
633
641
  const fs = require('fs');
634
642
  const path = require('path');
635
- const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug/cert-v2');
643
+ const debugDir = this.debugDir;
636
644
  if (!fs.existsSync(debugDir)) {
637
645
  fs.mkdirSync(debugDir, { recursive: true });
638
646
  }
@@ -789,7 +797,7 @@ class SiiCertificacion {
789
797
  // Guardar respuesta de pe_avance3 para debug
790
798
  const fs = require('fs');
791
799
  const path = require('path');
792
- const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug');
800
+ const debugDir = this.debugDir;
793
801
  if (!fs.existsSync(debugDir)) {
794
802
  fs.mkdirSync(debugDir, { recursive: true });
795
803
  }
@@ -822,6 +830,38 @@ class SiiCertificacion {
822
830
  errorMsg = 'Respuesta inválida al declarar avance';
823
831
  }
824
832
 
833
+ // ── Datos inconsistentes: error DEFINITIVO, no "todavía no" ────────────
834
+ //
835
+ // El SII contesta 200 con una página normal que dice, entre los antecedentes del
836
+ // set, "FECHA NO CORRESPONDE AL ENVIO". No es un error de sesión ni de contenido,
837
+ // así que caía en el camino de éxito: la verificación posterior encontraba los
838
+ // campos vacíos, el bucle reintentaba 10 veces y después el polling seguía hasta
839
+ // agotar el timeout de la etapa. Minutos quemados esperando algo imposible, y sin
840
+ // que el mensaje real del SII llegara nunca al usuario.
841
+ //
842
+ // Esta ruta la comparten declarar avance, libros y simulación, así que detectarlo
843
+ // acá los cubre a los tres.
844
+ //
845
+ // La causa habitual es la zona horaria: los runners arman las fechas con
846
+ // `new Date().getDate()`, que usa la TZ del proceso. Corriendo en UTC, entre las
847
+ // 20:00 y las 00:00 de Chile ya es el día siguiente y se declara con la fecha de
848
+ // mañana. Por eso `TZ=America/Santiago` es obligatorio (ver CLAUDE.md).
849
+ const _inconsistencia = body.replace(/<[^>]*>/g, ' ')
850
+ .match(/((?:FECHA|RUT|FOLIO|NUMERO|N[UÚ]MERO)[^.<]{0,40}?NO\s+(?:CORRESPONDE|COINCIDE)[^.<]{0,40})/i);
851
+ if (_inconsistencia) {
852
+ const _detalle = _inconsistencia[1].replace(/\s+/g, ' ').trim();
853
+ return {
854
+ success: false,
855
+ datoInconsistente: true,
856
+ error: `El SII rechazó la declaración: ${_detalle}. Los datos declarados no coinciden con lo que el SII tiene registrado del envío; corrígelos y vuelve a declarar (esperar no lo resuelve).`,
857
+ status: declareResponse.status,
858
+ rawHtml: body,
859
+ formHtml,
860
+ setsDeclarados: Object.keys(sets),
861
+ formDataSent: formData,
862
+ };
863
+ }
864
+
825
865
  // Éxito si el form se envió sin errores de sesión/contenido.
826
866
  // El estado del envío (ERRORES O REPAROS / EN REVISION / REVISADO CONFORME)
827
867
  // NO determina el éxito de la declaración — eso se resuelve vía polling.
@@ -843,7 +883,7 @@ class SiiCertificacion {
843
883
  {
844
884
  const fs = require('fs');
845
885
  const path = require('path');
846
- const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug/cert-v2');
886
+ const debugDir = this.debugDir;
847
887
  if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
848
888
  fs.writeFileSync(path.join(debugDir, 'pe_avance3_response.html'), body, 'utf8');
849
889
 
@@ -868,7 +908,7 @@ class SiiCertificacion {
868
908
  {
869
909
  const fs = require('fs');
870
910
  const path = require('path');
871
- const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug/cert-v2');
911
+ const debugDir = this.debugDir;
872
912
  if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
873
913
  fs.writeFileSync(path.join(debugDir, 'pe_avance2_verify.html'), verifyHtml, 'utf8');
874
914
 
@@ -1027,7 +1067,7 @@ class SiiCertificacion {
1027
1067
  if (process.env.DEBUG_SII) {
1028
1068
  const fs = require('fs');
1029
1069
  const path = require('path');
1030
- const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug');
1070
+ const debugDir = this.debugDir;
1031
1071
  if (!fs.existsSync(debugDir)) {
1032
1072
  fs.mkdirSync(debugDir, { recursive: true });
1033
1073
  }
@@ -1053,7 +1093,7 @@ class SiiCertificacion {
1053
1093
  if (process.env.DEBUG_SII) {
1054
1094
  const fs = require('fs');
1055
1095
  const path = require('path');
1056
- const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug');
1096
+ const debugDir = this.debugDir;
1057
1097
  if (!fs.existsSync(debugDir)) {
1058
1098
  fs.mkdirSync(debugDir, { recursive: true });
1059
1099
  }
@@ -1201,7 +1241,7 @@ class SiiCertificacion {
1201
1241
  try {
1202
1242
  const fs = require('fs');
1203
1243
  const path = require('path');
1204
- const debugDir = process.env.SII_DEBUG_DIR || path.join(__dirname, '../../debug/cert-v2');
1244
+ const debugDir = this.debugDir;
1205
1245
  if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
1206
1246
  fs.writeFileSync(path.join(debugDir, filename), body || '', 'utf8');
1207
1247
  } catch (_e) { /* debug best-effort, no bloquear el flujo real */ }
@@ -1475,6 +1515,13 @@ class SiiCertificacion {
1475
1515
  esReparos: upper.includes('REPAROS'),
1476
1516
  porRealizar: upper.includes('POR REALIZAR'),
1477
1517
  esAnulado: upper.includes('ANULADO'),
1518
+ // Estados que NO se resuelven esperando: el dato declarado no cuadra con lo
1519
+ // que el SII tiene registrado, así que hay que corregirlo y volver a declarar.
1520
+ // Sin esta categoría caían en el limbo (ni conforme, ni en revisión, ni
1521
+ // rechazado) y el polling seguía hasta agotar el timeout de la etapa.
1522
+ // Caso real: "FECHA NO CORRESPONDE AL ENVIO" cuando el proceso corre en UTC
1523
+ // y declara con la fecha del día siguiente (ver TZ en docker-compose.yml).
1524
+ datoInconsistente: /NO CORRESPONDE|NO COINCIDE|FECHA INVALIDA/i.test(upper),
1478
1525
  };
1479
1526
  }
1480
1527
  }
@@ -1540,7 +1587,9 @@ class SiiCertificacion {
1540
1587
  : result.estados;
1541
1588
 
1542
1589
  const todosConformes = Object.values(estadosRelevantes).every(e => e.esConforme);
1543
- const algunoRechazado = Object.values(estadosRelevantes).some(e => e.esRechazado);
1590
+ // `datoInconsistente` cuenta como rechazo: seguir esperando no lo arregla.
1591
+ const algunoRechazado = Object.values(estadosRelevantes)
1592
+ .some(e => e.esRechazado || e.datoInconsistente);
1544
1593
 
1545
1594
  if (onProgress) {
1546
1595
  onProgress({ intento, maxIntentos, estado: 'resultado', estados: estadosRelevantes });
@@ -1559,7 +1608,7 @@ class SiiCertificacion {
1559
1608
  // sin decir cuál set ni con qué estado. Se nombran solo los rechazados
1560
1609
  // para no ahogar el dato entre los que sí pasaron.
1561
1610
  const detalle = Object.values(estadosRelevantes)
1562
- .filter(e => e.esRechazado)
1611
+ .filter(e => e.esRechazado || e.datoInconsistente)
1563
1612
  .map(e => `${e.nombre}: ${e.estado}`)
1564
1613
  .join('; ');
1565
1614
  return {
package/SiiPortalAuth.js CHANGED
@@ -26,11 +26,12 @@ const https = require('https');
26
26
  const http = require('http');
27
27
  const fs = require('fs');
28
28
  const path = require('path');
29
- const os = require('os');
30
29
  const { URL } = require('url');
31
30
  const forge = require('node-forge');
32
31
  const crypto = require('crypto');
33
32
  const SiiSessionStore = require('./SiiSessionStore');
33
+ const { resolveDataDir } = require('./utils/paths');
34
+ const { registrarHttpDebug } = require('./utils/httpDebug');
34
35
 
35
36
  function _cookieObjToStr(obj) {
36
37
  return Object.entries(obj).map(([k, v]) => `${k}=${v}`).join('; ');
@@ -57,10 +58,12 @@ const SII_TLS_OPTS = {
57
58
  // ─── Ruta del caché de sesión ─────────────────────────────────────────────────
58
59
  // Guarda las cookies NETSCAPE_LIVEWIRE.* para reusar entre ejecuciones y evitar
59
60
  // el error "máximo de sesiones autenticadas" del SII.
60
- const SESSION_CACHE_PATH = path.join(
61
- process.env.DATADIR || path.join(os.homedir(), 'AppData', 'Roaming', 'POS'),
62
- 'sii_session_cache.json'
63
- );
61
+ //
62
+ // El directorio lo define el consumidor vía DATADIR; el fallback es os.tmpdir(),
63
+ // neutral respecto del sistema operativo. Antes caía a `~/AppData/Roaming/POS`,
64
+ // una convención de un producto Windows específico hardcodeada en una librería
65
+ // genérica de DTE — en Linux creaba esa ruta igual, porque Node no la valida.
66
+ const SESSION_CACHE_PATH = path.join(resolveDataDir(), 'sii_session_cache.json');
64
67
 
65
68
  /**
66
69
  * Registro global de instancias SiiPortalAuth por certHash (singleton por certificado).
@@ -196,6 +199,7 @@ class SiiPortalAuth {
196
199
 
197
200
  _request(urlStr, { method = 'GET', cookieJar = {}, body = null, headers = {}, usarCert = false } = {}) {
198
201
  return new Promise((resolve, reject) => {
202
+ const _t0 = Date.now();
199
203
  const url = new URL(urlStr);
200
204
  const isHttps = url.protocol === 'https:';
201
205
 
@@ -248,7 +252,23 @@ class SiiPortalAuth {
248
252
  const encoding = /charset=utf-?8/i.test(ct)
249
253
  ? 'utf8'
250
254
  : (isSiiHost || /iso-8859|latin-1|windows-1252/i.test(ct)) ? 'latin1' : 'utf8';
251
- resolve({ status: res.statusCode, headers: res.headers, body: buf.toString(encoding), cookieJar });
255
+ const bodyStr = buf.toString(encoding);
256
+
257
+ // Cliente HTTP independiente de SiiSession (https nativo, sin código en común):
258
+ // necesita su propio hook o quedarían fuera las llamadas del flujo de
259
+ // verificación de autorización, que hoy no dejan ningún rastro.
260
+ registrarHttpDebug({
261
+ url: urlStr,
262
+ method,
263
+ status: res.statusCode,
264
+ headers: res.headers,
265
+ body: bodyStr,
266
+ reqBody: body,
267
+ ms: Date.now() - _t0,
268
+ cliente: 'SiiPortalAuth',
269
+ });
270
+
271
+ resolve({ status: res.statusCode, headers: res.headers, body: bodyStr, cookieJar });
252
272
  });
253
273
  });
254
274
 
@@ -1036,6 +1056,111 @@ if (!fs.existsSync(SESSION_CACHE_PATH)) {
1036
1056
  SiiSessionStore.delete(hash);
1037
1057
  }
1038
1058
  }
1059
+
1060
+ /**
1061
+ * Autentica en el portal SII y devuelve los datos frescos del emisor
1062
+ * (resolución + datos del contribuyente) ya normalizados, junto con el
1063
+ * cookieJar de la sesión iniciada para reutilizarla.
1064
+ *
1065
+ * Es una operación completa del portal —autenticar, consultar dos endpoints,
1066
+ * reintentar con sesión fresca si la cacheada quedó inservible y normalizar el
1067
+ * resultado—, así que vive acá y no en el consumidor: no tiene ningún
1068
+ * conocimiento del proyecto que la use.
1069
+ *
1070
+ * @param {Object} opts
1071
+ * @param {Buffer} opts.pfxBuffer - Contenido del .pfx
1072
+ * @param {string} [opts.pfxPassword] - Contraseña del .pfx
1073
+ * @param {string} [opts.rutEmpresa] - RUT de la empresa (acepta puntos). Si no se
1074
+ * pasa, se intenta deducir de las cookies del portal tras autenticar.
1075
+ * @param {Function} [opts.onAviso] - Callback para avisos no fatales (default: console.warn).
1076
+ * @returns {Promise<{ emisor: Object, cookieJar: Object }>}
1077
+ * @throws Si no puede autenticar, si el RUT es inválido o si faltan datos de resolución.
1078
+ */
1079
+ static async obtenerEmisor({ pfxBuffer, pfxPassword = '', rutEmpresa = '', onAviso } = {}) {
1080
+ const aviso = onAviso || ((msg) => console.warn(msg));
1081
+ const auth = new SiiPortalAuth({ pfxBuffer, pfxPassword });
1082
+
1083
+ // Se resuelve antes de autenticar para poder reintentar con sesión fresca.
1084
+ let rutNum, dv;
1085
+ if (rutEmpresa) {
1086
+ const rutLimpio = String(rutEmpresa).replace(/\./g, '');
1087
+ const match = rutLimpio.match(/^(\d+)-([0-9Kk])$/);
1088
+ if (!match) {
1089
+ throw new Error(`RUT empresa inválido: "${rutEmpresa}". Formato esperado: "78206276-K"`);
1090
+ }
1091
+ rutNum = match[1];
1092
+ dv = match[2].toUpperCase();
1093
+ }
1094
+
1095
+ let cookieJar = await auth.autenticar();
1096
+
1097
+ const rutDesdeCookies = (jar) => [
1098
+ jar['NETSCAPE_LIVEWIRE.rutm'] || jar['NETSCAPE_LIVEWIRE.rut'] || null,
1099
+ jar['NETSCAPE_LIVEWIRE.dvm'] || jar['NETSCAPE_LIVEWIRE.dv'] || null,
1100
+ ];
1101
+
1102
+ if (!rutNum) {
1103
+ [rutNum, dv] = rutDesdeCookies(cookieJar);
1104
+ if (!rutNum || !dv) {
1105
+ throw new Error(
1106
+ 'No se pudo determinar el RUT empresa desde las cookies del portal SII. ' +
1107
+ 'Indica el RUT explícitamente en opts.rutEmpresa.',
1108
+ );
1109
+ }
1110
+ }
1111
+
1112
+ // Retry con sesión fresca: a veces la sesión cacheada pasa la validación básica
1113
+ // pero ad_empresa2 devuelve otra página (ej. "Actualización de Datos del
1114
+ // Contribuyente"). Secuencial a propósito: compartir el cookieJar en paralelo
1115
+ // genera una race condition — ce_consulta_muestra_e puede sobreescribir la cookie
1116
+ // Tivoli (TS0xxxxxxx) que ad_empresa1 dejó, y ad_empresa2 responde vacío.
1117
+ let datosResol, datosContrib;
1118
+ for (let intento = 1; intento <= 2; intento++) {
1119
+ try {
1120
+ datosResol = await auth.obtenerDatosEmpresa(rutNum, dv, cookieJar);
1121
+ datosContrib = await auth.obtenerDatosContribuyente(rutNum, dv, cookieJar).catch((e) => {
1122
+ aviso(`[SiiPortalAuth] obtenerDatosContribuyente falló (no crítico): ${e.message}`);
1123
+ return null;
1124
+ });
1125
+ break;
1126
+ } catch (e) {
1127
+ if (intento === 1) {
1128
+ aviso(`[SiiPortalAuth] Sesión cacheada inválida (${e.message}). Re-autenticando...`);
1129
+ SiiPortalAuth.limpiarSesionCache();
1130
+ cookieJar = await auth.autenticar();
1131
+ if (!rutEmpresa) {
1132
+ const [r, d] = rutDesdeCookies(cookieJar);
1133
+ rutNum = r || rutNum;
1134
+ dv = d || dv;
1135
+ }
1136
+ continue;
1137
+ }
1138
+ throw e;
1139
+ }
1140
+ }
1141
+
1142
+ if (!datosResol?.fch_resol) {
1143
+ throw new Error(
1144
+ `No se encontraron datos de resolución para ${rutNum}-${dv} en el portal SII.\n` +
1145
+ 'Verifica que la empresa esté habilitada para emitir DTE.',
1146
+ );
1147
+ }
1148
+
1149
+ // fch_resol ya viene normalizada a YYYY-MM-DD desde obtenerDatosEmpresa
1150
+ const emisor = {
1151
+ rut: `${rutNum}-${dv}`,
1152
+ razon_social: datosContrib?.razonSocial || datosResol.razonSocial || '',
1153
+ giro: datosContrib?.actividades?.[0]?.descripcion || datosContrib?.glosa || '',
1154
+ acteco: datosContrib?.acteco || datosContrib?.actividades?.[0]?.codigo || '',
1155
+ direccion: datosContrib?.direccion || '',
1156
+ comuna: datosContrib?.comuna || '',
1157
+ ciudad: datosContrib?.ciudad || datosContrib?.comuna || '',
1158
+ fch_resol: datosResol.fch_resol,
1159
+ nro_resol: datosResol.nro_resol ?? 0,
1160
+ };
1161
+
1162
+ return { emisor, cookieJar };
1163
+ }
1039
1164
  }
1040
1165
 
1041
1166
  module.exports = SiiPortalAuth;
package/SiiSession.js CHANGED
@@ -20,6 +20,8 @@ const {
20
20
  getHost,
21
21
  createScopedLogger,
22
22
  } = require('./utils');
23
+ const { resolveDebugDir, saveDebugFile } = require('./utils/paths');
24
+ const { registrarHttpDebug } = require('./utils/httpDebug');
23
25
 
24
26
  const log = createScopedLogger('SiiSession');
25
27
 
@@ -34,6 +36,8 @@ class SiiSession {
34
36
  * @param {string} [options.pfxPath] - Ruta al archivo PFX
35
37
  * @param {string} [options.pfxPassword] - Contraseña del PFX
36
38
  * @param {Object} [options.certificado] - Instancia de Certificado
39
+ * @param {string} [options.debugDir] - Dónde escribir HTML de diagnóstico. Si no se
40
+ * provee cae a `SII_DEBUG_DIR`; si tampoco existe, no se escribe nada.
37
41
  */
38
42
  constructor(options = {}) {
39
43
  // Validar parámetros obligatorios usando validador centralizado
@@ -41,9 +45,10 @@ class SiiSession {
41
45
  throw new Error('SiiSession: options.ambiente es obligatorio');
42
46
  }
43
47
  this.ambiente = validateAmbiente(options.ambiente);
44
-
48
+
45
49
  // Usar host centralizado desde endpoints
46
50
  this.baseHost = getHost(this.ambiente);
51
+ this.debugDir = options.debugDir || null;
47
52
  this.cookieJar = '';
48
53
  this.tlsOptions = null;
49
54
  // Agent nativo con el certificado + flags TLS legacy del SII. got no reenvía
@@ -204,6 +209,7 @@ class SiiSession {
204
209
  * @private
205
210
  */
206
211
  async _doRequest(url, options, isPost) {
212
+ const _t0 = Date.now();
207
213
  const res = await got(url, {
208
214
  method: options.method || 'GET',
209
215
  headers: {
@@ -257,6 +263,21 @@ class SiiSession {
257
263
  bodyStr = buffer.toString('utf8');
258
264
  }
259
265
 
266
+ // Punto único por el que pasa TODO el tráfico de SiiSession: cada salto de
267
+ // redirect, cada submit de formulario y cada reintento. Enganchar acá cubre a
268
+ // SiiCertificacion, CertRunner, CafSolicitor, FolioService, BoletaCert y
269
+ // SetsProvider sin tocar ninguno.
270
+ registrarHttpDebug({
271
+ url,
272
+ method: options.method || 'GET',
273
+ status: res.statusCode,
274
+ headers: res.headers,
275
+ body: bodyStr,
276
+ reqBody: options.body,
277
+ ms: Date.now() - _t0,
278
+ cliente: 'SiiSession',
279
+ });
280
+
260
281
  return {
261
282
  status: res.statusCode,
262
283
  headers: res.headers,
@@ -352,14 +373,11 @@ class SiiSession {
352
373
  async _tryForceCloseSessions(body) {
353
374
  if (!body || !body.includes('superado el m')) return false;
354
375
 
355
- // Guardar HTML para diagnóstico
356
- try {
357
- const fs = require('fs');
358
- const path = require('path');
359
- const dbgDir = path.resolve(__dirname, '..', 'devlas-cloud-api-node', 'debug', 'sii-sessions');
360
- fs.mkdirSync(dbgDir, { recursive: true });
361
- fs.writeFileSync(path.join(dbgDir, `demasiadas-sesiones-${Date.now()}.html`), body, 'utf-8');
362
- } catch (_) {}
376
+ // Guardar HTML para diagnóstico — solo si el consumidor definió dónde.
377
+ // Antes esto apuntaba a `../devlas-cloud-api-node/debug/sii-sessions`: la librería
378
+ // nombraba a un repo consumidor y asumía que estaba como carpeta hermana, así que
379
+ // en cualquier otra instalación escribía en un lugar inesperado o fallaba en silencio.
380
+ saveDebugFile(resolveDebugDir(this.debugDir), `demasiadas-sesiones-${Date.now()}.html`, body);
363
381
 
364
382
  // El SII a veces incluye un form o link para cerrar sesiones anteriores
365
383
  // Buscamos: action con "CierraAnt", "cerrar", "logout" o "Anular"
package/WsReclamo.js CHANGED
@@ -15,6 +15,10 @@
15
15
  */
16
16
 
17
17
  const forge = require('node-forge');
18
+ const { fetchRegistrado } = require('./utils/httpDebug');
19
+
20
+ /** `fetchRegistrado` con el cliente ya fijado, para no repetirlo en cada llamada. */
21
+ const fetchRegistradoWs = (url, opciones) => fetchRegistrado(url, opciones, 'WsReclamo');
18
22
  const {
19
23
  SOAP_ENDPOINTS,
20
24
  WSRECLAMO_ENDPOINTS,
@@ -82,14 +86,13 @@ class WsReclamo {
82
86
  <soapenv:Body><getSeed/></soapenv:Body>
83
87
  </soapenv:Envelope>`;
84
88
 
85
- const res = await fetch(this._seedUrl, {
89
+ const { response: res, text: xml } = await fetchRegistradoWs(this._seedUrl, {
86
90
  method: 'POST',
87
91
  headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: '' },
88
92
  body: envelope,
89
93
  });
90
94
  if (!res.ok) throw siiError(`Error semilla: ${res.status}`, ERROR_CODES.SII_CONNECTION_FAILED);
91
95
 
92
- const xml = await res.text();
93
96
  const semilla = extractTagContent(decodeXmlEntities(xml), 'SEMILLA');
94
97
  if (!semilla) throw siiError('No se obtuvo semilla del SII', ERROR_CODES.SII_INVALID_RESPONSE);
95
98
  return semilla;
@@ -114,14 +117,13 @@ class WsReclamo {
114
117
  </soapenv:Body>
115
118
  </soapenv:Envelope>`;
116
119
 
117
- const res = await fetch(this._tokenUrl, {
120
+ const { response: res, text: xml } = await fetchRegistradoWs(this._tokenUrl, {
118
121
  method: 'POST',
119
122
  headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: '' },
120
123
  body: envelope,
121
124
  });
122
125
  if (!res.ok) throw siiError(`Error token: ${res.status}`, ERROR_CODES.SII_CONNECTION_FAILED);
123
126
 
124
- const xml = await res.text();
125
127
  const decoded = xml.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
126
128
  const token = extractTagContent(decoded, 'TOKEN');
127
129
  if (!token) throw siiError('No se obtuvo TOKEN del SII', ERROR_CODES.SII_AUTH_FAILED);
@@ -247,7 +249,7 @@ class WsReclamo {
247
249
  </soapenv:Body>
248
250
  </soapenv:Envelope>`;
249
251
 
250
- const res = await fetch(this._wsUrl, {
252
+ const { response: res, text: xml } = await fetchRegistradoWs(this._wsUrl, {
251
253
  method: 'POST',
252
254
  headers: {
253
255
  'Content-Type': 'text/xml; charset=utf-8',
@@ -268,7 +270,6 @@ class WsReclamo {
268
270
  throw siiError(`WSRECLAMO ${metodo}: HTTP ${res.status}`, ERROR_CODES.SII_CONNECTION_FAILED);
269
271
  }
270
272
 
271
- const xml = await res.text();
272
273
  return xml;
273
274
  }
274
275
 
@@ -22,6 +22,7 @@
22
22
 
23
23
  const path = require('path');
24
24
  const fs = require('fs');
25
+ const { registrarHttpDebug } = require('../utils/httpDebug');
25
26
 
26
27
  class BoletaCert {
27
28
  /**
@@ -663,14 +664,23 @@ class BoletaCert {
663
664
 
664
665
  // Hacer request
665
666
  const requestWithCert = (options, payload) => new Promise((resolve, reject) => {
667
+ const t0 = Date.now();
666
668
  const req = https.request({ ...options, ...tlsOptions }, (res) => {
667
669
  let data = '';
668
670
  res.on('data', (chunk) => (data += chunk));
669
- res.on('end', () => resolve({
670
- status: res.statusCode,
671
- text: data,
672
- headers: res.headers,
673
- }));
671
+ res.on('end', () => {
672
+ registrarHttpDebug({
673
+ url: `https://${options.hostname}${options.path}`,
674
+ method: options.method || 'GET',
675
+ status: res.statusCode,
676
+ headers: res.headers,
677
+ body: data,
678
+ reqBody: payload,
679
+ ms: Date.now() - t0,
680
+ cliente: 'BoletaCert',
681
+ });
682
+ resolve({ status: res.statusCode, text: data, headers: res.headers });
683
+ });
674
684
  });
675
685
  req.on('error', reject);
676
686
  if (payload) req.write(payload);
@@ -784,14 +794,23 @@ class BoletaCert {
784
794
 
785
795
  // Hacer request
786
796
  const requestWithCert = (options, payload) => new Promise((resolve, reject) => {
797
+ const t0 = Date.now();
787
798
  const req = https.request({ ...options, ...tlsOptions }, (res) => {
788
799
  let data = '';
789
800
  res.on('data', (chunk) => (data += chunk));
790
- res.on('end', () => resolve({
791
- status: res.statusCode,
792
- text: data,
793
- headers: res.headers,
794
- }));
801
+ res.on('end', () => {
802
+ registrarHttpDebug({
803
+ url: `https://${options.hostname}${options.path}`,
804
+ method: options.method || 'GET',
805
+ status: res.statusCode,
806
+ headers: res.headers,
807
+ body: data,
808
+ reqBody: payload,
809
+ ms: Date.now() - t0,
810
+ cliente: 'BoletaCert',
811
+ });
812
+ resolve({ status: res.statusCode, text: data, headers: res.headers });
813
+ });
795
814
  });
796
815
  req.on('error', reject);
797
816
  if (payload) req.write(payload);