@devlas/dte-sii 2.17.0 → 2.18.0
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/FolioService.js +23 -4
- package/cert/CertRunner.js +163 -10
- package/dte-sii.d.ts +10 -1
- package/package.json +1 -1
package/FolioService.js
CHANGED
|
@@ -362,7 +362,7 @@ class FolioService {
|
|
|
362
362
|
*
|
|
363
363
|
* @returns {Promise<{ ok: boolean, cafPath?: string, motivo?: string, disponibles?: number }>}
|
|
364
364
|
*/
|
|
365
|
-
async reobtenerCaf({ tipoDte, cantidad }) {
|
|
365
|
+
async reobtenerCaf({ tipoDte, cantidad, yaEmitido = null }) {
|
|
366
366
|
if (!this.cafSolicitor) {
|
|
367
367
|
return { ok: false, motivo: 'CafSolicitor no inicializado' };
|
|
368
368
|
}
|
|
@@ -375,11 +375,30 @@ class FolioService {
|
|
|
375
375
|
return { ok: false, motivo: err.message };
|
|
376
376
|
}
|
|
377
377
|
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
// ⚠️ El listado del portal NO dice si un folio ya se emitió.
|
|
379
|
+
//
|
|
380
|
+
// `anulado` es lo único que marca, y eso deja pasar los folios que ya viajaron al SII
|
|
381
|
+
// dentro de un envío. Emitir de nuevo con ellos hace que el SII rechace cada documento
|
|
382
|
+
// con `(DTE-3-101) Folio para este Tipo de documento ya fue recibido en el SII`.
|
|
383
|
+
//
|
|
384
|
+
// Pasó de verdad (24/08/2026): la corrida descartó los CAF en disco por tener folios
|
|
385
|
+
// ya emitidos y, tres líneas después, la reobtención trajo esos mismos folios del
|
|
386
|
+
// portal y los usó. Los 7 documentos del tipo 61 fueron rechazados.
|
|
387
|
+
//
|
|
388
|
+
// Por eso el llamador puede pasar `yaEmitido`: es el mismo registro con el que descarta
|
|
389
|
+
// los CAF de disco (`CertRunner._rangoYaConsumido`), aplicado también acá. Sin ese dato
|
|
390
|
+
// la reobtención no puede saberlo, porque el SII no lo publica.
|
|
391
|
+
const emitidos = yaEmitido
|
|
392
|
+
? rangos.filter(r => yaEmitido({ folioDesde: r.folioDesde, folioHasta: r.folioHasta }))
|
|
393
|
+
: [];
|
|
394
|
+
const emitidosSet = new Set(emitidos);
|
|
395
|
+
const usables = rangos.filter(r => !r.anulado && !emitidosSet.has(r));
|
|
396
|
+
const anulados = rangos.filter(r => r.anulado).length;
|
|
380
397
|
console.log(
|
|
381
398
|
`[FolioService] Tipo ${tipoDte}: ${rangos.length} rango(s) reobtenible(s), ` +
|
|
382
|
-
`${usables.length} usable(s)
|
|
399
|
+
`${usables.length} usable(s)` +
|
|
400
|
+
`${anulados ? `, ${anulados} anulado(s) descartado(s)` : ''}` +
|
|
401
|
+
`${emitidos.length ? `, ${emitidos.length} ya emitido(s) descartado(s)` : ''}`
|
|
383
402
|
);
|
|
384
403
|
|
|
385
404
|
const total = usables.reduce((n, r) => n + r.cantidad, 0);
|
package/cert/CertRunner.js
CHANGED
|
@@ -809,20 +809,34 @@ class CertRunner {
|
|
|
809
809
|
// devuelve sin gastar cupo. Por eso va acá: después de reusar lo que hay en disco y
|
|
810
810
|
// antes de `solicitarCafExacto`, que pide folios nuevos y, si falla, anula.
|
|
811
811
|
//
|
|
812
|
-
//
|
|
813
|
-
// trabaja: `FOLIOS_DISP > 0`, o el timbraje bloqueado (ahí el SII no publica el campo
|
|
814
|
-
// y queda `null`, así que hay que intentar a ciegas). Con FOLIOS_DISP=0 todos los
|
|
815
|
-
// folios timbrados ya se emitieron y reobtener devolvería folios consumidos:
|
|
816
|
-
// `listarReobtenibles` filtra los anulados pero no distingue usados, así que emitir
|
|
817
|
-
// con ellos haría que el SII rechace el envío entero por folio repetido.
|
|
812
|
+
// ── Cuándo reobtener: solo si NO se pueden pedir folios nuevos ──────────
|
|
818
813
|
//
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
//
|
|
814
|
+
// La reobtención es el último recurso antes de anular, no un atajo para ahorrar
|
|
815
|
+
// cupo, porque tiene un riesgo que pedir folios nuevos no tiene: el listado del
|
|
816
|
+
// portal **no marca los folios ya emitidos**, y emitir de nuevo con uno hace que el
|
|
817
|
+
// SII rechace el documento con `(DTE-3-101) Folio ... ya fue recibido en el SII`.
|
|
818
|
+
//
|
|
819
|
+
// El filtro `yaEmitido` de abajo tapa lo que sabemos, pero no alcanza solo: el
|
|
820
|
+
// registro local puede estar incompleto (corridas viejas, disco efímero). Medido el
|
|
821
|
+
// 24/08/2026: de los folios reobtenidos del tipo 61, los rangos 1-3 y 10 sí estaban
|
|
822
|
+
// registrados, pero el 4-6 no, y el SII también lo tenía. Los 7 documentos del envío
|
|
823
|
+
// fueron rechazados.
|
|
824
|
+
//
|
|
825
|
+
// Por eso vuelve a exigirse que el cupo NO alcance: con timbraje bloqueado (el SII no
|
|
826
|
+
// publica MAX_AUTOR y queda `null`) o con un tope racionado por debajo de lo que hace
|
|
827
|
+
// falta. Con cupo holgado —el caso de esa corrida, MAX_AUTOR=19 para 7 folios— se
|
|
828
|
+
// piden folios nuevos, que es la vía sin riesgo.
|
|
822
829
|
const topeTipo = this._topeConsultado?.[tipoDte];
|
|
823
|
-
|
|
830
|
+
const cupoAlcanza = topeTipo?.sinTope === true
|
|
831
|
+
|| (topeTipo?.maxAutor != null && topeTipo.maxAutor >= Number(cantidad));
|
|
832
|
+
if (topeTipo?.bloqueado || (!cupoAlcanza && (topeTipo?.foliosDisp ?? 0) > 0)) {
|
|
824
833
|
const reob = await this.folioService.reobtenerCaf({
|
|
825
834
|
tipoDte: Number(tipoDte), cantidad: Number(cantidad),
|
|
835
|
+
// El portal lista folios ya emitidos sin marcarlos: el único que lo sabe es este
|
|
836
|
+
// registro, el mismo con el que se descartan los CAF de disco unas líneas arriba.
|
|
837
|
+
// Sin pasarlo, la reobtención devuelve folios ya recibidos por el SII y todo el
|
|
838
|
+
// envío se rechaza documento por documento.
|
|
839
|
+
yaEmitido: (r) => this._rangoYaConsumido(Number(tipoDte), r.folioDesde, r.folioHasta),
|
|
826
840
|
});
|
|
827
841
|
if (reob.ok) {
|
|
828
842
|
// Puede ser más de uno: el SII entrega los folios reobtenidos de a uno y cada
|
|
@@ -1117,6 +1131,7 @@ class CertRunner {
|
|
|
1117
1131
|
await sleep(intervalo);
|
|
1118
1132
|
} else {
|
|
1119
1133
|
console.log(` [ERR] Contenido rechazado por SII (ENVIO CON ERRORES O REPAROS): ${(result.nombresConError || []).join(', ')}`);
|
|
1134
|
+
await this._diagnosticarEnviosConError(sets, result.nombresConError, debugPrefix);
|
|
1120
1135
|
break;
|
|
1121
1136
|
}
|
|
1122
1137
|
} else if (result.allRejected) {
|
|
@@ -1142,6 +1157,144 @@ class CertRunner {
|
|
|
1142
1157
|
return lastResult;
|
|
1143
1158
|
}
|
|
1144
1159
|
|
|
1160
|
+
/**
|
|
1161
|
+
* Cuando el portal dice "ENVIO CON ERRORES O REPAROS", pregunta POR QUÉ.
|
|
1162
|
+
*
|
|
1163
|
+
* El portal de certificación solo da el titular: nombra el set y dice que tiene errores
|
|
1164
|
+
* o reparos, sin un solo dato del documento culpable. El detalle vive en la consulta de
|
|
1165
|
+
* estado del envío, que se hace con el TrackId — y ese TrackId lo tenemos en la mano
|
|
1166
|
+
* desde que se subió el set.
|
|
1167
|
+
*
|
|
1168
|
+
* Hasta acá nadie preguntaba. Caso real (24/08/2026): una corrida quedó trabada con
|
|
1169
|
+
* "SET BASICO, SET GUIA DE DESPACHO" con errores, y en las 105 respuestas HTTP de la
|
|
1170
|
+
* etapa los TrackIds de esos sets aparecían solo en el `DTEUpload` que los creó y en el
|
|
1171
|
+
* formulario que los declaró. Ni el comercio ni nosotros teníamos forma de saber qué
|
|
1172
|
+
* corregir; la corrida siguió a libros, avance y simulación arrastrando el problema.
|
|
1173
|
+
*
|
|
1174
|
+
* ⚠️ Se consulta por SOAP (`QueryEstUp.jws`), NO por el REST de `consultarEstado()`: ese
|
|
1175
|
+
* apunta a `apicert.sii.cl/recursos/v1/boleta.electronica.envio/`, que es el servicio de
|
|
1176
|
+
* BOLETAS. Pasarle el TrackId de un set de facturas devuelve un resultado que no
|
|
1177
|
+
* corresponde, y el diagnóstico saldría equivocado justo cuando más se necesita.
|
|
1178
|
+
*
|
|
1179
|
+
* La respuesta cruda queda en el debug dir y también en la captura HTTP de la librería.
|
|
1180
|
+
*
|
|
1181
|
+
* Nunca es fatal: es diagnóstico. Si la consulta falla, se deja constancia y se sigue.
|
|
1182
|
+
*
|
|
1183
|
+
* @private
|
|
1184
|
+
* @param {Object} sets - El mismo mapa que se declaró: { setBasico: { trackId }, ... }
|
|
1185
|
+
* @param {string[]} nombresConError - Nombres tal como los escribe el portal
|
|
1186
|
+
* @param {string} debugPrefix - Prefijo de los archivos de debug de esta declaración
|
|
1187
|
+
*/
|
|
1188
|
+
async _diagnosticarEnviosConError(sets, nombresConError, debugPrefix) {
|
|
1189
|
+
// Espejo de la tabla `patterns` de SiiCertificacion.declararAvance: ahí se traduce de
|
|
1190
|
+
// fila del portal a clave, acá de vuelta a la clave para encontrar el TrackId.
|
|
1191
|
+
const NOMBRE_A_CLAVE = {
|
|
1192
|
+
'SET BASICO': 'setBasico',
|
|
1193
|
+
'SET GUIA DE DESPACHO': 'setGuiaDespacho',
|
|
1194
|
+
'SET FACTURA EXENTA': 'setFacturaExenta',
|
|
1195
|
+
'SET CASO GENERAL FACTURA COMPRA': 'setFacturaCompra',
|
|
1196
|
+
'SET DE SIMULACION': 'setSimulacion',
|
|
1197
|
+
'LIBRO DE VENTAS': 'libroVentas',
|
|
1198
|
+
'LIBRO DE COMPRAS': 'libroCompras',
|
|
1199
|
+
'LIBRO DE COMPRAS PARA EXENTOS': 'libroComprasExentos',
|
|
1200
|
+
'LIBRO DE GUIAS': 'libroGuias',
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
const nombres = nombresConError || [];
|
|
1204
|
+
if (!nombres.length) return;
|
|
1205
|
+
|
|
1206
|
+
console.log('\n──────────────────────────────────────────────────────────');
|
|
1207
|
+
console.log(' DIAGNÓSTICO: consultando al SII por qué rechazó');
|
|
1208
|
+
console.log('──────────────────────────────────────────────────────────');
|
|
1209
|
+
|
|
1210
|
+
let enviador = null;
|
|
1211
|
+
try {
|
|
1212
|
+
enviador = new EnviadorSII(this.certificado, this.ambiente);
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
console.warn(` [!] No se pudo crear el consultor de estado: ${err.message}`);
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const rut = this.config.emisor.rut;
|
|
1219
|
+
|
|
1220
|
+
// Se consultan TODOS los envíos declarados, no solo los marcados. El contraste es la
|
|
1221
|
+
// mitad del diagnóstico: si el que falló dice RPR y los demás EPR, el problema es de
|
|
1222
|
+
// ese documento; si todos vuelven RCT o RFR, es de la carátula o de la firma y el
|
|
1223
|
+
// portal solo alcanzó a marcar los que ya procesó.
|
|
1224
|
+
const conError = new Set(nombres.map(n => NOMBRE_A_CLAVE[n]).filter(Boolean));
|
|
1225
|
+
const claves = [...new Set([...conError, ...Object.keys(sets || {})])];
|
|
1226
|
+
|
|
1227
|
+
for (const clave of claves) {
|
|
1228
|
+
const nombre = Object.keys(NOMBRE_A_CLAVE).find(n => NOMBRE_A_CLAVE[n] === clave) || clave;
|
|
1229
|
+
const marca = conError.has(clave) ? '[CON ERRORES]' : '[sin marca en el portal]';
|
|
1230
|
+
const trackId = sets?.[clave]?.trackId;
|
|
1231
|
+
|
|
1232
|
+
if (!trackId) {
|
|
1233
|
+
console.log(` [?] ${nombre} ${marca}: sin TrackId a mano — no se puede consultar el detalle`);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
console.log(`\n ${nombre} ${marca} — TrackId ${trackId}`);
|
|
1238
|
+
|
|
1239
|
+
try {
|
|
1240
|
+
const res = await enviador.consultarEstadoSoap(trackId, rut);
|
|
1241
|
+
|
|
1242
|
+
// Cruda y completa: el resumen de abajo es para leer en el momento, el archivo es
|
|
1243
|
+
// para poder revisar después qué dijo exactamente el SII.
|
|
1244
|
+
try {
|
|
1245
|
+
fs.writeFileSync(
|
|
1246
|
+
path.join(this.debugDir, `${debugPrefix}-estado-${clave}-${trackId}.xml`),
|
|
1247
|
+
res?.xmlRaw || res?.respuesta || JSON.stringify(res, null, 2),
|
|
1248
|
+
'utf8'
|
|
1249
|
+
);
|
|
1250
|
+
} catch { /* el debug no puede tumbar el diagnóstico */ }
|
|
1251
|
+
|
|
1252
|
+
if (!res || res.ok === false) {
|
|
1253
|
+
console.log(` sin estado: ${res?.error || 'sin detalle'}`);
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
console.log(` estado=${res.estado ?? '?'} ${res.mensaje || ''}`.trimEnd());
|
|
1258
|
+
if (res.glosa) console.log(` glosa: ${res.glosa}`);
|
|
1259
|
+
this._imprimirDetalleEstado(res.xmlRaw);
|
|
1260
|
+
} catch (err) {
|
|
1261
|
+
console.log(` falló la consulta de estado: ${err.message}`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
console.log('──────────────────────────────────────────────────────────\n');
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
/**
|
|
1268
|
+
* Saca a la luz el detalle por documento que trae el XML de estado.
|
|
1269
|
+
*
|
|
1270
|
+
* `consultarEstadoSoap` parsea solo ESTADO, GLOSA y NUM_ATENCION, que es lo que necesita
|
|
1271
|
+
* para decidir si seguir esperando. El detalle de los reparos viene en el mismo XML y se
|
|
1272
|
+
* perdía: son las líneas que nombran el documento culpable.
|
|
1273
|
+
*
|
|
1274
|
+
* No se asume una estructura fija — el SII varía los nombres según el tipo de envío — así
|
|
1275
|
+
* que se imprimen los campos que aparezcan y el XML completo queda en el archivo.
|
|
1276
|
+
* @private
|
|
1277
|
+
*/
|
|
1278
|
+
_imprimirDetalleEstado(xml) {
|
|
1279
|
+
if (!xml) return;
|
|
1280
|
+
|
|
1281
|
+
// Cualquier bloque que hable de reparos, rechazos o detalle por documento.
|
|
1282
|
+
const bloques = xml.match(/<(DETALLE_REP|REPARO|RECHAZO|DETALLE)[\s\S]{0,600}?<\/\1>/gi) || [];
|
|
1283
|
+
for (const b of bloques.slice(0, 20)) {
|
|
1284
|
+
const campos = [...b.matchAll(/<([A-Z_]+)>([^<]+)<\/\1>/gi)]
|
|
1285
|
+
.map(m => `${m[1]}=${m[2].trim()}`)
|
|
1286
|
+
.filter(t => !/^DETALLE(_REP)?=/.test(t));
|
|
1287
|
+
if (campos.length) console.log(` · ${campos.join(' | ')}`);
|
|
1288
|
+
}
|
|
1289
|
+
if (bloques.length > 20) console.log(` · (+${bloques.length - 20} más — ver el XML en el debug)`);
|
|
1290
|
+
|
|
1291
|
+
// Contadores del envío: cuántos documentos aceptó, cuántos objetó. Aunque no haya
|
|
1292
|
+
// bloques de detalle, esto ya dice si el problema es de uno o de todos.
|
|
1293
|
+
const contadores = [...xml.matchAll(/<(NUM_DOC[A-Z_]*|ACEPTADOS|RECHAZADOS|REPAROS)>([^<]+)<\/\1>/gi)]
|
|
1294
|
+
.map(m => `${m[1]}=${m[2].trim()}`);
|
|
1295
|
+
if (contadores.length) console.log(` · ${contadores.join(' | ')}`);
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1145
1298
|
/**
|
|
1146
1299
|
* Declara avance de los sets ejecutados con reintentos automáticos
|
|
1147
1300
|
* @param {Object} [resultadosExt] - Resultados externos (usa this.resultados si no se pasa)
|
package/dte-sii.d.ts
CHANGED
|
@@ -652,7 +652,16 @@ export class FolioService {
|
|
|
652
652
|
* el portal reporta como anulados (los documentos emitidos con esos folios se
|
|
653
653
|
* rechazan).
|
|
654
654
|
*/
|
|
655
|
-
reobtenerCaf(options: {
|
|
655
|
+
reobtenerCaf(options: {
|
|
656
|
+
tipoDte: number;
|
|
657
|
+
cantidad: number;
|
|
658
|
+
/**
|
|
659
|
+
* Descarta los rangos que ya se emitieron. El listado del portal solo marca los
|
|
660
|
+
* ANULADOS, así que sin esto la reobtención devuelve folios que el SII ya recibió y
|
|
661
|
+
* cada documento vuelve rechazado con `DTE-3-101`.
|
|
662
|
+
*/
|
|
663
|
+
yaEmitido?: (rango: { folioDesde: number; folioHasta: number }) => boolean;
|
|
664
|
+
}): Promise<ReobtenerCafResult | null>;
|
|
656
665
|
/** El CAF más reciente de un tipo para este RUT y ambiente, o null. */
|
|
657
666
|
findLatestCaf(tipoDte: number): string | null;
|
|
658
667
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.18.0",
|
|
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",
|