@devlas/dte-sii 2.12.24 → 2.12.25

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 CHANGED
@@ -238,6 +238,37 @@ class CafSolicitor {
238
238
  return /NO\s+(SE\s+)?AUTORIZA\s+TIMBRAJE/i.test(String(html || ''));
239
239
  }
240
240
 
241
+ /**
242
+ * Detecta el rechazo genérico "no está autorizado para ingresar a esta opción" en
243
+ * palena (producción). A diferencia de maullin/certificación, donde el bloqueo por
244
+ * Verificación de Actividades sale explícito ("no registra Verificación de
245
+ * Actividades..."), en producción el SII solo dice esto — mismo texto para tipo 39
246
+ * (boleta) y 33 (factura), sin indicar la causa real. Verificado 2026-07-24 contra
247
+ * 78441936-3, en ambos tipos, con el mismo request real repetido dos veces.
248
+ *
249
+ * Sin esta detección caía en el genérico `UNKNOWN: No se obtuvo CAF en la
250
+ * respuesta`, que no distingue este caso (probable Verificación de Actividades,
251
+ * no confirmado por el mensaje) de un fallo real desconocido.
252
+ */
253
+ static esNoAutorizadoIngresarOpcion(html) {
254
+ return /no\s+est.\s+autorizado\s+para\s+ingresar\s+a\s+esta\s+opci.n/i.test(String(html || ''));
255
+ }
256
+
257
+ /**
258
+ * Detecta el rechazo por permisos del usuario (estado 106 del SII).
259
+ *
260
+ * El RUT dueño del certificado puede estar enrolado en la empresa pero sin los
261
+ * permisos de firmar/enviar/solicitar folios marcados — el SII responde con esta
262
+ * página en vez de entregar el CAF. Sin detectarla caía en el genérico
263
+ * `UNKNOWN: No se obtuvo CAF en la respuesta`, que no dice qué arreglar; el remedio
264
+ * es reenviar los permisos del usuario (Paso 5 de la inscripción) EN EL MISMO
265
+ * AMBIENTE donde se está pidiendo el folio.
266
+ */
267
+ static esUsuarioSinPermiso(html) {
268
+ return /no\s+tiene\s+permiso\s+en\s+(la\s+)?empresa|usuario\s+no\s+autorizado\s+para\s+(la\s+)?empresa|no\s+est.\s+autorizado\s+para\s+operar\s+en\s+la\s+empresa/i
269
+ .test(String(html || ''));
270
+ }
271
+
241
272
  /**
242
273
  * Solicita un CAF al SII
243
274
  * @param {Object} params - Parámetros
@@ -314,7 +345,7 @@ class CafSolicitor {
314
345
  return {
315
346
  success: false,
316
347
  errorCode: 'VERIFICACION_ACTIVIDADES_PENDIENTE',
317
- error: 'SII: El RUT no tiene registrada la Verificación de Actividades para este tipo de documento. Solicítala en el sitio SII → Peticiones Administrativas → Verificación de actividad, o en oficinas del SII.',
348
+ error: 'SII: El RUT no tiene registrada la Verificación de Actividades para este tipo de documento. Realízala en https://www4.sii.cl/verificacionactividadesinternetui/?opcion=1#/ingreso (Verificación de Actividades online).',
318
349
  };
319
350
  }
320
351
 
@@ -364,13 +395,29 @@ class CafSolicitor {
364
395
  return { success: false, errorCode: 'TIMBRAJE_BLOQUEADO', error: 'SII: No se autoriza timbraje. Folios acumulados excesivos o situaciones tributarias pendientes. Revisa el portal SII → Factura Electrónica → Solicitud de Timbraje.' };
365
396
  }
366
397
 
398
+ if (response.body && CafSolicitor.esNoAutorizadoIngresarOpcion(response.body)) {
399
+ return {
400
+ success: false,
401
+ errorCode: 'NO_AUTORIZADO_INGRESAR_OPCION',
402
+ error: `SII (${this.ambiente}): "No está autorizado para ingresar a esta opción" — el SII no indica la causa exacta, pero el patrón coincide con la Verificación de Actividades pendiente para este RUT. Verifícala en https://www4.sii.cl/verificacionactividadesinternetui/?opcion=1#/ingreso`,
403
+ };
404
+ }
405
+
406
+ if (response.body && CafSolicitor.esUsuarioSinPermiso(response.body)) {
407
+ return {
408
+ success: false,
409
+ errorCode: 'USUARIO_SIN_PERMISO',
410
+ error: `SII (106): el RUT del certificado no tiene permisos para solicitar folios en la empresa (ambiente ${this.ambiente}). Vuelve a ejecutar la inscripción para reasignar permisos de firmar, enviar y obtener folios.`,
411
+ };
412
+ }
413
+
367
414
  // Rechazo por Verificación de Actividades detectado en un paso posterior del flujo
368
415
  // multi-paso (ver guarda defensiva en _processMultiStepFlow).
369
416
  if (response.body && response.body.includes('no registra Verificacion de Actividades')) {
370
417
  return {
371
418
  success: false,
372
419
  errorCode: 'VERIFICACION_ACTIVIDADES_PENDIENTE',
373
- error: 'SII: El RUT no tiene registrada la Verificación de Actividades para este tipo de documento. Solicítala en el sitio SII → Peticiones Administrativas → Verificación de actividad, o en oficinas del SII.',
420
+ error: 'SII: El RUT no tiene registrada la Verificación de Actividades para este tipo de documento. Realízala en https://www4.sii.cl/verificacionactividadesinternetui/?opcion=1#/ingreso (Verificación de Actividades online).',
374
421
  };
375
422
  }
376
423
 
@@ -3240,25 +3240,20 @@ class CertRunner {
3240
3240
  * @param {string} opts.correoProveedor
3241
3241
  * @returns {Promise<{success: boolean, mensaje?: string, error?: string}>}
3242
3242
  */
