@devlas/dte-sii 2.12.22 → 2.12.23
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 +131 -6
- package/FolioService.js +148 -2
- package/SiiSession.js +17 -2
- package/cert/CertRunner.js +50 -9
- package/package.json +1 -1
package/CafSolicitor.js
CHANGED
|
@@ -187,17 +187,78 @@ class CafSolicitor {
|
|
|
187
187
|
);
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Detecta la página de bloqueo duro de timbraje del SII.
|
|
192
|
+
*
|
|
193
|
+
* El SII usa al menos DOS redacciones para la misma página de rechazo:
|
|
194
|
+
* - "NO SE AUTORIZA TIMBRAJE ELECTRÓNICO" (la documentada en PLAN-MEJORAS-CAF.md)
|
|
195
|
+
* - "NO AUTORIZA TIMBRAJE ELECTRÓNICA" (observada 2026-07-22 en tipo 56)
|
|
196
|
+
*
|
|
197
|
+
* Detectar solo la primera hacía que la segunda cayera al genérico
|
|
198
|
+
* `UNKNOWN: No se obtuvo CAF en la respuesta`, sin disparar el remedio de
|
|
199
|
+
* anular folios — que es justamente lo que la página pide hacer. Ambas
|
|
200
|
+
* variantes carecen de <form> e <input>, así que sin esta detección el flujo
|
|
201
|
+
* sigue de largo hasta fallar sin diagnóstico.
|
|
202
|
+
*
|
|
203
|
+
* @param {string} html - Cuerpo de la respuesta del SII
|
|
204
|
+
* @returns {boolean}
|
|
205
|
+
*/
|
|
206
|
+
/**
|
|
207
|
+
* Campos de tipo de documento del formulario de postulación (`pe_confirma`),
|
|
208
|
+
* verificados contra el form real del SII el 2026-07-22.
|
|
209
|
+
*
|
|
210
|
+
* El listado original omitía `CONIVA`, `SET11`, `BOLELE` y `BOLEXE`, que el
|
|
211
|
+
* SII sí incluye. Se exportan acá para que el consumidor no tenga que
|
|
212
|
+
* mantener la lista duplicada.
|
|
213
|
+
*/
|
|
214
|
+
static get CAMPOS_POSTULACION() {
|
|
215
|
+
return {
|
|
216
|
+
// Factura electrónica
|
|
217
|
+
ESFAC: 'S', // habilita la sección factura
|
|
218
|
+
FACT: 'S', // Facturas
|
|
219
|
+
CONIVA: 'S', // con IVA
|
|
220
|
+
NC: 'S', // Notas de crédito
|
|
221
|
+
ND: 'S', // Notas de débito
|
|
222
|
+
SET03: 'S', // Guías de despacho
|
|
223
|
+
SET06: 'S', // Facturas exentas
|
|
224
|
+
SET72: 'S', // Factura de compra
|
|
225
|
+
// Boleta electrónica
|
|
226
|
+
ESBOL: 'S', // habilita la sección boleta
|
|
227
|
+
BOLELE: 'S',
|
|
228
|
+
BOLELEC: 'S', // Boleta electrónica
|
|
229
|
+
BOLEXE: 'S',
|
|
230
|
+
BOLEXEN: 'S', // Boleta exenta electrónica
|
|
231
|
+
// NO se marcan por defecto:
|
|
232
|
+
// SET11 → Documentos de exportación (requiere trámite aduanero aparte)
|
|
233
|
+
// SET84 → Liquidación factura electrónica (caso de uso específico)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
static esBloqueoTimbraje(html) {
|
|
238
|
+
return /NO\s+(SE\s+)?AUTORIZA\s+TIMBRAJE/i.test(String(html || ''));
|
|
239
|
+
}
|
|
240
|
+
|
|
190
241
|
/**
|
|
191
242
|
* Solicita un CAF al SII
|
|
192
243
|
* @param {Object} params - Parámetros
|
|
193
244
|
* @param {number} params.tipoDte - Tipo de DTE (33, 34, 39, 56, 61, etc.)
|
|
194
245
|
* @param {number} [params.cantidad=1] - Cantidad de folios a solicitar
|
|
195
|
-
* @
|
|
246
|
+
* @param {number} [params.minCantidad] - Mínimo aceptable. Si el SII declara un
|
|
247
|
+
* MAX_AUTOR menor, se aborta ANTES de emitir en vez de aceptar un rango corto
|
|
248
|
+
* (ver `_processStep3`). Útil para flujos que necesitan N folios exactos, como
|
|
249
|
+
* la simulación de certificación.
|
|
250
|
+
* @returns {Promise<Object>} - { success, cafPath, xml, maxAutor, foliosDisp, error, errorCode }
|
|
196
251
|
*/
|
|
197
|
-
async solicitar({ tipoDte, cantidad = 1 }) {
|
|
252
|
+
async solicitar({ tipoDte, cantidad = 1, minCantidad = null }) {
|
|
198
253
|
const { numero: rut, dv } = splitRut(this.rutEmisor);
|
|
199
254
|
const debugDir = this._getDebugDir(tipoDte);
|
|
200
255
|
|
|
256
|
+
// Estado por-solicitud leído desde el form del SII en _processStep3
|
|
257
|
+
this._lastMaxAutor = null;
|
|
258
|
+
this._lastFoliosDisp = null;
|
|
259
|
+
this._maxAutorInsuficiente = false;
|
|
260
|
+
this._minCantidad = minCantidad;
|
|
261
|
+
|
|
201
262
|
// Rate limiting: mínimo 1001ms entre solicitudes para no saturar el portal SII.
|
|
202
263
|
const _now = Date.now();
|
|
203
264
|
const _elapsed = _now - CafSolicitor._lastSolicitudAt;
|
|
@@ -272,13 +333,34 @@ class CafSolicitor {
|
|
|
272
333
|
return { success: false, errorCode: 'WAAP_BLOCKED', error: 'IP bloqueada por el firewall del SII. Espera antes de reintentar.' };
|
|
273
334
|
}
|
|
274
335
|
|
|
336
|
+
// Abort temprano por MAX_AUTOR insuficiente (ver _processStep3). Se chequea antes
|
|
337
|
+
// que el resto porque en este caso no se llegó a emitir nada: el body es la página
|
|
338
|
+
// del paso previo, sin <AUTORIZACION> ni marcador de error propio.
|
|
339
|
+
if (this._maxAutorInsuficiente) {
|
|
340
|
+
return {
|
|
341
|
+
success: false,
|
|
342
|
+
errorCode: 'MAX_AUTOR_INSUFICIENTE',
|
|
343
|
+
maxAutor: this._lastMaxAutor,
|
|
344
|
+
foliosDisp: this._lastFoliosDisp,
|
|
345
|
+
error: `SII: autoriza como máximo ${this._lastMaxAutor} folio(s) para el tipo ${tipoDte} ` +
|
|
346
|
+
`y se requieren ${minCantidad} (FOLIOS_DISP=${this._lastFoliosDisp}). ` +
|
|
347
|
+
`Ocurre cuando hay folios timbrados sin utilizar — anúlalos o emite documentos de este tipo.`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
275
351
|
// Verificar si obtuvimos el CAF
|
|
276
352
|
if (response.body && response.body.includes('<AUTORIZACION')) {
|
|
277
353
|
const cafPath = this._saveCafOrganized(response.body, tipoDte);
|
|
278
|
-
return {
|
|
354
|
+
return {
|
|
355
|
+
success: true,
|
|
356
|
+
cafPath,
|
|
357
|
+
xml: response.body,
|
|
358
|
+
maxAutor: this._lastMaxAutor ?? cantidad,
|
|
359
|
+
foliosDisp: this._lastFoliosDisp,
|
|
360
|
+
};
|
|
279
361
|
}
|
|
280
362
|
|
|
281
|
-
if (response.body && response.body
|
|
363
|
+
if (response.body && CafSolicitor.esBloqueoTimbraje(response.body)) {
|
|
282
364
|
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.' };
|
|
283
365
|
}
|
|
284
366
|
|
|
@@ -301,6 +383,26 @@ class CafSolicitor {
|
|
|
301
383
|
// (ej: ya hay un timbraje del mismo día que consume parte del cupo diario).
|
|
302
384
|
// Solución: reintentar con cantidad=1 para garantizar obtener al menos 1 folio.
|
|
303
385
|
if (response.body && response.body.includes('menor o igual al m')) {
|
|
386
|
+
// Con `minCantidad` el llamador necesita N folios exactos: caer a 1 le
|
|
387
|
+
// entrega un CAF inservible Y suma +1 a FOLIOS_DISP (el folio queda sin
|
|
388
|
+
// usar), agravando el racionamiento en el próximo intento. Este retry
|
|
389
|
+
// preexistente bypaseaba el chequeo temprano de _processStep3, que solo
|
|
390
|
+
// cubre el MAX_AUTOR declarado en el formulario — no el efectivo, que el
|
|
391
|
+
// SII recién aplica al recibir el submit.
|
|
392
|
+
if (minCantidad && minCantidad > 1) {
|
|
393
|
+
console.warn(
|
|
394
|
+
`[CafSolicitor] Tipo ${tipoDte}: MAX_AUTOR efectivo insuficiente para ${cantidad} folios ` +
|
|
395
|
+
`y se requieren al menos ${minCantidad} — no se cae a 1 folio.`
|
|
396
|
+
);
|
|
397
|
+
return {
|
|
398
|
+
success: false,
|
|
399
|
+
errorCode: 'MAX_AUTOR_INSUFICIENTE',
|
|
400
|
+
maxAutor: this._lastMaxAutor,
|
|
401
|
+
foliosDisp: this._lastFoliosDisp,
|
|
402
|
+
error: `SII rechazó ${cantidad} folios para el tipo ${tipoDte} por exceder el máximo autorizado, ` +
|
|
403
|
+
`y se requieren al menos ${minCantidad}. Ocurre cuando hay folios timbrados sin utilizar.`,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
304
406
|
if (cantidad > 1) {
|
|
305
407
|
console.warn(`[CafSolicitor] MAX_AUTOR excedido para ${cantidad} folios — reintentando con 1 folio...`);
|
|
306
408
|
// No limpiar cookieJar: el error es del formulario, no de la sesión.
|
|
@@ -436,7 +538,7 @@ class CafSolicitor {
|
|
|
436
538
|
|
|
437
539
|
// Rechazo duro antes del check de COD_DOCTO: la página de rechazo también contiene
|
|
438
540
|
// "COD_DOCTO" en su JavaScript, lo que causaría un POST innecesario con datos vacíos.
|
|
439
|
-
if (
|
|
541
|
+
if (CafSolicitor.esBloqueoTimbraje(currentHtml)) {
|
|
440
542
|
return response; // solicitar() detectará el bloqueo en response.body
|
|
441
543
|
}
|
|
442
544
|
|
|
@@ -459,7 +561,7 @@ class CafSolicitor {
|
|
|
459
561
|
currentHtml = response.body || '';
|
|
460
562
|
this._saveDebug(debugDir, 'select.html', currentHtml);
|
|
461
563
|
|
|
462
|
-
if (
|
|
564
|
+
if (CafSolicitor.esBloqueoTimbraje(currentHtml)) {
|
|
463
565
|
return response; // solicitar() detectará el bloqueo en response.body
|
|
464
566
|
}
|
|
465
567
|
}
|
|
@@ -487,9 +589,32 @@ class CafSolicitor {
|
|
|
487
589
|
|
|
488
590
|
// CANT_DOCTOS debe enviarse con un valor <= MAX_AUTOR.
|
|
489
591
|
// Si se omite o excede MAX_AUTOR, el SII rechaza devolviendo la página de inicio (rechazo silencioso).
|
|
592
|
+
//
|
|
593
|
+
// MAX_AUTOR/FOLIOS_DISP solo aparecen cuando el SII está LIMITANDO este tipo de
|
|
594
|
+
// documento (confirmado experimentalmente 2026-07-22: tras anular folios, los tipos
|
|
595
|
+
// 56 y 61 dejaron de exponerlos). Su ausencia significa "sin tope" → no se recorta.
|
|
596
|
+
const tieneMaxAutor =
|
|
597
|
+
inputs3.MAX_AUTOR !== undefined && inputs3.MAX_AUTOR !== '';
|
|
490
598
|
const maxAutor = parseInt(inputs3.MAX_AUTOR || String(cantidad), 10);
|
|
599
|
+
const foliosDisp = parseInt(inputs3.FOLIOS_DISP || '0', 10);
|
|
491
600
|
const cantReal = Math.min(cantidad, maxAutor);
|
|
492
601
|
this._lastMaxAutor = maxAutor; // guardado para retornarlo desde solicitar()
|
|
602
|
+
this._lastFoliosDisp = tieneMaxAutor ? foliosDisp : null;
|
|
603
|
+
|
|
604
|
+
// El SII raciona MAX_AUTOR según los folios timbrados sin utilizar (FOLIOS_DISP):
|
|
605
|
+
// con FOLIOS_DISP=0 autoriza 100, con 2 ya baja a 1, y si sigue subiendo pasa a
|
|
606
|
+
// bloqueo duro ("NO SE AUTORIZA"). Aceptar un rango corto es contraproducente para
|
|
607
|
+
// quien necesita N folios exactos (ej. simulación de certificación): el CAF chico
|
|
608
|
+
// no se usa, suma +1 a FOLIOS_DISP y empeora el tope en el próximo intento.
|
|
609
|
+
// Por eso, si el llamador declaró un mínimo, se aborta ANTES de emitir.
|
|
610
|
+
if (tieneMaxAutor && this._minCantidad && maxAutor < this._minCantidad) {
|
|
611
|
+
this._maxAutorInsuficiente = true;
|
|
612
|
+
console.warn(
|
|
613
|
+
`[CafSolicitor] Tipo ${tipoDte}: MAX_AUTOR=${maxAutor} < mínimo requerido ${this._minCantidad} ` +
|
|
614
|
+
`(FOLIOS_DISP=${foliosDisp}) — abortando sin emitir para no agravar el racionamiento.`
|
|
615
|
+
);
|
|
616
|
+
return response;
|
|
617
|
+
}
|
|
493
618
|
|
|
494
619
|
const step3Fields = {
|
|
495
620
|
...inputs3,
|
package/FolioService.js
CHANGED
|
@@ -213,7 +213,10 @@ class FolioService {
|
|
|
213
213
|
try {
|
|
214
214
|
await this.solicitarCaf({ tipoDte, cantidad });
|
|
215
215
|
} catch (error) {
|
|
216
|
-
// Continuar con fallback
|
|
216
|
+
// Continuar con fallback, pero dejar rastro: sin este log era imposible
|
|
217
|
+
// diagnosticar por qué aparecían CAFs de 1 folio (el fallback de más abajo)
|
|
218
|
+
// en vez del rango pedido.
|
|
219
|
+
console.warn(`[FolioService] solicitarCaf tipo ${tipoDte} x${cantidad} falló: ${error.message} — probando fallback`);
|
|
217
220
|
}
|
|
218
221
|
|
|
219
222
|
let resolvedPath = this.findLatestCaf(tipoDte);
|
|
@@ -247,6 +250,143 @@ class FolioService {
|
|
|
247
250
|
return resolvedPath;
|
|
248
251
|
}
|
|
249
252
|
|
|
253
|
+
/**
|
|
254
|
+
* Cuenta los folios de un CAF leyendo su rango <D>/<H>.
|
|
255
|
+
* @private
|
|
256
|
+
* @returns {number} Cantidad de folios, o 0 si no se pudo parsear
|
|
257
|
+
*/
|
|
258
|
+
_contarFoliosCaf(cafPath) {
|
|
259
|
+
try {
|
|
260
|
+
const xml = fs.readFileSync(cafPath, 'utf8');
|
|
261
|
+
const d = xml.match(/<D>(\d+)<\/D>/);
|
|
262
|
+
const h = xml.match(/<H>(\d+)<\/H>/);
|
|
263
|
+
if (!d || !h) return 0;
|
|
264
|
+
return Number(h[1]) - Number(d[1]) + 1;
|
|
265
|
+
} catch (_) {
|
|
266
|
+
return 0;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Solicita un CAF garantizando una cantidad EXACTA de folios.
|
|
272
|
+
*
|
|
273
|
+
* A diferencia de `solicitarCafConFallback` — que acepta lo que el SII dé y
|
|
274
|
+
* cae a 1 folio como último recurso, algo razonable para reponer folios de
|
|
275
|
+
* emisión en runtime — este método sirve a flujos que necesitan N folios
|
|
276
|
+
* exactos en una sola pasada (ej. la simulación de certificación, que genera
|
|
277
|
+
* un plan fijo de documentos y muere si falta un folio).
|
|
278
|
+
*
|
|
279
|
+
* El SII raciona vía MAX_AUTOR según los folios timbrados sin utilizar
|
|
280
|
+
* (FOLIOS_DISP), y si se acumulan pasa a bloqueo duro. Anularlos revierte
|
|
281
|
+
* ambos estados — verificado contra maullin el 2026-07-22: anular 6 folios
|
|
282
|
+
* llevó FOLIOS_DISP 6→0 y MAX_AUTOR 1→5, y levantó los bloqueos duros de los
|
|
283
|
+
* tipos 56 y 61. Por eso acá se anula y se reintenta una vez.
|
|
284
|
+
*
|
|
285
|
+
* @param {Object} params
|
|
286
|
+
* @param {number} params.tipoDte - Tipo de DTE
|
|
287
|
+
* @param {number} params.cantidad - Folios requeridos (mínimo aceptable)
|
|
288
|
+
* @param {boolean} [params.permitirAnular=true] - Si puede anular folios sin
|
|
289
|
+
* usar para destrabar el tope. Ponerlo en false lo deja en modo consulta.
|
|
290
|
+
* @returns {Promise<Object>} { ok, cafPath, otorgados, maxAutor, foliosDisp, errorCode, error }
|
|
291
|
+
*/
|
|
292
|
+
async solicitarCafExacto({ tipoDte, cantidad, permitirAnular = true }) {
|
|
293
|
+
if (!this.cafSolicitor) {
|
|
294
|
+
throw new Error('FolioService: CafSolicitor no inicializado (se requiere pfxPath y pfxPassword)');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const pedir = () =>
|
|
298
|
+
this.cafSolicitor.solicitar({ tipoDte, cantidad, minCantidad: cantidad });
|
|
299
|
+
|
|
300
|
+
let result = await pedir();
|
|
301
|
+
|
|
302
|
+
// Ambos códigos tienen la misma causa raíz (folios sin utilizar acumulados)
|
|
303
|
+
// y el mismo remedio. MAX_AUTOR_INSUFICIENTE aborta antes de emitir;
|
|
304
|
+
// TIMBRAJE_BLOQUEADO es el estado terminal al que se llega si se ignora.
|
|
305
|
+
const recuperable =
|
|
306
|
+
result.errorCode === 'MAX_AUTOR_INSUFICIENTE' ||
|
|
307
|
+
result.errorCode === 'TIMBRAJE_BLOQUEADO';
|
|
308
|
+
|
|
309
|
+
if (!result.success && recuperable && permitirAnular) {
|
|
310
|
+
console.warn(
|
|
311
|
+
`[FolioService] Tipo ${tipoDte}: ${result.errorCode} — anulando folios sin utilizar y reintentando...`
|
|
312
|
+
);
|
|
313
|
+
try {
|
|
314
|
+
const anul = await this.anularFolios({ tipoDte });
|
|
315
|
+
|
|
316
|
+
// Desglosar los motivos reales del rechazo: no todos son "ya recepcionado"
|
|
317
|
+
// (folio consumido en un DTE enviado). También caen acá los folios ya
|
|
318
|
+
// anulados en una pasada anterior, que el SII sigue listando como
|
|
319
|
+
// candidatos pero rechaza al reintentar.
|
|
320
|
+
const motivos = (anul.rechazados || []).reduce((acc, r) => {
|
|
321
|
+
const k = r.reason || 'sin motivo';
|
|
322
|
+
acc[k] = (acc[k] || 0) + (r.count || 1);
|
|
323
|
+
return acc;
|
|
324
|
+
}, {});
|
|
325
|
+
const detalleMotivos = Object.entries(motivos)
|
|
326
|
+
.map(([k, v]) => `${v} ${k}`)
|
|
327
|
+
.join(', ');
|
|
328
|
+
console.log(
|
|
329
|
+
`[FolioService] Tipo ${tipoDte}: ${anul.totalAnulados} folio(s) anulado(s), ` +
|
|
330
|
+
`${anul.totalRechazados} rechazado(s)${detalleMotivos ? ` (${detalleMotivos})` : ''}`
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
if (anul.totalAnulados === 0) {
|
|
334
|
+
// Sin folios liberados, el tope del SII no se movió: reintentar es un
|
|
335
|
+
// request garantizado a fallar. Se corta acá con un diagnóstico preciso.
|
|
336
|
+
return {
|
|
337
|
+
ok: false,
|
|
338
|
+
cafPath: null,
|
|
339
|
+
otorgados: 0,
|
|
340
|
+
maxAutor: result.maxAutor ?? null,
|
|
341
|
+
foliosDisp: result.foliosDisp ?? null,
|
|
342
|
+
errorCode: 'SIN_FOLIOS_ANULABLES',
|
|
343
|
+
error:
|
|
344
|
+
`El SII tiene bloqueado el timbraje del tipo ${tipoDte} y no quedan folios anulables ` +
|
|
345
|
+
`(${anul.totalRechazados} rechazado(s)${detalleMotivos ? `: ${detalleMotivos}` : ''}). ` +
|
|
346
|
+
`Para destrabarlo hay que emitir y enviar al SII documentos de este tipo con los folios ya timbrados, ` +
|
|
347
|
+
`o esperar: el SII recalcula el tope según la emisión y las anulaciones de los últimos 6 meses.`,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
result = await pedir();
|
|
352
|
+
} catch (err) {
|
|
353
|
+
console.warn(`[FolioService] Tipo ${tipoDte}: anulación falló: ${err.message}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const base = {
|
|
358
|
+
maxAutor: result.maxAutor ?? null,
|
|
359
|
+
foliosDisp: result.foliosDisp ?? null,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
if (!result.success) {
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
cafPath: null,
|
|
366
|
+
otorgados: 0,
|
|
367
|
+
...base,
|
|
368
|
+
errorCode: result.errorCode || 'UNKNOWN',
|
|
369
|
+
error: result.error || `No se pudo obtener CAF para tipo ${tipoDte}`,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Defensa final: aunque MAX_AUTOR haya dado el visto bueno, verificar que el
|
|
374
|
+
// rango realmente alcance antes de dárselo por bueno al llamador.
|
|
375
|
+
const otorgados = this._contarFoliosCaf(result.cafPath);
|
|
376
|
+
if (otorgados < Number(cantidad)) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
cafPath: result.cafPath,
|
|
380
|
+
otorgados,
|
|
381
|
+
...base,
|
|
382
|
+
errorCode: 'FOLIOS_INSUFICIENTES',
|
|
383
|
+
error: `SII otorgó ${otorgados} folio(s) de ${cantidad} requeridos para el tipo ${tipoDte}.`,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return { ok: true, cafPath: result.cafPath, otorgados, ...base };
|
|
388
|
+
}
|
|
389
|
+
|
|
250
390
|
/**
|
|
251
391
|
* Consulta el estado de folios en el SII
|
|
252
392
|
* @param {Object} params - Parámetros
|
|
@@ -425,8 +565,14 @@ class FolioService {
|
|
|
425
565
|
continue;
|
|
426
566
|
}
|
|
427
567
|
|
|
568
|
+
// El SII responde "ha sido anulado anteriormente" (no "efectuado"), variante
|
|
569
|
+
// que las tres cadenas originales no cubrían: el rango caía al POST de
|
|
570
|
+
// af_anular con inputs vacíos y volvía una página de error con "Error 500",
|
|
571
|
+
// registrándose como fallo genérico en vez de "ya anulado". Verificado
|
|
572
|
+
// 2026-07-22 contra maullin, tipo 56 folios 2/4/5/6.
|
|
428
573
|
if (body3.includes('ya ha sido efectuado') || body3.includes('ya fue anulado') ||
|
|
429
|
-
body3.includes('efectuado anteriormente')
|
|
574
|
+
body3.includes('efectuado anteriormente') ||
|
|
575
|
+
/anulad[oa]\s+anteriormente/i.test(body3)) {
|
|
430
576
|
rechazados.push({ folioDesde: iniA, folioHasta: finA, count, reason: 'ya-anulado' });
|
|
431
577
|
console.warn(`[FolioService] ✗ Rango ${iniA}-${finA}: ya anulado (af_anular3)`);
|
|
432
578
|
try { fs.writeFileSync(path.join(debugDirA, `rango-${iniA}-${finA}-af_anular3-error.html`), body3, 'utf8'); } catch (_) {}
|
package/SiiSession.js
CHANGED
|
@@ -620,8 +620,23 @@ SiiSession.extractInputValues = function(html) {
|
|
|
620
620
|
const regex = /<input[^>]+>/gi;
|
|
621
621
|
const matches = html.match(regex) || [];
|
|
622
622
|
matches.forEach((tag) => {
|
|
623
|
-
|
|
624
|
-
|
|
623
|
+
// El SII mezcla estilos en el mismo formulario: unos atributos van con
|
|
624
|
+
// comillas y otros sin ellas (`value = 1`). Con solo el patrón entrecomillado,
|
|
625
|
+
// esos campos se enviaban vacíos y el SII rebotaba a la misma página sin
|
|
626
|
+
// explicar por qué — observado 2026-07-22 en pe_datos_empresa, donde
|
|
627
|
+
// EXISTE_PROD/EXISTE_CERT vienen sin comillas y trababan la postulación.
|
|
628
|
+
// El fallback sin comillas exige un separador antes del atributo: sin eso
|
|
629
|
+
// captura el `value=` que aparece DENTRO de handlers JS
|
|
630
|
+
// (`onblur="this.value=this.value.toUpperCase()"`), devolviendo basura
|
|
631
|
+
// como valor del campo.
|
|
632
|
+
const nameMatch =
|
|
633
|
+
tag.match(/name\s*=\s*"([^"]+)"/i) ||
|
|
634
|
+
tag.match(/name\s*=\s*'([^']+)'/i) ||
|
|
635
|
+
tag.match(/(?:^|[\s<])name\s*=\s*([^\s>"']+)/i);
|
|
636
|
+
const valueMatch =
|
|
637
|
+
tag.match(/value\s*=\s*"([^"]*)"/i) ||
|
|
638
|
+
tag.match(/value\s*=\s*'([^']*)'/i) ||
|
|
639
|
+
tag.match(/(?:^|[\s<])value\s*=\s*([^\s>"']+)/i);
|
|
625
640
|
if (nameMatch) {
|
|
626
641
|
inputs[nameMatch[1]] = valueMatch ? valueMatch[1] : '';
|
|
627
642
|
}
|
package/cert/CertRunner.js
CHANGED
|
@@ -247,23 +247,64 @@ class CertRunner {
|
|
|
247
247
|
this.folioHelper.counters.clear();
|
|
248
248
|
this.folioHelper.usedFolios.clear();
|
|
249
249
|
|
|
250
|
+
// Limpieza previa de folios timbrados y nunca utilizados.
|
|
251
|
+
//
|
|
252
|
+
// Cada corrida de simulación regenera su plan de documentos completo, así que
|
|
253
|
+
// los folios que quedaron de un intento fallido NO se reutilizan jamás: son
|
|
254
|
+
// basura garantizada. Pero el SII los cuenta como "disponibles sin utilizar"
|
|
255
|
+
// (FOLIOS_DISP) y baja el tope de timbraje (MAX_AUTOR) hasta bloquearlo del
|
|
256
|
+
// todo. Observado 2026-07-22 en un RUT de certificación: 7 intentos fallidos
|
|
257
|
+
// dejaron 21 folios muertos en los tipos 34 y 52, y llevaron a los tipos 56 y
|
|
258
|
+
// 61 a bloqueo duro irreversible (sin folios anulables restantes).
|
|
259
|
+
//
|
|
260
|
+
// Limpiar ANTES de pedir rompe ese espiral. Es seguro: solo se anulan folios
|
|
261
|
+
// que el SII reporta como no recepcionados, y los que ya fueron anulados o
|
|
262
|
+
// usados se rechazan sin efecto.
|
|
263
|
+
for (const tipoDte of Object.keys(cafRequired)) {
|
|
264
|
+
try {
|
|
265
|
+
const limpieza = await this.folioService.anularFolios({ tipoDte: Number(tipoDte) });
|
|
266
|
+
if (limpieza.totalAnulados > 0) {
|
|
267
|
+
console.log(
|
|
268
|
+
` Tipo ${tipoDte}: ${limpieza.totalAnulados} folio(s) sin usar de intentos previos anulados`
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
} catch (err) {
|
|
272
|
+
// No es fatal: si la limpieza falla, la solicitud de abajo igual puede
|
|
273
|
+
// funcionar, y si no, solicitarCafExacto reintenta anulando.
|
|
274
|
+
console.warn(`[CertRunner] Limpieza previa tipo ${tipoDte} falló: ${err.message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
250
278
|
for (const [tipoDte, cantidad] of Object.entries(cafRequired)) {
|
|
251
279
|
emitProgress(STEPS.CAF_REQUESTING, { tipo: Number(tipoDte) });
|
|
252
280
|
console.log(` Tipo ${tipoDte}: ${cantidad} folios...`);
|
|
253
|
-
|
|
254
|
-
//
|
|
255
|
-
|
|
281
|
+
|
|
282
|
+
// solicitarCafExacto (no ConFallback): la simulación genera un plan fijo de
|
|
283
|
+
// documentos y muere si falta un folio, así que un CAF corto no sirve. El
|
|
284
|
+
// método aborta antes de emitir si el SII no autoriza lo suficiente, anula
|
|
285
|
+
// folios sin utilizar para destrabar el tope, y reintenta.
|
|
286
|
+
const res = await this.folioService.solicitarCafExacto({
|
|
256
287
|
tipoDte: Number(tipoDte),
|
|
257
288
|
cantidad: Number(cantidad),
|
|
258
289
|
});
|
|
259
|
-
|
|
260
|
-
if (!
|
|
261
|
-
|
|
290
|
+
|
|
291
|
+
if (!res.ok) {
|
|
292
|
+
const detalle = [
|
|
293
|
+
res.maxAutor != null ? `MAX_AUTOR=${res.maxAutor}` : null,
|
|
294
|
+
res.foliosDisp != null ? `FOLIOS_DISP=${res.foliosDisp}` : null,
|
|
295
|
+
].filter(Boolean).join(', ');
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Folios insuficientes para tipo ${tipoDte}: se requieren ${cantidad} y el SII ` +
|
|
298
|
+
`otorgó ${res.otorgados}${detalle ? ` (${detalle})` : ''}. ` +
|
|
299
|
+
`El SII limita el timbraje cuando hay folios ya autorizados sin utilizar; ` +
|
|
300
|
+
`emite o anula documentos electrónicos de este tipo y reintenta. ` +
|
|
301
|
+
`[${res.errorCode}] ${res.error || ''}`.trim()
|
|
302
|
+
);
|
|
262
303
|
}
|
|
263
|
-
|
|
264
|
-
cafs[tipoDte] = cafPath;
|
|
304
|
+
|
|
305
|
+
cafs[tipoDte] = res.cafPath;
|
|
265
306
|
emitProgress(STEPS.CAF_OK, { tipo: Number(tipoDte) });
|
|
266
|
-
console.log(` ✓ CAF tipo ${tipoDte}`);
|
|
307
|
+
console.log(` ✓ CAF tipo ${tipoDte} (${res.otorgados} folios)`);
|
|
267
308
|
}
|
|
268
309
|
|
|
269
310
|
return cafs;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.23",
|
|
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",
|