# TPV — Checkout, pagos mixtos y tickets

> Implementado en F-186 y ampliado en F-187. Complementa `POS_API.md` y sustituye el flujo inmediato de pago único descrito en documentos de discovery antiguos.

## Configuración en administración

En **Administración → TPV**, selecciona una tienda para configurar:

### Terminal

- Navegación táctil por categorías.
- Hasta **ocho** productos rápidos por terminal.
- **Permitir descuentos por línea**. Desactívalo en terminales de autopago. La restricción se aplica también en backend, no solo ocultando el botón.

### Formas de pago

Cada tienda puede activar o desactivar métodos con:

- código estable (`cash`, `card`, `bizum`, `stripe`, `apple_pay`, etc.);
- etiqueta visible;
- tipo `cash`, `card` u `other`.

Una etiqueta no integra automáticamente una pasarela. Stripe, Apple Pay, Bizum u otros métodos se registran manualmente hasta que exista un adaptador de proveedor.

### Empresa y ticket

Configura:

- nombre o razón social;
- NIF/CIF;
- dirección, teléfono y email;
- cabecera y pie;
- prefijo, próximo número y dígitos de relleno;
- política de devolución.

La asignación del siguiente número se bloquea dentro de la transacción de venta, evitando números duplicados entre terminales concurrentes.

### Cajeros

En **Administración → TPV → Cajeros** puedes crear cuentas con rol cajero y consultar su estado:

- **Activo**: puede iniciar sesión y abrir caja.
- **Inactivo**: no puede autenticarse; se puede reactivar.
- **Eliminado**: no puede autenticarse ni reactivarse.

**Desactivar** revoca inmediatamente todas las sesiones del cajero. Al reactivarlo tendrá que iniciar una sesión nueva.

**Eliminar** requiere confirmación y realiza una baja lógica irreversible. La fila y el UUID se conservan para que tickets, ventas, sesiones de caja, reporting y auditoría sigan indicando quién realizó la operación.

No se puede desactivar ni eliminar un cajero con una sesión de caja abierta. Primero debe cerrarse y cuadrarse esa sesión.

API administrativa:

- `GET /pos/users`: lista personal POS y estado de lifecycle.
- `POST /pos/users`: crea cajero/manager; contraseña mínima de ocho caracteres.
- `PATCH /pos/users/:id/status` con `{ "active": false }` o `{ "active": true }`.
- `DELETE /pos/users/:id`: baja lógica irreversible del cajero.

Las operaciones de estado y eliminación solo admiten objetivos con rol `pos_cashier`, requieren administrador y generan eventos de auditoría.

## Flujo de caja

1. Añade productos de catálogo o un **Artículo libre** (nombre y precio positivo).
2. Aplica descuentos por línea si el terminal los permite.
3. Pulsa una forma de pago configurada.
4. En el modal elige:
   - **Paga el total**: asigna todo el importe pendiente.
   - **Paga una parte**: introduce un importe menor y añade después otro método.
5. En efectivo, indica lo entregado. El TPV muestra el cambio.
6. Revisa las líneas de pago, el total pagado y el importe pendiente. Se puede quitar una asignación antes de confirmar.
7. Cuando el pendiente sea cero, pulsa **Confirmar y cerrar ticket**.
8. Imprime o envía el ticket por email. La caja se limpia después de completar una de estas acciones.

Una venta totalmente pagada queda en estado `COMPLETED`. Cuando los pagos asignados son menores al total, la venta queda en estado `PENDING` y se registra el importe pendiente (`outstandingCents`). El cobro del resto se hace con `POST /pos/sales/:id/payments`.

## Contrato de venta

`POST /pos/sales`

### Línea de stock

```json
{
  "kind": "stock",
  "variantId": "uuid",
  "quantity": 2,
  "discountCents": 100
}
```

El backend ignora snapshots antiguos enviados por el cliente y vuelve a cargar nombre, SKU, EAN y precio. También bloquea y actualiza el stock de la tienda.

### Artículo libre

```json
{
  "kind": "free",
  "name": "Servicio de asesoría",
  "unitPriceCents": 2500,
  "quantity": 1
}
```

No reserva ni descuenta inventario. La base de datos exige que `product_id` y `variant_id` sean nulos únicamente en estas líneas.

### Pago

```json
{
  "methodCode": "cash",
  "amountCents": 1000,
  "tenderedCents": 2000
}
```

- La suma de `amountCents` puede ser igual o menor al total. Si es menor, la venta queda `PENDING` con ese importe pendiente.
- Si supera el total, el backend rechaza con `POS_PAYMENT_OVERPAYMENT`.
- Solo un método de tipo efectivo acepta `tenderedCents`.
- `tenderedCents` debe ser mayor o igual que el importe aplicado.
- El cambio es `tenderedCents - amountCents`.
- Un código inactivo o de otra tienda se rechaza.

La venta, stock, pagos, líneas de reporting, saldo esperado de caja, número y ticket se confirman en una única transacción idempotente.

## Ticket

La respuesta de venta y `GET /pos/sales/:id/receipt` contienen:

- datos de empresa;
- fecha y hora;
- número de ticket;
- terminal, sesión y cajero;
- artículos, cantidad, precio, subtotal, descuento, IVA y total de línea;
- subtotal, descuentos, IVA y total de venta;
- métodos e importes pagados;
- efectivo entregado y cambio;
- política de devolución.

### Impresión

`GET /pos/sales/:id/print` devuelve el mismo payload estructurado. El cliente usa impresión estándar del navegador, sin controlador específico de hardware.

### Email

`POST /pos/sales/:id/receipt/email`

```json
{ "email": "cliente@example.es" }
```

Usa la configuración SMTP de **Ajustes → SMTP / Email**. El asunto y contenido se generan exclusivamente desde el ticket; el cliente no puede suministrar HTML arbitrario.

## Reporting y caja

Por cada asignación se crea:

- una transacción en `payments_transactions`;
- una línea `payment` en `reporting_payment_lines` con tienda, terminal, sesión y método.

El efectivo esperado aumenta por el importe aplicado, no por el efectivo entregado; el cambio no cuenta como ingreso ni efectivo retenido.

## Devoluciones

`POST /pos/sales/:id/returns` registra devoluciones parciales o totales que conservan el UUID de la venta, restauran el stock y emiten un ticket asociado con el prefijo `R-`. La operación queda registrada en `pos_returns`, `reporting_payment_lines` (status `refund`/`partial_refund`), `orders_order_history` y el log de auditoría. Las devoluciones solo operan sobre ventas `pos` y exigen la caja (`x-terminal-id`) y el rol POS. El endpoint antiguo `POST /pos/sales/:id/refund` queda desregistrado.

`GET /pos/sales/:id/items` expone las líneas de la orden con `quantity` y `returnedQuantity` para alimentar el modal de devolución.

## Próximas ampliaciones

- F-190: auditoría completa de actualización/refresco de reporting.
- F-190: auditoría completa de actualización/refresco de reporting.
- F-191: cierre de terminal y cierre diario conciliando efectivo, tarjetas, devoluciones y pendientes.
