@devlas/dte-sii 2.13.3 → 2.14.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/CafSolicitor.js +139 -4
- package/DTE.js +2 -2
- package/FolioService.js +149 -4
- package/Signer.js +2 -2
- package/SiiSession.js +35 -0
- package/cert/CertRunner.js +429 -22
- package/cert/SetBase.js +52 -1
- package/cert/SetBasico.js +3 -21
- package/cert/SetCompra.js +3 -21
- package/cert/SetExenta.js +3 -21
- package/cert/SetGuia.js +1 -15
- package/dte-sii.d.ts +29 -0
- package/package.json +2 -2
- package/utils/c14n.js +22 -17
- package/utils/httpDebug.js +14 -0
package/cert/SetBasico.js
CHANGED
|
@@ -104,11 +104,9 @@ class SetBasico extends SetBase {
|
|
|
104
104
|
const fs = require('fs');
|
|
105
105
|
|
|
106
106
|
// Cargar CAF
|
|
107
|
-
const cafXml =
|
|
108
|
-
const caf = new CAF(cafXml);
|
|
107
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
109
108
|
|
|
110
109
|
// Reservar folio
|
|
111
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
112
110
|
|
|
113
111
|
// Construir detalle e items
|
|
114
112
|
const detalle = buildDetalle(caso.items);
|
|
@@ -180,11 +178,9 @@ class SetBasico extends SetBase {
|
|
|
180
178
|
}
|
|
181
179
|
|
|
182
180
|
// Cargar CAF
|
|
183
|
-
const cafXml =
|
|
184
|
-
const caf = new CAF(cafXml);
|
|
181
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
185
182
|
|
|
186
183
|
// Reservar folio
|
|
187
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
188
184
|
|
|
189
185
|
// Determinar items a usar
|
|
190
186
|
let items = caso.itemsFromCaso
|
|
@@ -267,11 +263,9 @@ class SetBasico extends SetBase {
|
|
|
267
263
|
}
|
|
268
264
|
|
|
269
265
|
// Cargar CAF
|
|
270
|
-
const cafXml =
|
|
271
|
-
const caf = new CAF(cafXml);
|
|
266
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
272
267
|
|
|
273
268
|
// Reservar folio
|
|
274
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
275
269
|
|
|
276
270
|
// Items
|
|
277
271
|
const itemsFinal = caso.items || [{ nombre: 'SIN MONTO', cantidad: 1, precio: 0 }];
|
|
@@ -322,18 +316,6 @@ class SetBasico extends SetBase {
|
|
|
322
316
|
* Compatible con folioHelper de cert-base
|
|
323
317
|
* @private
|
|
324
318
|
*/
|
|
325
|
-
_reservarFolio(caf, cafXml) {
|
|
326
|
-
const cafFingerprint = this.folioHelper.createCafFingerprint(cafXml);
|
|
327
|
-
const folio = this.folioHelper.reserveNextFolio({
|
|
328
|
-
rutEmisor: this.config.emisor.rut,
|
|
329
|
-
tipoDte: caf.getTipoDTE(),
|
|
330
|
-
folioDesde: caf.getFolioDesde(),
|
|
331
|
-
folioHasta: caf.getFolioHasta(),
|
|
332
|
-
ambiente: this.config.ambiente || 'certificacion',
|
|
333
|
-
cafFingerprint,
|
|
334
|
-
});
|
|
335
|
-
return folio;
|
|
336
|
-
}
|
|
337
319
|
|
|
338
320
|
/**
|
|
339
321
|
* Obtiene la fecha de emisión en formato YYYY-MM-DD
|
package/cert/SetCompra.js
CHANGED
|
@@ -99,9 +99,7 @@ class SetCompra extends SetBase {
|
|
|
99
99
|
const { DTE, CAF, buildDetalleCompra, buildSetReferencia } = require('../index');
|
|
100
100
|
const fs = require('fs');
|
|
101
101
|
|
|
102
|
-
const cafXml =
|
|
103
|
-
const caf = new CAF(cafXml);
|
|
104
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
102
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
105
103
|
|
|
106
104
|
// Construir detalle con buildDetalleCompra (incluye CodImpAdic)
|
|
107
105
|
const items = this._normalizarItems(caso.items);
|
|
@@ -151,9 +149,7 @@ class SetCompra extends SetBase {
|
|
|
151
149
|
const { DTE, CAF, buildDetalleCompra, buildSetReferencia } = require('../index');
|
|
152
150
|
const fs = require('fs');
|
|
153
151
|
|
|
154
|
-
const cafXml =
|
|
155
|
-
const caf = new CAF(cafXml);
|
|
156
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
152
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
157
153
|
|
|
158
154
|
// Obtener documento referenciado
|
|
159
155
|
const docRef = this._docRefs[caso.referenciaCaso];
|
|
@@ -219,9 +215,7 @@ class SetCompra extends SetBase {
|
|
|
219
215
|
const { DTE, CAF, buildDetalleCompra, buildSetReferencia } = require('../index');
|
|
220
216
|
const fs = require('fs');
|
|
221
217
|
|
|
222
|
-
const cafXml =
|
|
223
|
-
const caf = new CAF(cafXml);
|
|
224
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
218
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
225
219
|
|
|
226
220
|
// Obtener documento referenciado
|
|
227
221
|
const docRef = this._docRefs[caso.referenciaCaso];
|
|
@@ -394,18 +388,6 @@ class SetCompra extends SetBase {
|
|
|
394
388
|
* Reserva el siguiente folio disponible
|
|
395
389
|
* @private
|
|
396
390
|
*/
|
|
397
|
-
_reservarFolio(caf, cafXml) {
|
|
398
|
-
const cafFingerprint = this.folioHelper.createCafFingerprint(cafXml);
|
|
399
|
-
const folio = this.folioHelper.reserveNextFolio({
|
|
400
|
-
rutEmisor: this.config.emisor.rut,
|
|
401
|
-
tipoDte: caf.getTipoDTE(),
|
|
402
|
-
folioDesde: caf.getFolioDesde(),
|
|
403
|
-
folioHasta: caf.getFolioHasta(),
|
|
404
|
-
ambiente: this.config.ambiente || 'certificacion',
|
|
405
|
-
cafFingerprint,
|
|
406
|
-
});
|
|
407
|
-
return folio;
|
|
408
|
-
}
|
|
409
391
|
|
|
410
392
|
/**
|
|
411
393
|
* Obtiene la fecha de emisión
|
package/cert/SetExenta.js
CHANGED
|
@@ -104,9 +104,7 @@ class SetExenta extends SetBase {
|
|
|
104
104
|
const { DTE, CAF, buildDetalle, calcularTotalesDesdeDetalle, buildSetReferencia } = require('../index');
|
|
105
105
|
const fs = require('fs');
|
|
106
106
|
|
|
107
|
-
const cafXml =
|
|
108
|
-
const caf = new CAF(cafXml);
|
|
109
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
107
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
110
108
|
|
|
111
109
|
// Marcar todos los items como exentos
|
|
112
110
|
const items = this._normalizarItemsExentos(caso.items);
|
|
@@ -156,9 +154,7 @@ class SetExenta extends SetBase {
|
|
|
156
154
|
const { DTE, CAF, buildDetalle, calcularTotalesDesdeDetalle, buildSetReferencia } = require('../index');
|
|
157
155
|
const fs = require('fs');
|
|
158
156
|
|
|
159
|
-
const cafXml =
|
|
160
|
-
const caf = new CAF(cafXml);
|
|
161
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
157
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
162
158
|
|
|
163
159
|
// Obtener documento referenciado
|
|
164
160
|
const docRef = this._docRefs[caso.referenciaCaso];
|
|
@@ -224,9 +220,7 @@ class SetExenta extends SetBase {
|
|
|
224
220
|
const { DTE, CAF, buildDetalle, calcularTotalesDesdeDetalle, buildSetReferencia } = require('../index');
|
|
225
221
|
const fs = require('fs');
|
|
226
222
|
|
|
227
|
-
const cafXml =
|
|
228
|
-
const caf = new CAF(cafXml);
|
|
229
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
223
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
230
224
|
|
|
231
225
|
// Obtener documento referenciado
|
|
232
226
|
const docRef = this._docRefs[caso.referenciaCaso];
|
|
@@ -412,18 +406,6 @@ class SetExenta extends SetBase {
|
|
|
412
406
|
* Reserva el siguiente folio disponible
|
|
413
407
|
* @private
|
|
414
408
|
*/
|
|
415
|
-
_reservarFolio(caf, cafXml) {
|
|
416
|
-
const cafFingerprint = this.folioHelper.createCafFingerprint(cafXml);
|
|
417
|
-
const folio = this.folioHelper.reserveNextFolio({
|
|
418
|
-
rutEmisor: this.config.emisor.rut,
|
|
419
|
-
tipoDte: caf.getTipoDTE(),
|
|
420
|
-
folioDesde: caf.getFolioDesde(),
|
|
421
|
-
folioHasta: caf.getFolioHasta(),
|
|
422
|
-
ambiente: this.config.ambiente || 'certificacion',
|
|
423
|
-
cafFingerprint,
|
|
424
|
-
});
|
|
425
|
-
return folio;
|
|
426
|
-
}
|
|
427
409
|
|
|
428
410
|
/**
|
|
429
411
|
* Obtiene la fecha de emisión
|
package/cert/SetGuia.js
CHANGED
|
@@ -82,11 +82,9 @@ class SetGuia extends SetBase {
|
|
|
82
82
|
const fs = require('fs');
|
|
83
83
|
|
|
84
84
|
// Cargar CAF
|
|
85
|
-
const cafXml =
|
|
86
|
-
const caf = new CAF(cafXml);
|
|
85
|
+
const { caf, cafXml, folio } = this._tomarFolio(cafPath);
|
|
87
86
|
|
|
88
87
|
// Reservar folio
|
|
89
|
-
const folio = this._reservarFolio(caf, cafXml);
|
|
90
88
|
|
|
91
89
|
// Construir detalle (buildDetalleGuia siempre incluye QtyItem)
|
|
92
90
|
const items = this._normalizarItems(caso.items);
|
|
@@ -178,18 +176,6 @@ class SetGuia extends SetBase {
|
|
|
178
176
|
* Reserva el siguiente folio disponible
|
|
179
177
|
* @private
|
|
180
178
|
*/
|
|
181
|
-
_reservarFolio(caf, cafXml) {
|
|
182
|
-
const cafFingerprint = this.folioHelper.createCafFingerprint(cafXml);
|
|
183
|
-
const folio = this.folioHelper.reserveNextFolio({
|
|
184
|
-
rutEmisor: this.config.emisor.rut,
|
|
185
|
-
tipoDte: caf.getTipoDTE(),
|
|
186
|
-
folioDesde: caf.getFolioDesde(),
|
|
187
|
-
folioHasta: caf.getFolioHasta(),
|
|
188
|
-
ambiente: this.config.ambiente || 'certificacion',
|
|
189
|
-
cafFingerprint,
|
|
190
|
-
});
|
|
191
|
-
return folio;
|
|
192
|
-
}
|
|
193
179
|
|
|
194
180
|
/**
|
|
195
181
|
* Obtiene la fecha de emisión en formato YYYY-MM-DD
|
package/dte-sii.d.ts
CHANGED
|
@@ -622,6 +622,17 @@ export interface FolioTope {
|
|
|
622
622
|
maxAutor: number | null;
|
|
623
623
|
/** Folios disponibles según el SII, o null si no vino. */
|
|
624
624
|
foliosDisp: number | null;
|
|
625
|
+
/** true si el SII bloqueó el timbraje para este tipo. Tiene prioridad sobre `sinTope`. */
|
|
626
|
+
bloqueado?: boolean;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export interface ReobtenerCafResult {
|
|
630
|
+
/** Rutas de los CAF recuperados. El SII entrega los folios de a uno, así que pueden ser varios. */
|
|
631
|
+
paths: string[];
|
|
632
|
+
/** Cantidad total de folios recuperados entre todos los rangos. */
|
|
633
|
+
folios: number;
|
|
634
|
+
/** Rangos que se descartaron por estar anulados en el SII. */
|
|
635
|
+
descartados: Array<{ desde: number; hasta: number; motivo: string }>;
|
|
625
636
|
}
|
|
626
637
|
|
|
627
638
|
export class FolioService {
|
|
@@ -633,6 +644,15 @@ export class FolioService {
|
|
|
633
644
|
* anular antes de pedir, en vez de anular a ciegas.
|
|
634
645
|
*/
|
|
635
646
|
consultarTope(options: { tipoDte: number }): Promise<FolioTope>;
|
|
647
|
+
/**
|
|
648
|
+
* Recupera CAF ya autorizados desde el portal, sin gastar cupo de timbraje.
|
|
649
|
+
*
|
|
650
|
+
* Es la salida cuando el SII bloqueó el timbraje: los folios ya están autorizados,
|
|
651
|
+
* solo se perdió el archivo. Junta tantos rangos como haga falta y descarta los que
|
|
652
|
+
* el portal reporta como anulados (los documentos emitidos con esos folios se
|
|
653
|
+
* rechazan).
|
|
654
|
+
*/
|
|
655
|
+
reobtenerCaf(options: { tipoDte: number; cantidad: number }): Promise<ReobtenerCafResult | null>;
|
|
636
656
|
}
|
|
637
657
|
|
|
638
658
|
export interface FolioRegistryOptions {
|
|
@@ -698,6 +718,15 @@ export class CafSolicitor {
|
|
|
698
718
|
* (`solicitar(tipoDte, cantidad)`) no coincidía con la implementación.
|
|
699
719
|
*/
|
|
700
720
|
solicitar(options: CafSolicitarOptions): Promise<CafSolicitarResult>;
|
|
721
|
+
/**
|
|
722
|
+
* Lista los rangos de folios ya autorizados que el portal permite volver a descargar.
|
|
723
|
+
*
|
|
724
|
+
* ⚠️ El listado incluye rangos ANULADOS sin distinguirlos: solo al abrir cada uno el
|
|
725
|
+
* portal lo avisa. Por eso `reobtenerCaf` hace un request por rango.
|
|
726
|
+
*/
|
|
727
|
+
listarReobtenibles(tipoDte: number): Promise<Array<{ desde: number; hasta: number; raw?: string }>>;
|
|
728
|
+
/** Descarga el CAF de un rango ya autorizado. Devuelve null si el rango está anulado. */
|
|
729
|
+
reobtenerCaf(tipoDte: number, rango: { desde: number; hasta: number }): Promise<string | null>;
|
|
701
730
|
}
|
|
702
731
|
|
|
703
732
|
// ============================================
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.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,7 @@
|
|
|
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": "node test/pfx.test.js"
|
|
11
|
+
"test": "node test/pfx.test.js && node test/folios-consolidados.test.js && node test/c14n-apostrofe.test.js"
|
|
12
12
|
},
|
|
13
13
|
"repository": {
|
|
14
14
|
"type": "git",
|
package/utils/c14n.js
CHANGED
|
@@ -38,23 +38,29 @@ function escapeText(text) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* ⚠️ NO agregar acá un paso que escape apóstrofes o comillas en el contenido.
|
|
42
|
+
*
|
|
43
|
+
* Existía un `fixEntities()` que recorría el XML canónico y convertía `'` en `'` y
|
|
44
|
+
* `"` en `"` dentro del texto. Canonical XML (REC-xml-c14n-20010315, §1.3 y §2.3)
|
|
45
|
+
* escapa en nodos de texto SOLO `&`, `<`, `>` y el retorno de carro; el apóstrofe y la
|
|
46
|
+
* comilla doble quedan literales. `escapeText()` acá arriba ya hace exactamente eso.
|
|
47
|
+
*
|
|
48
|
+
* Con ese paso de más, la forma canónica que firmábamos dejaba de ser la que el SII
|
|
49
|
+
* calcula al recibir el documento, así que el DigestValue no coincidía y el SII rechazaba
|
|
50
|
+
* el ENVÍO COMPLETO con `RFR - Rechazado por Error en Firma` — sin decir cuál documento ni
|
|
51
|
+
* por qué. Y solo pasaba cuando algún texto traía un apóstrofe, que es lo que lo hacía
|
|
52
|
+
* tan difícil de ver: los mismos sets pasaban o fallaban según el contenido del set.
|
|
53
|
+
*
|
|
54
|
+
* Medido el 14/08/2026 (RUT 78206276-K, maullin). Un solo ítem, "CAPACITACION USO PLC'S
|
|
55
|
+
* CNC", en un único documento del Set Factura Exenta:
|
|
56
|
+
*
|
|
57
|
+
* Set Básico → EPR 0 documentos con digest divergente
|
|
58
|
+
* Set Guía → EPR 0
|
|
59
|
+
* Set Exenta → RFR 1 (nota de débito 56/2616, la del apóstrofe)
|
|
60
|
+
* Set Compra → EPR 0
|
|
61
|
+
*
|
|
62
|
+
* El envío entero se cae por un carácter en un ítem. Ver `test/c14n-apostrofe.test.js`.
|
|
44
63
|
*/
|
|
45
|
-
function fixEntities(xml) {
|
|
46
|
-
let out = '';
|
|
47
|
-
let inContent = false;
|
|
48
|
-
for (let i = 0; i < xml.length; i++) {
|
|
49
|
-
const ch = xml[i];
|
|
50
|
-
if (ch === '>') inContent = true;
|
|
51
|
-
if (ch === '<') inContent = false;
|
|
52
|
-
if (inContent && ch === "'") { out += '''; continue; }
|
|
53
|
-
if (inContent && ch === '"') { out += '"'; continue; }
|
|
54
|
-
out += ch;
|
|
55
|
-
}
|
|
56
|
-
return out;
|
|
57
|
-
}
|
|
58
64
|
|
|
59
65
|
/**
|
|
60
66
|
* Serializa un nodo DOM a XML canónico
|
|
@@ -257,7 +263,6 @@ module.exports = {
|
|
|
257
263
|
// Escape/Text functions
|
|
258
264
|
escapeAttr,
|
|
259
265
|
escapeText,
|
|
260
|
-
fixEntities,
|
|
261
266
|
// Serialization
|
|
262
267
|
serializeNode,
|
|
263
268
|
serializeElement,
|
package/utils/httpDebug.js
CHANGED
|
@@ -171,6 +171,20 @@ function registrarHttpDebug(info = {}) {
|
|
|
171
171
|
truncado,
|
|
172
172
|
archivo: nombre,
|
|
173
173
|
archivoRequest: archivoReq,
|
|
174
|
+
// El cuerpo enviado va TAMBIÉN acá, no solo en el HTML.
|
|
175
|
+
//
|
|
176
|
+
// `index.jsonl` es lo único que la purga conserva siempre (ver cert-purga.ts), y
|
|
177
|
+
// el HTML con su cabecera es lo primero que se descarta. Depurando el 14/08/2026
|
|
178
|
+
// hacia atrás quedó a la vista: del día anterior se podía leer QUÉ endpoints se
|
|
179
|
+
// llamaron, pero no CON QUÉ datos, que era justo lo necesario para comparar una
|
|
180
|
+
// corrida buena contra una fallada.
|
|
181
|
+
//
|
|
182
|
+
// Se recorta a 2000 caracteres: el índice se guarda un año y tiene que seguir
|
|
183
|
+
// siendo liviano. El cuerpo completo, mientras exista, está en el HTML o en el
|
|
184
|
+
// `-request.txt`.
|
|
185
|
+
reqBody: reqTexto
|
|
186
|
+
? (reqTexto.length > 2000 ? reqTexto.slice(0, 2000) + ` …${REDACTADO} recortado` : reqTexto)
|
|
187
|
+
: null,
|
|
174
188
|
headers: redactarHeaders(headers),
|
|
175
189
|
}) + '\n',
|
|
176
190
|
'utf-8',
|