# 02 — POS / Caja Rápida

> Ruta: `/caja/rapida/:codigo` · Vista: `views/caja/CajaRapida.vue` · Lógica:
> `composables/useCajaRapida.ts` (1070 líneas) · Componentes: `components/caja-rapida/*`

Es la pantalla de venta táctil pensada para mostrador: **una sola pantalla, sin scroll de
página, sin navegación**. Todo ocurre en tres columnas fijas.

## Anatomía de la pantalla

```
┌──────────────────────┬────────────────────────────────────────┬──────────────────────────────┐
│ ENCABEZADO (280px)   │ ÁREA CENTRAL (flex)                    │ DETALLE (aside derecho)      │
│ CajaRapidaEncabezado │ CajaRapidaArticulos                    │ CajaRapidaDetalle            │
├──────────────────────┼────────────────────────────────────────┼──────────────────────────────┤
│ 🔍 Cliente (modal)   │  ┌────────┐ ┌────────┐ ┌────────┐      │ ART │CANT│PRECIO│DESC│ISV│TOT│
│    (input readonly)  │  │Artículo│ │Artículo│ │Artículo│      │ ─────────────────────────────│
│ RTN         [____]   │  │ código │ │ código │ │ código │      │ (ítems, el último arriba)    │
│ Teléfono    [____]   │  │Disp: n │ │Disp: n │ │Disp: n │      │ ← clic = ítem activo         │
│ Correo      [____]   │  │ L 0.00 │ │ L 0.00 │ │ L 0.00 │      │                              │
│ Término   [Contado▾] │  └────────┘ └────────┘ └────────┘      │ Subtotal          L 0.00     │
│ ──────────────────── │        (clic = agregar al carrito)     │ Descuento       - L 0.00     │
│ Entrega domicilio [⏻]│                                        │ ISV               L 0.00     │
│  ↳ Dirección         │  ┌──────────────────┬─────────────────┐│ TOTAL             L 0.00     │
│  ↳ Contacto          │  │📷 código/escanear│🔍 descripción   ││ ──────────────────────────── │
│  ↳ Teléfono entrega  │  └──────────────────┴─────────────────┘│ Valor: 1                     │
│ ──────────────────── │                                        │ [Cantidad][Descuento]        │
│ Cambiar Correlativo  │  ◀  [Todos][Grupo1][Grupo2][Grupo3]  ▶ │ [Envío*][Almacén][Eliminar]  │
│ Buscar Factura       │     [Grupo4][Grupo5][Grupo6][Grupo7]   │ ┌───┬───┬───┐                │
│ Posponer Factura     │              ● ○ ○  (páginas)          │ │ 7 │ 8 │ 9 │ ...            │
│ Facturas Pospuestas  │                                        │ └───┴───┴───┘                │
│                      │                                        │ [    FACTURAR    ]           │
└──────────────────────┴────────────────────────────────────────┴──────────────────────────────┘
                                                        * Envío solo si "Entrega a domicilio" ON
```

## Arranque de la pantalla

`onMounted → inicializar()` dispara **6 llamadas en paralelo** (`Promise.all`):

```mermaid
sequenceDiagram
    participant V as CajaRapida.vue
    participant API as API /api/v1
    V->>API: GET /configuracion (sucursal, moneda)
    V->>API: GET /caja/{cod}/sesion-activa (cajaSesionId, almacén por defecto)
    V->>API: GET /parametro/impuestos (cache de tasas)
    V->>API: POST /parametro/metodos-pago (opciones de pago)
    V->>API: GET /parametro/grupos (categorías + "Todos")
    V->>API: POST /parametro/catalogo-articulos (catálogo inicial)
    Note over V: Overlay "Cargando caja rápida..." hasta que todas resuelven
```

Si *cualquiera* falla solo aparece un toast; **la pantalla se abre igual** y el error
recién bloquea al facturar. Ver hallazgo UX-03.

## Catálogo de acciones del usuario

### Zona 1 — Encabezado (izquierda)

