Saltar a contenido

Errores

Todas las excepciones del SDK heredan de SmartDocError, y se dividen según de dónde vienen.

SmartDocError
├── APIError                  ← la respondió SmartDoc
│   ├── AuthenticationError   401
│   ├── PermissionError       403
│   ├── NotFoundError         404
│   ├── ValidationError       422, 400, o una validación local
│   ├── ConflictError         409
│   ├── RateLimitError        429
│   └── ServerError           5xx
├── ConfigurationError        ← el SDK no pudo resolver la configuración
├── SignatureError            ← un webhook no validó
└── TimeoutError              ← se agotó wait_until_final

Todas las APIError traen .code, .message, .status_code y .raw.

ValidationError

Puede venir de la API o detectarse localmente, antes de salir a la red.

try:
    sd.invoices.create(recipient=..., items=[...])
except smartdoc.ValidationError as e:
    print(e.code)            # "VALIDATION_ERROR", o None si es local
    print(e.field_errors)    # {"amount": "Must be a valid amount."}

Las validaciones locales fallan sin gastar una llamada y dicen qué corregir:

sd.invoices.create(..., sale_type="Contado")
# ValidationError: 'Contado' no es un valor válido de SaleType.
#                  ¿Quisiste decir 'cash'? Valores válidos: 'cash', 'credit'.

sd.invoices.create(..., currency="USD")
# ValidationError: Con moneda USD hace falta exchange_rate.

smartdoc.Item("X", quantity=1, unit_amount=100, iva=smartdoc.Iva.MIXED_TEN)
# ValidationError: El tipo de IVA 'mixed_10_percent' es mixto y necesita iva_base.

Un ValidationError no se arregla reintentando: hay que corregir los datos.

PermissionError

try:
    sd.invoices.list()
except smartdoc.PermissionError as e:
    print(e.missing_permission)   # "read_invoices"

Los permisos no son jerárquicos

write_invoices no habilita nada que pida read_invoices. Si una clave crea facturas y después las consulta, necesita los dos marcados.

La clave pertenece al contribuyente, no al usuario: los permisos de quien la creó no influyen.

Acciones no permitidas

Una acción que no aplica al estado actual del documento llega como ValidationError con código ACTION_NOT_ALLOWED:

try:
    sd.invoices.cancel(factura.id, reason="...")
except smartdoc.ValidationError as e:
    if e.code == "ACTION_NOT_ALLOWED":
        # Por ejemplo, la factura todavía no está aprobada.
        ...

RateLimitError

El SDK regula las solicitudes por debajo del límite y reintenta con backoff exponencial. Si aun así llega esta excepción, es que se agotaron los reintentos.

sd = smartdoc.Client(max_retries=5)                 # más reintentos
sd = smartdoc.Client(rate_limit_per_second=3)       # más conservador
sd = smartdoc.Client(rate_limit_per_second=0)       # sin regulación

ConfigurationError

El SDK no pudo resolver algo que necesita para emitir:

ConfigurationError: El contribuyente tiene 2 establecimientos y no se indicó
cuál usar. Elegilo con establishment='001' al construir el cliente, o
establishment='001' en la llamada. Opciones disponibles: 001, 002.

Nunca elige por su cuenta: el código elegido queda impreso en el documento y en su CDC, y equivocarlo obliga a anular y reemitir.

TimeoutError

try:
    factura = sd.invoices.wait_until_final(factura.id)
except smartdoc.TimeoutError as e:
    print(e.last_status)    # "uploaded_to_set"

No significa que el documento haya fallado: sigue su curso. Lo que se agotó es la espera.

Documentos que terminan en error

Un documento puede emitirse bien y aun así terminar rechazado. Eso no levanta una excepción —la llamada fue correcta—, queda en el estado del documento:

if factura.is_error:
    print(factura.error_code)      # "0160"
    print(factura.error_message)

Ramificá por is_error

error_code puede venir vacío aunque el documento haya fallado, según de dónde venga el rechazo; error_message trae el detalle en todos los casos. La pregunta que corresponde es if factura.is_error:, que mira el estado.

Cómo se corrigen

Editando el documento. SmartDoc lo vuelve a procesar solo: no hay que reenviarlo ni emitir uno nuevo.

if factura.is_error:
    sd.invoices.update(factura.id, recipient=..., items=[...])
    factura = sd.invoices.wait_until_final(factura.id)

update reemplaza el documento entero, así que hay que mandar todos los campos, no solo el que se corrige.

Se puede editar en cualquier estado menos uploaded_to_set y approved_by_set. Ahí la API responde EDIT_NOT_ALLOWED, y lo que corresponde es anular y reemitir.

Idempotencia

La API exige una clave de idempotencia al crear documentos. El SDK la pone: cada create() va con una clave nueva, así que no hay que hacer nada.

factura = sd.invoices.create(recipient=..., items=[...])

Para controlarla vos, pasala con idempotency_key:

factura = sd.invoices.create(..., idempotency_key="venta-4821")

Dos llamadas con la misma clave devuelven el mismo documento, sin emitirlo dos veces. La respuesta queda guardada 24 horas.

Reintentos

El SDK reintenta ante 429, 5xx y fallas de red, con backoff exponencial y jitter.

Solicitud ¿Reintenta ante 5xx? ¿Ante 429?
GET, PUT, DELETE
POST de creación Sí, lleva clave de idempotencia
POST de acción (cancel, …) No

Una acción no lleva clave de idempotencia, así que ante un 5xx no se puede saber si el servidor alcanzó a ejecutarla y reintentar podría repetirla. Un 429, en cambio, se rechaza antes de ejecutar nada, y por eso siempre se reintenta.

Loguear

import logging
logging.getLogger("smartdoc.webhooks").setLevel(logging.INFO)

Para ver el detalle de las solicitudes HTTP:

logging.getLogger("httpx").setLevel(logging.DEBUG)