3243
- async completarDeclaracionBoletaPortal({
3244
- linkConsulta = 'www.sii.cl',
3245
- rutProveedor = '',
3246
- nombreProveedor = '',
3247
- correoProveedor = '',
3248
- } = {}) {
3243
+ /**
3244
+ * Constantes y helper GWT compartidos por las llamadas al portal de Certificación
3245
+ * de Boleta Electrónica (certBolElectDteInternet — GWT-RPC, distinto de pe_avance
3246
+ * que es HTML). Factorizado para no duplicar la construcción del request entre
3247
+ * el chequeo de solo-lectura y la declaración real.
3248
+ */
3249
+ _gwtBoletaClient(cookieJar) {
3249
3250
  const https = require('https');
3250
3251
  const crypto = require('crypto');
3251
3252
  const CBE_BASE = 'https://www4.sii.cl/certBolElectDteInternet/';
3252
3253
  const CBE_PERM = '0FC3D987613537E6E13E9BB93A406F13';
3253
3254
  const CBE_POLICY = '082D0AC4BC4D75A5DF38F116C53877D4';
3254
3255
  const CBE_SVC = 'cl.sii.sdi.diii.certBolElectDte.web.client.service.Facade';
3255
-
3256
- const cookieJar = await this._obtenerCookiesSII();
3257
3256
  const cookieStr = Object.entries(cookieJar).map(([k, v]) => `${k}=${v}`).join('; ');
3258
- const [rutNum, dvChar] = this.config.emisor.rut.replace(/\./g, '').split('-');
3259
- const dvUp = dvChar.toUpperCase();
3260
- const razonSocial = this.config.emisor.razon_social || this.config.emisor.razonSocial || '';
3261
-
3262
3257
  const tlsOpts = {
3263
3258
  rejectUnauthorized: false,
3264
3259
  maxVersion: 'TLSv1.2',
@@ -3279,11 +3274,32 @@ class CertRunner {
3279
3274
  ...tlsOpts,
3280
3275
  }, (res) => { const ch = []; res.on('data', c => ch.push(c)); res.on('end', () => resolve(Buffer.concat(ch).toString('utf-8'))); });
3281
3276
  req.on('error', reject);
3282
- req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout gwtPost CBE declaracion')); });
3277
+ req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout gwtPost CBE')); });
3283
3278
  req.write(buf); req.end();
3284
3279
  });