| # | Acción | Interacción | Efecto |
|---|---|---|---|
| 1 | **Buscar cliente** | Clic en 🔍 | `SearchModal` paginado server-side (`POST /parametro/socios-negocio`, `tipo:"C"`). Al elegir, rellena `codigoSdn`, `rtn`, `nombre` |
| 2 | Editar RTN / teléfono / correo | Escribir | Sobrescriben los datos del cliente **solo en la factura** (no actualizan el maestro) |
| 3 | Cambiar término de pago | Select | `Contado` / `Crédito`. Si es Contado, `condicionPago` se fuerza a 0 al construir el payload |
| 4 | **Entrega a domicilio** | Switch | Muestra bloque verde con Dirección / Contacto / Teléfono (los 3 obligatorios) y **habilita la columna ENVÍO y el botón Envío del pad**. Al apagarlo, `limpiarCamposEntrega()` borra los 3 campos y pone `cantidadEnvio = null` en todas las líneas |
| 5 | Cambiar Correlativo | Botón | ⚠ **No implementado** — `console.log('Cambiar correlativo')` |
| 6 | Buscar Factura | Botón | ⚠ **No implementado** — `console.log('Buscar factura')` |
| 7 | **Posponer Factura** | Botón | Guarda la venta como `estado: "Borrador"` y limpia el carrito |
| 8 | **Facturas Pospuestas** | Botón | Abre `FacturasPospuestasModal` para retomar un borrador de **esta sesión de caja** |

### Zona 2 — Área central (catálogo)

| # | Acción | Interacción | Efecto |
|---|---|---|---|
| 9 | **Agregar artículo del catálogo** | Clic en la tarjeta | Abre `AlmacenSelectModal` → al elegir almacén, agrega al carrito con cantidad 1 |
| 10 | **Buscar por código / escanear** | Escribir + `Enter` | `POST /parametro/buscar-articulo`. Si existe → modal de almacén → agrega y limpia el input. Si no → toast "Artículo no encontrado" |
| 11 | Buscar por descripción | Escribir + `Enter` | Refiltra el catálogo (`catalogo-articulos` con `descripcion`), respetando la categoría activa |
| 12 | Filtrar por categoría | Clic en botón de grupo | Un `watch` recarga el catálogo con `codigoGrupo` + el filtro de descripción vigente |
| 13 | Paginar categorías | Flechas ◀ ▶ o puntos | Rejilla de 8 categorías (2×4) por página, paginación solo en cliente |

Cada tarjeta muestra **nombre, código, "Disp: n" y precio**. `Disp` es informativo: la UI
no impide agregar ni sobrepasar la existencia (ver hallazgo UX-04).

### Zona 3 — Detalle (carrito + pad numérico)

| # | Acción | Interacción | Efecto |
|---|---|---|---|
| 14 | **Seleccionar ítem activo** | Clic en la línea | Marca la línea y **carga su cantidad en el display del pad** |
| 15 | Teclear valor | Pad `0-9`, `C`, `←` | Compone `valorPad` como texto (no hay punto decimal) |
| 16 | **Aplicar Cantidad** | Botón `Cantidad` | `parseInt(valorPad)` (o 1) → cantidad del ítem activo |
| 17 | **Aplicar Descuento** | Botón `Descuento` | Abre modal "Seleccionar descuento" (`/descuento/mis-descuentos`) + botón "Solicitar descuento" para teclear 0–100 % a mano. Si el ítem ya tenía descuento, pide confirmación antes de sobrescribir |
| 18 | **Cantidad de envío** | Botón `Envío` (solo con entrega a domicilio) | `min(valorPad, cantidad)` → `cantidadEnvio`; marca `esEntregaParcial` si es menor a la cantidad |
| 19 | **Cambiar almacén** | Botón `Almacén` | `AlmacenSelectModal` con `permitirSinStock = true` (a diferencia del alta, que no lo permite) |
| 20 | **Eliminar ítem** | Botón `Eliminar` | Quita la línea activa **sin confirmación** |
| 21 | **FACTURAR** | Botón grande | Valida → abre `RecepcionPagoModal` |

Los ítems se listan **en orden inverso** (el último agregado arriba), como una pila de
tickets. Agregar dos veces el mismo artículo **en el mismo almacén** incrementa la
cantidad; en distinto almacén crea una línea nueva.

## Cómo se calcula el total (en el navegador)

```
subtotalLínea  = cantidad × precio
descuentoLínea = subtotalLínea × (descuento% / 100)
baseLínea      = subtotalLínea − descuentoLínea
isvLínea       = baseLínea × (impuesto% / 100)
totalLínea     = baseLínea + isvLínea
```

