@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 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;
@@ -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 { 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
+ };
266
361
  }
267
362
 
268
- if (response.body && response.body.includes('NO SE AUTORIZA')) {
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 (currentHtml.includes('NO SE AUTORIZA')) {
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 (currentHtml.includes('NO SE AUTORIZA')) {
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
- 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.21",
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",