@devlas/dte-sii 2.18.2 → 2.19.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.
- package/Certificado.js +2 -1
- package/Envio.js +2 -2
- package/README.md +29 -0
- package/dte-sii.d.ts +79 -12
- package/package.json +9 -3
- package/utils/rut.js +6 -2
package/Certificado.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const forge = require('node-forge');
|
|
10
|
+
const { formatRutSii } = require('./utils/rut');
|
|
10
11
|
const {
|
|
11
12
|
certError,
|
|
12
13
|
ERROR_CODES,
|
|
@@ -55,7 +56,7 @@ class Certificado {
|
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
// Usar datos extraídos por la utilidad centralizada
|
|
58
|
-
this.rut = pfxData.rut;
|
|
59
|
+
this.rut = formatRutSii(pfxData.rut);
|
|
59
60
|
this.nombre = pfxData.cn;
|
|
60
61
|
|
|
61
62
|
|
package/Envio.js
CHANGED
|
@@ -170,8 +170,8 @@ class EnvioDTE extends EnvioBase {
|
|
|
170
170
|
});
|
|
171
171
|
|
|
172
172
|
this.caratula = {
|
|
173
|
-
RutEmisor: caratula.RutEmisor,
|
|
174
|
-
RutEnvia: caratula.RutEnvia,
|
|
173
|
+
RutEmisor: formatRutSii(caratula.RutEmisor),
|
|
174
|
+
RutEnvia: formatRutSii(caratula.RutEnvia),
|
|
175
175
|
RutReceptor: caratula.RutReceptor || '',
|
|
176
176
|
FchResol: caratula.FchResol,
|
|
177
177
|
NroResol: caratula.NroResol,
|
package/README.md
CHANGED
|
@@ -38,6 +38,8 @@ npm install @devlas/dte-sii
|
|
|
38
38
|
- [Certificación SII](#certificación-sii)
|
|
39
39
|
- [Depuración: captura de llamadas al SII](#depuración-captura-de-llamadas-al-sii)
|
|
40
40
|
- [Ambientes](#ambientes)
|
|
41
|
+
- [Contribuir](#contribuir)
|
|
42
|
+
- [Seguridad](#seguridad)
|
|
41
43
|
- [Licencia](#licencia)
|
|
42
44
|
|
|
43
45
|
---
|
|
@@ -1078,6 +1080,33 @@ se ven correctos. Si generas el PDF417 por tu cuenta, respeta esa misma regla.
|
|
|
1078
1080
|
|
|
1079
1081
|
---
|
|
1080
1082
|
|
|
1083
|
+
## Contribuir
|
|
1084
|
+
|
|
1085
|
+
Los PRs son bienvenidos. Antes de escribir código, lee
|
|
1086
|
+
[CONTRIBUTING.md](CONTRIBUTING.md).
|
|
1087
|
+
|
|
1088
|
+
Esta librería emite documentos tributarios reales, así que el estándar de evidencia
|
|
1089
|
+
es más alto que en una librería común. En resumen: un PR por cambio, evidencia
|
|
1090
|
+
declarada por nivel (lectura, test, artefacto adjunto, respuesta del SII), y trazar
|
|
1091
|
+
el radio de impacto, porque un valor que entra acá sale por siete lugares distintos
|
|
1092
|
+
y el código de barras firmado es uno de ellos.
|
|
1093
|
+
|
|
1094
|
+
```bash
|
|
1095
|
+
npm test # suite completa, sin red ni SII
|
|
1096
|
+
npm run scan # barrido de datos reales (este repo es público)
|
|
1097
|
+
npm run types # verificación de dte-sii.d.ts
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
Las tres corren en CI sobre cada PR, en Node 18, 20 y 22.
|
|
1101
|
+
|
|
1102
|
+
Si trabajas con un agente de código, el repo trae la skill
|
|
1103
|
+
[`pr-dte-sii`](.claude/skills/pr-dte-sii/) con estas reglas en formato ejecutable.
|
|
1104
|
+
|
|
1105
|
+
## Seguridad
|
|
1106
|
+
|
|
1107
|
+
Si encuentras un problema de seguridad, **no abras un issue público**. Ver
|
|
1108
|
+
[SECURITY.md](SECURITY.md) para el canal privado y el alcance.
|
|
1109
|
+
|
|
1081
1110
|
## Licencia
|
|
1082
1111
|
|
|
1083
1112
|
MIT - Copyright (c) 2026 [Devlas SpA](https://devlas.cl)
|
package/dte-sii.d.ts
CHANGED
|
@@ -626,14 +626,73 @@ export interface FolioTope {
|
|
|
626
626
|
bloqueado?: boolean;
|
|
627
627
|
}
|
|
628
628
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
629
|
+
/** Un rango ya autorizado que el portal ofrece para reobtener. */
|
|
630
|
+
export interface RangoReobtenible {
|
|
631
|
+
/** Campos ocultos del formulario del portal. Hay que devolverlos tal cual para bajar el CAF. */
|
|
632
|
+
campos: Record<string, string>;
|
|
633
|
+
folioDesde: number;
|
|
634
|
+
folioHasta: number;
|
|
635
|
+
/** Folios que cubre el rango. */
|
|
636
|
+
cantidad: number;
|
|
637
|
+
/**
|
|
638
|
+
* true si el SII anuló el rango. Los documentos emitidos con esos folios se
|
|
639
|
+
* rechazan, así que hay que saltarlo.
|
|
640
|
+
*
|
|
641
|
+
* Ojo: el portal marca los ANULADOS y nada más. Un folio ya emitido aparece
|
|
642
|
+
* como reobtenible igual, y por eso `FolioService.reobtenerCaf` recibe
|
|
643
|
+
* `yaEmitido`: ese dato el SII no lo publica.
|
|
644
|
+
*/
|
|
645
|
+
anulado: boolean;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Resultado de `CafSolicitor.reobtenerCaf`, para un solo rango. */
|
|
649
|
+
export type ReobtenerRangoResult =
|
|
650
|
+
| {
|
|
651
|
+
success: true;
|
|
652
|
+
/** Ruta donde quedó el CAF descargado. */
|
|
653
|
+
cafPath: string;
|
|
654
|
+
/** El XML del CAF tal como lo entregó el portal. */
|
|
655
|
+
xml: string;
|
|
656
|
+
reobtenido: true;
|
|
657
|
+
folioDesde: number;
|
|
658
|
+
folioHasta: number;
|
|
659
|
+
otorgados: number;
|
|
660
|
+
}
|
|
661
|
+
| {
|
|
662
|
+
success: false;
|
|
663
|
+
errorCode:
|
|
664
|
+
| 'RANGO_ANULADO'
|
|
665
|
+
| 'REOBTENCION_SIN_FORMULARIO'
|
|
666
|
+
| 'REOBTENCION_SIN_DESCARGA'
|
|
667
|
+
| 'REOBTENCION_SIN_CAF';
|
|
668
|
+
[extra: string]: unknown;
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Resultado de `FolioService.reobtenerCaf`.
|
|
673
|
+
*
|
|
674
|
+
* Union discriminada por `ok` a propósito: obliga a comprobarlo antes de leer
|
|
675
|
+
* las rutas. El método **nunca devuelve null**, ni siquiera al fallar, así que
|
|
676
|
+
* `if (!resultado)` no detecta nada.
|
|
677
|
+
*/
|
|
678
|
+
export type ReobtenerCafResult =
|
|
679
|
+
| {
|
|
680
|
+
ok: true;
|
|
681
|
+
/** Rutas de los CAF recuperados. El SII entrega los rangos de a uno, así que pueden ser varios. */
|
|
682
|
+
cafPaths: string[];
|
|
683
|
+
/** Atajo al primero de `cafPaths`. */
|
|
684
|
+
cafPath: string;
|
|
685
|
+
}
|
|
686
|
+
| {
|
|
687
|
+
ok: false;
|
|
688
|
+
/** Qué impidió completar la reobtención, en texto legible. */
|
|
689
|
+
motivo: string;
|
|
690
|
+
/**
|
|
691
|
+
* Folios que sí se alcanzaron a cubrir, cuando el fallo es por cantidad.
|
|
692
|
+
* Ausente si el fallo fue antes de mirar los rangos.
|
|
693
|
+
*/
|
|
694
|
+
disponibles?: number;
|
|
695
|
+
};
|
|
637
696
|
|
|
638
697
|
export class FolioService {
|
|
639
698
|
constructor(config?: FolioServiceConfig);
|
|
@@ -661,7 +720,7 @@ export class FolioService {
|
|
|
661
720
|
* cada documento vuelve rechazado con `DTE-3-101`.
|
|
662
721
|
*/
|
|
663
722
|
yaEmitido?: (rango: { folioDesde: number; folioHasta: number }) => boolean;
|
|
664
|
-
}): Promise<ReobtenerCafResult
|
|
723
|
+
}): Promise<ReobtenerCafResult>;
|
|
665
724
|
/** El CAF más reciente de un tipo para este RUT y ambiente, o null. */
|
|
666
725
|
findLatestCaf(tipoDte: number): string | null;
|
|
667
726
|
/**
|
|
@@ -771,9 +830,17 @@ export class CafSolicitor {
|
|
|
771
830
|
* ⚠️ El listado incluye rangos ANULADOS sin distinguirlos: solo al abrir cada uno el
|
|
772
831
|
* portal lo avisa. Por eso `reobtenerCaf` hace un request por rango.
|
|
773
832
|
*/
|
|
774
|
-
listarReobtenibles(tipoDte: number): Promise<
|
|
775
|
-
/**
|
|
776
|
-
|
|
833
|
+
listarReobtenibles(tipoDte: number): Promise<RangoReobtenible[]>;
|
|
834
|
+
/**
|
|
835
|
+
* Descarga el CAF de un rango ya autorizado.
|
|
836
|
+
*
|
|
837
|
+
* `rango` es un elemento tal cual sale de `listarReobtenibles`: lleva los
|
|
838
|
+
* campos ocultos del formulario del portal en `campos`, que el portal exige de
|
|
839
|
+
* vuelta. No se puede construir a mano con solo los folios.
|
|
840
|
+
*
|
|
841
|
+
* **No devuelve el XML ni null**, devuelve un objeto con `success`.
|
|
842
|
+
*/
|
|
843
|
+
reobtenerCaf(tipoDte: number, rango: RangoReobtenible): Promise<ReobtenerRangoResult>;
|
|
777
844
|
}
|
|
778
845
|
|
|
779
846
|
// ============================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.1",
|
|
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",
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"author": "Devlas SpA <ti@devlas.cl> (https://devlas.cl)",
|
|
9
9
|
"homepage": "https://github.com/devlas-cl/dte-sii",
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "for f in test/*.test.js; do echo \"\n── $f\"; node \"$f\" || exit 1; done",
|
|
12
|
+
"scan": "node scripts/scan-datos-reales.js",
|
|
13
|
+
"types": "tsc --noEmit --strict --types node dte-sii.d.ts"
|
|
12
14
|
},
|
|
13
15
|
"repository": {
|
|
14
16
|
"type": "git",
|
|
@@ -49,5 +51,9 @@
|
|
|
49
51
|
"*.js",
|
|
50
52
|
"utils/",
|
|
51
53
|
"cert/"
|
|
52
|
-
]
|
|
54
|
+
],
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^26.3.0",
|
|
57
|
+
"typescript": "^7.0.2"
|
|
58
|
+
}
|
|
53
59
|
}
|
package/utils/rut.js
CHANGED
|
@@ -31,7 +31,10 @@ function formatRut(rut) {
|
|
|
31
31
|
*/
|
|
32
32
|
function cleanRut(rut) {
|
|
33
33
|
if (!rut) return '';
|
|
34
|
-
|
|
34
|
+
const cleaned = rut.replace(/[^0-9kK]/g, '').toUpperCase();
|
|
35
|
+
if (cleaned.length < 2) return cleaned;
|
|
36
|
+
const numero = cleaned.slice(0, -1).replace(/^0+(?=\d)/, '');
|
|
37
|
+
return `${numero || '0'}${cleaned.slice(-1)}`;
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
/**
|
|
@@ -74,7 +77,8 @@ function formatRutWithDots(rut) {
|
|
|
74
77
|
*/
|
|
75
78
|
function formatRutSii(rut) {
|
|
76
79
|
const { numero, dv } = splitRut(rut);
|
|
77
|
-
if (!numero) return '';
|
|
80
|
+
if (!numero && !dv) return '';
|
|
81
|
+
if (!dv) throw new Error('RUT demasiado corto para formato SII');
|
|
78
82
|
return `${numero}-${dv}`;
|
|
79
83
|
}
|
|
80
84
|
|