Totales del pie: `Subtotal` es **bruto** (sin descuento), luego `Descuento` (si > 0),
`ISV` y `TOTAL`. Redondeo a 2 decimales.

> ⚠ El backend **recalcula todo por su cuenta** al guardar (precio desde `Articulo`, tasa
> desde `Impuesto`) y no usa los montos que envía la UI — el payload ni siquiera lleva
> precios ni totales. Si las convenciones de tasa difieren, el cliente ve un total y se
> registra otro. Ver hallazgo **UX-01**.

## Flujo principal de una venta

```mermaid
flowchart TD
    A([Cajero en la caja rápida]) --> B[Buscar y elegir cliente]
    B --> C{Cómo agrega artículos}
    C -->|Escanea o teclea código + Enter| D[Modal: elegir almacén]
    C -->|Clic en tarjeta del catálogo| D
    C -->|Filtra por categoría o descripción| C
    D --> E[Línea en el carrito, cantidad 1]
    E --> F{Ajustar la línea}
    F -->|Sí| G[Clic en la línea y pad numérico:<br/>Cantidad · Descuento · Envío · Almacén · Eliminar]
    G --> E
    F -->|No| H{Entrega a domicilio}
    H -->|Sí| I[Switch ON + dirección, contacto, teléfono<br/>y cantidad de envío por línea]
    H -->|No| J
    I --> J{Cobra ahora}
    J -->|No| K[Posponer Factura:<br/>POST/PUT factura estado Borrador] --> L[Carrito limpio]
    J -->|Sí| M[FACTURAR]
    M --> N{Validaciones}
    N -->|falla| O[Toast de validación] --> F
    N -->|ok| P[Modal Recepción de Pago]
    P --> Q[Elegir métodos, montos y referencia]
    Q --> R{pagado mayor o igual al total}
    R -->|No| S[Toast: Pago incompleto] --> Q
    R -->|Sí| T[POST /factura estado Procesada + pagos]
    T --> U[Backend: valida stock, descuenta inventario,<br/>crea órdenes de entrega si aplica]
    U --> V[Toast con el número de factura, 4 s<br/>Carrito limpio]
    V --> L
```

### Validaciones antes de cobrar (`validarFormulario`)

1. Configuración cargada
2. **Sesión de caja activa**
3. Cliente seleccionado
4. Al menos un artículo
5. Si hay entrega a domicilio: dirección, contacto y teléfono no vacíos

Todas se comunican con un toast amarillo de 4 s; ninguna resalta el campo culpable.

## Modal de Recepción de Pago

Compartido con la factura empresarial (`components/factura/RecepcionPagoModal.vue`).

```mermaid
flowchart LR
    subgraph M[RECEPCIÓN DE PAGO - 3 columnas]
        A[Datos:<br/>Nº factura · Cliente<br/>Término editable]
        B[Métodos de pago:<br/>línea = método · monto · referencia<br/>Pagado / Faltante]
        C[Acciones:<br/>Agregar Método<br/>Confirmar<br/>Cancelar]
    end
    B -.-> D[La referencia solo aparece<br/>si el método no es de tipo EFEC]
```

- Al abrir, precarga **una línea con el primer método de la lista y el monto = total**.
- "Agregar Método" añade una línea con el **faltante** como monto.
- Se admite **pago mixto** (varias líneas).
- Confirmar exige `pagado ≥ total` y método elegido en toda línea con monto > 0.
- **No calcula vuelto/cambio** y el excedente se envía tal cual al backend — hallazgo UX-02.

## Posponer y retomar una venta

```mermaid
sequenceDiagram
    actor C as Cajero
    participant UI as Caja Rápida
    participant API as API

    C->>UI: Posponer Factura
    alt primera vez
        UI->>API: POST /factura (estado Borrador)
    else ya venía de un borrador
        UI->>API: PUT /factura/{codigoFac} (estado Borrador)
    end
    API-->>UI: codigoFac
    UI->>UI: limpiarCarrito() y facturaBorradorActual = null

    C->>UI: Facturas Pospuestas
    UI->>API: POST /parametro/facturas-borrador?cajaSesionId=...
    Note right of API: solo borradores de ESTA sesión de caja
    C->>UI: Elegir una
    UI->>API: GET /factura/{codigoFac}
    UI->>UI: Rellena encabezado y detalles<br/>facturaBorradorActual = codigoFac
    C->>UI: FACTURAR
    UI->>API: POST /factura (estado Procesada)
    Note over UI,API: crea una factura NUEVA;<br/>el borrador original queda vivo
```

