OrderVision API

Fastify 5 · SQL Server (erp) · JWT · Firebase · Gemini · Zod

Estado del servicio

El API expone dos probes:

GET /health

Liveness — responde 200 siempre que el proceso esté arriba.

GET /ready

Readiness — ejecuta SELECT 1 contra SQL Server. 200 si la DB responde, 503 si está caída.

Verificación rápida

curl http://192.168.1.3:3005/ready

Autenticación

Todos los endpoints (salvo /health, /ready y /auth/login) requieren un JWT en el header Authorization: Bearer <token>.

POST /auth/login

curl -X POST http://192.168.1.3:3005/auth/login \
  -H "Content-Type: application/json" \
  -d '{"codigo":"<USUARIO>","pwd":"<PASSWORD>"}'

Respuesta entrega token (JWT) y firebaseToken.

Clients JWT

Read-only sobre la tabla dbo.clientes del ERP. Excluye inactivos por defecto.

GET /clients

ParámetroTipoDefaultDescripción
pageint ≥ 11Página
limitint 1-10010Tamaño de página
searchstringBusca en código, nombre o RNC
includeInactiveboolfalseIncluir clientes inactivos
curl -H "Authorization: Bearer $TOKEN" \
  "http://192.168.1.3:3005/clients?search=ALPA&limit=5"

GET /clients/:codigo

curl -H "Authorization: Bearer $TOKEN" \
  http://192.168.1.3:3005/clients/8AA01

Forma de la respuesta

{
  "codigo": "8AA01",
  "nombre": "ALMACENES ALPA",
  "rnc": null,
  "telefono": "8095242249",
  "email": null,
  "vendedor": "MG",
  "moneda": "DOP",
  "ciudad": "Barahona, R. D.",
  "sector": null,
  "limite_credito": 10000,
  "inactivo": false,
  "excento": false,
  "direcciones_entrega": [
    {
      "id": 12,
      "nombre": "Sucursal Centro",
      "direccion": "Calle Duarte 123",
      "sector": "Centro",
      "ciudad": "Barahona",
      "pais": "DO"
    }
  ]
}

direcciones_entrega es un arreglo de sucursales alternativas de entrega leído desde dbo.sucursal_cliente (join cliente = clientes.codigo). Clientes sin sucursales devuelven []. Útil para resolver direccion_entrega al crear órdenes vía /orders/insertar.

excento indica si el cliente está exento de ITBIS (lee clientes.excento).

Merchandise JWT

Read-only. Fuente: inventario_vision (mercancías) UNION mercs WHERE servicio=1 AND inactivo=0 (servicios facturables sin stock físico). Los servicios devuelven existencia_inventario, empaque_cantidad, paleta_cantidad en 0.

GET /merchandise

ParámetroTipoDefaultDescripción
pageint1Página
limitint 1-10010Tamaño
searchstringBusca en nombre y codigo (palabras separadas por espacio, AND)
tipoenum todo/mercancia/serviciotodoFiltra por clase de item
clientestringCódigo de cliente. Si se envía, precio sale de preciosd (lista del cliente vía clientes.listap); si no, de mercs.precio1.
# Listado con precio según lista del cliente 8AA01
curl -H "Authorization: Bearer $TOKEN" \
  "http://192.168.1.3:3005/merchandise?cliente=8AA01&limit=5"

# Sin cliente → precio = mercs.precio1
curl -H "Authorization: Bearer $TOKEN" \
  "http://192.168.1.3:3005/merchandise?search=ASIENTO"

GET /merchandise/:codigo

curl -H "Authorization: Bearer $TOKEN" \
  "http://192.168.1.3:3005/merchandise/AAINOD?cliente=8AA01"

Acepta también ?cliente=<codigo> para resolver precio por lista del cliente.

Respuesta

{
  "codigo": "AAINOD",
  "nombre": "ASIENTO Y TAPA PARA INODORO",
  "existencia_inventario": 0,
  "empaque_cantidad": 0,
  "paleta_cantidad": 0,
  "servicio": false,
  "excento": false,
  "precio": 150.00
}

