facturas resuelve ese problema con una clave estable de tu aplicación y un
almacenamiento compartido, llamado store en la API. El SDK guarda ahí el
número reservado, el input y el resultado. Cuando repetís la misma operación
con la misma idempotencyKey, consulta esa reserva en vez de empezar otra.
Podés usar el Postgres o Redis que tu aplicación ya tiene. También hay
adaptadores para un directorio privado y para memoria. El SDK no instala una
base de datos ni un servicio externo.
Qué guarda cada opción
Con un store que provee
withLock, issue(), issueCreditNote() e
issueDebitNote() coordinan la numeración. Dos llamadas simultáneas para el
mismo punto de venta y tipo de comprobante reciben números consecutivos y cada
una escribe una sola vez. Sin withLock, otros procesos que usan ese punto de
venta quedan fuera de la coordinación.
Antes de reservar un número, el SDK revisa si la operación anterior quedó sin
resolver. Primero consulta ARCA. Si todavía no puede saber qué pasó, devuelve
indeterminate con lookup: { kind: "blocked", by: <clave> } sin reservar ni
emitir nada. Conciliá esa clave con recover() y repetí la llamada.
El bloqueo se renueva mientras el proceso está activo. Si el proceso se cae, el
bloqueo vence, pero la reserva pendiente sigue frenando la secuencia. Así, otro
proceso puede continuar sin reutilizar a ciegas un número cuyo resultado todavía
no se conoce.
Postgres
Usá el cliente que ya tiene tu aplicación. Neon, Supabase Postgres, Vercel Postgres,pg y postgres pueden proveer la función de consulta
parametrizada. Los resultados pueden ser una lista de filas o { rows }. Con
postgres, adaptá sql.unsafe(text, params). Creá la tabla por defecto una
sola vez:
INSERT ... ON CONFLICT DO NOTHING RETURNING key. El
adaptador no crea la tabla. El bloqueo de secuencia es una fila más de esa tabla,
tomada con el mismo INSERT y liberada con un DELETE que verifica el dueño,
y una fila abandonada se recupera con un UPDATE condicional sobre
updated_at. No usa bloqueos de sesión, así que funciona detrás de PgBouncer en
modo transacción.
Redis
call usa ioredis SET key value NX. Si no, el adaptador usa
Upstash set(key, value, { nx: true }). Usá un Redis persistente, sin desalojo de
las claves de reserva: los registros no llevan TTL. El bloqueo de secuencia sí es
una clave con vencimiento, tomada con SET NX PX, renovada mientras se la
tiene y borrada solo si sigue siendo tuya. Necesita del en el cliente: sin
del, el adaptador no expone withLock y la secuencia no se coordina.
Archivos
rename. Los archivos quedan con modo 0600 y los
directorios
nuevos con 0700. El bloqueo de secuencia es un directorio .lock creado con
mkdir, con el dueño y el vencimiento adentro: coordina varios procesos sobre
el mismo volumen, no varios servidores sobre un NFS compartido.
Memoria
Implementar otro store
add tiene que devolver false de forma atómica sin cambiar un valor existente.
El withLock opcional coordina los refrescos de ticket WSAA y la secuencia del
punto de venta. Tiene que ser exclusivo entre procesos y soltarse siempre, aun
si el proceso muere. Un store sin withLock conserva las reservas y la
recuperación, pero no coordina la secuencia entre procesos. Cuando pasás las dos
opciones, un wsaaSessionStore
explícito gana para los tickets.
Formato y vida de los registros
Formato y vida de los registros
Las claves usan
arca:v1:wsaa:{environment}:{service}:{fingerprint} y
arca:v1:attempt:{environment}:{taxId}:{idempotencyKey}. Los registros de
reserva contienen el hash del input, la operación, las coordenadas reservadas y
el input exacto enviado. Contienen datos fiscales y de clientes. Restringí el
acceso y protegé los backups.arca:v1:settled:{environment}:{taxId}:{idempotencyKey} guarda un resultado
que tiene que sobrevivir al reintento, y se escribe una sola vez con add. Con
{ v: 1, kind: "conflict", number, found, settledAt }, otro comprobante ocupa
el número reservado. Una repetición con esa clave o un recover() devuelven el
mismo conflict sin consultar al proveedor. Con
{ v: 1, kind: "superseded", number, by, settledAt }, la barrera probó que el
número estaba vacío y se lo dio a la clave by. La clave vieja consulta el
número una vez y nunca reenvía. Devuelve indeterminate con
lookup: { kind: "superseded", by } si el número sigue vacío o si el
comprobante que hay ahí es de by o de una clave que a su vez se lo quitó a
by. Emití bajo una clave nueva. Devuelve conflict solo si alguna clave de
esa cadena anotó un conflicto en ese número, porque entonces un desconocido
llegó al número y el comprobante se atribuye a mano. Las autorizaciones no se
anotan porque ARCA es la fuente de verdad y cada repetición la consulta. Los
rechazos tampoco, porque el input se corrige bajo una clave nueva. Si el store
falla al anotar el conflicto,
la llamada lanza ArcaConfigurationError en vez de devolver un conflicto que
un reintento podría no volver a ver.Cada registro lleva su versión. v: 1 es una reserva de WSFE sin detalle y la
lee cualquier versión desde la 0.9. v: 2 es una reserva de WSMTXCA o con
detalle de ítems. Siempre nombra su proveedor, y la 0.10 no puede reproducirla,
justamente para que volver a la versión anterior no reenvíe por WSFE un comprobante que era de
WSMTXCA. Preservá las dos versiones.arca:v1:sequence:{environment}:{taxId}:{salesPoint}:{voucherType} guarda la
última reserva reclamada en esa secuencia y si ARCA ya informó su desenlace. Se
escribe antes que la reserva que nombra.
arca:v1:lock:sequence:... es la clave del bloqueo. Los dos son registros de
coordinación, no evidencia fiscal. Se reescriben en cada reclamo y se pueden
borrar si hacen falta, siempre que ninguna reserva quede sin conciliar. Repetir
una clave con otro input, otra
operación, otro proveedor u otro number explícito es una incompatibilidad de
idempotencia y lanza ARCA_INPUT_IDEMPOTENCY_MISMATCH.No podés borrar, vencer ni reescribir los registros de reserva. El SDK solo
los crea, nunca guarda resultados encima y siempre consulta a ARCA en una
repetición. Borrar una reserva puede hacer que un reintento posterior emita
otra factura.