⚠ Dos comportamientos a revisar (hallazgo UX-07):
- Retomar un borrador **pisa el carrito actual sin avisar**.
- Facturar un borrador retomado hace `POST` (alta nueva), no `PUT` sobre el borrador: el
  borrador queda en la lista de pospuestas y puede volver a facturarse.

## Payload que sale del POS

`construirPayload(estado, pagos)` — `POST /api/v1/factura`:

```jsonc
{
  "codigoScs": "...", "codigoMnd": "LPS",
  "cajaSesionId": 123,          // de la sesión activa
  "codigoUsr": "USR0000001",
  "codigoCtz": null,
  "claveFiscal": "", "correlativoFiscal": "",   // siempre vacíos
  "codigoSdn": "SDN000001", "rtnsdn": "...", "nombreSdn": "...",
  "telefonoSdn": "...", "emailSdn": "...",
  "exoneradoSdn": false,        // el POS nunca lo expone
  "termino": "Contado", "condicionPago": 0,
  "entregaDomicilio": false, "direccionEntrega": "", "contactoEntrega": "", "telefonoEntrega": "",
  "estado": "Procesada",        // o "Borrador"
  "estadoDescuentos": "SinDescuento",   // fijo, aunque haya descuentos
  "observacion": "",
  "detalles": [{
    "codigoArt": "ART000001", "codigoAlm": "ALM001", "codigoMdd": "UND",
    "cantidad": 2, "cantidadEnvio": 0,
    "descuentoSolicitado": 10, "descuentoAprobado": 10,  // auto-aprobado
    "esEntregaParcial": false, "comentario": ""
  }],
  "pagos": [{ "codigoBnc": "BNC001", "monto": 250.00, "referencia": null }]
}
```

Observaciones: no viajan precios ni totales (el backend los recalcula); el POS nunca
envía correlativo fiscal; el descuento solicitado se aprueba solo.

## Qué hace el backend al recibir `estado: "Procesada"`

```mermaid
flowchart TD
    A[POST /api/v1/factura] --> B{Estado Procesada sin pagos}
    B -->|Sí| E1[Error: requiere pagos]
    B -->|No| C[Abre transacción]
    C --> D[Por cada detalle: valida<br/>InventarioAlmacen.Disponible >= cantidad]
    D -->|insuficiente| E2[Error: Stock insuficiente para artículo X]
    D -->|ok| F[Calcula precio, descuento, ISV y total<br/>desde Articulo + Impuesto]
    F --> G[Genera código FAC###### con CodigoGeneratorService]
    G --> H[Inserta Factura + FacturaDetalle + FacturaPagos]
    H --> I[Descuenta Disponible en InventarioAlmacen]
    I --> J{entregaDomicilio}
    J -->|Sí| K[Crea OrdenEntrega en estado Borrador<br/>por la cantidad pendiente de envío]
    J -->|No| L[Commit]
    K --> L
```

## Lo que el POS **no** hace hoy

| Falta | Detalle |
|---|---|
| Imprimir ticket | No hay impresión ni vista previa tras cobrar. `PdfModal.vue` existe pero **no se importa en ningún lado** |
| Correlativo fiscal | Se envía vacío; el botón "Cambiar Correlativo" es un stub. La tabla `CajaCorrelativo` y la asignación en la configuración de caja no se consumen al facturar |
| Cancelar la venta en curso | No hay botón "Nueva venta" / "Limpiar": hay que borrar ítem por ítem |
| Atajos de teclado | Solo `Enter` en los dos buscadores. No hay F1..F12 ni foco automático en el campo de escaneo |
| Cliente rápido / genérico | El cliente es obligatorio y siempre por modal; no hay "Consumidor final" ni alta rápida |
| Exoneración | El POS envía siempre `exoneradoSdn: false` (en factura empresarial sí es editable) |
| Devoluciones / notas de crédito | Existen tablas `Devolucion` y `NotaCredito` en la BD; sin API ni UI |
| Arqueo asistido | El cierre de sesión pide los totales **a mano** (ver doc 03) |
