Tema
Errores
Todos los errores 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ó waitUntilFinalTodos los APIError traen .code, .apiMessage, .statusCode y .raw. .message es lo que se ve al loguear e incluye el código adelante: [NOT_FOUND] El recurso no existe.
Se distinguen con instanceof:
ts
import { RateLimitError, ValidationError } from '@araitek/smartdocjs';
try {
await sd.invoices.create({ recipient, items });
} catch (error) {
if (error instanceof ValidationError) {
// datos mal armados; no sirve reintentar
} else if (error instanceof RateLimitError) {
// transitorio; el SDK ya reintentó
} else {
throw error;
}
}ValidationError
Puede venir de la API o detectarse localmente, antes de salir a la red.
ts
try {
await sd.invoices.create({ recipient, items });
} catch (error) {
if (error instanceof ValidationError) {
console.log(error.code); // 'VALIDATION_ERROR', o undefined si es local
console.log(error.fieldErrors); // { amount: 'Must be a valid amount.' }
}
}Las validaciones locales fallan sin gastar una llamada y dicen qué corregir:
js
await sd.invoices.create({ recipient, items, saleType: 'Contado' });
// ValidationError: "Contado" no es un valor válido de SaleType.
// ¿Quisiste decir "cash"? Valores válidos: "cash", "credit".
await sd.invoices.create({ recipient, items, currency: 'USD' });
// ValidationError: Con moneda USD hace falta exchangeRate.
new Item('X', { quantity: 1, unitAmount: 100, iva: Iva.MIXED_TEN });
// ValidationError: El tipo de IVA "mixed_10_percent" es mixto y necesita ivaBase.En TypeScript, el primero ni siquiera compila
Los catálogos son uniones de literales, así que un valor escrito a mano lo rechaza el compilador. La validación en tiempo de ejecución es la que ataja los valores que vienen de afuera —un formulario, un CSV, otra API— y que el compilador no puede ver.
Un ValidationError no se arregla reintentando: hay que corregir los datos.
PermissionError
ts
try {
await sd.invoices.list();
} catch (error) {
if (error instanceof PermissionError) {
console.log(error.missingPermission); // '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:
ts
try {
await sd.invoices.cancel(factura.id, 'Error en el monto');
} catch (error) {
if (error instanceof ValidationError && error.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 este error, es que se agotaron los reintentos.
ts
new Client({ maxRetries: 5 }); // más reintentos
new Client({ rateLimitPerSecond: 3 }); // más conservador
new Client({ rateLimitPerSecond: 0 }); // sin regulaciónConfigurationError
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.Las opciones también vienen en error.options, por si querés mostrarlas.
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
ts
import { TimeoutError } from '@araitek/smartdocjs';
try {
await sd.invoices.waitUntilFinal(factura.id);
} catch (error) {
if (error instanceof TimeoutError) {
console.log(error.lastStatus); // '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 lanza un error —la llamada fue correcta—, queda en el estado del documento:
ts
if (factura.isError) {
console.log(factura.errorCode); // '0160'
console.log(factura.errorMessage);
}Ramificá por isError
errorCode puede venir vacío aunque el documento haya fallado, según de dónde venga el rechazo; errorMessage trae el detalle en todos los casos. La pregunta que corresponde es if (factura.isError), 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.
ts
if (factura.isError) {
await sd.invoices.update(factura.id, { recipient, items });
const corregida = await sd.invoices.waitUntilFinal(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.
ts
const factura = await sd.invoices.create({ recipient, items });Para controlarla vos, pasala con idempotencyKey:
ts
const factura = await sd.invoices.create({
recipient,
items,
idempotencyKey: `venta-${venta.id}`,
});Dos llamadas con la misma clave devuelven el mismo documento, sin emitirlo dos veces. La respuesta queda guardada 24 horas, incluidos los errores: si reintentás con la misma clave después de corregir el cuerpo, te devuelve el error viejo.
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 | Sí | Sí |
POST de creación | Sí, lleva clave de idempotencia | Sí |
POST de acción (cancel, …) | No | Sí |
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.
Para ajustar las esperas:
ts
import { Client, RetryPolicy } from '@araitek/smartdocjs';
new Client({
retry: new RetryPolicy({ maxRetries: 5, backoffBase: 1, backoffMax: 30 }),
});Ver qué manda el SDK
El transporte acepta un fetch propio, así que se puede envolver el que trae Node para loguear cada solicitud:
ts
const sd = new Client({
fetch: async (url, init) => {
console.log(init.method, url, init.body);
const respuesta = await fetch(url, init);
console.log('→', respuesta.status);
return respuesta;
},
});Sirve también para salir por un proxy o agregar métricas.
