# API v1 — contrato utilizado por el frontend

Base de desarrollo: `http://localhost:8016/api/v1`.

Todas las rutas salvo login exigen `Authorization: Bearer TOKEN` y un usuario activo. `Accept: application/json` y `Content-Type: application/json`. Importes enteros en **centavos de boliviano**; cantidades enteras de unidad base. Sin valores monetarios flotantes en persistencia.

## Endpoints y permisos

| Método / ruta | Uso | Acceso |
|---|---|---|
| POST auth/login | Correo + contraseña → token y usuario | Público, 5 intentos/minuto |
| GET auth/me / POST auth/logout | Sesión actual / revocar token actual | Autenticado |
| PUT auth/password | current_password, password, password_confirmation | Autenticado |
| GET dashboard | Ventas del día, alertas y resumen | Admin global; operador/regente, sus ventas |
| GET products / products/{id} | Búsqueda, stock habilitado y detalle con lotes | Autenticado |
| POST products / PUT products/{id} | Crear, editar, desactivar; no eliminar historial | Admin / regente |
| GET suppliers | Proveedores | Admin / regente |
| POST suppliers / PUT suppliers/{id} | Crear, editar, desactivar | Admin |
| GET batches | Lotes; filtros search, status, expiring | Autenticado |
| PUT batches/{id}/status | status y reason (mínimo 5 caracteres) | Admin / regente |
| POST batches/{id}/adjust | quantity (+/−) y reason | Admin |
| GET stock-movements | Kardex; filtro batch_id | Admin / regente |
| GET purchases / purchases/{id} | Compras y detalle | Admin / regente |
| POST purchases | Compra definitiva e ingreso de lotes | Admin |
| POST purchases/{id}/payments | Abono: amount_cents y reference | Admin |
| GET sales / sales/{id} | Historial y datos del recibo | Admin global; otros, propias ventas |
| GET sales/by-request/{uuid} | Recuperar resultado de una operación | Misma visibilidad que la venta |
| POST sales | Registrar una venta cobrada; request_id obligatorio | Autenticado con caja abierta |
| POST sales/{id}/void | Anulación completa; reason obligatorio | Admin, caja de origen abierta |
| GET cash/current | Turno y efectivo esperado | Turno propio |
| GET cash/sessions | Historial de turnos | Admin global; otros, propios |
| POST cash/open | opening_cents | Autenticado |
| POST cash/movements | amount_cents (+/−), concept | Turno propio abierto |
| POST cash/close | counted_cents, notes | Turno propio abierto |
| GET settings | Datos públicos para recibo, sin secretos | Autenticado |
| PUT settings | Nombre, logo, dirección, papel y pie | Admin |
| GET users / POST users / PUT users/{id} | Usuarios, roles y estado | Admin |
| GET audit | Auditoría paginada | Admin |
| GET qr/providers | Ambientes, campos configurados y proveedores habilitados, sin secretos | Autenticado |
| POST qr/attempts | Crea intento QR y reserva stock; no crea venta hasta verificar pago | Autenticado |

Los listados usan `page`. Productos, proveedores y usuarios admiten `search` y `per_page` (10–100); los demás listados usan 25, salvo el selector de proveedores que puede pedir 100. Ventas admite `from` y `to` (YYYY-MM-DD). `GET sales/{id}` devuelve `{sale,pharmacy}`. Las creaciones pueden devolver 201 y los reintentos de ventas 200.

## Producto

```json
{
 "barcode":"CODIGO-INTERNO-1",
 "name":"Nombre comercial",
 "generic_name":"Principio activo",
 "concentration":"Concentración real",
 "dosage_form":"Forma real",
 "laboratory":"Laboratorio",
 "registration":"NUMERO-REAL-VERIFICADO",
 "registration_status":"pending",
 "registration_verified_at":null,
 "registration_evidence":"",
 "dispensing":"prescription",
 "location":"A-1",
 "min_stock":5,
 "price_cents":250,
 "active":true,
 "presentations":[{"name":"Unidad","factor":1},{"name":"Caja","factor":100}]
}
```

`registration_status`: pending / verified / blocked. `verified` exige número, fecha no futura y evidencia. `dispensing`: otc / prescription / archived / valued. Las dos últimas modalidades quedan bloqueadas en la venta de v1.

## Compra

```json
{
 "supplier_id":1,"document":"COMPRA-001","purchased_at":"2026-11-05",
 "paid_cents":1000,"notes":"Recepción revisada",
 "items":[{"product_id":1,"lot":"LOTE-REAL","expires_at":"2027-11-05","quantity":100,"cost_cents":100}]
}
```

No repetir documento de un mismo proveedor. No admite ingreso de lotes ya vencidos. Una compra puede dejar el medicamento en inventario aunque su registro esté pendiente; el POS bloqueará su venta hasta la revisión.

## Venta

```json
{
 "request_id":"61460a20-dcc7-4a82-b3bd-e4dfd25ef307",
 "customer":"Cliente","customer_document":"",
 "discount_cents":0,
 "items":[{"product_id":1,"presentation":"Unidad","quantity":2}],
 "payments":[{"method":"cash","amount_cents":500}]
}
```

