@devlas/dte-sii 2.12.24 → 2.12.26
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 +49 -2
- package/DTE.js +7 -0
- package/Envio.js +5 -0
- package/README.md +15 -0
- package/cert/BoletaCert.js +8 -0
- package/cert/CertRunner.js +101 -21
- package/package.json +1 -1
- package/utils/sanitize.js +13 -1
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.
|
|
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.
|
|
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
|
|
package/DTE.js
CHANGED
|
@@ -350,6 +350,13 @@ class DTE {
|
|
|
350
350
|
format: false,
|
|
351
351
|
});
|
|
352
352
|
|
|
353
|
+
// NO QUITAR estos dos xmlns del <DTE>. NO son redundantes aunque el sobre
|
|
354
|
+
// <EnvioBOLETA>/<EnvioDTE> también los declare.
|
|
355
|
+
// El DigestValue de la firma se calcula en _c14nDocumento(), que lee el xmlns
|
|
356
|
+
// desde el <DTE> y lo hereda al <Documento> canonicalizado. Si se quita, se
|
|
357
|
+
// firma un <Documento> sin namespace, pero el SII lo canonicaliza CON
|
|
358
|
+
// xmlns="http://www.sii.cl/SiiDte" (lo hereda igual) → los digests no coinciden
|
|
359
|
+
// → "firma inválida" / rechazo. Tampoco stripearlos después en Envio.js.
|
|
353
360
|
const dteConVersion = {
|
|
354
361
|
'@_xmlns': 'http://www.sii.cl/SiiDte',
|
|
355
362
|
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
package/Envio.js
CHANGED
|
@@ -81,6 +81,11 @@ class EnvioBase {
|
|
|
81
81
|
|
|
82
82
|
/**
|
|
83
83
|
* Extraer XML de DTEs sin declaration
|
|
84
|
+
*
|
|
85
|
+
* NO stripear el xmlns del <DTE> aquí (aunque el sobre ya lo declare). Ese
|
|
86
|
+
* namespace es parte del documento firmado: el DigestValue de la firma se
|
|
87
|
+
* calculó heredándolo al <Documento>. Si se quita, el SII recalcula el digest
|
|
88
|
+
* CON el namespace y no coincide → "firma inválida". Solo quitar el prolog XML.
|
|
84
89
|
*/
|
|
85
90
|
_extractDTEsXml() {
|
|
86
91
|
return this.dtes.map(dte => {
|
package/README.md
CHANGED
|
@@ -197,6 +197,21 @@ const estadoDte = await enviador.consultarEstadoDte({
|
|
|
197
197
|
|
|
198
198
|
## Boletas electrónicas
|
|
199
199
|
|
|
200
|
+
> **Recomendado para CERTIFICAR boletas: usar `CertRunner` + `BoletaCert`, no el camino manual.**
|
|
201
|
+
>
|
|
202
|
+
> Para el proceso de certificación de boletas ante el SII, use el orquestador de
|
|
203
|
+
> alto nivel (`require('@devlas/dte-sii/cert')` → `CertRunner` / `BoletaCert`). Ese
|
|
204
|
+
> camino descarga el set, genera y firma las boletas, arma el `EnvioBOLETA` + RCOF,
|
|
205
|
+
> obtiene el TrackId y declara el cumplimiento. Es el flujo probado end-to-end.
|
|
206
|
+
>
|
|
207
|
+
> El camino manual de bajo nivel (`DTE` + `EnvioBOLETA` + `EnviadorSII`) que se
|
|
208
|
+
> muestra más abajo funciona, pero es fácil romperlo. **Error frecuente:** "optimizar"
|
|
209
|
+
> quitando los `xmlns` del `<DTE>` por parecer redundantes con el sobre. NO lo haga:
|
|
210
|
+
> el `xmlns="http://www.sii.cl/SiiDte"` del `<DTE>` es parte del documento firmado
|
|
211
|
+
> (se hereda al `<Documento>` al calcular el DigestValue). Si lo quita, el SII
|
|
212
|
+
> recalcula el digest con el namespace y no coincide → rechazo por "firma inválida".
|
|
213
|
+
> Ver [Certificación SII](#certificación-sii).
|
|
214
|
+
|
|
200
215
|
> **Diferencia crítica por ambiente**
|
|
201
216
|
>
|
|
202
217
|
> - **Producción** → usar la API **REST** del SII (`enviarBoleta`) con los datos reales de resolución de la empresa.
|
package/cert/BoletaCert.js
CHANGED
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
* 3. Se genera RCOF (ConsumoFolio) para reportar los folios usados
|
|
13
13
|
* 4. Se envían ambos documentos al SII
|
|
14
14
|
* 5. SII valida y aprueba
|
|
15
|
+
*
|
|
16
|
+
* NOTA sobre el RCOF (ConsumoFolio):
|
|
17
|
+
* El RCOF está deprecado en producción y ya no se usa en la operación normal.
|
|
18
|
+
* Solo se mantiene aquí porque este paso de certificación todavía lo exige.
|
|
19
|
+
* Al enviarlo, el SII puede devolver reparos: eso es esperado en certificación
|
|
20
|
+
* y no bloquea el avance del proceso.
|
|
15
21
|
*/
|
|
16
22
|
|
|
17
23
|
const path = require('path');
|
|
@@ -482,6 +488,8 @@ class BoletaCert {
|
|
|
482
488
|
}
|
|
483
489
|
|
|
484
490
|
process.stderr.write(`[PROGRESS]${JSON.stringify({ step: 'BOLETA_RCOF' })}\n`);
|
|
491
|
+
// RCOF deprecado en producción: solo se envía aquí porque la certificación aún
|
|
492
|
+
// lo exige. Reparos del SII en este envío son esperados y NO bloquean el avance.
|
|
485
493
|
// 5 & 6. Generar y enviar RCOF — loop hasta que SII lo acepte o se agoten intentos.
|
|
486
494
|
// Estrategia: enviar → si ok, esperar 30s → consultar estado (EPR o RPR = éxito).
|
|
487
495
|
// Si DUPLICADO: el SII ya tiene un RCOF para este RUT/período. El entorno de certificación
|
package/cert/CertRunner.js
CHANGED
|
@@ -3073,6 +3073,20 @@ class CertRunner {
|
|
|
3073
3073
|
// ═══════════════════════════════════════════════════════════════
|
|
3074
3074
|
// BOLETA ELECTRÓNICA — automatización portal certBolElectDteInternet
|
|
3075
3075
|
// ═══════════════════════════════════════════════════════════════
|
|
3076
|
+
//
|
|
3077
|
+
// Métodos que se usan para la CERTIFICACIÓN DE BOLETAS (flujo, en orden):
|
|
3078
|
+
// 1. obtenerSetBoletaPortal → descarga el Set de Pruebas (?SET=1)
|
|
3079
|
+
// 2. (BoletaCert.ejecutarCertificacion emite EnvioBOLETA + RCOF → devuelve trackId)
|
|
3080
|
+
// 3. solicitarValidacionBoletaPortal → declara el trackId del envío (?SET=2)
|
|
3081
|
+
// 4. completarDeclaracionBoletaPortal→ declara cumplimiento y autoriza producción
|
|
3082
|
+
//
|
|
3083
|
+
// Apoyo (solo lectura / recuperación de estado):
|
|
3084
|
+
// - consultarEstadoBoletaPortal → estado en el portal GWT de boleta
|
|
3085
|
+
// - verificarAutorizacionBoleta → confirma si la empresa ya emite tipo 39
|
|
3086
|
+
//
|
|
3087
|
+
// El portal de boleta usa GWT-RPC (certBolElectDteInternet), distinto del portal
|
|
3088
|
+
// pe_avance usado para facturas/DTE. La certificación de boletas NO usa las fases
|
|
3089
|
+
// de sets/libros/simulación/intercambio/muestras; esas son solo del track DTE.
|
|
3076
3090
|
|
|
3077
3091
|
/**
|
|
3078
3092
|
* Descarga el Set de Pruebas de Boleta Electrónica desde el portal SII.
|
|
@@ -3240,25 +3254,20 @@ class CertRunner {
|
|
|
3240
3254
|
* @param {string} opts.correoProveedor
|
|
3241
3255
|
* @returns {Promise<{success: boolean, mensaje?: string, error?: string}>}
|
|
3242
3256
|
*/
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3257
|
+
/**
|
|
3258
|
+
* Constantes y helper GWT compartidos por las llamadas al portal de Certificación
|
|
3259
|
+
* de Boleta Electrónica (certBolElectDteInternet — GWT-RPC, distinto de pe_avance
|
|
3260
|
+
* que es HTML). Factorizado para no duplicar la construcción del request entre
|
|
3261
|
+
* el chequeo de solo-lectura y la declaración real.
|
|
3262
|
+
*/
|
|
3263
|
+
_gwtBoletaClient(cookieJar) {
|
|
3249
3264
|
const https = require('https');
|
|
3250
3265
|
const crypto = require('crypto');
|
|
3251
3266
|
const CBE_BASE = 'https://www4.sii.cl/certBolElectDteInternet/';
|
|
3252
3267
|
const CBE_PERM = '0FC3D987613537E6E13E9BB93A406F13';
|
|
3253
3268
|
const CBE_POLICY = '082D0AC4BC4D75A5DF38F116C53877D4';
|
|
3254
3269
|
const CBE_SVC = 'cl.sii.sdi.diii.certBolElectDte.web.client.service.Facade';
|
|
3255
|
-
|
|
3256
|
-
const cookieJar = await this._obtenerCookiesSII();
|
|
3257
3270
|
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
3271
|
const tlsOpts = {
|
|
3263
3272
|
rejectUnauthorized: false,
|
|
3264
3273
|
maxVersion: 'TLSv1.2',
|
|
@@ -3279,11 +3288,32 @@ class CertRunner {
|
|
|
3279
3288
|
...tlsOpts,
|
|
3280
3289
|
}, (res) => { const ch = []; res.on('data', c => ch.push(c)); res.on('end', () => resolve(Buffer.concat(ch).toString('utf-8'))); });
|
|
3281
3290
|
req.on('error', reject);
|
|
3282
|
-
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout gwtPost CBE
|
|
3291
|
+
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout gwtPost CBE')); });
|
|
3283
3292
|
req.write(buf); req.end();
|
|
3284
3293
|
});
|
|
3294
|
+
return { CBE_BASE, CBE_POLICY, CBE_SVC, gwtPost };
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
/**
|
|
3298
|
+
* Chequeo de SOLO LECTURA del estado de la certificación de Boleta en el portal
|
|
3299
|
+
* certBolElectDteInternet. No declara nada — sirve para:
|
|
3300
|
+
* - gatear el botón "Declarar cumplimiento" (evita el viaje fallido si aún no hay P90)
|
|
3301
|
+
* - recuperar el estado de boleta cuando las banderas locales se resetearon (ese
|
|
3302
|
+
* portal NO aparece en pe_avance, así que consultarEstadoAvance no lo cubre)
|
|
3303
|
+
*
|
|
3304
|
+
* Verificado 2026-07-24 contra 78441936-3 con curl crudo (independiente de este
|
|
3305
|
+
* código) — misma respuesta `//OK[0,[],0,7]` cuando no hay P90 todavía.
|
|
3306
|
+
*
|
|
3307
|
+
* @returns {Promise<{success: boolean, inscrita: boolean, listaParaDeclarar: boolean, estado: string|null, error?: string}>}
|
|
3308
|
+
*/
|
|
3309
|
+
async consultarEstadoBoletaPortal() {
|
|
3310
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
3311
|
+
const [rutNum, dvChar] = this.config.emisor.rut.replace(/\./g, '').split('-');
|
|
3312
|
+
const dvUp = dvChar.toUpperCase();
|
|
3313
|
+
const { CBE_BASE, CBE_POLICY, CBE_SVC, gwtPost } = this._gwtBoletaClient(cookieJar);
|
|
3285
3314
|
|
|
3286
|
-
// 1.
|
|
3315
|
+
// 1. Representante legal vigente ⇒ si hay uno, la empresa está postulada/inscrita
|
|
3316
|
+
// en el portal de boleta (aunque aún no tenga P90).
|
|
3287
3317
|
const reprResp = await gwtPost(
|
|
3288
3318
|
`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
3319
|
);
|
|
@@ -3293,17 +3323,67 @@ class CertRunner {
|
|
|
3293
3323
|
const reprTable = JSON.parse(reprTableStr);
|
|
3294
3324
|
rutRepreNum = reprTable.filter(s => /^\d{7,8}$/.test(s)).pop() || '';
|
|
3295
3325
|
} catch {}
|
|
3296
|
-
|
|
3326
|
+
const inscrita = !!rutRepreNum;
|
|
3327
|
+
if (!inscrita) {
|
|
3328
|
+
return { success: true, inscrita: false, listaParaDeclarar: false, estado: null };
|
|
3329
|
+
}
|
|
3297
3330
|
|
|
3298
|
-
// 2.
|
|
3331
|
+
// 2. Estado — P90 = SOK recibido, lista para declarar. Lista vacía = SII aún no
|
|
3332
|
+
// creó el registro (SOK pendiente). Cualquier otro P\d+ = otro estado del flujo.
|
|
3299
3333
|
const estadoResp = await gwtPost(
|
|
3300
3334
|
`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
3335
|
);
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3336
|
+
const listaParaDeclarar = estadoResp.includes('P90');
|
|
3337
|
+
const estadoMatch = estadoResp.match(/"(P\d+)"/);
|
|
3338
|
+
const listaVacia = /\/\/OK\[0,\[\]/.test(estadoResp);
|
|
3339
|
+
const estado = listaParaDeclarar ? 'P90'
|
|
3340
|
+
: estadoMatch ? estadoMatch[1]
|
|
3341
|
+
: listaVacia ? 'sin_estado_sok_pendiente'
|
|
3342
|
+
: 'desconocido';
|
|
3343
|
+
|
|
3344
|
+
return { success: true, inscrita: true, listaParaDeclarar, estado };
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
async completarDeclaracionBoletaPortal({
|
|
3348
|
+
linkConsulta = 'www.sii.cl',
|
|
3349
|
+
rutProveedor = '',
|
|
3350
|
+
nombreProveedor = '',
|
|
3351
|
+
correoProveedor = '',
|
|
3352
|
+
} = {}) {
|
|
3353
|
+
const cookieJar = await this._obtenerCookiesSII();
|
|
3354
|
+
const [rutNum, dvChar] = this.config.emisor.rut.replace(/\./g, '').split('-');
|
|
3355
|
+
const dvUp = dvChar.toUpperCase();
|
|
3356
|
+
const razonSocial = this.config.emisor.razon_social || this.config.emisor.razonSocial || '';
|
|
3357
|
+
const { CBE_BASE, CBE_POLICY, CBE_SVC, gwtPost } = this._gwtBoletaClient(cookieJar);
|
|
3358
|
+
|
|
3359
|
+
// Chequeo de solo-lectura primero: evita el viaje de autorizarEmpresaBolProd
|
|
3360
|
+
// (que escribe en el SII) cuando ya sabemos que no va a poder completarse.
|
|
3361
|
+
const estadoActual = await this.consultarEstadoBoletaPortal();
|
|
3362
|
+
if (!estadoActual.success) {
|
|
3363
|
+
return { success: false, error: 'No se pudo consultar el estado en el portal de boleta.' };
|
|
3364
|
+
}
|
|
3365
|
+
if (!estadoActual.inscrita) {
|
|
3366
|
+
return { success: false, error: 'No se pudo obtener representante vigente (facade CBE)' };
|
|
3367
|
+
}
|
|
3368
|
+
if (!estadoActual.listaParaDeclarar) {
|
|
3369
|
+
const legible = estadoActual.estado === 'sin_estado_sok_pendiente'
|
|
3370
|
+
? 'sin estado registrado (SOK aún no emitido)'
|
|
3371
|
+
: (estadoActual.estado ?? '(desconocido)');
|
|
3372
|
+
return { success: false, pendingSok: true, error: `El SII aún no habilita la declaración: ${legible}. Espere el SOK del set de boleta enviado.` };
|
|
3373
|
+
}
|
|
3374
|
+
// Representante legal vigente. Se vuelve a pedir (no se reusa el de
|
|
3375
|
+
// consultarEstadoBoletaPortal) porque autorizarEmpresaBolProd necesita el dato
|
|
3376
|
+
// fresco justo antes de escribir, sin depender de estado interno de otra llamada.
|
|
3377
|
+
const reprResp = await gwtPost(
|
|
3378
|
+
`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|`
|
|
3379
|
+
);
|
|
3380
|
+
const reprTableStr = reprResp.substring(reprResp.lastIndexOf(',[') + 1, reprResp.lastIndexOf('],0,7]') + 1);
|
|
3381
|
+
let rutRepreNum = '';
|
|
3382
|
+
try {
|
|
3383
|
+
const reprTable = JSON.parse(reprTableStr);
|
|
3384
|
+
rutRepreNum = reprTable.filter(s => /^\d{7,8}$/.test(s)).pop() || '';
|
|
3385
|
+
} catch {}
|
|
3386
|
+
if (!rutRepreNum) return { success: false, error: 'No se pudo obtener representante vigente (facade CBE)' };
|
|
3307
3387
|
|
|
3308
3388
|
// 3. Obtener datos de postulacion: fchAutorizacion y longCharValue
|
|
3309
3389
|
const postulResp = await gwtPost(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.26",
|
|
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
|
|