Inventario externo de la Caja
FINORA Caja puede trabajar con su propio inventario o consultar el sistema que tu comercio ya usa. La caja hace una llamada por cada código que lee la pistola y, si lo configuras, una llamada por cada venta cerrada. Nada más: no sincroniza catálogos ni necesita acceso a tu base de datos.
pistola lee 7590010000013
└─▶ GET {url}/articulos/7590010000013 → {sku, nombre, precio, iva?, existencias?, imagenUrl?}
(la caja guarda la respuesta en su caché local: origen EXTERNO + hora)
venta pagada
└─▶ POST {ventasUrl} → ticket, líneas y total (opcional, en segundo plano)
Si tu sistema no responde, la caja vende con la caché de la última respuesta buena y avisa en pantalla desde cuándo es el dato. Un artículo que nunca se consultó no se puede vender hasta que vuelva la red.
Lo que tu sistema expone
GET {url}/articulos/{codigo}
O una plantilla con {codigo} sustituido, por ejemplo https://inventario.miempresa.com/api/items?ean={codigo}. {codigo} es lo que leyó la pistola (EAN-13, UPC o SKU interno en Code 128), codificado para URL. Cabeceras: Accept: application/json y la de autenticación que configures (X-API-Key: <clave> por defecto; el nombre es configurable, por ejemplo Authorization: Bearer …).
Tiempo máximo de espera: 4 segundos; después la caja usa la caché. Idealmente responde en menos de 500 ms: se llama en cada escaneo.
{
"sku": "ABA-002",
"nombre": "Arroz blanco 1 kg",
"precio": 46.00,
"iva": "exento",
"existencias": 180,
"imagenUrl": "https://cdn.miempresa.com/img/aba-002.jpg",
"descripcion": "Grano largo",
"categoria": "Abasto",
"codigoBarras": "7590020000027"
}
| Campo | Obligatorio | Formato |
|---|---|---|
sku | sí | Texto único del artículo. |
nombre | sí | Texto. |
precio | sí | Número en bolívares con IVA (46 o 46.5) o texto ("46,50"). |
iva | no | "exento" / "reducido" / "general", porcentaje (16, "16 %") o puntos básicos (1600). Ausente = general (16 %). |
existencias | no | Entero. Ausente = se conserva lo que la caja tenía. |
imagenUrl | no | URL de la foto (informativa). |
descripcion, categoria, codigoBarras | no | Texto. |
Se admite un envoltorio {"data": {...}} o {"articulo": {...}}.
404→ el código no existe: la caja lo dice y no usa la caché.- Cualquier otro error, JSON inválido o sin respuesta → caché.
Mapeo de campos
Si tus nombres difieren, en la caja (Configuración → «Mapeo de campos (JSON)») indicas clave = campo de la caja, valor = nombre en tu respuesta, con rutas por punto:
{ "sku": "codigo", "nombre": "descripcion", "precio": "pvp", "existencias": "stock", "imagenUrl": "foto.url" }
Ejemplo de endpoint
// Express
app.get("/api/articulos/:codigo", autenticar, async (req, res) => {
const a = await db.articulos.porCodigo(req.params.codigo);
if (!a) return res.status(404).json({ codigo: "NO_EXISTE" });
res.json({ sku: a.sku, nombre: a.nombre, precio: a.pvpConIva, iva: a.iva, existencias: a.stock, imagenUrl: a.foto });
});
// Spring
@GetMapping("/api/articulos/{codigo}")
ResponseEntity<ArticuloDto> articulo(@PathVariable String codigo) {
return repo.findByCodigo(codigo).map(a -> ResponseEntity.ok(ArticuloDto.de(a))).orElse(ResponseEntity.notFound().build());
}
// ASP.NET minimal API
app.MapGet("/api/articulos/{codigo}", async (string codigo, Inventario db) =>
await db.PorCodigo(codigo) is { } a ? Results.Ok(new { a.Sku, nombre = a.Nombre, precio = a.PvpConIva, existencias = a.Stock }) : Results.NotFound());
# FastAPI
@app.get("/api/articulos/{codigo}")
def articulo(codigo: str):
a = inventario.por_codigo(codigo)
if not a: raise HTTPException(404)
return {"sku": a.sku, "nombre": a.nombre, "precio": a.pvp_con_iva, "existencias": a.stock}
// Laravel
Route::get('/api/articulos/{codigo}', function (string $codigo) {
$a = Articulo::where('codigo_barras', $codigo)->first();
return $a ? ['sku' => $a->sku, 'nombre' => $a->nombre, 'precio' => $a->pvp, 'existencias' => $a->stock] : response()->json([], 404);
});
Aviso de venta (opcional)
POST {ventasUrl} con la misma cabecera de autenticación al quedar pagada cada venta (en segundo plano; no retrasa el recibo):
{
"ticket": "T003-MTZ3XJYG",
"terminalId": "TERM0042",
"fecha": "2026-09-13T14:02:11.376Z",
"medioPago": "tarjeta",
"totalCentimos": 9200,
"total": 92.00,
"lineas": [ { "sku": "ABA-002", "nombre": "Arroz blanco 1 kg", "cantidad": 2, "precioUnitario": 46.00, "total": 92.00 } ],
"autorizacion": { "codigo": "052913", "rrn": "625512004871", "tarjeta": "455555******1234" }
}
Responde 2xx. Si no llega (red, 5xx), la caja lo anota en su auditoría y en bitacora.log; en 1.2.0 no reintenta. Usa ticket como clave de idempotencia y concilia con la exportación de ventas de la caja o con Ventas del portal.
Configurar la caja
Configuración (credencial de supervisor) → Sistema de ventas → «Sistema externo»:
| Campo | Ejemplo |
|---|---|
| URL del sistema de inventario | https://inventario.miempresa.com/api o https://…/items?ean={codigo} |
| Cabecera de autenticación | X-API-Key (vacío = sin autenticación) |
| Clave | Se guarda cifrada por el perfil de usuario; nunca vuelve a la pantalla |
| Mapeo de campos (JSON) | Solo si los nombres difieren |
| URL para avisar cada venta | https://inventario.miempresa.com/api/ventas (vacío = no avisar) |
Probar conexión consulta un código con lo que hay en pantalla, sin guardar, y muestra qué entendió. Por entorno: CAJA_INVENTARIO_ORIGEN=EXTERNO, CAJA_INVENTARIO_URL, CAJA_INVENTARIO_CABECERA, CAJA_INVENTARIO_CLAVE, CAJA_INVENTARIO_MAPEO, CAJA_INVENTARIO_VENTAS_URL.
Checklist
- Un
GETpor código que responda JSON consku,nombreyprecio, y404cuando no exista. - Menos de 4 s (idealmente < 500 ms).
- HTTPS y una clave por comercio en la cabecera que prefieras.
- Opcional: un
POSTde ventas que responda2xx. - Probar con «Probar conexión» desde la caja con un código real.