Saltar al contenido principal

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"
}
CampoObligatorioFormato
skusíTexto único del artículo.
nombresíTexto.
preciosíNúmero en bolívares con IVA (46 o 46.5) o texto ("46,50").
ivano"exento" / "reducido" / "general", porcentaje (16, "16 %") o puntos básicos (1600). Ausente = general (16 %).
existenciasnoEntero. Ausente = se conserva lo que la caja tenía.
imagenUrlnoURL de la foto (informativa).
descripcion, categoria, codigoBarrasnoTexto.

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​

Node.js
// 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 });
});
Java
// 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());
}
C#
// 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());
Python
# 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}
PHP
// 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»:

CampoEjemplo
URL del sistema de inventariohttps://inventario.miempresa.com/api o https://…/items?ean={codigo}
Cabecera de autenticaciónX-API-Key (vacío = sin autenticación)
ClaveSe 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 ventahttps://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​

  1. Un GET por código que responda JSON con sku, nombre y precio, y 404 cuando no exista.
  2. Menos de 4 s (idealmente < 500 ms).
  3. HTTPS y una clave por comercio en la cabecera que prefieras.
  4. Opcional: un POST de ventas que responda 2xx.
  5. Probar con «Probar conexión» desde la caja con un código real.