servicio=true → item es servicio facturable (mercs.servicio). excento=true → exento de ITBIS (mercs.excento). precio = unitario, resuelto por preciosd (lista del cliente vía clientes.listap) si se envía cliente; si no, mercs.precio1. Cliente inexistente devuelve 404.

Orders — Inserción vía SP JWT

Crea cabecera + detalle en imtr/imtrd ejecutando el SP dbo.insertar_orden. La sucursal se fija en L1 dentro del SP — no debe enviarse en el body.

POST /orders/insertar

Body

{
  "usuario":   "string ≤10 chars",
  "cliente":   "string ≤20 chars (clientes.codigo)",
  "comentario":"string ≤200 chars (opcional)",
  "almacen":   "string ≤5 chars",
  "moneda":    "USD" | "DOP" (obligatorio),
  "descuento": porcentaje 0-100 (opcional, default 0) — aplica a toda la orden,
  "fecha_entrega": "YYYY-MM-DD (opcional)",
  "direccion_entrega": string ≤4000 chars (opcional) — destino físico de entrega. Si se omite, el SP usa `clientes.direccion`.,
  "detalle": [
    {
      "merc":     "string ≤60 chars (mercs.codigo)",
      "descrip":  "string ≤60 chars (opcional)",
      "cantidad": número positivo,
      "precio":   número >= 0 (obligatorio) — precio unitario de la línea
    }
  ]
}

Ejemplo

curl -X POST http://192.168.1.3:3005/orders/insertar \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "usuario": "JPEREZ",
    "cliente": "8AA01",
    "comentario": "Entrega urgente",
    "almacen": "L1",
    "moneda": "DOP",
    "descuento": 10,
    "fecha_entrega": "2026-05-25",
    "direccion_entrega": "Av. 27 de Febrero #123, Santo Domingo",
    "detalle": [
      { "merc": "AAINOD", "descrip": "ASIENTO Y TAPA", "cantidad": 3, "precio": 150.00 },
      { "merc": "ACADUA", "cantidad": 2, "precio": 75.50 }
    ]
  }'

Respuesta 201

{
  "success": true,
  "message": "Orden insertada correctamente",
  "data": {
    "control_generado": "L1000001559210",
    "numero_orden": "000942241",
    "cliente": "8AA01",
    "nombre_cliente": "ALMACENES ALPA",
    "valor": 602.73,
    "itbis": 108.49,
    "total": 711.22,
    "fecha_creacion": "2026-05-19T18:30:00.000Z",
    "fecha_entrega": "2026-05-25"
  }
}

Lógica del SP

Rates — Tasas de cambio JWT

Fuente: función oficial del ERP dbo.lista_tasas (tabla tasas, alimentada a diario). compra es la tasa que usa insertar_orden para las órdenes; valor es la de venta.

GET /rates · GET /rates/:moneda

No se envía ningún valor — solo la moneda en la URL. El endpoint devuelve las dos tasas vigentes; para convertir precios usa compra.

# Lo que TÚ envías (solo la moneda):
curl -H "Authorization: Bearer $TOKEN" http://192.168.1.3:3005/rates/USD

# Lo que el sistema TE DEVUELVE:
{
  "success": true,
  "message": "Tasa vigente",
  "data": { "moneda": "USD", "valor": 59.4770, "compra": 58.6823, "fecha": "2026-06-11T00:00:00.000Z" }
}

# Conversión: precioUSD = precioDOP / data.compra

Eventos del ERP — Despacho / Facturado automático Webhook firmado

Para el desarrollador del ERP. Cuando una orden se despacha o factura, el ERP avisa a OrderVision y este actualiza el pedido sin intervención manual.

Flujo (push + pull)