El servidor obtiene los precios actuales y asigna FEFO. La suma de pagos debe igualar exactamente el total. El efectivo enviado es **el aplicado**, no el recibido con cambio. Solo admin puede descontar. `card` y `transfer` exigen referencia; son registros manuales.

Para receta médica agrega `customer`, `prescription_reference` y `prescriber`; si el usuario vendedor no es regente, envía además `reviewer_email` y `reviewer_password` del regente activo que revisó. No los registres en logs. No guardes la contraseña del regente en almacenamiento del navegador.

`request_id` debe ser nuevo para cada nueva venta y debe **reutilizarse para todos sus reintentos**. Si se pierde la respuesta, consulta `sales/by-request/{uuid}`. Un 404 no demuestra que otra solicitud en curso haya terminado: reintenta la operación original con el mismo identificador. El bloqueo del usuario y del turno, la transacción y la unicidad del identificador impiden duplicar ventas en el mismo flujo.

## Errores

- 401: token ausente/vencido.
- 403: rol insuficiente, cuenta desactivada o venta ajena.
- 404: recurso inexistente o solicitud todavía no registrada.
- 409: conflicto de estado (caja cerrada/ya abierta, compra repetida, anulación repetida).
- 422: validación o regla de negocio; puede incluir `errors` por campo.
- 429: exceso de intentos de acceso.
- Los estados bancarios no concluyentes se conservan en el intento; no generan una venta.

Las operaciones no implementadas no retornan datos simulados.


## Agregado en v1.1.0 — importación de catálogo

Acceso: administrador o regente activo; las vistas previas e historial pertenecen al usuario que las creó.

| Método / ruta | Contrato |
|---|---|
| GET product-imports/template/xls | Descarga plantilla Excel BIFF8 real |
| GET product-imports/template/xlsx | Descarga plantilla OOXML |
| POST product-imports/preview | Multipart: file (hasta 5 MB, XLS/XLSX), mode=create o skip. Máximo 2000 filas, 10 revisiones/minuto |
| GET product-imports | Mis importaciones, 15 por página |
| GET product-imports/{uuid} | Vista previa con summary, rows, can_commit, expires_at y result |
| POST product-imports/{uuid}/commit | JSON: acknowledge=true. Confirma exactamente el contenido revisado en el servidor |
| GET product-imports/{uuid}/report | Revisión CSV separada por punto y coma y con protección de fórmulas |

`create` informa como error los códigos existentes. `skip` los omite sin sobrescribir. Los códigos duplicados dentro del mismo archivo son errores en ambos modos. Se exige la hoja Productos y las 13 columnas de la plantilla. Los códigos deben ser celdas de texto. Los nuevos productos tienen registro pendiente, sin fecha de verificación, active=true y sin stock. El precio de la plantilla está en bolivianos y se convierte a centavos.

La vista previa persiste contenido normalizado cifrado y metadatos, no conserva el archivo subido. `summary` contiene total/create/skip/invalid; cada fila expone row/barcode/name/price_cents/status/messages. La confirmación es transaccional, vuelve a comprobar colisiones y es idempotente por UUID. El cuerpo cliente no puede sustituir los productos revisados. Una vista previa pendiente vence a los 30 minutos. Se conserva historial para consulta; todavía no hay borrado ni depuración automática de ese historial.

`GET products` ahora incluye next_expiry (fecha o null), obtenida de lotes disponibles, no vencidos y con stock. `GET batches` admite expiry=expired,30,60,90 y product_id. Los filtros 30/60/90 excluyen vencidos. `GET dashboard` añade expired_count y expiring_count (stock físico, cualquier estado). El contrato previo de expiring=1 se conserva para clientes de v1.


## Agregado en v1.2.0

| Método / ruta | Respuesta / acceso |
|---|---|
| GET products/{id}/overview | product, stats y can_view_purchases. Ventas acumuladas globales; compras/costos solo admin o regente |
| GET products/{id}/sales | Historial agregado por venta, 20/página; admin/regente global, cajero solo propias |
| GET products/{id}/purchases | Historial agregado por compra, 20/página; admin/regente |
| GET suppliers/{id} | supplier y stats: compras, pagadas/pendientes, totales, saldo y última compra; admin/regente |
| GET suppliers/{id}/purchases | 20/página, status=paid/pending opcional; admin/regente |
| GET suppliers/{id}/payments | Abonos con compra/usuario y origen initial/payment/legacy; admin/regente |
| GET cash/sessions/{id} | Desglose e instantánea del cierre; propio turno o admin |
| POST sales/{id}/print | request_id UUID requerido; id, sale_id, sequence, is_copy, issued_at e issued_by; misma visibilidad que el recibo |

POST products permite barcode nulo/vacío/ausente para generar código en alta individual. En PUT products/{id}, vacío conserva el actual. `request_id` UUID opcional en el API y siempre enviado por el nuevo formulario para altas repetibles sin duplicar. Los Excel siguen exigiendo códigos. La secuencia global y las colisiones se resuelven dentro de una transacción.

