Saltar a contenido

Resiliencia e idempotencia

Cada llamada externa está protegida por reintentos, un circuit breaker y un timeout.

Política de reintentos

Solo se reintentan las fallas transitorias:

  • errores de red y timeouts
  • HTTP 408, 429, 5xx

Un e-CF rechazado (por ejemplo 400, 401) nunca se reenvía a ciegas.

Opción Valor por defecto
Intentos máximos 3
Umbral del breaker 5 fallas consecutivas
Semiabierto después de 30 s
Timeout por intento 15 s
const ecf = new ECF(credentials, Environment.Prod, {
  resilience: { maxRetries: 3, breakerThreshold: 5, timeoutMs: 15_000 },
});
from py_dgii_ecf import ECF, ResilienceOptions

ecf = ECF(credentials, resilience=ResilienceOptions(max_retries=3))

Idempotencia

Los envíos se identifican por RNC + eNCF + SHA-256(signed XML). Una llamada repetida devuelve la respuesta en caché en lugar de emitir dos veces. El mismo flujo cubre e-CF, resumen (RFCE), aprobación comercial (ACECF) y anulación de rango (ANECF); la anulación usa RangoNCF en lugar de eNCF.

Usa un almacén persistente (Redis/Postgres) en producción:

const ecf = new ECF(credentials, Environment.Prod, {
  idempotencyStore: myRedisStore, // { get, set, delete }
});
ecf = ECF(credentials, idempotency_store=my_redis_store)

Persiste antes de transmitir

Guarda el XML firmado, su hash y la clave de idempotencia antes de la llamada de red, para que un envío interrumpido se pueda conciliar en lugar de reemitirse.

Vida útil del almacén en memoria

El InMemoryIdempotencyStore por defecto expira las entradas a las 24 horas y las barre cuando el mapa pasa de 1.000 entradas, de modo que un proceso de larga duración no crece sin límite. Ambos valores son configurables:

const store = new InMemoryIdempotencyStore({ ttlMs: 60 * 60 * 1000 });
from py_dgii_ecf import InMemoryIdempotencyStore

store = InMemoryIdempotencyStore(ttl_s=3600)

Sigue siendo local al proceso: usa Redis o Postgres cuando más de una instancia pueda enviar el mismo documento.

Reconciliar un envío pendiente

Un envío escribe un marcador pendiente antes de transmitir. Si el proceso muere a mitad de camino, el marcador queda y el siguiente intento con el mismo documento devuelve IDEMPOTENCY en lugar de reenviar. Ese es el default seguro: un reenvío nunca debe duplicar un comprobante fiscal.

reconcileSubmission / reconcile_submission resuelve ese estado preguntándole a la DGII qué sabe del e-NCF:

const outcome = await ecf.reconcileSubmission(signedXml);
if (outcome.isOk()) {
  if (outcome.value.delivered) {
    // La DGII ya tiene el documento — no reintentar.
  } else if (outcome.value.cleared) {
    // La DGII no tiene registro; el marcador se limpió y reintentar es seguro.
  }
}
outcome = ecf.reconcile_submission(signed_xml)
if outcome.is_ok():
    result = outcome.unwrap()
    if result.delivered:
        ...  # la DGII ya lo tiene — no reintentar
    elif result.cleared:
        ...  # sin registro; reintentar es seguro

El marcador se limpia solo cuando la DGII responde No encontrado. Si la DGII tiene registro, el marcador se mantiene — es el candado que impide un segundo envío — y delivered te dice que pares.

canReuseSequence / can_reuse_sequence responde la pregunta siguiente ante un rechazo: si la secuencia del e-NCF puede volver a usarse. La DGII pone secuenciaUtilizada: false cuando el rechazo deja la secuencia reutilizable (certificado o firma inválida, XML inválido, firmante no delegado, e-NCF no autorizado o vencido, emisor no electrónico, RNC inexistente o inactivo). Un flag ausente se trata como no reutilizable.