3280
+ return { CBE_BASE, CBE_POLICY, CBE_SVC, gwtPost };
3281
+ }
3282
+
3283
+ /**
3284
+ * Chequeo de SOLO LECTURA del estado de la certificación de Boleta en el portal
3285
+ * certBolElectDteInternet. No declara nada — sirve para:
3286
+ * - gatear el botón "Declarar cumplimiento" (evita el viaje fallido si aún no hay P90)
3287
+ * - recuperar el estado de boleta cuando las banderas locales se resetearon (ese
3288
+ * portal NO aparece en pe_avance, así que consultarEstadoAvance no lo cubre)
3289
+ *
3290
+ * Verificado 2026-07-24 contra 78441936-3 con curl crudo (independiente de este
3291
+ * código) — misma respuesta `//OK[0,[],0,7]` cuando no hay P90 todavía.
3292
+ *
3293
+ * @returns {Promise<{success: boolean, inscrita: boolean, listaParaDeclarar: boolean, estado: string|null, error?: string}>}
3294
+ */
3295
+ async consultarEstadoBoletaPortal() {
3296
+ const cookieJar = await this._obtenerCookiesSII();
3297
+ const [rutNum, dvChar] = this.config.emisor.rut.replace(/\./g, '').split('-');
3298
+ const dvUp = dvChar.toUpperCase();
3299
+ const { CBE_BASE, CBE_POLICY, CBE_SVC, gwtPost } = this._gwtBoletaClient(cookieJar);
3285
3300
 
3286
- // 1. Obtener representante legal vigente
3301
+ // 1. Representante legal vigente ⇒ si hay uno, la empresa está postulada/inscrita
3302
+ // en el portal de boleta (aunque aún no tenga P90).
3287
3303
  const reprResp = await gwtPost(
3288
3304
  `7|0|7|${CBE_BASE}|${CBE_POLICY}|${CBE_SVC}|recuperarRepresentantesVigentesUsuariosAutorizados|java.lang.Integer/3438268394|java.lang.String/2004016611|${dvUp}|1|2|3|4|2|5|6|5|${rutNum}|7|`
3289
3305
  );
@@ -3293,17 +3309,67 @@ class CertRunner {
3293
3309
  const reprTable = JSON.parse(reprTableStr);
3294
3310
  rutRepreNum = reprTable.filter(s => /^\d{7,8}$/.test(s)).pop() || '';
3295
3311
  } catch {}
3296
- if (!rutRepreNum) return { success: false, error: 'No se pudo obtener representante vigente (facade CBE)' };
3312
+ const inscrita = !!rutRepreNum;
3313
+ if (!inscrita) {
3314
+ return { success: true, inscrita: false, listaParaDeclarar: false, estado: null };
3315
+ }
3297
3316
 
3298
- // 2. Verificar estado portal debe ser P90 (SOK recibido, listo para declarar)
3317
+ // 2. EstadoP90 = SOK recibido, lista para declarar. Lista vacía = SII aún no
3318
+ // creó el registro (SOK pendiente). Cualquier otro P\d+ = otro estado del flujo.
3299
3319
  const estadoResp = await gwtPost(
3300
3320
  `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|90|0|`
3301
3321
  );
3302
- if (!estadoResp.includes('P90')) {
3303
- const estadoMatch = estadoResp.match(/"(P\d+)"/);
3304
- const estado = estadoMatch ? estadoMatch[1] : '(desconocido)';
3305
- return { success: false, pendingSok: true, error: `Estado portal no es P90 (es ${estado}) — espere SOK del SII` };
3306
- }
3322
+ const listaParaDeclarar = estadoResp.includes('P90');
3323
+ const estadoMatch = estadoResp.match(/"(P\d+)"/);
3324
+ const listaVacia = /\/\/OK\[0,\[\]/.test(estadoResp);
3325
+ const estado = listaParaDeclarar ? 'P90'
3326
+ : estadoMatch ? estadoMatch[1]
3327
+ : listaVacia ? 'sin_estado_sok_pendiente'
3328
+ : 'desconocido';
3329
+
3330
+ return { success: true, inscrita: true, listaParaDeclarar, estado };
3331
+ }
3332
+
3333
+ async completarDeclaracionBoletaPortal({
3334
+ linkConsulta = 'www.sii.cl',
3335
+ rutProveedor = '',
3336
+ nombreProveedor = '',
3337
+ correoProveedor = '',
3338
+ } = {}) {
3339
+ const cookieJar = await this._obtenerCookiesSII();
3340
+ const [rutNum, dvChar] = this.config.emisor.rut.replace(/\./g, '').split('-');
3341
+ const dvUp = dvChar.toUpperCase();
3342
+ const razonSocial = this.config.emisor.razon_social || this.config.emisor.razonSocial || '';
3343
+ const { CBE_BASE, CBE_POLICY, CBE_SVC, gwtPost } = this._gwtBoletaClient(cookieJar);
3344
+
3345
+ // Chequeo de solo-lectura primero: evita el viaje de autorizarEmpresaBolProd
3346
+ // (que escribe en el SII) cuando ya sabemos que no va a poder completarse.
3347
+ const estadoActual = await this.consultarEstadoBoletaPortal();
3348
+ if (!estadoActual.success) {
3349
+ return { success: false, error: 'No se pudo consultar el estado en el portal de boleta.' };
3350
+ }
3351
+ if (!estadoActual.inscrita) {
3352
+ return { success: false, error: 'No se pudo obtener representante vigente (facade CBE)' };
3353
+ }
3354
+ if (!estadoActual.listaParaDeclarar) {
3355
+ const legible = estadoActual.estado === 'sin_estado_sok_pendiente'
3356
+ ? 'sin estado registrado (SOK aún no emitido)'
3357
+ : (estadoActual.estado ?? '(desconocido)');
3358
+ return { success: false, pendingSok: true, error: `El SII aún no habilita la declaración: ${legible}. Espere el SOK del set de boleta enviado.` };
3359
+ }
3360
+ // Representante legal vigente. Se vuelve a pedir (no se reusa el de
3361
+ // consultarEstadoBoletaPortal) porque autorizarEmpresaBolProd necesita el dato
3362
+ // fresco justo antes de escribir, sin depender de estado interno de otra llamada.
3363
+ const reprResp = await gwtPost(
3364
+ `7|0|7|${CBE_BASE}|${CBE_POLICY}|${CBE_SVC}|recuperarRepresentantesVigentesUsuariosAutorizados|java.lang.Integer/3438268394|java.lang.String/2004016611|${dvUp}|1|2|3|4|2|5|6|5|${rutNum}|7|`
3365
+ );
3366
+ const reprTableStr = reprResp.substring(reprResp.lastIndexOf(',[') + 1, reprResp.lastIndexOf('],0,7]') + 1);
3367
+ let rutRepreNum = '';
3368
+ try {
3369
+ const reprTable = JSON.parse(reprTableStr);
3370
+ rutRepreNum = reprTable.filter(s => /^\d{7,8}$/.test(s)).pop() || '';
3371
+ } catch {}
3372
+ if (!rutRepreNum) return { success: false, error: 'No se pudo obtener representante vigente (facade CBE)' };
3307
3373
 
3308
3374
  // 3. Obtener datos de postulacion: fchAutorizacion y longCharValue
3309
3375
  const postulResp = await gwtPost(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devlas/dte-sii",
3
- "version": "2.12.24",
3
+ "version": "2.12.25",
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",
package/utils/sanitize.js CHANGED
@@ -15,7 +15,16 @@
15
15
  /**
16
16
  * Sanitizar texto para DTE SII
17
17
  * Elimina caracteres que causan problemas de firma XML
18
- *
18
+ *
19
+ * El envío al SII codifica el XML como ISO-8859-1 vía `Buffer.from(xml, 'latin1')`
20
+ * (EnviadorSII.js), que trunca cada code point a su byte bajo (code & 0xFF) en vez de
21
+ * transliterar. Cualquier caracter fuera del rango Latin-1 (ej. em dash — = U+2014)
22
+ * se corrompe en un byte de control arbitrario (0x2014 & 0xFF = 0x14), lo que el SII
23
+ * rechaza como "invalid character" a nivel de schema — un error genérico que no apunta
24
+ * a la causa real. Por eso se reemplazan primero los casos comunes (guiones/puntos
25
+ * suspensivos tipográficos) por su equivalente ASCII, y como red de seguridad se
26
+ * elimina cualquier otro caracter fuera de Latin-1 antes de que llegue a esa conversión.
27
+ *
19
28
  * @param {string} text - Texto a sanitizar
20
29
  * @returns {string} - Texto sanitizado
21
30
  */
@@ -25,6 +34,9 @@ function sanitizeSiiText(text) {
25
34
  .replace(/[''´`]/g, '') // Eliminar apóstrofes
26
35
  .replace(/[""]/g, '') // Eliminar comillas tipográficas
27
36
  .replace(/"/g, '') // Eliminar comillas dobles ASCII
37
+ .replace(/[—–]/g, '-') // Guion largo/corto tipográfico -> guion ASCII
38
+ .replace(/…/g, '...') // Puntos suspensivos tipográficos -> ASCII
39
+ .replace(/[^\x00-\xFF]/g, '') // Cualquier otro caracter fuera de Latin-1 (emojis, etc.)
28
40
  .trim();
29
41
  }
30
42