POST purchases/{id}/payments admite request_id UUID opcional, enviado por el nuevo frontend. Los pagos iniciales y abonos nuevos se conservan en purchase_payments; GET purchases/{id} añade payments. El saldo sigue siendo total_cents − paid_cents, sin cambiar el contrato anterior.

POST cash/close admite request_id UUID y count_breakdown (11 elementos). Cada elemento tiene denomination_cents (20000,10000,5000,2000,1000,500,200,100,50,20,10) y quantity entero de 0 a 100000. No puede repetir denominación. La suma debe igualar counted_cents; el servidor calcula expected_cents y difference_cents. Para compatibilidad, clientes antiguos pueden enviar counted_cents sin desglose, pero no se fabrica un conteo.

El resumen de caja ahora siempre devuelve totals para cash/transfer/qr/card, cash_income_cents, cash_expense_cents, sales_collected_cents y expected_cents. CashSession añade count_breakdown, closing_snapshot y close_request_id. Los QR se cobran por el módulo de intentos; POST sales no acepta un método qr enviado por el navegador.

POST sales/{id}/print incrementa receipt_print_count bajo bloqueo y guarda receipt_issues con UUID único. El contador representa solicitudes del botón, no confirmaciones físicas de impresión. Los recibos previos se migran con contador 1.

## Nuevas rutas v1.3.0

Todas bajo /api/v1. Autenticación bearer, salvo el callback SIP (HTTP Basic propio).

| Método / ruta | Contrato |
|---|---|
| GET customers?search=&active=1 | Catálogo paginado, nombre/documento; autenticado |
| GET customers/lookup?document= | {customer: objeto o null}; solo clientes activos |
| POST customers | name, document?, phone?, address?, active; autenticado |
| PUT customers/{id} | Mismos campos; admin/regente |
| PUT qr/providers/{id} | enabled, auth_contract account/username, bank_label?, credentials objeto; admin; blancos conservan secretos |
| POST qr/providers/{id}/test | Prueba autenticación; admin; no devuelve token ni genera QR |
| POST qr/attempts | request_id UUID, provider bnb/sip, auto_confirm boolean, datos de cliente/receta, discount_cents e items iguales a ventas; no recibe pagos |
| GET qr/attempts | Historial paginado del operador; sin imagen ni secretos |
| GET qr/attempts/{uuid} | Detalle propio, imagen PNG data URL cuando está disponible |
| POST qr/attempts/{uuid}/refresh | Consulta bancaria; confirmación automática según preferencia del intento |
| POST qr/attempts/{uuid}/confirm | Consulta y confirma únicamente si existe prueba de pago verificada |
| POST qr/attempts/{uuid}/cancel | Consulta previa; anulación con proveedor y liberación si la acepta |
| POST qr/callback/sip | HTTP Basic contra credenciales del intento, alias requerido; reconsulta banco. 200 codigo 0000 cuando pago verificado; 503 si aún no se verifica |

POST sales añade customer_id y customer_phone opcionales. Las ventas conservan customer/customer_document como instantánea histórica. No se permite modificar esa instantánea desde Clientes.

Un intento devuelve id, alias, provider, environment, amount_cents, status, auto_confirm, provider_id, expires_at, paid_at, sale_id, last_error, customer y, solo en detalle, qr_image. Nunca devuelve checkout ni connection. Estados: creating, pending, generation_unknown, paid, attention, completed, cancelled, expired, rejected. paid significa pago bancario verificado; completed significa venta creada. Las operaciones repetidas con el mismo UUID no regeneran el QR.

batches añade reserved_stock. products.available_stock y los acumulados de disponibilidad excluyen reservas; el stock físico solo se descuenta al completar la venta. cash/close responde 409 si hay intentos sin resolver.

## Cambios v1.4.0

- GET qr/providers devuelve ttl_minutes por conexión. PUT qr/providers/{id} admite ttl_minutes entero 1..60, solo admin. Si se omite, conserva el actual. Predeterminado 3.
- Los nuevos intentos devuelven enforce_timeout=true y expires_at como plazo local de cobro; server_time permite sincronizar la cuenta regresiva. No equivale a un vencimiento bancario intradía: los contratos documentados solo aceptan fecha. El servidor consulta y solicita anulación al llegar al plazo. El estado expired se asigna tras anulación aceptada o vencimiento informado por banco; nunca libera stock solo por reloj.
- GET qr/attempts/{id}/draft, propio operador, devuelve items con producto/precio/disponibilidad actuales, presentación, factor y cantidad; customer_id/customer/customer_document, discount_cents, prescription_reference, prescriber y reviewer_email. Solo se permite si el intento está cancelled/expired/rejected sin pago ni venta, en otro caso 409. No devuelve contraseñas, credenciales, precios congelados internos ni prueba de aprobación del regente.
- POST qr/attempts/{id}/refresh y el planificador también aplican el plazo. Pago verificado tiene prioridad; una anulación incierta conserva estado/reservas y last_error. QR anteriores a v1.4 mantienen enforce_timeout=false.