El ERP manda un aviso corto (solo control + tipo, sin cantidades). OrderVision responde yendo a buscar el detalle al SP dbo.ov_orden_estado y aplica los acumulados por mercancía. Es idempotente: reenviar el mismo aviso nunca duplica.

  ERP                                   OrderVision
   │  POST /erp/eventos                     │
   │  { tipo, control, eventId } ─────────► │  valida firma + eventId
   │  ◄──── EXEC dbo.ov_orden_estado ────── │  pide acumulados por merc
   │  merc | despachada_acum | facturada_acum
   │  ─────────────────────────────────────►│  actualiza orden + pedido

POST /erp/eventos

Autenticación — firma HMAC (sin JWT)

Cada petición se firma con un secreto compartido que entrega OrderVision:

Body

{
  "eventId":      "string único por evento (obligatorio)",  // ej. "DESP-2026-000123"
  "tipo":         "DESPACHO" | "FACTURA",                   // obligatorio
  "control":      "L1000001569510",                         // obligatorio — control de la orden
  "numeroOrden":  "000942241",                              // opcional (informativo)
  "numeroFactura":"F-00123",                                // opcional (en FACTURA)
  "numeroConduce":"C-00045",                                // opcional (en DESPACHO)
  "fechaEvento":  "2026-06-08T14:30:00.000Z"                // opcional
}

eventId = identificador único del aviso (lo genera el ERP); evita procesar dos veces. El body NO lleva cantidades — OrderVision las lee del SP.

Ejemplo (con firma)

BODY='{"eventId":"DESP-2026-000123","tipo":"DESPACHO","control":"L1000001569510","numeroConduce":"C-00045"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRETO" | awk '{print $2}')
curl -X POST http://192.168.1.3:3005/erp/eventos \
  -H "Content-Type: application/json" \
  -H "x-ov-signature: $SIG" \
  -d "$BODY"

Respuestas

HTTPdata.resultadoSignificado¿Reintentar?
200procesadoAviso aplicado, pedido/orden actualizados.No
200duplicadoEse eventId ya se procesó. No toca nada.No
200ignoradoEl control no existe en OrderVision.No
401Firma inválida o ausente.No (revisar secreto)
503Integración apagada (aún no activada).Sí, más tarde
500Error temporal procesando.

Reintentar solo ante 5xx/timeout, con backoff. Como es idempotente por eventId, reintentar es seguro.

Stored procedure dbo.ov_orden_estado Ya creado

Ya está creado por OrderVision (solo lectura): despachada_acum = imtrd.cantidad_despachada de la orden; facturada_acum = suma de la factura FT con control = <control>-ft. El dev del ERP no necesita crearlo. OrderVision lo ejecuta en cada evento: EXEC dbo.ov_orden_estado @control = '<control>'. Devuelve una fila por mercancía con los acumulados totales de la orden (no el delta del último movimiento):

ColumnaTipoDescripción
mercvarcharCódigo de mercancía (mercs.codigo).
despachada_acumnuméricoTotal despachado de esa merc en la orden, a la fecha.
facturada_acumnuméricoTotal facturado de esa merc en la orden, a la fecha.

Clave: son acumulados. Si se despachan 500 y luego 300 más, el SP debe devolver despachada_acum = 800 (no 300). OrderVision calcula la diferencia internamente.

Activación

La integración llega apagada (503). Para arrancar: (1) el ERP implementa el webhook (el SP ya está creado); (2) OrderVision entrega el secreto; (3) prueba con un despacho real; (4) OrderVision activa el flag. Mientras tanto, el cierre manual sigue como respaldo de emergencia.

AI JWT

POST /ai/extract-order

Recibe texto/imagen/PDF y devuelve una orden estructurada (Gemini). Útil para alimentar el body de /orders/insertar.

Prompt sugerido — IA del cliente

Para que la IA del lado del cliente genere el body de /orders/insertar a partir de texto libre:

Eres un asistente que crea órdenes de venta en OrderVision.
Cuando el usuario describa lo que quiere ordenar (voz, correo, WhatsApp, foto),
debes:

1) Construir el cuerpo JSON exacto para:

   POST http://192.168.1.3:3005/orders/insertar
   Headers:
     Authorization: Bearer <JWT>
     Content-Type: application/json

2) Enviar la petición y reportar al usuario el control_generado y el total.

ESTRUCTURA DEL BODY:
{
  "usuario":   "<código del usuario logueado, ≤10 chars, MAYÚSCULAS>",
  "cliente":   "<código de clientes.codigo, ≤20 chars, MAYÚSCULAS>",
  "comentario":"<texto libre, ≤200 chars, opcional>",
  "almacen":   "<código de almacén, ≤5 chars, MAYÚSCULAS>",
  "moneda":    "<USD o DOP, obligatorio>",
  "descuento": <porcentaje 0-100, opcional, default 0>,
  "fecha_entrega": "<YYYY-MM-DD, opcional>",
  "direccion_entrega": "<texto ≤4000 chars, opcional — destino físico; si se omite, el SP usa clientes.direccion>",
  "detalle": [
    {
      "merc":     "<código de mercs.codigo, ≤60 chars, MAYÚSCULAS>",
      "descrip":  "<descripción ≤60 chars, opcional>",
      "cantidad": <número positivo>,
      "precio":   <número >= 0, obligatorio — precio unitario>
    }
  ]
}

REGLAS
- NO envíes sucursal — el servidor la fija en L1.
- No inventes códigos. Si el usuario menciona un producto/cliente por nombre
  y no tienes el código exacto, pide aclaración antes de llamar al endpoint.
  Para resolver códigos puedes consultar /clients?search=... y
  /merchandise?search=...
- Convierte cantidades habladas a número (media docena = 6, dos y medio = 2.5).
- Si el endpoint responde con error, muestra el mensaje al usuario tal cual.

EJEMPLO
Usuario: "Soy JPEREZ. Orden para Almacenes Alpa 8AA01 desde almacén L1:
          tres asientos AAINOD y dos ACADUA. Anótale entrega urgente."

Body que envías:
{
  "usuario": "JPEREZ",
  "cliente": "8AA01",
  "comentario": "ENTREGA URGENTE",
  "almacen": "L1",
  "moneda": "DOP",
  "detalle": [
    { "merc": "AAINOD", "descrip": "ASIENTO Y TAPA PARA INODORO", "cantidad": 3, "precio": 150.00 },
    { "merc": "ACADUA", "descrip": "ASA CAJA DURALON",            "cantidad": 2, "precio": 75.50 }
  ]
}

Cambios recientes

FechaCambio
2026-05-26/merchandise y /merchandise/:codigo ahora devuelven precio. Param opcional cliente=<codigo> resuelve la lista de precio del cliente (preciosd vía clientes.listap); sin cliente, precio = mercs.precio1.
2026-05-25/clients ahora expone excento (ITBIS). /merchandise expone excento y servicio, acepta param tipo (todo/mercancia/servicio) y la lista incluye servicios activos vía UNION con mercs.
2026-05-22/clients y /clients/:codigo ahora incluyen direcciones_entrega (sucursales alternativas desde dbo.sucursal_cliente).
2026-05-22direccion_entrega (opcional) en /orders/insertarimtr.destino. Fallback automático a clientes.direccion cuando se omite.
2026-05-20Breaking: moneda (USD/DOP) y precio por línea ahora son obligatorios en /orders/insertar. SP ya no consulta preciosd/mercs.precio1 ni clientes.moneda.
2026-05-19Endpoint /orders/insertar + SP dbo.insertar_orden adaptado al schema real (vence/fpago int, longitudes ajustadas).
2026-05-19SP forzado a sucursal='L1'; campo removido del body.
2026-05-19Endpoint /clients (list + getByCode) sobre tabla clientes del ERP.
2026-05-19Endpoint /ready con probe de DB.
2026-05-19Esta guía HTML en /.