@inowu/cashdro
v0.3.1
Published
TypeScript client for CashDro automated cash recyclers over the Web Service integration API.
Maintainers
Readme
CashDro TypeScript SDK
@inowu/cashdro proporciona acceso conveniente y tipado a los recicladores
de efectivo automatizados CashDro desde TypeScript y JavaScript — ejecutando
ventas, pagos, depósitos, retiros y operaciones de gestión de efectivo, además
de consultas de niveles de efectivo y reportes.
Tabla de contenidos
- Instalación
- Inicio rápido
- Autenticación
- Configuración
- Cómo funcionan las transacciones
- Uso no bloqueante
- Modo debug
- Referencia del cliente
- Tipos de transacción
- Respuestas
- Manejo de errores
- Avanzado
Instalación
npm i @inowu/cashdroRequisitos: Node.js 18+ (o cualquier runtime con fetch global).
Inicio rápido
import { CashDroClient } from "@inowu/cashdro";
const cashdro = new CashDroClient({
baseUrl: "https://your-cashdro/",
username: "manager",
password: "••••",
});
// 1. Verificar conectividad + credenciales
const ping = await cashdro.doTest();
if (ping.code !== 1) throw new Error("CashDro unreachable or bad credentials");
// 2. Realizar una venta de 12.50 (los montos están en unidades menores — valor × 100)
const { operationId } = await cashdro.sale(1250, { posuser: "Ana" });
// 3. Esperar a que el cliente termine de pagar
const final = await cashdro.pollUntilFinished(operationId);
const op = final.response?.operation?.operation;
console.log(`paid=${op?.totalin} change=${op?.totalout}`); // el cambio es negativo
// 4. Confirmar que registraste la transacción
await cashdro.setOperationImported(operationId);Autenticación
Cada llamada se autentica como un usuario de CashDro mediante HTTP Basic. El usuario debe existir en la máquina y tener permiso para la operación.
const cashdro = new CashDroClient({ baseUrl, username: "manager", password: "••••" });Los campos opcionales posuser, posid y aliasid aceptados por las
transacciones no son credenciales — solo etiquetan quién (cajero),
qué terminal y un id de referencia del lado del host inició una operación
(≤ 50 caracteres cada uno).
Configuración
CashDroClient acepta las siguientes opciones:
| Opción | Descripción |
|--------|-------------|
| baseUrl | URL de la unidad CashDro. Apunta esto a tu dispositivo / unidad de producción. |
| username, password | Credenciales de usuario de CashDro (HTTP Basic). |
| timeoutInSeconds | Timeout por solicitud (por defecto 60). |
| maxRetries | Reintentos automáticos con backoff exponencial (por defecto 2). |
| headers | Encabezados adicionales para enviar en cada solicitud. |
| fetch | Implementación personalizada de fetch (ver Avanzado). |
| debug | Traza los helpers de multi-operación — true para loguear, o una función handler (ver Modo debug). |
const cashdro = new CashDroClient({
baseUrl: "https://cashdro.prod.example/",
username: "manager",
password: "••••",
timeoutInSeconds: 30,
maxRetries: 3,
});Cómo funcionan las transacciones
Una transacción de movimiento de efectivo (venta, pago, depósito, retiro, …) corre de forma asíncrona en el hardware, por lo que se conduce mediante un ciclo de vida corto:
start ─▶ acknowledge ─▶ (poll status) ─▶ finish? ─▶ import
id launch I → Q → E → F finalize registrarlastartabre la transacción y devuelve unoperationId(el handle para todo lo demás). La unidad aún no hace nada.acknowledgelanza la ejecución (los aceptadores se abren / comienza la dispensación).- poll status hasta que
state === "F". Estados:Ipendiente,Qen cola,Eejecutando,Ffinalizado. finishfinaliza o cancela (solo algunas transacciones necesitan un finish explícito).importle indica a la unidad que el host ya registró el resultado.
Los métodos de conveniencia (sale, payment, …) y los helpers
(runOperation, pollUntilFinished) colapsan este boilerplate por ti
mientras siguen devolviendo el operationId y cada respuesta cruda.
Dinero: todos los montos son enteros en unidades menores (valor × 100 —
1250 = 12.50). Los montos dispensados (totalout) son negativos. La
moneda es la que tenga configurada la unidad.
Una a la vez: un CashDro ejecuta una sola operación a la vez; iniciar otra
mientras una está activa falla con code -2. Usa askOperationExecuting()
para detectar y recuperar una operación atascada.
Uso no bloqueante
Una venta puede tardar — la unidad espera a que el cliente inserte efectivo.
No hagas await de toda la cadena start → wait → import dentro de un
request handler; en su lugar déjala correr en segundo plano y reacciona
mediante callbacks. El callback onStart te entrega el operationId en el
instante en que se conoce (antes de cualquier espera), para que la UI pueda
mostrar una pantalla de pago y ofrecer un botón de Cancelar de inmediato.
import { CashDroClient, CashDroTimeoutError } from "@inowu/cashdro";
// Fire-and-forget: ejecuta start → acknowledge → poll → import en segundo plano.
void cashdro
.sale(1250, {
waitForFinish: true, // sondear hasta que el cliente termine de pagar
autoImport: true, // registrarlo en la unidad cuando termine
onStart: (operationId) => showPayScreen(operationId), // se devuelve de inmediato
poll: { onPoll: (status) => updateUi(status), timeoutMs: 120_000 },
})
.then((r) => onPaid(r.final?.response?.operation?.operation)) // totalin / totalout
.catch((err) => {
if (err instanceof CashDroTimeoutError) return; // nadie pagó a tiempo
onError(err);
});Para permitir que un botón de Cancelar detenga una venta en curso,
cancélala en la unidad — el poll en segundo plano se resuelve entonces en
el estado F en su siguiente tick:
cancelButton.onclick = () => cashdro.finishOperation(operationId, 2);De forma equivalente puedes conducir los pasos tú mismo: await cashdro.sale(...)
retorna después de start+acknowledge (rápido), luego llama a
cashdro.pollUntilFinished(id) desde una tarea en segundo plano. Pasa un
AbortSignal en las opciones de poll/pollUntilFinished si necesitas
detener el sondeo sin cancelar la transacción, o cancelOnTimeout: true
para auto-cancelar la venta si nadie paga a tiempo.
Modo debug
Los helpers de multi-operación (sale, runOperation, pollUntilFinished, …)
pueden trazar exactamente lo que hacen — cada llamada subyacente, su
resultado, y cada sondeo de estado. Pasa debug: true para loguear a la
consola:
const cashdro = new CashDroClient({ baseUrl, username, password, debug: true });
await cashdro.sale(100, { waitForFinish: true, autoImport: true });[cashdro] sale: startOperation(type=4, amount=100)
[cashdro] sale: startOperation → operationId=128 (code=1, 41ms)
[cashdro] sale: acknowledgeOperationId(128) → code=1
[cashdro] pollUntilFinished(128): polling every 1000ms, timeout 120000ms
[cashdro] pollUntilFinished(128): poll #1 → state=Q
[cashdro] pollUntilFinished(128): poll #2 → state=E
[cashdro] pollUntilFinished(128): finished after 3 poll(s)
[cashdro] sale: setOperationImported(128) → code=1
[cashdro] sale: done (operationId=128)Para logging estructurado, pasa un handler en su lugar y enruta los
CashDroDebugEvent a tu propio logger:
const cashdro = new CashDroClient({
baseUrl,
username,
password,
debug: (e) => logger.debug({ seq: e.seq, helper: e.helper, op: e.op, ...e.detail }, e.message),
});Cada evento es { seq, at, helper, op, phase, detail, message } — phase
es "call", "result", "poll", o "info". Esto traza la orquestación
del helper y es independiente de la capa de transporte.
Referencia del cliente
Los métodos que mapean 1:1 a una operación del dispositivo devuelven la respuesta cruda; los helpers de conveniencia y orquestación combinan varias llamadas pero nunca ocultan los datos subyacentes.
Conectividad y salud
| Método | Qué hace / cuándo usarlo |
|--------|----------------------------|
| doTest() | Ping autenticado. Verifica accesibilidad + credenciales antes de transaccionar. |
| getDiagnosis() | Estado de error del dispositivo (WithError) + lista de dispositivos. Usa esto como gate antes de transaccionar para evitar hardware atascado. |
| getAlerts() | Denominaciones en niveles críticos / de inhibición / por debajo del mínimo / por encima del máximo. Para avisos de reabastecimiento/vaciado. |
| askOperationExecuting() | La operación que se está ejecutando actualmente (o OperationId: -1 cuando está inactiva). Recupera una operación huérfana al iniciar. |
Transacciones — ciclo de vida
Bloques de construcción de bajo nivel (normalmente usarás los métodos de conveniencia de abajo):
| Método | Qué hace / cuándo usarlo |
|--------|----------------------------|
| startOperation({ type, amount?, parameters?, … }) | Abre una transacción de un tipo dado; devuelve operationId. |
| acknowledgeOperationId(id) | Lanza una operación iniciada. |
| askOperation(id) | Sondea el estado completo (estado, total, totalin, totalout, niveles por denominación, mensajes). |
| finishOperation(id, type) | type 1 finaliza/acepta, 2 cancela, 4 finaliza con una lista explícita de piezas. |
| setOperationImported(id) | Marca la operación como registrada por el host (la elimina de la lista pendiente). |
Transacciones — métodos de conveniencia
Cada uno abre + confirma una transacción y devuelve { operationId, started, … }.
Las opciones aceptan posid / posuser / aliasid y las opciones de
orquestación.
| Método | Transacción |
|--------|-------------|
| sale(amount, opts?) | Venta — el cliente paga, se devuelve el cambio. |
| payment(amount, opts?) | Pago / desembolso de un monto. |
| paymentByDenomination(pieces, opts?) | Pagar piezas específicas (ver abajo). |
| depositByAmount(amount, opts?) | Depositar hasta un monto objetivo. |
| deposit(opts?) | Depósito abierto; finaliza con finishOperation(id, 1). |
| change(opts?) | Cambio / romper un billete. |
| load(opts?) | Cargar efectivo en los recicladores. |
| withdraw(opts?) | Retirar efectivo (el operador selecciona en la unidad). |
| transferToCassette(opts?) | Mover efectivo de los recicladores al cassette. |
| emptyNoteCassette(opts?) / emptyCoinCassette(opts?) | Registrar un cassette como vaciado. |
| finishWithPieces(id, { recycler?, cassette? }) | Finalizar un retiro/transferencia con una lista explícita de piezas. |
// Pagar exactamente 3×1.00 monedas y 1×20.00 billete
await cashdro.paymentByDenomination([
{ value: 100, level: 3, isBill: false },
{ value: 2000, level: 1, isBill: true },
]);
// Retirar 1×500 billete directamente de los recicladores (sin selección en el dispositivo)
const { operationId } = await cashdro.withdraw();
await cashdro.finishWithPieces(operationId, { recycler: [{ value: 500, level: 1, isBill: true }] });
await cashdro.setOperationImported(operationId);Helpers de orquestación
| Método | Qué hace |
|--------|--------------|
| runOperation(params, opts?) | El motor detrás de los métodos de conveniencia: start → (acknowledge) → poll opcional → import opcional. Devuelve { operationId, started, final?, imported? }. |
| pollUntilFinished(id, opts?) | Sondea askOperation hasta que state === "F"; devuelve la respuesta final completa. Lanza CashDroTimeoutError en caso de timeout. |
Opciones de runOperation: acknowledge (por defecto true), waitForFinish
(por defecto false — una venta bloquea en espera del efectivo del cliente),
autoImport, un callback onStart(id, started), y poll (reenviado a
pollUntilFinished).
Opciones de pollUntilFinished: intervalMs, timeoutMs, signal
(AbortSignal), onPoll(status), y cancelOnTimeout — cuando es true,
un timeout cancela e importa la operación antes de lanzar el error, para que
no quede abierta bloqueando la siguiente transacción con code -2 (un
error común de testing al probar ventas sin efectivo).
const { operationId } = await cashdro.sale(500, {
onStart: (id) => showOnScreen(id), // operationId en el instante en que se conoce
});
const final = await cashdro.pollUntilFinished(operationId, {
timeoutMs: 120_000,
onPoll: (s) => updateUi(s.response?.operation?.operation),
});Consultas y reportes
| Método | Qué hace / cuándo usarlo |
|--------|----------------------------|
| getPiecesCurrency({ currencyId, includeLevels?, includeImages? }) | Niveles de efectivo por denominación. Para UI de inventario y para pre-verificar un retiro. |
| askPendingOperations({ terminal?, importManualOperations? }) | Operaciones aún no importadas por el host. Reconciliar antes de un cierre de caja. |
| askMovements({ fromDate, toDate, group?, operationType? }) | Operaciones entre dos fechas, detalladas o agregadas. Para reportes. |
const levels = await cashdro.getPiecesCurrency({ currencyId: "EUR", includeLevels: 1 });
const report = await cashdro.askMovements({
fromDate: "2026-06-01_00:00:00",
toDate: "2026-06-30_23:59:59",
group: true,
operationType: JSON.stringify([{ Id: 4 }]), // solo ventas
});Tipos de transacción
startOperation (y los métodos de conveniencia) aceptan un type numérico:
| type | Transacción | Monto/piezas |
|-------:|-------------|---------------|
| 1 | carga | — |
| 2 | retiro | en el dispositivo, o finishWithPieces |
| 3 | pago | amount, o paymentByDenomination |
| 4 | venta | amount |
| 8 | transferencia al cassette | en el dispositivo, o finishWithPieces |
| 10 | vaciar cassette de billetes | — |
| 11 | vaciar cassette de monedas | — |
| 16 | depósito | finaliza con finishOperation(id, 1) |
| 17 | depósito por monto | amount |
| 18 | cambio | — |
Una "pieza" (para paymentByDenomination / finishWithPieces) es
{ value: number, level: number, isBill: boolean } — value en unidades
menores, level la cantidad, isBill true para billetes.
Respuestas
Cada llamada resuelve a un envelope tipado:
{ code: number; response: { errorMessage: string; /* datos de la operación */ } }code >= 1 significa éxito; un code negativo es un error (los métodos de
conveniencia/helper lanzan una excepción en su lugar — ver abajo). Los tipos
de solicitud y respuesta están disponibles bajo el namespace CashDroApi:
import { CashDroApi } from "@inowu/cashdro";
const res: CashDroApi.AskOperationResponse = await cashdro.askOperation(id);Manejo de errores
import { CashDroError, CashDroTimeoutError } from "@inowu/cashdro";
try {
const { operationId } = await cashdro.sale(1250);
await cashdro.pollUntilFinished(operationId, { timeoutMs: 60_000 });
await cashdro.setOperationImported(operationId);
} catch (err) {
if (err instanceof CashDroTimeoutError) {
// no se insertó efectivo a tiempo — la operación sigue abierta; cancélala
await cashdro.finishOperation(err.operationId, 2);
} else if (err instanceof CashDroError) {
console.error("CashDro rejected the request:", err.response);
} else {
throw err; // error de transporte/HTTP
}
}CashDroError— una llamada devolvió uncodede no-éxito (contiene laresponsecruda).CashDroTimeoutError—pollUntilFinishedalcanzó el timeout (contieneoperationIdy el último poll).
Códigos de dispositivo comunes: -1 credenciales incorrectas, -2 ocupado /
no encontrado, -3 monto incorrecto, -4 sin permiso, -998/-999
dispositivo no disponible (revisar diagnóstico).
Avanzado
Acceso a la respuesta cruda
Los métodos que mapean 1:1 a una operación del dispositivo devuelven un awaitable que también expone la respuesta HTTP cruda:
const { data, rawResponse } = await cashdro.askOperation(id).withRawResponse();
console.log(rawResponse.headers);Fetch personalizado / TLS autofirmado
Proporciona tu propio fetch para plataformas sin uno incorporado, o para
alcanzar una unidad que sirve un certificado autofirmado. Prefiere
instalar la CA de la unidad en tu almacén de confianza; solo relaja la
verificación en entornos confiables y que no sean de producción.
import { Agent, setGlobalDispatcher } from "undici";
setGlobalDispatcher(new Agent({ connect: { rejectUnauthorized: false } })); // solo no-prodOpciones por solicitud
Cada método 1:1 acepta un segundo argumento opcional para overrides por llamada:
const controller = new AbortController();
await cashdro.askOperation(id, {
timeoutInSeconds: 10,
maxRetries: 0,
abortSignal: controller.signal,
headers: { "X-Trace-Id": "abc" },
});Cliente de bajo nivel
Para endpoints aún no envueltos, cashdro.api expone el cliente subyacente
usado para hacer las solicitudes.
Compatibilidad de runtime
Funciona en Node.js 18+, Deno, Bun, Cloudflare Workers, Vercel y React Native.
