@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.
package/cert/SetParser.js CHANGED
@@ -124,12 +124,12 @@ function mapearTipoDocLibro(tipo) {
124
124
  if (t.includes('FACTURA EXENTA ELECTRONICA') || (t.includes('NO AFECTA') && t.includes('ELECTRONICA'))) return 34;
125
125
  if (t.includes('FACTURA ELECTRONICA')) return 33;
126
126
  if (t.includes('FACTURA EXENTA') || t.includes('FACTURA NO AFECTA')) return 30; // exenta papel
127
- if (t === 'FACTURA') return 30;
127
+ if (t === 'FACTURA') return 30; // factura afecta papel → TpoDoc=30 (regular libro); exentos libro usa lógica especial
128
128
  if (t.includes('NOTA DE CREDITO') && (t.includes('ELECTRONICA') || t.includes('ELECTRONICO'))) return 61;
129
129
  if (t.includes('NOTA DE CREDITO')) return 60;
130
130
  if (t.includes('NOTA DE DEBITO') && (t.includes('ELECTRONICA') || t.includes('ELECTRONICO'))) return 56;
131
131
  if (t.includes('NOTA DE DEBITO')) return 55;
132
- return 30;
132
+ return null;
133
133
  }
134
134
 
135
135
  // ═══════════════════════════════════════════════════════════════
