@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 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
- * @returns {Promise<Object>} - { success, cafPath, xml, error }
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 { success: true, cafPath, xml: response.body, maxAutor: this._lastMaxAutor ?? cantidad };
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.includes('NO SE AUTORIZA')) {
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 (currentHtml.includes('NO SE AUTORIZA')) {
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 (currentHtml.includes('NO SE AUTORIZA')) {
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
- const nameMatch = tag.match(/name\s*=\s*"([^"]+)"/i);
624
- const valueMatch = tag.match(/value\s*=\s*"([^"]*)"/i);
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
  }
@@ -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
- // Usar solicitarCafConFallback que solicita y retorna el path
255
- const cafPath = await this.folioService.solicitarCafConFallback({
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 (!cafPath) {
261
- throw new Error(`No se pudo obtener CAF para tipo ${tipoDte}`);
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.22",
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",