@devlas/dte-sii 2.12.23 → 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 +49 -2
- package/FolioService.js +89 -4
- package/SiiCertificacion.js +14 -1
- package/cert/CertRunner.js +100 -22
- 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/FolioService.js
CHANGED
|
@@ -311,7 +311,11 @@ class FolioService {
|
|
|
311
311
|
`[FolioService] Tipo ${tipoDte}: ${result.errorCode} — anulando folios sin utilizar y reintentando...`
|
|
312
312
|
);
|
|
313
313
|
try {
|
|
314
|
-
|
|
314
|
+
// Acotado: acá sí interesan los folios viejos (son los que inflan
|
|
315
|
+
// FOLIOS_DISP), pero sin barrer el historial entero — anular de a
|
|
316
|
+
// cientos satura al SII y suma al contador de "anulados últimos 6
|
|
317
|
+
// meses", que también juega en contra del cupo.
|
|
318
|
+
const anul = await this.anularFolios({ tipoDte, maxRangos: 20 });
|
|
315
319
|
|
|
316
320
|
// Desglosar los motivos reales del rechazo: no todos son "ya recepcionado"
|
|
317
321
|
// (folio consumido en un DTE enviado). También caen acá los folios ya
|
|
@@ -500,7 +504,53 @@ class FolioService {
|
|
|
500
504
|
* @param {string} [params.motivo]
|
|
501
505
|
* @returns {Promise<{ok:boolean, anulados:Array, rechazados:Array, totalAnulados:number, totalRechazados:number}>}
|
|
502
506
|
*/
|
|
503
|
-
|
|
507
|
+
/**
|
|
508
|
+
* @param {number} [params.soloUltimosDias] - Si se indica, solo se anulan los
|
|
509
|
+
* rangos timbrados dentro de esos días. Sirve para limpiar los sobrantes de
|
|
510
|
+
* una corrida reciente sin tocar el historial de la empresa: sin este filtro
|
|
511
|
+
* la función barre TODOS los rangos sin utilizar, lo que en un RUT con
|
|
512
|
+
* volumen real significa cientos de anulaciones contra el SII — y, peor, el
|
|
513
|
+
* propio SII cuenta los folios anulados de los últimos 6 meses en contra del
|
|
514
|
+
* cupo de timbraje. Verificado 2026-07-22: una limpieza sin acotar anuló 296
|
|
515
|
+
* folios de un RUT antes de detenerla.
|
|
516
|
+
*/
|
|
517
|
+
/**
|
|
518
|
+
* Ruta del registro de rangos ya anulados, por RUT y tipo de DTE.
|
|
519
|
+
*
|
|
520
|
+
* El SII sigue listando un folio anulado como "sin utilizar" —anulado es, en
|
|
521
|
+
* efecto, sin usar— así que `consultarFolios` lo devuelve para siempre y cada
|
|
522
|
+
* limpieza vuelve a intentarlo. Sin memoria entre corridas eso son
|
|
523
|
+
* round-trips al SII que solo pueden fallar, y peor: van comiendo el cupo de
|
|
524
|
+
* `maxRangos`, hasta dejar fuera a los sobrantes que sí hay que anular.
|
|
525
|
+
*/
|
|
526
|
+
_anuladosPath(tipoDte) {
|
|
527
|
+
const rutLimpio = String(this.rutEmisor || '').replace(/[^0-9kK]/g, '');
|
|
528
|
+
return path.join(this.debugDir, `folios-anulados-${rutLimpio}-${tipoDte}.json`);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** Set de claves "desde-hasta" que el SII ya reportó como anuladas. */
|
|
532
|
+
_cargarAnulados(tipoDte) {
|
|
533
|
+
try {
|
|
534
|
+
const raw = fs.readFileSync(this._anuladosPath(tipoDte), 'utf8');
|
|
535
|
+
const arr = JSON.parse(raw);
|
|
536
|
+
return new Set(Array.isArray(arr) ? arr : []);
|
|
537
|
+
} catch (_) {
|
|
538
|
+
// Sin registro previo (o ilegible): se parte de cero. Nunca es fatal —
|
|
539
|
+
// como mucho se repite el intento una vez más.
|
|
540
|
+
return new Set();
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
_guardarAnulados(tipoDte, set) {
|
|
545
|
+
try {
|
|
546
|
+
fs.mkdirSync(this.debugDir, { recursive: true });
|
|
547
|
+
fs.writeFileSync(this._anuladosPath(tipoDte), JSON.stringify([...set]), 'utf8');
|
|
548
|
+
} catch (err) {
|
|
549
|
+
console.warn(`[FolioService] No se pudo persistir registro de anulados: ${err.message}`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async anularFolios({ tipoDte, folioDesde = null, folioHasta = null, motivo = 'Folios no utilizados', maxRangos = 50, soloUltimosDias = null }) {
|
|
504
554
|
const debugStampA = new Date().toISOString().replace(/[:.]/g, '-');
|
|
505
555
|
const debugDirA = path.join(this.debugDir, 'auto-caf', 'anulacion', debugStampA);
|
|
506
556
|
fs.mkdirSync(debugDirA, { recursive: true });
|
|
@@ -509,18 +559,38 @@ class FolioService {
|
|
|
509
559
|
const anulados = [];
|
|
510
560
|
const rechazados = [];
|
|
511
561
|
const vistos = new Set(); // claves "folioDesde-folioHasta" ya procesadas en esta ejecución
|
|
562
|
+
// Rangos que el SII ya reportó como anulados en corridas anteriores: se
|
|
563
|
+
// saltan de entrada para no gastar el cupo de `maxRangos` en reintentos
|
|
564
|
+
// que solo pueden volver a fallar. Ver `_anuladosPath`.
|
|
565
|
+
const yaAnulados = this._cargarAnulados(tipoDte);
|
|
566
|
+
const yaAnuladosInicial = yaAnulados.size;
|
|
512
567
|
|
|
513
568
|
const maxPasadas = 4;
|
|
514
569
|
|
|
515
570
|
for (let pasada = 0; pasada < maxPasadas; pasada++) {
|
|
516
571
|
const consulta = await this.consultarFolios({ tipoDte });
|
|
517
572
|
|
|
573
|
+
// Corte por antigüedad: `fecha` viene como DD-MM-AAAA en cada rango.
|
|
574
|
+
// Un rango sin fecha parseable se descarta cuando el filtro está activo —
|
|
575
|
+
// ante la duda, no anular.
|
|
576
|
+
const limiteMs = Number.isFinite(soloUltimosDias)
|
|
577
|
+
? Date.now() - soloUltimosDias * 24 * 60 * 60 * 1000
|
|
578
|
+
: null;
|
|
579
|
+
|
|
518
580
|
// Filtrar rangos dentro del rango solicitado y que no hayamos intentado aún
|
|
519
581
|
let rangos = consulta.ranges.filter(r => {
|
|
520
582
|
if (Number.isFinite(folioDesde) && Number.isFinite(folioHasta)) {
|
|
521
583
|
if (r.folioDesde > folioHasta || r.folioHasta < folioDesde) return false;
|
|
522
584
|
}
|
|
523
|
-
|
|
585
|
+
if (limiteMs !== null) {
|
|
586
|
+
const { dia, mes, ano } = this._parseFecha(r.fecha);
|
|
587
|
+
if (!dia || !mes || !ano) return false;
|
|
588
|
+
const t = new Date(`${ano}-${mes}-${dia}T00:00:00`).getTime();
|
|
589
|
+
if (!Number.isFinite(t) || t < limiteMs) return false;
|
|
590
|
+
}
|
|
591
|
+
const clave = `${r.folioDesde}-${r.folioHasta}`;
|
|
592
|
+
if (yaAnulados.has(clave)) return false;
|
|
593
|
+
return !vistos.has(clave);
|
|
524
594
|
});
|
|
525
595
|
|
|
526
596
|
// Limitar a los más recientes para evitar operaciones masivas
|
|
@@ -574,6 +644,8 @@ class FolioService {
|
|
|
574
644
|
body3.includes('efectuado anteriormente') ||
|
|
575
645
|
/anulad[oa]\s+anteriormente/i.test(body3)) {
|
|
576
646
|
rechazados.push({ folioDesde: iniA, folioHasta: finA, count, reason: 'ya-anulado' });
|
|
647
|
+
// Se recuerda para que la próxima corrida no lo reintente.
|
|
648
|
+
yaAnulados.add(`${iniA}-${finA}`);
|
|
577
649
|
console.warn(`[FolioService] ✗ Rango ${iniA}-${finA}: ya anulado (af_anular3)`);
|
|
578
650
|
try { fs.writeFileSync(path.join(debugDirA, `rango-${iniA}-${finA}-af_anular3-error.html`), body3, 'utf8'); } catch (_) {}
|
|
579
651
|
continue;
|
|
@@ -603,6 +675,11 @@ class FolioService {
|
|
|
603
675
|
bodyBulk.includes('SOLICITUD ANULACION DE FOLIOS');
|
|
604
676
|
if (exitoBulk) {
|
|
605
677
|
anulados.push({ folioDesde: iniA, folioHasta: finA, count });
|
|
678
|
+
// Un rango recién anulado sigue apareciendo en `consultarFolios` como
|
|
679
|
+
// "sin utilizar": si no se recuerda acá, la próxima corrida lo
|
|
680
|
+
// reintenta y el SII contesta "ya anulado". Ésta es la fuente
|
|
681
|
+
// principal de los rangos zombis.
|
|
682
|
+
yaAnulados.add(`${iniA}-${finA}`);
|
|
606
683
|
try { fs.writeFileSync(path.join(debugDirA, `rango-${iniA}-${finA}-af_anular-ok.html`), bodyBulk, 'utf8'); } catch (_) {}
|
|
607
684
|
console.log(`[FolioService] ✓ Rango ${iniA}-${finA} (${count} folios) anulado en bulk`);
|
|
608
685
|
continue;
|
|
@@ -643,6 +720,7 @@ class FolioService {
|
|
|
643
720
|
const ok = bs.includes('ha autorizado la anulaci') || bs.includes('SOLICITUD ANULACION DE FOLIOS');
|
|
644
721
|
if (ok) {
|
|
645
722
|
anulados.push({ folioDesde: folio, folioHasta: folio, count: 1 });
|
|
723
|
+
yaAnulados.add(`${folio}-${folio}`);
|
|
646
724
|
} else {
|
|
647
725
|
const razon = this._parseAnulacionResult(bs).reason || 'error';
|
|
648
726
|
rechazados.push({ folioDesde: folio, folioHasta: folio, count: 1, reason: razon });
|
|
@@ -655,9 +733,16 @@ class FolioService {
|
|
|
655
733
|
if (pasada < maxPasadas - 1) await this._sleep(1500);
|
|
656
734
|
}
|
|
657
735
|
|
|
736
|
+
if (yaAnulados.size !== yaAnuladosInicial) {
|
|
737
|
+
this._guardarAnulados(tipoDte, yaAnulados);
|
|
738
|
+
}
|
|
739
|
+
|
|
658
740
|
const totalAnulados = anulados.reduce((s, r) => s + r.count, 0);
|
|
659
741
|
const totalRechazados = rechazados.reduce((s, r) => s + r.count, 0);
|
|
660
|
-
console.log(
|
|
742
|
+
console.log(
|
|
743
|
+
`[FolioService] Completado: ${totalAnulados} anulados, ${totalRechazados} rechazados` +
|
|
744
|
+
(yaAnulados.size ? ` (${yaAnulados.size} rango(s) en memoria, se omiten en la próxima corrida)` : '')
|
|
745
|
+
);
|
|
661
746
|
|
|
662
747
|
return { ok: true, anulados, rechazados, totalAnulados, totalRechazados };
|
|
663
748
|
}
|
package/SiiCertificacion.js
CHANGED
|
@@ -1553,7 +1553,20 @@ class SiiCertificacion {
|
|
|
1553
1553
|
|
|
1554
1554
|
if (algunoRechazado) {
|
|
1555
1555
|
emitProgress(STEPS.SETS_REJECTED);
|
|
1556
|
-
|
|
1556
|
+
// El detalle va en el error, no solo en `estados`: el llamador suele
|
|
1557
|
+
// interpolar `error` en el mensaje que ve el usuario, y un literal
|
|
1558
|
+
// "Sets rechazados" rendía el inútil "Sets rechazados: Sets rechazados"
|
|
1559
|
+
// sin decir cuál set ni con qué estado. Se nombran solo los rechazados
|
|
1560
|
+
// para no ahogar el dato entre los que sí pasaron.
|
|
1561
|
+
const detalle = Object.values(estadosRelevantes)
|
|
1562
|
+
.filter(e => e.esRechazado)
|
|
1563
|
+
.map(e => `${e.nombre}: ${e.estado}`)
|
|
1564
|
+
.join('; ');
|
|
1565
|
+
return {
|
|
1566
|
+
success: false,
|
|
1567
|
+
error: detalle || 'el SII rechazó uno o más sets',
|
|
1568
|
+
estados: estadosRelevantes,
|
|
1569
|
+
};
|
|
1557
1570
|
}
|
|
1558
1571
|
}
|
|
1559
1572
|
|
package/cert/CertRunner.js
CHANGED
|
@@ -262,7 +262,19 @@ class CertRunner {
|
|
|
262
262
|
// usados se rechazan sin efecto.
|
|
263
263
|
for (const tipoDte of Object.keys(cafRequired)) {
|
|
264
264
|
try {
|
|
265
|
-
|
|
265
|
+
// ACOTADA a propósito. Solo interesan los folios que ESTA certificación
|
|
266
|
+
// timbró y dejó sin usar, que son de hoy. Sin el corte por antigüedad la
|
|
267
|
+
// limpieza barre el historial completo de la empresa: en un RUT con
|
|
268
|
+
// volumen real eso son cientos de anulaciones contra el SII y, peor, el
|
|
269
|
+
// SII cuenta los folios anulados de los últimos 6 meses EN CONTRA del
|
|
270
|
+
// cupo de timbraje — la limpieza terminaría provocando el bloqueo que
|
|
271
|
+
// intenta evitar. Verificado 2026-07-22: sin acotar anuló 296 folios de
|
|
272
|
+
// un RUT (rango 1851–2909) antes de detenerla a mano.
|
|
273
|
+
const limpieza = await this.folioService.anularFolios({
|
|
274
|
+
tipoDte: Number(tipoDte),
|
|
275
|
+
soloUltimosDias: 1,
|
|
276
|
+
maxRangos: 10,
|
|
277
|
+
});
|
|
266
278
|
if (limpieza.totalAnulados > 0) {
|
|
267
279
|
console.log(
|
|
268
280
|
` Tipo ${tipoDte}: ${limpieza.totalAnulados} folio(s) sin usar de intentos previos anulados`
|
|
@@ -3228,25 +3240,20 @@ class CertRunner {
|
|
|
3228
3240
|
* @param {string} opts.correoProveedor
|
|
3229
3241
|
* @returns {Promise<{success: boolean, mensaje?: string, error?: string}>}
|
|
3230
3242
|
*/
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
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) {
|
|
3237
3250
|
const https = require('https');
|
|
3238
3251
|
const crypto = require('crypto');
|
|
3239
3252
|
const CBE_BASE = 'https://www4.sii.cl/certBolElectDteInternet/';
|
|
3240
3253
|
const CBE_PERM = '0FC3D987613537E6E13E9BB93A406F13';
|
|
3241
3254
|
const CBE_POLICY = '082D0AC4BC4D75A5DF38F116C53877D4';
|
|
3242
3255
|
const CBE_SVC = 'cl.sii.sdi.diii.certBolElectDte.web.client.service.Facade';
|
|
3243
|
-
|
|
3244
|
-
const cookieJar = await this._obtenerCookiesSII();
|
|
3245
3256
|
const cookieStr = Object.entries(cookieJar).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
3246
|
-
const [rutNum, dvChar] = this.config.emisor.rut.replace(/\./g, '').split('-');
|
|
3247
|
-
const dvUp = dvChar.toUpperCase();
|
|
3248
|
-
const razonSocial = this.config.emisor.razon_social || this.config.emisor.razonSocial || '';
|
|
3249
|
-
|
|
3250
3257
|
const tlsOpts = {
|
|
3251
3258
|
rejectUnauthorized: false,
|
|
3252
3259
|
maxVersion: 'TLSv1.2',
|
|
@@ -3267,11 +3274,32 @@ class CertRunner {
|
|
|
3267
3274
|
...tlsOpts,
|
|
3268
3275
|
}, (res) => { const ch = []; res.on('data', c => ch.push(c)); res.on('end', () => resolve(Buffer.concat(ch).toString('utf-8'))); });
|
|
3269
3276
|
req.on('error', reject);
|
|
3270
|
-
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout gwtPost CBE
|
|
3277
|
+
req.setTimeout(30000, () => { req.destroy(); reject(new Error('timeout gwtPost CBE')); });
|
|
3271
3278
|
req.write(buf); req.end();
|
|
3272
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);
|
|
3273
3300
|
|
|
3274
|
-
// 1.
|
|
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).
|
|
3275
3303
|
const reprResp = await gwtPost(
|
|
3276
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|`
|
|
3277
3305
|
);
|
|
@@ -3281,17 +3309,67 @@ class CertRunner {
|
|
|
3281
3309
|
const reprTable = JSON.parse(reprTableStr);
|
|
3282
3310
|
rutRepreNum = reprTable.filter(s => /^\d{7,8}$/.test(s)).pop() || '';
|
|
3283
3311
|
} catch {}
|
|
3284
|
-
|
|
3312
|
+
const inscrita = !!rutRepreNum;
|
|
3313
|
+
if (!inscrita) {
|
|
3314
|
+
return { success: true, inscrita: false, listaParaDeclarar: false, estado: null };
|
|
3315
|
+
}
|
|
3285
3316
|
|
|
3286
|
-
// 2.
|
|
3317
|
+
// 2. Estado — P90 = 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.
|
|
3287
3319
|
const estadoResp = await gwtPost(
|
|
3288
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|`
|
|
3289
3321
|
);
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
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)' };
|
|
3295
3373
|
|
|
3296
3374
|
// 3. Obtener datos de postulacion: fchAutorizacion y longCharValue
|
|
3297
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.
|
|
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
|
|