@@ -991,14 +991,26 @@ function generarEstructuraSetExportacion(set) {
991
991
  /**
992
992
  * Genera estructura para LIBRO DE COMPRAS (IECV)
993
993
  */
994
- function generarEstructuraLibroCompras(set) {
994
+ function generarEstructuraLibroCompras(set, opts = {}) {
995
+ const esExentos = opts.esExentos === true;
995
996
  const detalle = [];
996
997
  const resumen = {};
997
998
 
998
999
  for (const doc of set.documentosLibro) {
999
- const tipoDoc = mapearTipoDocLibro(doc.tipoDocumento);
1000
+ let tipoDoc = mapearTipoDocLibro(doc.tipoDocumento);
1000
1001
  const tasaIva = 0.19;
1001
1002
 
1003
+ // En libro de compras EXENTOS, los TpoDoc correctos son:
1004
+ // FACTURA (papel afecta) → 30 (con IVANoRec)
1005
+ // FACTURA EXENTA (papel) → 32 (Fac. Venta B&S No Afectos/Exentos, papel)
1006
+ // FACTURA ELECTRONICA → 33 (con IVANoRec si tiene montoAfecto)
1007
+ // FACTURA EXENTA ELECTRONICA → 34 (puramente exenta)
1008
+ // NC/NCE/ND/NDE → 60/61/55/56
1009
+ // Total: 7 tipos distintos = 7 líneas de resumen
1010
+ const esFacturaExentaPapel = esExentos && tipoDoc === 30
1011
+ && !doc.montoAfecto && !!doc.montoExento;
1012
+ if (esFacturaExentaPapel) tipoDoc = 32;
1013
+
1002
1014
  const detalleDoc = {
1003
1015
  TpoDoc: tipoDoc,
1004
1016
  NroDoc: doc.folio,
@@ -1035,16 +1047,56 @@ function generarEstructuraLibroCompras(set) {
1035
1047
  detalleDoc.IVARetTotal = Math.round((doc.montoAfecto || 0) * tasaIva);
1036
1048
  }
1037
1049
 
1038
- // Referencia para notas de crédito
1039
- if (tipoDoc === 60 && doc.observacion) {
1040
- const matchRef = doc.observacion.match(/FACTURA(?:\s+ELECTRONICA)?\s+(\d+)/i);
1041
- if (matchRef) {
1042
- detalleDoc.TpoDocRef = doc.observacion.includes('ELECTRONICA') ? 33 : 30;
1043
- detalleDoc.FolioDocRef = parseInt(matchRef[1]);
1050
+ // Referencia para notas de crédito/débito (TpoDoc 60/61/55/56)
1051
+ const esNota = (tipoDoc === 60 || tipoDoc === 61 || tipoDoc === 55 || tipoDoc === 56);
1052
+ if (esNota && doc.observacion) {
1053
+ // El folio referenciado es siempre el último número de la observación
1054
+ const matchFolio = doc.observacion.match(/(\d+)\s*$/i);
1055
+ if (matchFolio) {
1056
+ detalleDoc.FolioDocRef = parseInt(matchFolio[1]);
1057
+ if (doc.observacion.match(/FACTURA EXENTA ELECTRONICA/i)) {
1058
+ detalleDoc.TpoDocRef = 34;
1059
+ } else if (doc.observacion.match(/FACTURA EXENTA/i)) {
1060
+ detalleDoc.TpoDocRef = esExentos ? 32 : 30;
1061
+ } else if (doc.observacion.match(/FACTURA.*ELECTRONICA/i)) {
1062
+ detalleDoc.TpoDocRef = 33;
1063
+ } else if (doc.observacion.match(/FACTURA/i)) {
1064
+ detalleDoc.TpoDocRef = 30;
1065
+ }
1066
+ // Sin TpoDocRef si no se menciona FACTURA (ej. referencia a otra NC/ND)
1044
1067
  }
1045
1068
  }
1046
1069
 
1047
- detalleDoc.MntTotal = (detalleDoc.MntNeto || 0) + (detalleDoc.MntIVA || 0) + (detalleDoc.MntExe || 0);
1070
+ // En libro de compras EXENTOS: lógica por tipo de doc
1071
+ // Docs con montoAfecto → IVANoRec (IVA no recuperable)
1072
+ // Docs puramente exentos (TpoDoc=32/34) → sin IVANoRec
1073
+ // TpoDoc=46 → usa IVARetTotal (no IVANoRec)
1074
+ const esPuramenteExento = !doc.montoAfecto && !!doc.montoExento;
1075
+ if (esExentos && tipoDoc !== 46 && !esPuramenteExento) {
1076
+ const mntIVA = detalleDoc.MntIVA || 0;
1077
+ detalleDoc.IVANoRec = { CodIVANoRec: 1, MntIVANoRec: mntIVA };
1078
+ detalleDoc.MntIVA = 0;
1079
+ if (detalleDoc.MntNeto === undefined || detalleDoc.MntNeto === null) {
1080
+ detalleDoc.MntNeto = 0;
1081
+ }
1082
+ }
1083
+ // TpoDoc=32 (Fac. Exenta papel): solo MntExe + MntTotal, sin TasaImp/MntNeto/MntIVA/IVANoRec
1084
+ if (esFacturaExentaPapel) {
1085
+ delete detalleDoc.TasaImp;
1086
+ delete detalleDoc.MntNeto;
1087
+ delete detalleDoc.MntIVA;
1088
+ delete detalleDoc.IVANoRec;
1089
+ }
1090
+ // Para docs puramente exentos que NO son TpoDoc=32: TasaImp=19 + MntNeto=0 + MntIVA=0
1091
+ if (esExentos && esPuramenteExento && !esFacturaExentaPapel) {
1092
+ detalleDoc.TasaImp = Math.round(tasaIva * 100); // TasaImp=19 requerido
1093
+ detalleDoc.MntNeto = 0;
1094
+ detalleDoc.MntIVA = 0;
1095
+ }
1096
+
1097
+ // MntTotal incluye IVANoRec (IVA pagado pero no deducible)
1098
+ const mntIvaNoRecTotal = (esExentos && detalleDoc.IVANoRec) ? (detalleDoc.IVANoRec.MntIVANoRec || 0) : 0;
1099
+ detalleDoc.MntTotal = (detalleDoc.MntNeto || 0) + (detalleDoc.MntIVA || 0) + (detalleDoc.MntExe || 0) + mntIvaNoRecTotal;
1048
1100
  if (doc.retencionTotal && tipoDoc === 46) {
1049
1101
  detalleDoc.MntTotal = detalleDoc.MntNeto || 0; // Sin IVA pagado
1050
1102
  }
@@ -1069,6 +1121,36 @@ function generarEstructuraLibroCompras(set) {
1069
1121
  resumen[tipoDoc].TotMntTotal += detalleDoc.MntTotal || 0;
1070
1122
  }
1071
1123
 
1124
+ // Post-proceso para libro de compras EXENTOS:
1125
+ // NC/NCE con sin montos declarados que anulan un doc con montos deben heredar esos montos
1126
+ if (esExentos) {
1127
+ for (const doc of detalle) {
1128
+ const esNotaAnulacion = (doc.TpoDoc === 60 || doc.TpoDoc === 61 || doc.TpoDoc === 55 || doc.TpoDoc === 56)
1129
+ && !doc.MntNeto && !doc.MntExe && doc.FolioDocRef;
1130
+ if (esNotaAnulacion) {
1131
+ const refDoc = detalle.find(d => d.NroDoc === doc.FolioDocRef);
1132
+ if (refDoc) {
1133
+ if (refDoc.MntExe) doc.MntExe = refDoc.MntExe;
1134
+ if (refDoc.MntNeto) {
1135
+ doc.MntNeto = refDoc.MntNeto;
1136
+ // Heredar IVANoRec del doc referenciado
1137
+ if (refDoc.IVANoRec) {
1138
+ doc.IVANoRec = { CodIVANoRec: 1, MntIVANoRec: refDoc.IVANoRec.MntIVANoRec || 0 };
1139
+ }
1140
+ }
1141
+ const mntIvaNoRecDoc = (doc.IVANoRec ? (doc.IVANoRec.MntIVANoRec || 0) : 0);
1142
+ doc.MntTotal = (doc.MntNeto || 0) + (doc.MntExe || 0) + mntIvaNoRecDoc;
1143
+ // Actualizar resumen para este TpoDoc
1144
+ if (resumen[doc.TpoDoc]) {
1145
+ resumen[doc.TpoDoc].TotMntExe += doc.MntExe || 0;
1146
+ resumen[doc.TpoDoc].TotMntNeto += doc.MntNeto || 0;
1147
+ resumen[doc.TpoDoc].TotMntTotal += doc.MntTotal;
1148
+ }
1149
+ }
1150
+ }
1151
+ }
1152
+ }
1153
+
1072
1154
  return {
1073
1155
  numeroAtencion: set.numeroAtencion,
1074
1156
  factorProporcionalidad: set.factorProporcionalidad || 0.6,
@@ -1136,8 +1218,9 @@ function generarEstructurasParaScripts(datosExtraidos, receptorConfig = {}) {
1136
1218
  estructuras.libroCompras = generarEstructuraLibroCompras(set);
1137
1219
  break;
1138
1220
  case 'LIBRO_COMPRAS_EXENTOS':
1139
- // Misma estructura que LIBRO_COMPRAS pero marcado como exentos
1140
- estructuras.libroComprasExentos = generarEstructuraLibroCompras(set);
1221
+ // En el libro de exentos, FACTURA afecta papel usa TpoDoc=29 (Factura de Inicio)
1222
+ // para distinguirse de FACTURA EXENTA (TpoDoc=30) en el ResumenPeriodo
1223
+ estructuras.libroComprasExentos = generarEstructuraLibroCompras(set, { esExentos: true });
1141
1224
  break;
1142
1225
  case 'LIBRO_VENTAS':
1143
1226
  // El libro de ventas se genera con los datos del set básico o exento
@@ -105,13 +105,13 @@ class SetsProvider {
105
105
  * @returns {Promise<boolean>}
106
106
  */
107
107
  async initSession() {
108
- this.logger.log('[SetsProvider] 🔐 Inicializando sesión SII...');
108
+ this.logger.log('[SessionSii] Inicializando sesión SII...');
109
109
 
110
110
  const siiCert = this._getSiiCert();
111
111
 
112
112
  // Intentar cargar sesión existente
113
113
  if (this.sessionPath && SiiCertificacion.isSessionValid(this.sessionPath)) {
114
- this.logger.log('[SetsProvider] ✓ Sesión existente válida');
114
+ this.logger.log('[SessionSii] ✓ Sesión existente válida');
115
115
  const loaded = siiCert.loadSession(this.sessionPath);
116
116
  if (loaded) {
117
117
  return true;
@@ -119,7 +119,7 @@ class SetsProvider {
119
119
  }
120
120
 
121
121
  // Crear nueva sesión
122
- this.logger.log('[SetsProvider] Estableciendo nueva sesión...');
122
+ this.logger.log('[SessionSii] Estableciendo nueva sesión...');
123
123
  try {
124
124
  // verAvance() fuerza el login
125
125
  await siiCert.verAvance();
@@ -128,12 +128,12 @@ class SetsProvider {
128
128
  if (this.sessionPath) {
129
129
  this._ensureDir(path.dirname(this.sessionPath));
130
130
  siiCert.saveSession(this.sessionPath);
131
- this.logger.log('[SetsProvider] ✓ Sesión guardada');
131
+ this.logger.log('[SessionSii] ✓ Sesión guardada');
132
132
  }
133
133
 
134
134
  return true;
135
135
  } catch (error) {
136
- this.logger.error(`[SetsProvider] Error: ${error.message}`);
136
+ this.logger.error(`[SessionSii] [ERR] Error: ${error.message}`);
137
137
  return false;
138
138
  }
139
139
  }
@@ -153,11 +153,11 @@ class SetsProvider {
153
153
  setsOpcionales = DEFAULT_SETS_OPCIONALES,
154
154
  } = options;
155
155
 
156
- this.logger.log('\n[SetsProvider] 📦 Obteniendo sets de prueba...');
156
+ this.logger.log('\n[SessionSii] Obteniendo sets de prueba...');
157
157
 
158
158
  // Si hay cache y no se fuerza refresh, retornar cache
159
159
  if (!forceRefresh && this._estructurasCache) {
160
- this.logger.log('[SetsProvider] ✓ Usando cache de estructuras');
160
+ this.logger.log('[SetsProvider] ✓ Usando cache de estructuras');
161
161
  return {
162
162
  success: true,
163
163
  regenerated: false,
@@ -212,7 +212,7 @@ class SetsProvider {
212
212
  // Detectar si el set fue regenerado
213
213
  const regenerated = this._detectRegeneration(result);
214
214
  if (regenerated) {
215
- this.logger.log('[SetsProvider] ⚠️ Set regenerado - reiniciar proceso');
215
+ this.logger.log('[SetsProvider] [!] Set regenerado - reiniciar proceso');
216
216
  // Invalidar cualquier cache anterior
217
217
  this.invalidateCache();
218
218
  }
@@ -228,13 +228,13 @@ class SetsProvider {
228
228
  (htmlLower.includes('postulacion factura electronica') && !htmlLower.includes('caso'))) {
229
229
  // Distinguir: ¿es "no inscrito" o expiró la sesión?
230
230
  if (htmlLower.includes('no inscrito') || (htmlLower.includes('no est') && htmlLower.includes('inscrito'))) {
231
- this.logger.log('[SetsProvider] ℹ️ pe_generar2: empresa no inscrita en Postulación (fase avanzada).');
231
+ this.logger.log('[SessionSii] pe_generar2: empresa no inscrita en Postulación (fase avanzada).');
232
232
  return { success: false, error: 'El contribuyente no está inscrito en Postulación en el portal SII.' };
233
233
  }
234
234
  // Sesión expiró en el portal aunque el archivo no lo sabía → forzar re-autenticación
235
235
  // Cookies vencidas server-side: limpiar sesión guardada y pedir a
236
236
  // generarSetPruebas que re-autentique por sí mismo via pe_generar
237
- this.logger.log('[SetsProvider] ⚠️ Sesión rechazada por portal, limpiando y reintentando...');
237
+ this.logger.log('[SessionSii] [!] Sesión rechazada por portal, limpiando y reintentando...');
238
238
  if (this.sessionPath) {
239
239
  const SiiSession = require('../SiiSession');
240
240
  SiiSession.clearSession(this.sessionPath);
@@ -284,7 +284,7 @@ class SetsProvider {
284
284
  }
285
285
  }
286
286
 
287
- this.logger.log('[SetsProvider] ✓ Sets obtenidos correctamente');
287
+ this.logger.log('[SetsProvider] ✓ Sets obtenidos correctamente');
288
288
 
289
289
  return {
290
290
  success: true,
@@ -296,7 +296,7 @@ class SetsProvider {
296
296
  };
297
297
 
298
298
  } catch (error) {
299
- this.logger.error(`[SetsProvider] Error: ${error.message}`);
299
+ this.logger.error(`[SetsProvider] [ERR] Error: ${error.message}`);
300
300
  return {
301
301
  success: false,
302
302
  error: error.message,
@@ -329,7 +329,7 @@ class SetsProvider {
329
329
  if (result.setDescargado?.rawHtml && this._lastNumeroAtencion) {
330
330
  const match = result.setDescargado.rawHtml.match(/NUMERO DE ATENCI[^:]*:\s*(\d+)/i);
331
331
  if (match && match[1] !== this._lastNumeroAtencion) {
332
- this.logger.log(`[SetsProvider] Número de atención cambió: ${this._lastNumeroAtencion} → ${match[1]}`);
332
+ this.logger.log(`[SetsProvider] Número de atención cambió: ${this._lastNumeroAtencion} → ${match[1]}`);
333
333
  return true;
334
334
  }
335
335
  }
@@ -351,19 +351,16 @@ class SetsProvider {
351
351
  if (this.debugDir) {
352
352
  this._saveDebug('set-texto.txt', textoLimpio);
353
353
  }
354
-
355
- // Usar SetParser del core
356
- this.logger.log('[SetsProvider] Usando SetParser del core...');
357
354
 
358
355
  const datosExtraidos = SetParser.extraerCasosDelSet(textoLimpio);
359
356
 
360
357
  if (datosExtraidos.sets.length === 0) {
361
- this.logger.log('[SetsProvider] ⚠️ No se encontraron sets en el contenido');
358
+ this.logger.log('[SetsProvider] [!] No se encontraron sets en el contenido');
362
359
  return { estructuras: null, datosExtraidos: null };
363
360
  }
364
361
 
365
- this.logger.log(`[SetsProvider] 📦 Sets encontrados: ${datosExtraidos.sets.length}`);
366
- this.logger.log(`[SetsProvider] 📄 Total casos: ${datosExtraidos.totalCasos}`);
362
+ this.logger.log(`[SetsProvider] Sets encontrados: ${datosExtraidos.sets.length}`);
363
+ this.logger.log(`[SetsProvider] Total casos: ${datosExtraidos.totalCasos}`);
367
364
 
368
365
  // Guardar número de atención para detectar regeneración
369
366
  if (datosExtraidos.sets[0]?.numeroAtencion) {
@@ -53,7 +53,7 @@ class Simulacion {
53
53
  const docRefs = {};
54
54
 
55
55
  if (plan.length < 10) {
56
- console.warn('⚠️ Se recomienda mínimo 10 documentos para simulación.');
56
+ console.warn('[!] Se recomienda mínimo 10 documentos para simulación.');
57
57
  }
58
58
 
59
59
  const envioDte = new EnvioDTE({ certificado: this.certificado });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devlas/dte-sii",
3
- "version": "2.5.12",
3
+ "version": "2.5.14",
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",
@@ -31,6 +31,7 @@
31
31
  "dependencies": {
32
32
  "@xmldom/xmldom": "^0.8.11",
33
33
  "bwip-js": "^4.8.0",
34
+ "dotenv": "^17.3.1",
34
35
  "fast-xml-parser": "^5.3.3",
35
36
  "got": "^11.8.6",
36
37
  "node-forge": "^1.3.3",
@@ -0,0 +1,78 @@
1
+ // Copyright (c) 2026 Devlas SpA
2
+ // Sistema de progreso estructurado para el runner de certificacion SII.
3
+ // El runner y la libreria emiten lineas [PROGRESS]{...json...} que la API
4
+ // parsea sin regex frágiles sobre texto libre.
5
+
6
+ const STEPS = {
7
+ // Autenticacion
8
+ AUTH_INIT: 'AUTH_INIT',
9
+ AUTH_OK: 'AUTH_OK',
10
+ // Sets
11
+ SETS_DOWNLOADING: 'SETS_DOWNLOADING',
12
+ SETS_LOADED: 'SETS_LOADED',
13
+ // CAFs
14
+ CAF_REQUESTING: 'CAF_REQUESTING', // data: { tipo }
15
+ CAF_OK: 'CAF_OK', // data: { tipo }
16
+ // Ejecucion de sets
17
+ SET_START: 'SET_START', // data: { set }
18
+ SET_SIGNING: 'SET_SIGNING', // data: { set }
19
+ SET_SENDING: 'SET_SENDING', // data: { set }
20
+ SET_OK: 'SET_OK', // data: { set, trackId }
21
+ SET_ERROR: 'SET_ERROR', // data: { set, error }
22
+ // Declaracion
23
+ SETS_DECLARING: 'SETS_DECLARING',
24
+ SETS_DECLARED: 'SETS_DECLARED',
25
+ // Polling aprobacion sets
26
+ POLLING: 'POLLING', // data: { intento, max }
27
+ SET_APPROVED: 'SET_APPROVED', // data: { set }
28
+ SETS_APPROVED: 'SETS_APPROVED',
29
+ SETS_REJECTED: 'SETS_REJECTED',
30
+ // Libros (Fase 4)
31
+ BOOKS_START: 'BOOKS_START',
32
+ BOOK_SENDING: 'BOOK_SENDING', // data: { book }
33
+ BOOK_OK: 'BOOK_OK', // data: { book, trackId }
34
+ BOOK_SKIPPED: 'BOOK_SKIPPED', // data: { book }
35
+ BOOK_ERROR: 'BOOK_ERROR', // data: { book, error }
36
+ BOOKS_DECLARING: 'BOOKS_DECLARING',
37
+ BOOKS_DONE: 'BOOKS_DONE',
38
+ // Avance (Fase 5)
39
+ ADVANCE_WAITING: 'ADVANCE_WAITING',
40
+ ADVANCE_DONE: 'ADVANCE_DONE',
41
+ // Simulacion (Fase 6)
42
+ SIM_START: 'SIM_START',
43
+ SIM_SENDING: 'SIM_SENDING',
44
+ SIM_OK: 'SIM_OK', // data: { trackId }
45
+ SIM_DECLARING: 'SIM_DECLARING',
46
+ SIM_POLLING: 'SIM_POLLING', // data: { intento, max }
47
+ SIM_DONE: 'SIM_DONE',
48
+ // Intercambio (Fase 7)
49
+ INTERCAMBIO_START: 'INTERCAMBIO_START',
50
+ INTERCAMBIO_DONE: 'INTERCAMBIO_DONE',
51
+ // Muestras impresas (Fase 8)
52
+ MUESTRAS_START: 'MUESTRAS_START',
53
+ MUESTRAS_PDFS: 'MUESTRAS_PDFS',
54
+ MUESTRAS_UPLOADING: 'MUESTRAS_UPLOADING',
55
+ MUESTRAS_DONE: 'MUESTRAS_DONE',
56
+ // Boleta electronica
57
+ BOLETA_START: 'BOLETA_START',
58
+ BOLETA_SENDING: 'BOLETA_SENDING',
59
+ BOLETA_OK: 'BOLETA_OK',
60
+ BOLETA_DECLARING: 'BOLETA_DECLARING',
61
+ BOLETA_DONE: 'BOLETA_DONE',
62
+ // Fin
63
+ CERT_DONE: 'CERT_DONE',
64
+ CERT_ERROR: 'CERT_ERROR', // data: { error }
65
+ };
66
+
67
+ /**
68
+ * Emite una linea de progreso estructurada a stdout.
69
+ * Formato: [PROGRESS]{"step":"...","clave":"valor",...}
70
+ *
71
+ * @param {string} step - Una de las constantes STEPS
72
+ * @param {Object} [data] - Datos adicionales opcionales
73
+ */
74
+ function emitProgress(step, data = {}) {
75
+ process.stdout.write(`[PROGRESS]${JSON.stringify({ step, ...data })}\n`);
76
+ }
77
+
78
+ module.exports = { STEPS, emitProgress };
package/utils/xml.js CHANGED
@@ -54,7 +54,7 @@ const prettyBuilder = new XMLBuilder({
54
54
  ignoreAttributes: false,
55
55
  attributeNamePrefix: '@_',
56
56
  format: true,
57
- indentBy: ' ',
57
+ indentBy: ' ',
58
58
  suppressEmptyNode: true,
59
59
  });
60
60
 
@@ -326,7 +326,7 @@ function saveEnvioArtifacts({
326
326
  fs.writeFileSync(path.join(debugDir, `respuesta-${debugPrefix}.xml`), responseText, 'utf-8');
327
327
  }
328
328
  } catch (saveError) {
329
- console.warn('⚠️ No se pudo guardar histórico/debug:', saveError.message || saveError);
329
+ console.warn('[!] No se pudo guardar histórico/debug:', saveError.message || saveError);
330
330
  }
331
331
  }
332
332