@devlas/dte-sii 2.12.21 → 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 +166 -7
- 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;
|
|
@@ -244,6 +305,19 @@ class CafSolicitor {
|
|
|
244
305
|
this.session.saveSession(this.sessionPath);
|
|
245
306
|
}
|
|
246
307
|
|
|
308
|
+
// Rechazo duro por falta de Verificación de Actividades: debe detectarse ANTES de
|
|
309
|
+
// _processMultiStepFlow. La página de rechazo también contiene el string
|
|
310
|
+
// "of_solicita_folios_dcto" dentro del JS changeRegregion(), lo que hace que
|
|
311
|
+
// _processMultiStepFlow la confunda con el paso 2 normal y siga de largo el flujo
|
|
312
|
+
// multi-paso en vez de detectar el rechazo aquí, terminando en errorCode UNKNOWN.
|
|
313
|
+
if (response.body && response.body.includes('no registra Verificacion de Actividades')) {
|
|
314
|
+
return {
|
|
315
|
+
success: false,
|
|
316
|
+
errorCode: 'VERIFICACION_ACTIVIDADES_PENDIENTE',
|
|
317
|
+
error: 'SII: El RUT no tiene registrada la Verificación de Actividades para este tipo de documento. Solicítala en el sitio SII → Peticiones Administrativas → Verificación de actividad, o en oficinas del SII.',
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
247
321
|
// Procesar flujo multi-paso del SII
|
|
248
322
|
response = await this._processMultiStepFlow(response, rut, dv, tipoDte, cantidad, debugDir);
|
|
249
323
|
|
|
@@ -259,16 +333,47 @@ class CafSolicitor {
|
|
|
259
333
|
return { success: false, errorCode: 'WAAP_BLOCKED', error: 'IP bloqueada por el firewall del SII. Espera antes de reintentar.' };
|
|
260
334
|
}
|
|
261
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
|
+
|
|
262
351
|
// Verificar si obtuvimos el CAF
|
|
263
352
|
if (response.body && response.body.includes('<AUTORIZACION')) {
|
|
264
353
|
const cafPath = this._saveCafOrganized(response.body, tipoDte);
|
|
265
|
-
return {
|
|
354
|
+
return {
|
|
355
|
+
success: true,
|
|
356
|
+
cafPath,
|
|
357
|
+
xml: response.body,
|
|
358
|
+
maxAutor: this._lastMaxAutor ?? cantidad,
|
|
359
|
+
foliosDisp: this._lastFoliosDisp,
|
|
360
|
+
};
|
|
266
361
|
}
|
|
267
362
|
|
|
268
|
-
if (response.body && response.body
|
|
363
|
+
if (response.body && CafSolicitor.esBloqueoTimbraje(response.body)) {
|
|
269
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.' };
|
|
270
365
|
}
|
|
271
366
|
|
|
367
|
+
// Rechazo por Verificación de Actividades detectado en un paso posterior del flujo
|
|
368
|
+
// multi-paso (ver guarda defensiva en _processMultiStepFlow).
|
|
369
|
+
if (response.body && response.body.includes('no registra Verificacion de Actividades')) {
|
|
370
|
+
return {
|
|
371
|
+
success: false,
|
|
372
|
+
errorCode: 'VERIFICACION_ACTIVIDADES_PENDIENTE',
|
|
373
|
+
error: 'SII: El RUT no tiene registrada la Verificación de Actividades para este tipo de documento. Solicítala en el sitio SII → Peticiones Administrativas → Verificación de actividad, o en oficinas del SII.',
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
272
377
|
if (response.body && response.body.includes('Autenticaci')) {
|
|
273
378
|
return { success: false, errorCode: 'SESSION_EXPIRED', error: 'El SII devolvió página de autenticación' };
|
|
274
379
|
}
|
|
@@ -278,6 +383,26 @@ class CafSolicitor {
|
|
|
278
383
|
// (ej: ya hay un timbraje del mismo día que consume parte del cupo diario).
|
|
279
384
|
// Solución: reintentar con cantidad=1 para garantizar obtener al menos 1 folio.
|
|
280
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
|
+
}
|
|
281
406
|
if (cantidad > 1) {
|
|
282
407
|
console.warn(`[CafSolicitor] MAX_AUTOR excedido para ${cantidad} folios — reintentando con 1 folio...`);
|
|
283
408
|
// No limpiar cookieJar: el error es del formulario, no de la sesión.
|
|
@@ -375,6 +500,13 @@ class CafSolicitor {
|
|
|
375
500
|
*/
|
|
376
501
|
async _processMultiStepFlow(response, rut, dv, tipoDte, cantidad, debugDir) {
|
|
377
502
|
let currentHtml = response.body || '';
|
|
503
|
+
|
|
504
|
+
// Defensivo: mismo rechazo por Verificación de Actividades puede aparecer si el SII
|
|
505
|
+
// lo entrega recién en un paso posterior en vez del response inicial de solicitar().
|
|
506
|
+
if (currentHtml.includes('no registra Verificacion de Actividades')) {
|
|
507
|
+
return response; // solicitar() detectará el rechazo en response.body
|
|
508
|
+
}
|
|
509
|
+
|
|
378
510
|
const realFormAction = SiiSession.extractFormAction(currentHtml);
|
|
379
511
|
|
|
380
512
|
// Si el <form> real ya apunta a of_confirma_folio, el SII preseleccionó
|
|
@@ -406,7 +538,7 @@ class CafSolicitor {
|
|
|
406
538
|
|
|
407
539
|
// Rechazo duro antes del check de COD_DOCTO: la página de rechazo también contiene
|
|
408
540
|
// "COD_DOCTO" en su JavaScript, lo que causaría un POST innecesario con datos vacíos.
|
|
409
|
-
if (
|
|
541
|
+
if (CafSolicitor.esBloqueoTimbraje(currentHtml)) {
|
|
410
542
|
return response; // solicitar() detectará el bloqueo en response.body
|
|
411
543
|
}
|
|
412
544
|
|
|
@@ -429,7 +561,7 @@ class CafSolicitor {
|
|
|
429
561
|
currentHtml = response.body || '';
|
|
430
562
|
this._saveDebug(debugDir, 'select.html', currentHtml);
|
|
431
563
|
|
|
432
|
-
if (
|
|
564
|
+
if (CafSolicitor.esBloqueoTimbraje(currentHtml)) {
|
|
433
565
|
return response; // solicitar() detectará el bloqueo en response.body
|
|
434
566
|
}
|
|
435
567
|
}
|
|
@@ -447,15 +579,42 @@ class CafSolicitor {
|
|
|
447
579
|
*/
|
|
448
580
|
async _processStep3(response, rut, dv, tipoDte, cantidad, debugDir) {
|
|
449
581
|
let currentHtml = response.body || '';
|
|
450
|
-
|
|
582
|
+
|
|
583
|
+
if (currentHtml.includes('no registra Verificacion de Actividades')) {
|
|
584
|
+
return response; // solicitar() detectará el rechazo en response.body
|
|
585
|
+
}
|
|
586
|
+
|
|
451
587
|
const formAction3 = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_confirma_folio';
|
|
452
588
|
const inputs3 = SiiSession.extractInputValues(currentHtml);
|
|
453
589
|
|
|
454
590
|
// CANT_DOCTOS debe enviarse con un valor <= MAX_AUTOR.
|
|
455
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 !== '';
|
|
456
598
|
const maxAutor = parseInt(inputs3.MAX_AUTOR || String(cantidad), 10);
|
|
599
|
+
const foliosDisp = parseInt(inputs3.FOLIOS_DISP || '0', 10);
|
|
457
600
|
const cantReal = Math.min(cantidad, maxAutor);
|
|
458
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
|
+
}
|
|
459
618
|
|
|
460
619
|
const step3Fields = {
|
|
461
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",
|