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ámetro | Tipo | Default | Descripción |
|---|---|---|---|
| page | int ≥ 1 | 1 | Página |
| limit | int 1-100 | 10 | Tamaño de página |
| search | string | — | Busca en código, nombre o RNC |
| includeInactive | bool | false | Incluir 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ámetro | Tipo | Default | Descripción |
|---|---|---|---|
| page | int | 1 | Página |
| limit | int 1-100 | 10 | Tamaño |
| search | string | — | Busca en nombre y codigo (palabras separadas por espacio, AND) |
| tipo | enum todo/mercancia/servicio | todo | Filtra por clase de item |
| cliente | string | — | Có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
preciopor línea viene del body (obligatorio). El SP no consultapreciosdnimercs.precio1: el cliente debe enviar el precio unitario resuelto.monedade cabecera viene del body (obligatorio,USDoDOP). Tasa de cambio se lee detasassegún la moneda (fallback 1).- ITBIS fijo 18%.
- Genera
controlde cabecera y por cada línea víadbo.next_id. - Cabecera:
doc='ORV',numero='Nuevo',diascr=30,vence=30,sucursal='L1'. descuentoglobal (porcentaje 0-100, opcional). Aplica el mismo % a cada línea:dvalor = valor * descuento/100,neto = valor - dvalor,itbis = neto * 0.18. Cabeceradescuento= suma dedvalorde líneas.direccion_entrega(opcional) se persiste enimtr.destino. Si el body no la envía (o llega vacía), el SP usaclientes.direcciondel cliente referenciado. Si el cliente tampoco tiene dirección,imtr.destinoqueda enNULL.
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:
- Calcular
HMAC-SHA256(secreto, cuerpoCrudo)en hex. - Enviarlo en el header
x-ov-signature. - Firmar exactamente los mismos bytes que se envían en el body. Firma inválida → 401.
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
| HTTP | data.resultado | Significado | ¿Reintentar? |
|---|---|---|---|
| 200 | procesado | Aviso aplicado, pedido/orden actualizados. | No |
| 200 | duplicado | Ese eventId ya se procesó. No toca nada. | No |
| 200 | ignorado | El control no existe en OrderVision. | No |
| 401 | — | Firma inválida o ausente. | No (revisar secreto) |
| 503 | — | Integración apagada (aún no activada). | Sí, más tarde |
| 500 | — | Error temporal procesando. | Sí |
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):
| Columna | Tipo | Descripción |
|---|---|---|
merc | varchar | Código de mercancía (mercs.codigo). |
despachada_acum | numérico | Total despachado de esa merc en la orden, a la fecha. |
facturada_acum | numérico | Total 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
| Fecha | Cambio |
|---|---|
| 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-22 | direccion_entrega (opcional) en /orders/insertar → imtr.destino. Fallback automático a clientes.direccion cuando se omite. |
| 2026-05-20 | Breaking: 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-19 | Endpoint /orders/insertar + SP dbo.insertar_orden adaptado al schema real (vence/fpago int, longitudes ajustadas). |
| 2026-05-19 | SP forzado a sucursal='L1'; campo removido del body. |
| 2026-05-19 | Endpoint /clients (list + getByCode) sobre tabla clientes del ERP. |
| 2026-05-19 | Endpoint /ready con probe de DB. |
| 2026-05-19 | Esta guía HTML en /. |