# Clínica — Análisis de paridad vs MQN y pendientes

> Estado tras cerrar los 4 gaps de paridad (Laboratorio/Recepcionista/Ingresos/Reprogramar).
> Entorno verificado: LOCAL, tenant de prueba `mqn_efp` en `jena.enterfarmaplus.test`. NO deployado a prod.
> Última pasada de análisis con Playwright: 2026-06-30.

## ✅ Lo que funciona (verificado en vivo)

- Las 8 pantallas de clínica cargan: Panel, Citas, Laboratorio, Pacientes, Reportes, Especialidades, Procedimientos, Config.
- **Citas**: lista/Día/Calendario, nueva cita (wizard), Vitales, Pagar→POS, Sin cobro, Ticket, FUA, **Reprogramar** (modal calendario + slots 12h).
- **Atención (doctor)**: tabs Atención Médica (EVA, vitales con IMC auto, CIE) / Ayuda Dx (adjuntos) / Procedimientos y Tratamientos / **Ventas** (carrito→POS) / Historial (Citas + Laboratorio con "Ver detalle"). Header sticky, badges, panel resumen.
- **Laboratorio**: "Solicitar examen" (paciente/doctor/prioridad/fecha/notas/búsqueda+seleccionados) y "Resultados" (#/Examen/Resultado/Acciones); marca la orden Atendido al completar.
- **Reportes**: Resumen + **Ingresos** (S/ por rango, por doctor/especialidad/día).
- **Configuración**: Personal (CRUD doctor/recepcionista), Signos vitales, Servicio de cita.
- **Recepcionista** (rol): aterriza en su dashboard, menú reducido (Dashboard/Citas/Pacientes), bloqueado de atención/settings/reportes/laboratorio, permitido en citas/pacientes; botón "Atender" oculto.

## ✅ Resueltos desde la primera pasada (ver sección de perfiles + memoria del proyecto)

1. ~~El recepcionista NO puede cobrar~~ → **RESUELTO**: RBAC centralizado da `pos`+`charge` al rol 2; recep cobra
   consulta y productos (verificado e2e). Ya no depende del admin.
2. ~~Recepcionista ve FUA/Cotizaciones~~ → **RESUELTO**: FUA es intencional para recepción (`can.fua=true`);
   el botón **"Cotizaciones" se eliminó** para todos (decisión: carrito).
3. ~~Roles faltantes vs MQN~~ → **RESUELTO**: los **5 perfiles** (Doctor/Recep/Admin/Farmacia/Laboratorio) están
   implementados vía `User::clinicProfile()`. (Prod aún no tiene usuario Laboratorio → asignar uno al cutover.)

## 🟡 Pendientes menores (no bloqueantes)

- **Error de consola del layout** (`configurations/visual/get_menu`, `/persons/tables` en **https**): gotcha del 301
  cacheado; son calls del *layout*, no de clínica. Benigno en local; resuelve en prod (force_https ON).
- **Login**: el botón "Iniciar Sesión" no submitea por click simple (handler JS); en pruebas Playwright hubo que
  forzar el submit nativo. Confirmar que el flujo real de usuario funciona.

## 👤 Cómo funciona cada perfil (estado actual — 2026-06-30)

**NO hay tabla de roles.** El "perfil" es el entero **`users.clinic_role_id`**. Todo el comportamiento
sale de un mapa central **`User::clinicProfile(roleId)`** → `{landing, modules, prefixes, pos, clinic_scoped, can}`,
consumido por `LoginController@authenticated` (landing), `RedirectModule` + `ClinicRoleGate` (rutas),
`sidebar.blade.php` (menú) y `Configuration::getCollectionData` (expone `clinic_role`/`clinic_can` al JS).
**Defensa en 3 capas**: menú oculto + ruta bloqueada + backend `abort(403)` vía `User::clinicCan(acción)`.

| Perfil (rol) | Aterriza en | Atiende | Crea cita | Cobra | FUA | POS | En el clon (mqn_efp) |
|---|---|:--:|:--:|:--:|:--:|:--:|---|
| **Doctor** (1) | `/clinica/dashboard-doctor` | ✅ | ❌ | ❌ | ❌ | ❌ | quezada@gmail.com, hector@gmail.com |
| **Recepcionista** (2) | `/clinica/dashboard-recepcionist` | ❌ | ✅ | ✅ | ✅ | ✅ | cita@gmail.com |
| **Administrador** (3) | `/dashboard` | ✅ | ✅ | ✅ | ✅ | ✅ | mqn@gmail.com |
| **Farmacia** (4) | `/pos` | ❌ | ❌ | POS normal | ❌ | ✅ | erav1598@gmail.com |
| **Laboratorio** (5) | `/clinica/laboratory` | ❌ | ❌ | ❌ | ❌ | ❌ | yas@gmail.com |

**👨‍⚕️ Doctor** — Panel doctor, Mis Citas, Citas, Consultas, Pacientes, Especialidades, Procedimientos.
Atiende (ficha: vitales+IMC, CIE-10, procedimientos, EVA), **arma el carrito "Ventas"**, solicita laboratorio,
ve historial, reprograma, finaliza (→AT). **No cobra** (ni consulta ni productos → 403, sin botones); arma el
carrito y lo cobra recepción.

**🧑‍💼 Recepcionista** — *centro del cobro*. Panel recepción, Citas, Pacientes, POS. Crea/agenda citas, reprograma,
toma vitales, **cobra la consulta** ("Pagar"→POS) y **los productos** del carrito ("Cobrar prod."→POS), "Sin cobro"
(gratuita), Ticket, **FUA**. No atiende (ficha), no Config/Reportes/Lab.

**🛡️ Administrador** — acceso pleno (conserva todos sus módulos). Atiende, crea, cobra, FUA, Config, Reportes, Lab, POS.

**💊 Farmacia** — vendedor POS normal de EFP (POS/Documentos/Inventario/Ítems/Personas). **No entra a `/clinica`**.

**🔬 Laboratorio** — solo el módulo Laboratorio: ve órdenes y **registra resultados**. Nada más.

**Cobro (estado actual)**: consulta al inicio + productos después = dos comprobantes (la consulta se cobra antes de
que existan los productos; fusionar la doble-cobraría). Ambos **vinculan de vuelta a la cita** (`linkSale` /
`linkProductsSale`) y **no se doble-cobran** (`products_charged` oculta el botón → "Prod. cobrados"). El botón
**"Cotizaciones" fue eliminado** (decisión: carrito, no módulo Quotation).

## Incidente pendiente: comprobante existe, pero la cita aparece sin cobro

**Referencia reportada:** boleta `B004-98` (21/09/2026). Se informó que la
conexión fue inestable durante el cobro y que la boleta existe, pero la cita
continúa apareciendo como no cobrada.

### Diseño actual que debe auditarse

- El listado de citas calcula `consulta_charged` únicamente con
  `appointments.sale_note_id` o `appointments.document_id`.
- El cobro llama a `AppointmentController::linkSale()` después de generar o
  confirmar la venta. Si la boleta se genera pero la conexión cae antes de que
  llegue ese POST, queda un comprobante válido sin vínculo de vuelta a la cita.
- `linkSale()` es idempotente si se reintenta con el mismo `sale_note_id` o
  `document_id`; el primer remedio debe ser verificar o reconstruir el vínculo,
  no volver a emitir la boleta.
- El vínculo de productos usa columnas distintas (`products_sale_note_id` y
  `products_document_id`); no debe confundirse con el cobro de la consulta.

### Verificación requerida para B004-98 (solo lectura)

1. Identificar el tenant y localizar `documents` por serie y número (`B004`,
   `98`), confirmando cliente, fecha y total.
2. Buscar la cita candidata por paciente, fecha, total y líneas de servicio.
3. Comparar `appointments.document_id`/`sale_note_id` con el documento y revisar
   también `cash_documents` y pagos para confirmar que el cobro llegó a Caja.
4. Revisar logs alrededor de la hora del cobro para distinguir: boleta creada y
   vínculo perdido, timeout antes de crearla, o boleta de otra cita.
5. No reenviar, anular ni crear otra boleta hasta cerrar esa conciliación.

### Hallazgo de la revisión profunda (21/09/2026)

El backend actual ya intenta resolver la carrera: cuando el POS envía
`clinic_appointment_id`, `DocumentController` bloquea la cita, crea el
documento y guarda `appointments.document_id` dentro de la misma transacción.
La ruta equivalente de Nota de Venta también guarda `sale_note_id` dentro de
la transacción. Esto evita que una caída posterior del navegador deje el
vínculo pendiente, pero solo si el contexto clínico llegó al request.

El contexto todavía nace en `localStorage` al navegar desde Citas al POS. Si el
contexto se pierde antes de enviar la boleta (recarga, pestaña distinta, datos
antiguos o bundle sin el campo), la boleta puede crearse sin saber a qué cita
pertenece. El callback posterior (`linkVetCobro`) muestra el aviso, pero no
puede reparar automáticamente una respuesta perdida.

Por tanto, el caso `B004-98` debe comprobar primero si fue emitido con o sin
`clinic_appointment_id`; no se debe asumir que la transacción falló.

### Prevención recomendada, por etapas

1. **Inmediato:** prueba de regresión que cree una boleta con
   `clinic_appointment_id` y compruebe que documento y cita quedan vinculados;
   repetir con respuesta/callback perdido sin duplicar el comprobante.
2. **Seguro y pequeño:** reemplazar la dependencia exclusiva de `localStorage`
   por un intento de cobro clínico persistido en servidor (appointment, paciente,
   líneas, usuario y clave idempotente). El POS carga ese intento por token y el
   backend lo consume en la misma transacción.
3. **Recuperación:** en Citas mostrar “Cobro pendiente de confirmar” y permitir
   consultar/reconciliar por la clave idempotente. La acción debe vincular un
   documento existente o indicar que no existe; nunca volver a emitir a ciegas.

No conviene resolverlo buscando automáticamente por paciente, fecha y total:
esa coincidencia puede asociar una boleta a la cita equivocada.

### Decisión de diseño pendiente

El flujo debe tratar la generación del comprobante y la vinculación con la cita
como una operación recuperable. Si el comprobante ya existe, la pantalla debería
permitir una acción segura de **Reconciliar cobro** que ejecute el mismo vínculo
idempotente y deje auditoría, sin emitir nuevamente ni afectar stock.

Antes de implementarlo hay que confirmar el caso real de `B004-98` con la
conciliación anterior.

### Resolucion de B004-98 y correccion aplicada

- En produccion se confirmo la boleta `B004-98` aceptada por S/100, con saldo
  0 y pago registrado. La cita 000289 correspondia al mismo paciente e
  importe, pero no tenia `document_id` y aun ofrecia cobrar.
- Se asocio de forma idempotente el documento existente a la cita, sin emitir
  otra boleta, sin tocar stock y sin duplicar el pago. La cita quedo `PA`
  (Pagada) y `consulta_charged=true`.
- El POS ahora conserva en `localStorage` los vinculos clinicos que fallen por
  red y los reintenta al recuperar conexion o al volver a abrir el POS. Usa la
  misma referencia de documento y tolera que el servidor ya haya aplicado el
  vinculo.
- La agenda ya no muestra "Cobrar visita" cuando la visita ya esta pagada en
  modo `reception_desk`; muestra "Visita cobrada" para evitar un segundo cobro.
  Se cubrio con 33 pruebas Jest del POS y compilacion local exitosa.

## Pendientes futuros (no bloqueantes)

- **Procedimientos sin precio**: `procedures`/`medical_consultation_procedures` no tienen `item_id`/monto → no se
  cobran. Para cobrarlos habría que mapearlos a ítems de catálogo. (En MQN sí iban en la cotización con precio.)
- **Cutover a prod**: correr la migración de columnas de cobro (idempotente) en el tenant real + limpieza de
  duplicados `module_user`/`module_level_user` (ver memoria del proyecto).
- **Error de consola del layout** (`get_menu`/`persons/tables` en https): gotcha del 301 cacheado, benigno; resuelve
  en prod con force_https. Ver si falta cache-buster en el layout.
