# Clínica — Plan de desarrollo 2.º tenant (auditoría completa + fases)

> Generado 2026-08-23. Base: 2 pasadas de auditoría (13 agentes, 100% del módulo: 9 controllers,
> 14 modelos, 13 vues, 4 collections, rutas, providers, blades, PDFs, estilos, middlewares,
> migraciones, BD real `mqn_efp`) + recorrido en vivo con Playwright (§6) + **3.ª pasada de
> arquitectura (Codex)** incorporada. Nada de este plan está aplicado.
>
> **REGLA DURA (usuario, 2026-08-23): NADA de deploy a prod hasta terminar TODO** (todas las
> fases F0-F12 completas y validadas). Trabajo 100% local; push a rama cuando el usuario lo pida;
> deploy recién al final. Correr el audit read-only en prod NO es deploy y está permitido.
>
> **Regla de oro: MQN (mqn.sysfarma.pe) usa el módulo estable TAL CUAL. Con la Configuración
> por defecto (todos los JSON NULL → helpers devuelven el valor actual) el comportamiento debe
> ser bit a bit el actual.** Los bugs objetivos se corrigen para todos (también benefician a
> MQN); lo demás es Configuración editable por tenant (Tier 1) o interruptores de función
> (Tier 2), siempre con default = comportamiento actual. Ver §0.1 y §0.4.

---

## 0. Decisiones marco

### 0.1 Filosofía: Configuración del vertical, editable por cada tenant

**No son flags escondidos: es Configuración que el admin de cada tenant ajusta desde
Clínica › Configuración.** El módulo YA funciona así para Signos vitales (CRUD por
especialidad, verificado en vivo). Extendemos esa misma filosofía a etiquetas, estados y
funciones. Tres invariantes que pidió el usuario:

1. **Cada tenant en su propia BD** → personalizar uno NUNCA afecta a otro.
2. **Defaults = valores actuales** → un tenant existente (MQN) que no toca nada se ve
   idéntico a hoy. Un tenant nuevo arranca con esos defaults y se auto-personaliza.
3. **Personalizar no afecta la data ya creada** (renombrar un estado no reescribe consultas
   viejas; ocultar un parámetro no borra valores tomados).

**Dónde vive todo**: la pantalla Clínica › Configuración (hoy 3 tabs: Personal · Signos
vitales · Servicio de cita). Le agregamos tabs. Dos tipos de ajuste:

#### Tier 1 — Catálogos y valores EDITABLES (texto/listas, patrón ya existente)

| Ajuste | Estado | Cubre | Almacén |
|--------|--------|-------|---------|
| **Signos vitales** por especialidad | ✅ YA EXISTE (CRUD Orden/Parámetro/Unidad) | P1 (el "que vayan así") | `vital_signs_parameters` |
| **Etiquetas / renombres** | NUEVO: mapa de overrides de texto, default = actuales | P10 (Ayuda Dx→Estudios complementarios), P13 (Descripción→Enfermedad actual) | `configurations.clinic_labels` JSON |
| **Estados del paciente** | NUEVO: lista editable {código, etiqueta, activo}, default = los 5 actuales | P15 (ocultar Hospitalización), P16 (renombrar Requiere Recita→Reevaluación) | `configurations.clinic_patient_states` JSON |
| **Servicios de enfermería/TTO** | NUEVO: CRUD de ítems-servicio con precio (calca "Servicio de cita") | P7/P8 catálogo de sutura/IM/VEV/nebulización… | Items `ZZ` + categoría configurable |
| Especialidades · Procedimientos · Exámenes lab | ✅ ya son catálogos | P18 (precio a exámenes) | tablas propias |

> Etiquetas configurables = un **set curado** de ~6-10 claves conocidas (no "toda cadena de
> la app"). Cada clave default = el texto actual. Si el tenant no la toca, no cambia nada.

#### Tier 2 — Interruptores de funciones (on/off, mismos tabs de Configuración)

Funciones estructurales (agregan tablas/secciones/roles). Se prenden por tenant con un
switch; default OFF = MQN intacto. Almacén: `configurations.clinic_features` JSON.

| Interruptor | Enciende | Cubre |
|-------------|----------|-------|
| `extended_history` | Antecedentes + Examen físico + Motivo/Enf. actual en la ficha + reorden + Análisis complementario | P12, P14, P17, P19 |
| `rx_details` | Tab Ventas→"Receta médica", días+indicación por ítem, receta PDF, FUA con productos | P11 |
| `nursing` | Rol Enfermera operativo (cola de trabajo, derivaciones) | P1, P2, P3 |
| `treatment_plans` | Tratamientos programados (aplicaciones multi-día) | P4, P9 |
| `free_appointments` | Cita libre TTO externo | P6, P7, P8 |
| `lab_billing` | Precio + cobro de exámenes de laboratorio | P18 |
| `pharmacy_dispatch` | Pedido del doctor visible/cobrable desde farmacia (rol 4) | P5 |

#### Mecánica común (backend)

- Migración tenant: `ALTER TABLE configurations ADD clinic_features JSON NULL`,
  `ADD clinic_labels JSON NULL`, `ADD clinic_patient_states JSON NULL` (idempotente, patrón
  `2026_06_30_140000_add_charge_columns...`).
- Helpers: `Configuration::clinicFeature($k, false)`, `clinicLabel($k, $default)`,
  `clinicPatientStates()` (devuelve la lista o el default sembrado).
- **Exposición al front (CORREGIDO, Codex)**: las pantallas clínicas NO comparten un objeto de
  config global — Citas recibe `clinicRole`/`clinicCan` por props del blade a mano, la ficha no
  recibe nada, Configuración tampoco. Por eso NO basta con `getCollectionData()`. Contrato
  explícito: **`GET /clinica/configuration/runtime`** que devuelve solo `{ features, labels,
  patient_states, can }` (permisos EFECTIVOS del usuario) — no toda la config del tenant. Cada
  pantalla clínica lo pide al montar (o se inyecta por prop del blade). Helper JS `t('clave','default')`
  resuelve etiquetas contra ese payload.
- UI: tabs nuevos en Clínica › Configuración (solo admin): "Etiquetas", "Estados del
  paciente", "Servicios de enfermería", y una sección "Funciones" con los switches.
- **Seeding de defaults**: al alta del tenant clínico (o primera carga), los JSON arrancan
  NULL y los helpers devuelven el default hardcodeado = el texto/estado actual. No hace falta
  seed explícito; el default vive en el helper. MQN nunca ve diferencia hasta que alguien edita.

### 0.2 Reglas de trabajo (no negociables, ya establecidas en el repo)

1. **Branch fresco desde `main`** por fase (`feat/clinica2-f0`, `feat/clinica2-f1`, …) y
   **squash-merge** al release (regla del vertical: la historia de feat/clinica cargaba
   app.js DEV de 100MB que GitLab rechaza).
2. Toda migración nueva → **`tenancy:migrate` en TODOS los tenants** al deploy
   (lección MQN/Clínica: columnas sin gatear + tenants sin migrar = 500).
3. Assets compilados **local/CI, nunca en prod** (main = `npm run prod`).
4. Cambios que tocan totales/IGV/cobro → **con test** (regla del repo). Hoy el vertical
   tiene **cero tests**: cada fase deja al menos su test de humo.
5. Antes de escribir código de una fase: listar archivos a modificar (regla CLAUDE.md).

### 0.3 Orden recomendado

**Base sólida (F0–F3) antes de features.** Orden (detalle en §3): tests+auditoría → seguridad →
integridad de cobros → bugs de arranque → recién ahí config y features. Razones concretas:
- Sin **tests** (F0) cada fase puede romper MQN sin que nos enteremos (hoy hay CERO).
- **S1** (archivos médicos en disco público) empeora con cada feature que sube archivos
  (la cita libre exige receta adjunta obligatoria) → seguridad primero.
- **D1** (reporte ingresos ciego a boletas) + máquina de estados de cobro sostienen TODO cobro
  posterior (TTO, lab, farmacia) → integridad de cobros antes de agregar cobros.
- **B1/B2** revientan la carga inicial de catálogos del tenant nuevo (error 1062/1048);
  **B3** hace imposible P18 (el buscador de exámenes excluye servicios).
Construir features sobre estos bugs = retrabajo garantizado.

### 0.4 Separación de dominios (corrección de arquitectura — 3.ª pasada Codex)

**Error del plan v1**: reusar `clinic_appointment_items` (carrito comercial: item/cantidad/precio)
para receta + pedido a farmacia + tratamiento. Son **4 dominios con ciclos de vida distintos**;
mezclarlos rompe en: compra externa, compra parcial, medicamento no administrado, reimpresión
de receta, cambio de precio, anulación de venta, sustitución en farmacia, aplicación parcial.

**Diseño correcto — 4 entidades:**

```
1. HISTORIA CLÍNICA   medical_consultations (+ patient_backgrounds, examen físico)
                      → lo que el doctor evaluó. Fuente clínica.

2. RECETA             clinic_prescriptions → clinic_prescription_items
                      { item_id?, med_name (snapshot), cantidad, días, frecuencia,
                        indicación, administrar_en_clinica (bool), enviar_a_farmacia (bool) }
                      → lo que el doctor prescribió. Sobrevive a anulaciones/sustituciones.

3. PEDIDO A FARMACIA  clinic_pharmacy_orders → clinic_pharmacy_order_items
                      cabecera { status: pending|claimed|charged|cancelled, claimed_by,
                        claimed_at, patient_id, appointment_id }
                      item { prescription_item_id?, item_id, description, unit_price,
                        currency_id, igv_affectation, quantity }  ← snapshot comercial COMPLETO
                      → derivado comercial. Estado persistente + compare-and-set (2 cajeros).

4. TRATAMIENTO        clinic_treatment_plans → clinic_treatment_applications
                      (+ clinic_treatment_application_vitals)
                      → ejecución por enfermería. Ver P4.
```

**Convivencia con MQN (ponytail)**: `clinic_appointment_items` (el carrito actual) **se queda
tal cual** para el camino sin `rx_details` — MQN sigue igual. Las 4 entidades nuevas son
aditivas y gateadas por flag. El carrito viejo pasa a ser "productos rápidos"; la receta
estructurada (dominio 2) es el camino nuevo del tenant que activa `rx_details`.

**Snapshot comercial completo** (dominios 3 y P18 lab): NO guardar solo `unit_price`. Guardar
`item_id, description, unit_price, currency_id, igv_affectation, quantity` para que la orden
histórica no dependa del catálogo vivo.

---

## 1. Catálogo de errores (fix compartido, sin flag — benefician también a MQN)

### 1.1 Bloqueantes de arranque del tenant nuevo (→ Fase F3)

| # | Bug | Dónde | Fix |
|---|-----|-------|-----|
| B1 | 2.ª especialidad sin código → 1062 `Duplicate entry ''` (UNIQUE `specialties_code_unique` + form manda `''`) | `modules/Clinica/Http/Controllers/SpecialtyController.php:59` | normalizar `'' → null` en store |
| B2 | Alta rápida de procedimiento sin código → 1048 (`procedures.code` NOT NULL, front manda `null`) | `ConsultationController.php:542` + `ficha.vue:915` | `?: ''` |
| B3 | Buscador de exámenes de lab del doctor EXCLUYE servicios (`/items/search-items` → `ForProductionSupply` filtra `unit_type_id != 'ZZ'`) | `ficha.vue:789` | apuntar a `/clinica/consultations/search-sale-items` (1 línea) |
| B4 | Paginación de Pacientes rota: paginator crudo sin `meta` → solo 20 visibles, numeración desde 0 | `PatientController.php:115,125` vs `patients/index.vue:19,30,66` | devolver ResourceCollection |
| B5 | Editar paciente pierde birthdate/gender (no viajan en `getCollectionData`) → selects vacíos + validación bloquea guardar + riesgo de sobrescribir dato bueno | `app/Models/Tenant/Person.php:598-652` | agregar 2 claves al array |
| B6 | Recepcionista: "Ver detalle" en historia clínica → 403 (`consultations` fuera de sus prefixes) | `history.vue:184` + `User.php:432` | **→ se resuelve en F1 con S10** (payload acotado bajo `patients`), no en F3: es seguridad + minimización de datos |
| B7 | "Próximas Citas" del doctor ordenadas DESC + truncadas a 20 → oculta la cita de mañana | `ConsultationController.php:103` | asc para mode=upcoming |
| B8 | Panel admin `/clinica` con KPIs 0/0/0 hardcodeados; tile "Consultas" lleva a página "En construcción" | `ClinicaController.php:19-27`, `index.vue:48` | conectar queries reales (existen en ReportController); tile → /clinica/appointments |

### 1.2 Seguridad (→ Fase F1)

| # | Bug | Dónde | Fix |
|---|-----|-------|-----|
| S1 | **PHI en disco público sin auth** + carpeta única sin tenant + nombre `Ymd_His_original` colisionable (sobrescritura cruzada; delete borra el archivo de OTRO paciente) | `ConsultationController.php:355-356`, `AppointmentController.php:264-265`, `LaboratoryController.php:162-163` | disco `local` + ruta autenticada `GET /clinica/files/{doc}` con chequeo de rol; nombre con `Str::random(24)`; **comando de migración de archivos existentes** (MQN prod tiene ~7+ adjuntos + resultados lab) |
| S2 | Acceso a la atención sin validar pertenencia del recurso: `attend/ficha/store/documents/deleteDocument/record/saleItems` cargan por id crudo | `ConsultationController.php:109,131,342,371,381`; `AppointmentController.php:67,123` | **REVISADO con normativa (ver abajo): NO aislar doctor-de-doctor.** Guard = el recurso pertenece a una cita de ESTE tenant + requester es rol clínico. Enumerar ids ajenos/basura falla. La minimización real (recep/lab sin relato clínico) es S10. **Falta control estándar: audit trail de accesos → nueva tarea S13.** |
| S14 | **Riesgo de stock: el cobro de cita abre el POS COMPLETO** (`applyVetCobroPreload` en `pos/index.vue`) → recep puede agregar cualquier producto con el buscador y manipular stock / emitir comprobante con ítems ajenos a la cita (pedido usuario 2026-08-24) | `resources/js/views/tenant/pos/index.vue` | **Modo "cobro de clínica" bloqueado**: flag `clinicLocked` al precargar un ref clínico → oculta/deshabilita buscador+agregar+editar cantidad+borrar; solo comprobante + Cobrar. El POS normal de farmacia (no originado en cita) queda intacto. Complementa F12 (medicamentos los cobra farmacia). |
| S13 | **Sin registro de accesos a la historia clínica** (NTS 139-MINSA exige "auditoría informática"; Ley 29733 traza acceso a datos sensibles). Hoy no se sabe quién vio/editó una ficha | (no existe) | Tabla `clinic_access_log` (user_id, patient_id, consultation_id, acción ver/editar, ip, timestamp) + registro en attend/ficha/store. Es el control que reemplaza al siloing entre doctores. Programado con F5. |
| S3 | `RedirectModule` no gatea XHR (`if (!ajax())`) → perfiles scoped pueden POSTear fuera de /clinica (`/documents`, `/items/...`) | `app/Http/Middleware/RedirectModule.php:46` | responder 403 en XHR fuera de scope (como ClinicRoleGate:67) |
| S4 | Quitar el rol clínico ESCALA privilegios: queda `clinic_role_id=NULL` + módulo `clinica` → `clinicCan()` true para todo | `SettingController.php:84-90` + `User.php:373` | al poner rol null, retirar módulo clinica (o denegar clinicCan a sin-rol en tenant clínica) |
| S5 | `updateStatus` sin gate, sin whitelist de estados, sin máquina de transiciones; nadie lo usa desde el front | `AppointmentController.php:270-277` + `web.php:50` | eliminar la ruta (o gate + `in:RE,PA,EN,AT,CA,GR` + transiciones válidas) |
| S6 | `store()` de citas edita cualquier cita vía `id` del request sin autorización ni guardas de estado (reescribe fecha/paciente de una `AT` cobrada) | `AppointmentController.php:216,223` | ignorar `id` (la UI nunca edita por acá) o exigir estado RE + gate |
| S7 | `attend` y `finish` son GET que mutan estado (CSRF vía `<img src>`) | `web.php:92,94` | `finish` → POST; `attend` separar vista (GET) de transición (POST) |
| S8 | Usuario sin `clinic_role_id` en tenant clínica = `next()` incondicional (acceso total /clinica incl. settings) | `ClinicRoleGate.php:35-37` | en tenant clínica, sin rol → solo lectura de nada: redirigir a dashboard |
| S9 | Sin gate backend: `saveVitals`, `saveSaleItems`, `reschedule`; `readOnly` de ficha finalizada solo front (POST directo escribe sobre `AT`) | `AppointmentController.php:405`, `ConsultationController.php:304,381` | `abort_unless(clinicCan(...))` + guard de estado en store (permitir solo con flujo Modificar) |
| S10 | Recepcionista ve relato clínico + resultados de lab (contradice su contrato) vía `information()` bajo prefijo `patients` | `PatientController.php:65-83` + `history.vue:91-92` | payload por rol: recep ve datos demográficos + citas, no evaluación/tratamiento/lab |
| S11 | `POST /clinica/laboratory` abierto al rol 5 con `doctor_id`/`patient_id` del request (fabricar órdenes a nombre de terceros) | `web.php:104` + `LaboratoryController.php:84-85` | derivar doctor/patient del `Appointment::findOrFail`; quitar la ruta del prefijo laboratory |
| S12 | `information()`/`historial()` sin `whereType('customers')` → id de proveedor devuelve sus datos como paciente | `PatientController.php:36,131` | agregar el filtro |

> **Decisión S2 fundamentada en normativa (2026-08-24, investigación web).** Estándares de
> acceso a historia clínica (HIPAA "minimum necessary" + role-based + treatment access;
> break-the-glass solo para registros de alta sensibilidad; Perú: Ley 30024/RENHICE para
> acceso ENTRE establecimientos con autorización del paciente, y NTS 139-MINSA/2018 +
> Ley 29733 para la gestión y auditoría interna) concluyen:
> - **Dentro de un mismo establecimiento, el acceso del profesional al paciente es legítimo por
>   "acto médico" / treatment purpose.** Aislar doctor-de-doctor NO es estándar y rompería
>   cobertura entre colegas y continuidad — la guía HIPAA dice explícitamente que si el "break
>   the glass" se vuelve diario, hay que ajustar los roles base, no normalizar la excepción.
> - El control que SÍ exigen las normas y HOY falta: **audit trail** de quién accede/edita
>   (NTS 139 "auditoría informática", Ley 29733) → tarea S13.
> - La **minimización** aplica a roles NO clínicos (recepción/laboratorio no ven el relato
>   clínico) → S10. Eso es el verdadero "minimum necessary" acá.
> - Break-the-glass / registros de alta sensibilidad (VIP, empleados): no aplica a una clínica
>   chica de MG ahora → YAGNI.
> - Tenant aislado (cada clínica su BD) ya cubre el acceso entre establecimientos.
> **Conclusión: S2 = opción B refinada** (recurso-del-tenant + rol clínico, sin siloing entre
> doctores) **+ S10** (minimización recep/lab) **+ S13** (audit trail). Fuentes en la respuesta.

### 1.3 Dinero (→ Fase F2)

| # | Bug | Dónde | Fix |
|---|-----|-------|-----|
| D1 | Reporte Ingresos: solo `sale_note_id` (boleta/factura = S/ 0), ignora productos (`products_*`), suma ANULADAS (sin `state_type_id NOT IN ('09','11')`) y `changed`, mezcla monedas, agrupa por fecha de cita | `ReportController.php:84-112` | portar `linkedRevenueExpr` de `modules/Vet/Http/Controllers/ReportController.php:220-236` extendido a las 4 columnas de cobro |
| D2 | `linkSale` marca `PA` aunque no llegue ningún id de venta → cita "cobrada" sin comprobante | `AppointmentController.php:334-343` + `pos/index.vue:2099` | exigir sale_note_id o document_id para transicionar |
| D3 | Doble cobro sin detección: `linkSale` pisa referencias sin verificar si ya había | `AppointmentController.php:327-347` | rechazar si ya hay sale_note_id/document_id |
| D4 | `linkVetCobro` con `.catch(()=>{})`: si falla el link, la venta se cobró pero el botón "Cobrar prod." sigue visible = doble cobro | `pos/index.vue:2110` | reintento + aviso visible |
| D5 | `vetCobroRef` no se limpia si se abandona el cobro → la venta de un cliente de mostrador queda vinculada a la cita | `pos/index.vue:2083,2090-2114` | limpiar ref al vaciar carrito / cambiar cliente |
| D6 | Precio del carrito del doctor ≠ precio cobrado (POS re-tarifa por catálogo; botón dice S/50, cobra S/80) | `appointments/index.vue:907` vs `ficha.vue:764` | decisión: el POS manda (mostrar precio de catálogo en ficha, no editable) — documentar |
| B9 | Atender cita `RE` mata el cobro para siempre (pasa a EN/AT y desaparecen Pagar/Sin cobro) | `doctor/appointments.vue:118`, `doctor/dashboard.vue:187`, `ConsultationController.php:122` | quitar RE de canAttend en front; backend attend rechaza RE (o permitir Pagar también en EN/AT) |
| B10 | Carrito de Ventas fuera del autosave y del payload → "Finalizar atención" lo pierde en silencio | `ficha.vue:624-630,940-951` | incluir saleItems en watchers/save, o guardar antes de finish |
| B12 | Doble click en "Atender" duplica la consulta (`firstOrCreate` sin UNIQUE) → FUA sale vacío | `ConsultationController.php:113` + migración | UNIQUE `medical_consultations.appointment_id` previa dedup + manejo 1062 |
| I9 | `vital_signs_values` sin UNIQUE (recep+doctor concurrentes duplican filas) | migración `:145-146` | UNIQUE (appointment_id, vital_sign_id) previa dedup |

### 1.4 Importantes (intercalar en F1–F3 o justo después)

- I1 KPIs mal calculados (7): "Mis Pacientes" = todos los clientes (`ConsultationController.php:54`); "Pacientes"/"Especialidades" sin filtros (`ClinicaController.php:41-44`); reservadas/atendidas sin ventana temporal; "Total" incluye CA.
- I2 Estado lab `'E'` traducido distinto en 3 sitios; catálogo de estados de cita duplicado en 4 archivos → constante única en modelo.
- I3 `medical_laboratories.notes` write-only: el laboratorista nunca ve las indicaciones del médico → exponer en `MedicalLaboratoryCollection`.
- I4 Índices: `appointments` sin índice en `date` ni `state_appointment_id`; `whereDate()` sobre columna DATE anula índice (usar `where('date',$d)`); tablas de lab sin ningún índice; N+1 en `PatientController::historial`.
- I5 Delete duro de especialidades/procedimientos sin validar uso (corrompe FUA/historial retroactivamente) y accesible al rol Doctor → guard `exists()` + preferir `status=0`; `abort_unless` en destroy.
- I6 `deleteVitalSign` deja `vital_signs_values` huérfanos.
- I7 FUA: "N° Historia Clínica" = id de consulta (cambia por visita); sin EDAD; diagnóstico sin código CIE-10; `$establishment` muerto; firmas sin nombre/CMP; hora 24h con segundos.
- I8 Ticket: estimador de alto no mide doctor/especialidad (2.ª hoja en blanco); ignora `establishments.print_format`; hora 24h.
- I10 Correlativo LAB con carrera + columna `order` TEXT.
- I11 Resultado de lab: vacío válido (falta `required_without_all`), vaciar observaciones borra sin aviso, `saveResult` fuera de transacción, sobrescribible sin rastro (dato médico-legal).
- I12 "Mis Citas": KPI del mes vs tabla sin filtro de mes (dice 3, lista 148); botón FUA visible al doctor que no puede (403).
- I13 Validación de turno pasado solo en cliente; `store()` acepta fechas pasadas.
- I14 Tres nociones de "hoy" (TZ `America/Lima` hardcodeada en 2 vues, navegador en otra, Carbon en backend); `todayStr` no se refresca tras medianoche.
- I15 Patrón `.catch(()=>{})` en 5+ deletes: error del server indistinguible de "cancelar"; `markFree` devuelve success aunque no cambió nada.
- I16 Permisos: niveles `clinic_*` solo gobiernan el menú, no rutas; árbol de permisos muestra "Clínica > Clínica" (filtrar nivel legado como `digemid`, `UserController.php:114-119`); el árbol se filtra por los niveles del EDITOR, no del editado.
- I17 `quickCie10` con código existente devuelve la descripción vieja sin avisar.
- I18 Paciente nuevo del wizard no se autoselecciona (`personTarget` nunca se lee).
- I19 Listado Pacientes muestra deshabilitados (sin `whereIsEnabled`).
- I20 Homónimos bloqueados: `name` UNIQUE por type impide dos "JUAN PEREZ GARCIA" → **decisión de producto** (relajar solo si tenant clínico; afecta POS/clientes — evaluar con cuidado).

### 1.5 Menores / deuda (oportunistas, cuando se toque el archivo)

- Tabla fantasma `medical_consultation_vouchers`; columnas muertas: `mc_procedures.start_date/end_date`, `appointments.father_appointment_id`, `specialties.item_id` (write-only), `procedures.is_available`, `doctor_schedules.day_of_week`, `vital_signs_parameters.type`.
- Endpoints muertos: `LaboratoryController@tables`, `PatientController@historial` + modal huérfano (`patients/index.vue:37-49,57`) + ruta `web.php:110`; `SpecialtyController@record`, `ProcedureController@record`.
- Upload divergente 2MB-sin-gif vs 5MB-con-gif → unificar en 5MB.
- Mensaje "Cobro veterinaria cargado" en clínica humana (`pos/index.vue:2084`).
- `to12h()`/`stateClass()` triplicados; URLs literales en vez de rutas nombradas.
- Fallback `#0D9488` distinto del token real `--mn-primary #009688` en porto-app.scss (drift cosmético).
- `wip.blade.php` con jerga interna visible ("clon clínico").
- Búsqueda de procedimientos: `take(300)` sin aviso de corte, sin debounce en remote-method, sin búsqueda por palabras sueltas.

---

## 2. Mapeo punto por punto de los pedidos (P1–P19)

> Columna "Modo": **C** = compartido, sin config (fix objetivo o no cambia UX de MQN) ·
> **Tier 1** = config editable por tenant (etiquetas/estados/catálogos; default = actual) ·
> **Tier 2 (`x`)** = interruptor de función on/off, default OFF (ver 0.1).

### P1 — Signos vitales los llena la enfermera, no la recepcionista
- **QUÉ vitales se toman ya es configurable por tenant** (Tier 1, verificado en vivo: Configuración › Signos vitales, CRUD por especialidad). Otro tenant que quiera "sus signos vitales así" solo edita ahí. **Nada que construir para eso.**
- **QUIÉN los toma** (este pedido): hoy botón "Vitales" en estados PA/GR (`appointments/index.vue:77,126`), lo usa recep; flag `can.vitals` existe pero NADIE lo consume (ni front ni backend).
- **Va en**: mapa `User::clinicProfile()` — enfermera con `vitals=true`; recep MANTIENE `vitals=true` (MQN intacto). Consumir el flag: `v-if="allow('vitals')"` en el botón + `abort_unless(clinicCan('vitals'))` en `saveVitals` (cierra S9 de paso).
- **Bloqueo a recep (CORREGIDO, Codex)**: tu pedido dice "vitales NO las llena la recepcionista". Regla correcta por tenant vía interruptor `nursing_only_vitals`:
  - MQN (default OFF) → recep conserva vitales (intacto).
  - Tenant nuevo (ON) → SOLO enfermería toma vitales; a recep se le oculta el botón y el backend la rechaza.
  - El doctor siempre puede verlos y corregirlos. Front y backend consumen el MISMO permiso efectivo (del endpoint runtime).
- **Modo**: C (gates) + Tier 2 (`nursing`, `nursing_only_vitals`) · Fase **F7**.

### P2 — Crear usuario/rol Enfermera (clinic_role_id = 6)
- **Va en** (7 puntos obligatorios):
  1. `app/Models/Tenant/User.php:408-482` — entrada 6: `landing: tenant.clinica.nursing.index` (nueva), `modules: ['clinica']`, `prefixes: ['nursing']` + acceso acotado a vitales de citas, `pos:false`, `clinic_scoped:true`, `can: {vitals:true, apply_treatment:true}`. **NO darle `patients`/`consultations` amplios** (Codex): esos endpoints devuelven evaluación/tratamiento/lab. La enfermería recibe un **payload propio** (`GET /clinica/nursing/patient/{id}`): paciente, edad, **alergias**, indicación del tratamiento, aplicación pendiente, signos vitales, observaciones — NO la ficha médica completa.
  2. `User.php:358-361` `isClinicScopedRole()` — **refactor**: derivar de `clinic_scoped` del mapa (elimina la lista `[1,2,5]` hardcodeada).
  3. `resources/views/tenant/layouts/partials/sidebar.blade.php:59-72` + rama `@elseif($isClinicNurse)` antes del `@else` (108-208): menú Enfermería (Cola de hoy, Citas, Pacientes).
  4. `SettingController.php:80` validate `in:1,2,4,5` → `+6`; `:57` roleName `+ 'Enfermera'`.
  5. `modules/Clinica/Resources/assets/js/view/settings/index.vue:81-87` — `el-option` Enfermera.
  6. Landing nueva: ruta + controller + vue `nursing/index.vue` (en F2 puede ser la agenda del día filtrada; la cola real llega en F5).
  7. Helper `User::isClinicNurse()` junto a los otros (`User.php:318-346`).
- **Sin migración** (columna int ya existe). Rol invisible para MQN mientras nadie lo asigne.
- **Modo**: C (aditivo) + Tier 2 (`nursing`) · Fase **F6**.

### P3 — Enfermera administra tratamiento (aplica medicamentos del doctor)
- **Hoy**: cero estructura. Ver P4 (tabla de aplicaciones). La "administración" = confirmar aplicación en la cola de enfermería.
- **Modo**: Tier 2 (`treatment_plans` + `nursing`) · Fase **F9**.

### P4 — Tratamiento multi-día (ampollas ×3, paciente acude a diario, enfermera confirma cada aplicación + vitales por visita)
- **Hoy**: `medical_consultation_procedures.days` es un número suelto; `start_date/end_date` muertas; vitales = 1 registro por (cita, parámetro).
- **Va en** (tablas nuevas, migración tenant raw idempotente como `create_clinica_tables`):
  - `clinic_treatment_plans`: id, appointment_id, medical_consultation_id NULL, prescription_item_id NULL (de qué línea de receta salió), patient_id, kind (`MED|IM|VEV|NEB|SUTURA|CURA|RET_PUNTOS|RET_IMPLANTES`), item_id NULL, med_name (snapshot), description, **total_sessions** (nº de aplicaciones), **frequency** (ej. "c/24h"), **duration_days** (separados: "3 días" ≠ "3 aplicaciones", Codex), notes, status (`active|done|cancelled`), created_by_user_id, timestamps. Índices: patient_id, status, appointment_id.
  - `clinic_treatment_applications`: id, plan_id (idx), session_number, **scheduled_at** (datetime, no solo fecha), **dose** (cantidad administrada), **route** (IM/VEV/oral/nebulización), applied_at NULL, applied_by_user_id NULL (quién confirma), observations, **skip_reason** (motivo de omisión/reprogramación), status (`pending|applied|missed|rescheduled|cancelled`), lot NULL + expiry NULL (si consume stock clínico), timestamps.
  - **`clinic_treatment_application_vitals`** (tabla SEPARADA, corrección Codex — NO columna `application_id` en `vital_signs_values`): application_id, vital_sign_id, value, recorded_by_user_id, recorded_at, UNIQUE(application_id, vital_sign_id). Así los vitales normales conservan su UNIQUE(appointment_id, vital_sign_id) sin conflicto.
- **Generación del plan**: (a) desde la receta (P11): línea con `administer_in_clinic=true` → plan `MED` con N aplicaciones; (b) desde cita libre TTO (P6/P7): plan del `kind` elegido. **Atómico** (Codex): consulta finalizada → cita AT → plan → aplicaciones en UNA transacción.
- **UI enfermera**: landing = cola con secciones **Hoy / Atrasadas / Próximas / Completadas / Omitidas** + botón **Aplicar** → modal: confirma, dosis, vía, observaciones, vitales de la visita. Descartada "citas hijas por día" (`father_appointment_id`): infla la agenda.
- **P9 (acceso desde /clinica/appointments)**: fila de cita `AT` con plan activo → botón "Aplicaciones (2/3)" → mismo modal. Gate `clinicCan('apply_treatment')`.
- **Modo**: Tier 2 (`treatment_plans`) · Fase **F9**.

### P5 — Medicamentos del doctor → pedido a farmacia, farmacia cobra (no re-seleccionar)
- **Hoy**: el carrito `clinic_appointment_items` YA ES el pedido, pero lo cobra RECEPCIÓN desde /clinica/appointments vía `localStorage['vet_cobro']` (one-shot, no cruza máquinas). Farmacia (rol 4) está bloqueada de /clinica (`prefixes: []`).
- **Descartado**: reusar `pos_parked_sales` — 5 bloqueos server-side (caja abierta obligatoria `PosController.php:1184`, `cash_id` NOT NULL, listado filtra por caja del cajero, `assertParkedSaleCanResume` = solo el creador, establishment 404). Adaptarlo cuesta más que una tabla propia.
- **CORRECCIÓN (Codex): "sin tabla nueva" es frágil** — derivar pedidos de citas lista bien pero NO controla dos cajeros cargando el mismo pedido, ni la idempotencia del cobro. Se necesita **estado persistente** (dominio 3 de §0.4):
  - `clinic_pharmacy_orders` (status `pending|claimed|charged|cancelled`, claimed_by, claimed_at, patient_id, appointment_id, prescription_id NULL, sale_note_id/document_id NULL) + `clinic_pharmacy_order_items` (snapshot comercial completo: item_id, description, unit_price, currency_id, igv_affectation, quantity).
  - Se crea al guardar la receta con líneas `send_to_pharmacy=true` (o desde el carrito viejo en el camino sin `rx_details`).
  - **Cobro con compare-and-set**: cajero "reclama" (`pending→claimed` con `claimed_by`), cobra, y el link es transaccional (`claimed→charged` solo si seguía claimed por él). Rechazo si ya `charged`. Elimina el doble cobro y la carrera.
- **UI**: pantalla "Pedidos de clínica" en el POS para rol 4 (`GET /pos/clinic-orders`, FUERA de /clinica): lista pendientes con paciente+total → reclamar → precarga carrito (server-fetch) → cobra. Recep conserva su botón (MQN intacto); con flag ON, farmacia también.
- **Idempotencia del link (Codex, ver D4)**: preferible mandar la referencia clínica DENTRO del request de venta y vincular en la MISMA transacción que crea el comprobante; si no es viable, outbox de links pendientes idempotentes. El `.catch(()=>{})` actual (`pos/index.vue:2110`) se elimina.
- **Extra**: precargar prescripción DIGEMID cuando el ítem tiene `requires_prescription`: `prescription_date`←cita, `doctor_name`+CMP←doctor (`users.cmp`), `diagnosis_text`/`cie10`←consulta, `customer_id`←paciente.
- **Modo**: Tier 2 (`pharmacy_dispatch`) · Fase **F12**.

### P6/P7/P8 — Cita libre (paciente externo, TTO con receta, cobro por servicio)
- **Hoy**: no existe walk-in; `doctor_id`+`specialty_id` required (`AppointmentController.php:200-201`); upload ya existe (`appointment_documents`, 2MB).
- **Va en**:
  - Migración: `appointments` + `type` VARCHAR(12) DEFAULT 'medical' (`medical|nursing`), `treatment_kind` VARCHAR(20) NULL, `service_item_id` INT NULL. `doctor_id/specialty_id` ya son nullable en la tabla (la validación es del request).
  - **Catálogo de servicios TTO** = Items servicio (ZZ) con precio c/u, igual que el ítem de cita: nueva pantalla en Clínica › Configuración "Servicios de enfermería" (CRUD calcado de `VetSettingController@billingServices/storeBillingService/destroyBillingService` — soft-disable, no delete) + flag en items `is_clinic_nursing_service` (o categoría configurable `clinic_nursing_category_id` en configurations — MÁS BARATO, cero migración de items; decidir al implementar).
  - **UI**: botón "Libre" junto a "Nuevo" en `appointments/index.vue` (v-if flag + `canCreateAppointment`) → modal de UN paso (calcado del paso 3): paciente (+alta con `tenant-person-form`, edad sale de birthdate), motivo, enfermedad actual, **selector de servicio TTO** (`IM / VEV / Nebuloterapia / Sutura / Cura de heridas / Retiro de puntos / Retiro de implantes` — vienen del catálogo, el precio se muestra al elegir), **receta adjunta OBLIGATORIA**, sin doctor/especialidad/turno (fecha=hoy, hora=now).
  - **Upload temporal (CORREGIDO, Codex)**: hoy el wizard sube el archivo a `/appointments/upload` ANTES de crear la cita → si se cancela, queda huérfano. Crítico acá porque la receta es obligatoria. Contrato: **upload temporal privado con token + vencimiento + limpieza programada**; al guardar la cita se "confirma" el token y se vincula. Alternativa: crear borrador de cita y adjuntar contra él. Unificar límite a 5MB. Ver F1 (misma familia que S1/archivos privados).
  - **Backend**: `store()` rama `type=nursing`: valida servicio + receta, `state='RE'`.
  - **Cobro**: botón "Pagar" existente pero con `service_item_id` en lugar del `clinic_appointment_item_id` global (tocar `pagar()` en `appointments/index.vue:921-934` para leer el item de la fila si type=nursing). Precio varía por servicio ✓.
  - **Derivación**: al pasar a `PA` (o `GR`) se crea el `clinic_treatment_plan` del `kind` (1 sesión por defecto; ampliable) → aparece en la cola de enfermería (P3/P4). Enfermera aplica → confirma → cita `AT`.
- **Modo**: Tier 2 (`free_appointments`) · Fase **F10** (después de F6 enfermería y F9 motor de tratamientos — corrección de dependencia Codex; ya NO se entrega antes que el motor).

### P10 — "Ayuda Dx" → "Estudios complementarios"
- `ficha.vue:157` label del tab. Contenido real: "Archivos Adjuntos / estudios médicos e imágenes" (verificado) — el rename calza.
- **Va en**: etiqueta editable Tier 1, clave `tab_ayuda_dx`, default "Ayuda Dx". El admin del tenant nuevo la cambia a "Estudios complementarios"; MQN la deja como está.
- **Modo**: Config editable (Tier 1, `clinic_labels`) · Fase **F4**.

### P11 — Tab Ventas → "Receta médica" + días de administración + indicación por medicamento

> **ACTUALIZADO 2026-08-25 — el criterio original decía "Con En clínica / A farmacia por
> medicamento". La columna "A farmacia" YA NO EXISTE, y es a propósito.**
>
> El ruteo a farmacia dejó de ser decisión del doctor: no le compete (es lo mismo que el precio) y
> para decidirlo necesitaba mirar un stock congelado al recetar, que ya no vale cuando el paciente
> llega a la caja. Ahora el pedido lleva la receta ENTERA y la farmacia despacha lo que hay,
> calculando la disponibilidad en el momento en que el cajero mira.
>
> Lo que SÍ sigue cumplido de P11: días, frecuencia e indicación por medicamento, receta PDF (ahora
> 80mm) y la columna "En clínica". Esa última también está en revisión — ver "Pendientes de
> consulta", punto 1: debería salir del producto, no tildarla el doctor.
>
> **Si alguien revisa la checklist y no encuentra "A farmacia": no se rompió, se sacó.**

- **Hoy**: `clinic_appointment_items` = item/qty/precio, nada clínico. Molde exacto: `vet_prescriptions`/`vet_prescription_items` (tabla SEPARADA, no el carrito) + PDF `vet::prescription.pdf` + `VetPrescriptionController@pdf`.
- **CORRECCIÓN (Codex)**: NO agregar campos clínicos al carrito comercial. **Receta = entidad propia** (dominio 2 de §0.4):
  - Migración: `clinic_prescriptions` (appointment_id, medical_consultation_id, patient_id, doctor_id, prescription_date, notes) + `clinic_prescription_items` (prescription_id, item_id NULL, med_name snapshot, quantity, days, frequency NULL, indication, administer_in_clinic BOOL, send_to_pharmacy BOOL).
  - `users.cmp` VARCHAR NULL (para firma de receta).
  - `ficha.vue`: el tab pasa a "Receta médica" (etiqueta), con editor de líneas (busca ítem del catálogo **o** texto libre, calca `pets/index.vue` receta Vet). El **carrito comercial viejo** (`clinic_appointment_items`) sigue existiendo para MQN sin flag; con `rx_details` el tab muestra la receta estructurada.
  - **Receta PDF**: `GET /clinica/prescriptions/{id}/pdf` → blade clon de `vet::prescription.pdf` (logo, paciente+edad, doctor+CMP, tabla medicamento/cantidad/días/indicación, firma).
  - **FUA**: llenar `$quotationItems` con los items de la receta (`AppointmentController.php:449`) — gateado por flag (MQN espera la sección vacía "como MQN").
  - `administer_in_clinic=true` → alimenta el motor de tratamientos (P4); `send_to_pharmacy=true` → alimenta el pedido a farmacia (P5). Un mismo medicamento puede ser una cosa, la otra, ambas o ninguna (compra externa) — por eso son flags por línea, no una tabla única.
- **Modo**: Tier 2 (`rx_details`) · Fase **F8** (receta clínica separada; ver 3.).

### P12 — Antecedentes personales y familiares
- **Hoy**: cero (grep alergia/diabetes/antecedente = 0 hits; ni columnas ni UI).
- **Diseño**: los antecedentes son del PACIENTE (persisten entre atenciones), no de la consulta.
- **Va en**:
  - Tabla nueva `patient_backgrounds`: id, patient_id UNIQUE, `drug_allergies` TEXT, `has_diabetes` BOOL, `has_hypertension` BOOL, `has_asthma` BOOL, `personal_other` TEXT, `family_metabolic` BOOL + `family_metabolic_note`, `family_cardiovascular` BOOL + `_note`, `family_other` TEXT, `updated_by_user_id`, timestamps.
  - **Auditoría (Codex)**: mostrar `updated_by` + `updated_at` en la UI (quién/cuándo se actualizó). Es un dato de seguridad clínica; una fila única editable pierde el rastro. Log de cambios ligero (opcional pero recomendado). **Snapshot de alergias**: al prescribir (P11) guardar copia de `drug_allergies` vigente en la receta/consulta, para saber qué sabía el doctor en esa atención.
  - Ficha (`ficha.vue`): sección "Antecedentes" después de la atención médica (posición según P19), editable por el doctor, upsert `POST /clinica/consultations/patient/{id}/background`. Alerta visual si hay alergias (patrón del banner ámbar de Vet `ConsultationForm.vue:273-280`).
  - Historia clínica (`history.vue`): card de antecedentes siempre visible.
- **Modo**: Tier 2 (`extended_history`) · Fase **F5**.

### P13 — En la cita: "Descripción" → "Enfermedad actual"
- Es `symptom_details` (`appointments/index.vue:321-323`, verificado en vivo: label "Descripción", placeholder "Detalle de síntomas / observaciones…").
- **Va en**: etiqueta editable Tier 1, claves `appt_desc_label` / `appt_desc_placeholder`, default = textos actuales.
- **Modo**: Config editable (Tier 1, `clinic_labels`) · Fase **F4**.

### P14 — Motivo de consulta + Enfermedad actual visibles/editables por el doctor, entre Evaluación Clínica (vitales) y Diagnóstico
- **Hoy — gap objetivo**: el API los envía (`ConsultationController.php:267-268`) pero `ficha.vue` NUNCA los renderiza; el doctor no ve lo que escribió la recep.
- **Va en**: `ficha.vue` tab 1 — bloque nuevo entre la card de vitales (`:56-71`) y Diagnóstico (`:74`): 2 textareas bindeados a `appointment.reason_for_consultation` / `symptom_details`, editables, persistidos en `store()` (agregar los 2 campos al payload y al update del appointment).
- **Modo**: **mostrarlos = C** (fix de gap, read-only también sirve a MQN); **edición + posición exacta** = Tier 2 (`extended_history`). · Fase **F5** (la lectura mínima puede adelantarse a F3).

### P15 — Quitar Hospitalización / P16 — "Requiere Recita" → "Reevaluación"
- **CORRECCIÓN (verificado en BD mqn_efp)**: `patient_status` guarda **CÓDIGOS** T01-T05, no texto (`ficha.vue:565-571` bindea `code`). Distribución real: T02=342, NULL=56, T01=24, T03/T04/T05=0. Mi versión anterior ("es texto guardado") era incorrecta.
- **Diseño correcto**: el **código es inmutable** (identidad); la **etiqueta es editable**. Lista Tier 1 `clinic_patient_states` = [{code, label, active}], default = los 5 actuales.
  - Renombrar T02 "Requiere Recita"→"Reevaluación" = solo cambia la etiqueta; las 342 consultas T02 pasan a mostrarse como "Reevaluación" (deseado).
  - Desactivar T04 "Hospitalización" = se oculta del dropdown para nuevas; T04 tiene **0 registros**, 100% seguro.
- El computed `patientStatusOptions` lee la lista (filtra `active`); JSON NULL → default. La etiqueta histórica se resuelve por código → etiqueta vigente (política simple: no guardamos snapshot de etiqueta; el código manda).
- **Modo**: Config editable (Tier 1, `clinic_patient_states`) · Fase **F4**.

### P17 — Análisis complementario (exámenes externos) debajo de Laboratorio
- **Va en**: tabla `medical_consultation_external_exams` (id, medical_consultation_id idx, description, notes, timestamps) + en `ficha.vue`, bajo la card Laboratorio (`:140-150`): lista simple agregar/quitar (sin orden interna, sin cobro). Sale en la receta/indicaciones impresas (F8).
- **Modo**: Tier 2 (`extended_history`) · Fase **F5**.

### P18 — Exámenes de laboratorio con precio c/u
- **Hoy**: `medical_laboratory_items` solo `item_id`; sin precio ni cobro; el modal ni muestra el precio; **B3 impide siquiera encontrar los exámenes** (excluye servicios).
- **Va en**:
  - Prerrequisito: B3 (buscador → `search-sale-items`).
  - Migración: `medical_laboratory_items` + `unit_price` DECIMAL(12,6) DEFAULT 0 (snapshot al solicitar — SIEMPRE se guarda, invisible sin flag) + `medical_laboratories` + `sale_note_id`/`document_id` NULL.
  - Con flag: modal de solicitud muestra precio por examen y total; en /clinica/appointments (o en la pantalla de lab) botón "Cobrar lab S/X" → mismo circuito de preload POS (`ref_type:'clinic_lab'`) → `linkLabSale` nuevo (gate `charge`).
  - Reporte de ingresos (ya arreglado en D1/F2) suma la columna de lab.
- **Modo**: snapshot C, cobro Tier 2 (`lab_billing`) · Fase **F11** (prerrequisito B3 en F3).

### P19 — Estructura del "detalle del doctor" (orden de la ficha)
> DATOS DEL PACIENTE → SIGNOS VITALES → MOTIVO DE CONSULTA → ENFERMEDAD ACTUAL →
> ANTECEDENTES PERSONALES (alergia a medicamentos / enf. metabólicas / enf. cardiovasculares) →
> EXAMEN FÍSICO (piel y anexos / cabeza-ORL / tórax / abdomen / miembros sup-inf)
- **Examen físico — hoy no existe**: migración `medical_consultations` + 5 TEXT: `exam_skin`, `exam_head_orl`, `exam_thorax`, `exam_abdomen`, `exam_limbs`; 5 textareas en sección colapsable; en `$fillable` + `store()` + payload + autosave.
- **Realidad hoy (verificada en vivo)**: el relato clínico está DISPERSO en 2 tabs:
  - Tab "Atención Médica" = EVA → Evaluación Clínica (vitales) → Diagnóstico.
  - Tab "Procedimientos y Tratamientos" = Procedimientos (+días) → sub-tabs **Evaluación / Tratamiento / Plan de Trabajo / Resultado** (este último con Estado del Paciente + Fecha de recita + Observaciones).
  → Motivo y Enfermedad actual **no aparecen en ninguna** (gap P14). El flujo lineal que pedís no existe: hay que traer motivo/enf.actual/antecedentes/examen físico a un orden coherente.
- **Examen físico — hoy no existe**: migración `medical_consultations` + 5 TEXT: `exam_skin`, `exam_head_orl`, `exam_thorax`, `exam_abdomen`, `exam_limbs`; 5 textareas en sección colapsable; en `$fillable` + `store()` + payload + autosave.
- **Reorden (con flag)** — dos opciones de diseño (DECISIÓN ABIERTA #6):
  - (A) **Flujo único** como tu outline: una sola columna scrolleable en el tab "Atención Médica": Datos paciente (sidebar) → Vitales → Motivo → Enfermedad actual → Antecedentes → Examen físico → Diagnóstico; y dejar EVA + Tratamiento/Plan/Estado/Recita en el 2.º tab. Más fiel a lo que pediste, más trabajo de remaquetado.
  - (B) **Mínimo**: mantener las tabs actuales e insertar Motivo/Enf.actual/Antecedentes/Examen físico dentro del tab "Atención Médica" en el orden pedido, sin mover EVA ni las sub-tabs. Menos riesgo, menos fiel.
  - Sin flag: orden actual intacto (MQN).
- **Modo**: Tier 2 (`extended_history`) · Fase **F5**.

---

## 3. Fases de desarrollo (reordenadas — 3.ª pasada)

> Cada fase = branch propio desde main + squash. Al cierre: build assets prod, smoke local
> (`tenancy_demo`/`mqn_efp`), y **verificación explícita de que MQN sin config nueva no cambió**
> (login doctor/recep/lab, ficha, cobro, FUA).
>
> **Cambio de orden clave (Codex)**: seguridad → integridad de cobros → bugs de arranque, ANTES
> de cualquier feature. Y las features respetan la dependencia real de datos:
> Rol Enfermera → Motor de tratamientos → Cita libre (no al revés).

### F0 — Baseline de pruebas + auditoría de datos — ✅ IMPLEMENTADO (commit `4c5f9d19`)

> Hecho en worktree `../enterfarmaplus-clinica2-f0` (rama `feat/clinica2-f0`), sin push.
> Entregado: `app/Console/Commands/ClinicaAuditBaseline.php` + `tests/Unit/ClinicaProfileMatrixTest.php`
> (5 tests, fija la matriz RBAC — guarda F6) + `tests/Feature/ClinicaAuditBaselineCommandTest.php`
> (3 tests, comando read-only). Ambos suites verdes.
>
> **Hallazgos del baseline local (mqn_efp + tenancy_demo, is_clinic=1):**
> - patient_status: T02=342, (null)=56, T01=24 (confirma: son códigos). Cita: AT=417 PA=20 EN=4 GR=1 RE=1.
> - **0 duplicados** de vitales y consultas → migraciones UNIQUE (I9/B12) triviales en estos tenants.
> - 0 referencias de cobro colgadas/dobles/anuladas; moneda única PEN.
> - mqn_efp: 8 adjuntos registrados, **7 faltantes en disco local** (los físicos están en prod; re-auditar allá para S1).
> - **3 usuarios scoped sin módulo `clinica`** (rol clínico sin el módulo) — inconsistencia a revisar en el cutover.
> - Pendiente: correr el comando en **prod, todos los tenants is_clinic** antes de las migraciones de F1/F2.

**a) Auditoría como comando read-only repetible** (no SQL suelto ejecutado a mano):
```
php artisan clinica:audit-baseline                 # todos los tenants con is_clinic=1
php artisan clinica:audit-baseline --tenant=mqn_efp # uno
php artisan clinica:audit-baseline --format=json    # salida máquina (CI/diff entre corridas)
```
- **Descubre los tenants por `configurations.is_clinic = 1`**, NO por nombres conocidos (mqn_efp era solo el de prueba). Solo lee; nunca escribe.
- Informe por tenant:
  - estados y códigos clínicos (patient_status, estados de cita/lab, prioridades) con conteos;
  - duplicados previos a los UNIQUE (`vital_signs_values` por appointment+parám, `medical_consultations` por appointment);
  - referencias de cobro inválidas o dobles (sale_note_id/document_id colgados, citas con doble vínculo, ventas anuladas aún referenciadas);
  - monedas utilizadas en las ventas vinculadas (para la política de F2);
  - archivos: registrados en BD vs físicos en disco vs faltantes (registro sin archivo) vs huérfanos (archivo sin registro), y **nombres de archivo duplicados** (riesgo de colisión S1/S2);
  - roles clínicos y módulos inconsistentes (usuarios con `clinic_role_id` sin módulo, o módulo sin rol, o rol fuera de 1-5);
  - volumen: citas, consultas, vitales, laboratorios, adjuntos;
  - versión/migraciones de las tablas clínicas (qué migraciones corrieron; detectar drift entre tenants).
- Decide, por tenant, qué migraciones UNIQUE (I9/B12) y de storage (S1) necesitan dedup/limpieza previa.

**b) Tests de CARACTERIZACIÓN** (fijan el comportamiento ACTUAL, con transacciones reversibles;
NO codificar como "esperado" los bugs que F1–F3 van a corregir — esos se marcan `@group known-bug`
o se dejan fuera hasta su fase):
- matriz de acceso de roles 1–5 (qué ruta/acción permite cada uno hoy);
- carga de una ficha existente;
- listado + paginación (citas, pacientes);
- generación de FUA y de ticket (que sigan saliendo);
- lectura del vínculo de cobro existente de una cita;
- invariantes de MQN sin flags (login por rol, dashboard, cobro de consulta).

**Criterio**: `clinica:audit-baseline` corre en todos los tenants is_clinic y emite informe (texto + json); suite de caracterización verde contra `mqn_efp`; ningún bug de F1–F3 quedó fijado como comportamiento esperado.

> **Condición operativa antes de crear la rama** (árbol compartido, ver §0.2): hay cambios ajenos
> sin commitear en `mobile/` y este doc está sin trackear, y puede haber otra sesión en la misma
> carpeta. **Coordinar** el pase a `feat/clinica2-f0` y **preservar exactamente** esos cambios
> (add por path explícito, nunca `git add -A`; nunca `checkout/reset` sin `git status` previo).

### F1 — Seguridad / RBAC / archivos privados — ✅ COMPLETO (commits acaaa28c, 367dd7fe, e5352e1d, cda2c510, e83e24bb)

> Cerrado: S1 (archivos PHI a disco privado + ruta autenticada + `clinica:migrate-files`),
> S2 (sin siloing, documentado), S4 (quitar rol retira módulo), S5 (updateStatus eliminado),
> S6 (store no edita por id), S8 (sin rol clínico: solo admin/owner pasa), S9 (gates backend
> vitals/reschedule/sale-items), S10 (recep no ve relato clínico), S11 (lab deriva doctor/paciente),
> S12 (whereType customers), B6 (recep sin "Ver detalle"), S3 (XHR mutante fuera de scope → 403,
> GET intactos). **S13 (audit log) → diferido a F5.** 14 tests + Playwright. Assets se recompilan
> PROD al release (no commiteados en la rama DEV).

**(referencia del alcance original abajo)**
S1–S12 + uploads temporales privados. **S1** = comando `clinica:migrate-files` (mueve
`storage/app/public/clinica/medical_files` → disco privado `storage/app/clinica/{tenant}/…`,
nombres con `Str::random`, reescribe referencias) + **ruta de archivo por ID de registro con
autorización por rol** (no por nombre físico; recep NO baja resultados de lab). Corre en prod
con backup. Incluye **B6/S10** (payload de recep acotado: ve demográficos + citas, NO
evaluación/tratamiento/lab — es seguridad + minimización de datos, va acá, no en F3). El payload
de enfermería va en F6 (el rol recién existe ahí).
**Criterio**: URL directa a archivo médico sin sesión = 403; doctor B no abre ficha de doctor A; `finish`/`attend` no mutan por GET; sin rol clínico no entra a /clinica/settings; laboratorista no POSTea fuera de scope; archivo huérfano no queda tras cancelar el wizard; recep abre "Ver detalle" con payload acotado (sin relato clínico).

### F2 — Máquina de estados + integridad de cobros — ✅ COMPLETO (commits 15f79357, 8c0c4671)

> Cerrado: D1 (reporte de ingresos real: 4 fuentes de cobro, excluye anuladas/changed, PEN,
> por fecha de emisión; verificado S/35,845 en mqn_efp), D2 (no PA sin comprobante), D3 (no doble
> cobro), D4 (POS no silencia fallos de vínculo), D5 (no vincula venta de otro cliente),
> D6 (decisión: POS manda el precio — ya es el comportamiento, ficha precio solo-lectura; sin
> código), B9 (cobro desacoplado del estado: "Pagar" mientras no esté cobrada, verificado en cita
> AT/EN), B10 (carrito persistido al Finalizar), B12 (UNIQUE medical_consultations.appointment_id),
> I9 (UNIQUE vital_signs_values). Migraciones idempotentes verificadas en mqn_efp. 14 tests + Playwright.

**(alcance original abajo)**
D1–D6, B9, B10, B12, I9. D1 con test (cálculo de totales, regla del repo). Incluye política de
**moneda** del reporte (convertir a PEN con `exchange_rate_sale` o separar por moneda) y
**agrupar por fecha de emisión/cobro**, no de cita. Link de venta idempotente (elimina `.catch(()=>{})`).
**Criterio**: reporte Ingresos cuadra con boleta + nota + anulada + 2 monedas de prueba; cita RE no atendible; Finalizar con carrito sin guardar avisa/guarda; doble-click Atender = 1 consulta; **doble vínculo/cobro de una misma cita o carrito = rechazado** (el pedido persistente de farmacia con su máquina de estados llega en F12).

### F3 — Bugs funcionales de arranque — ✅ COMPLETO (commit 44358446)

> Cerrado: B1 (code '' → null), B2 (code NOT NULL → ''), B3 (buscador lab usa
> search-sale-items, incluye servicios ZZ — verificado en vivo: el endpoint viejo NO
> encontraba el ítem servicio, el nuevo sí), B4 (records devuelve {data,meta} —
> verificado: paginador funciona, página 2 arranca en 21), B5 (birthdate/gender en
> getCollectionData), B7 (upcoming ordena ASC), B8 (dashboard con datos reales —
> verificado: Pacientes=232 en mqn_efp). 6 tests + Playwright. Nota: mqn_efp (BD local
> polucionada) tenía 2 migraciones ATC pendientes que rompían B3 con 500 para
> CUALQUIER búsqueda (bug preexistente ajeno); se aplicaron solo para poder validar.

**(alcance original abajo)**
B1, B2, B3, B4, B5, B7, B8. Quirúrgico, ~10 archivos. (**B6 NO va acá**: es seguridad/
minimización de datos → se resuelve con S10 en F1.)
**Criterio**: 3 especialidades sin código OK; [+Nuevo] procedimiento sin código OK; el modal de lab encuentra ítems servicio; Pacientes pagina; editar paciente conserva birthdate/gender; "Próximas" ordena asc; panel muestra números reales.

### F4 — Configuración runtime por tenant — ✅ COMPLETO (commits fd8bf1bf + UI)

> **Infra**: migración `configurations` += `clinic_features`/`clinic_labels`/`clinic_patient_states`
> (json, idempotente, aplicada en mqn_efp y demo) · constantes de default + helpers
> `clinicFeature()`/`clinicLabel()`/`clinicPatientStates()`/`clinicRuntimeConfig()` ·
> `GET /clinica/configuration/runtime` (whitelist de ClinicRoleGate, lo leen los 4 perfiles) ·
> `POST /clinica/configuration` (solo acceso pleno, sanitiza claves/códigos).
> **UI**: tab "Vertical" en Clínica › Configuración con 3 secciones (Funciones · Estados del
> paciente · Etiquetas), en lenguaje de usuario final (no jerga técnica).
> **Consumo**: ficha usa `t()` para los labels de tabs y el select de estados lee la config
> (incluye el estado guardado aunque esté desactivado, para no perder históricos); wizard de
> cita usa los labels de motivo/descripción; los 3 botones "Sin cobro" se ocultan con
> `allow_free=false` **y** `markFree` rechaza con 403 (defensa en profundidad).
> Entrega **P10, P13, P15, P16** + el toggle "solo cobrar". Aislamiento verificado: editar demo
> no altera mqn_efp. 29 tests verdes.
- Migraciones: `configurations` += `clinic_features`, `clinic_labels`, `clinic_patient_states` (JSON).
- Helpers + **endpoint `GET /clinica/configuration/runtime`** (§0.1) + helper JS `t()` + tabs de Configuración (Etiquetas · Estados del paciente · Funciones), solo admin.
- Entrega Tier 1 que no necesita ficha extendida: **P10, P13** (etiquetas), **P15, P16** (estados).
- **Toggle `clinic_allow_free`** (default ON = MQN conserva "Sin cobro"/GR): OFF → oculta el botón
  "Sin cobro" en citas + `markFree` rechaza (defensa en profundidad). Para tenants que "solo cobran"
  (pedido del usuario 2026-08-24). Se enforce sobre el botón (appointments/index.vue) y `markFree`
  (AppointmentController). En F2 el cobro se desacopla del estado (B9); acá se agrega la visibilidad configurable.
**Criterio**: MQN sin tocar config = idéntico; tenant nuevo cambia etiquetas, oculta "Hospitalización"
y (si "solo cobra") oculta "Sin cobro", desde la pantalla; editar un tenant NO toca al otro.

### F5 — Historia clínica extendida — ✅ COMPLETO (commit 062922c1)

> Todo bajo `extended_history` (default OFF → MQN intacto). 3 migraciones idempotentes
> aplicadas en mqn_efp y demo.
> - **P12** `patient_backgrounds` (1 fila/paciente, UNIQUE): alergias, diabetes/HTA/asma, otros
>   personales, familiares metabólicos/cardiovasculares con detalle + otros. **Traza
>   updated_by/updated_at** (NTS 139) y **alerta** si hay alergias o crónicos. Endpoints
>   GET/POST `/clinica/consultations/patient/{id}/background` (escritura exige `attend`).
> - **P14** motivo/enfermedad actual: el doctor SIEMPRE los ve (antes el API los mandaba y la
>   ficha no los pintaba); con el flag puede corregirlos → se guardan en la cita.
> - **P17** `medical_consultation_external_exams` (análisis complementario, sin orden ni cobro).
> - **P19** examen físico por segmentos en `medical_consultations` (piel/ORL/tórax/abdomen/
>   miembros) + orden pedido: Vitales → Motivo → Enf. actual → Antecedentes → Examen físico →
>   Diagnóstico. **Se eligió la opción (B) refinada**: las secciones nuevas se insertan en el
>   tab "Atención Médica" en ese orden, sin mover EVA ni las sub-tabs (menos riesgo para MQN).
> - Historia clínica del paciente: card de antecedentes con la misma alerta; **respeta S10**
>   (recepción no los ve, verificado por test).
> 5 tests nuevos (35 de clínica verdes) + verificación E2E por HTTP. **Decisión abierta #6
> (P19 A/B) → resuelta como B.**
P12 (antecedentes + auditoría + snapshot alergia), P14 (motivo/enf.actual; lectura = C, edición = flag), P17 (análisis complementario), P19 (examen físico + reorden — opción A/B pendiente). Migraciones: `patient_backgrounds`, `medical_consultations` +5 examen físico, `medical_consultation_external_exams`.
**Criterio**: MQN (flag off) → ficha igual pero el doctor VE motivo/síntomas; tenant nuevo (flag on) → orden P19 completo, antecedentes persisten entre 2 atenciones, alerta de alergia visible, se ve quién/cuándo editó antecedentes.

### F6 — Rol Enfermería — ✅ COMPLETO (commit 755f0e3a)

> Perfil 6: landing `/clinica/nursing`, scoped, sin POS. `can`: vitals + apply_treatment; NO
> attend/charge/create_appointment/fua/reschedule. **prefixes `['nursing','appointments','files']`
> — deliberadamente SIN `consultations` ni `patients`**, para que no alcance la ficha médica ni
> la historia (S10). `NursingController` con cola del turno (edad, motivo, estado de vitales y
> **alerta de alergias/crónicos**) y payload propio del paciente; pantalla Vue con KPIs +
> registro de vitales reusando el endpoint existente; sidebar reducido; selector en Configuración.
> **`isClinicScopedRole()` refactorizado**: ahora deriva del mapa (antes lista hardcodeada
> `[1,2,5]` que había que recordar actualizar — la trampa exacta que un rol nuevo dispara).
> 4 tests nuevos + gate de enfermería + matriz actualizada (40 verdes). Verificado E2E: login →
> su pantalla; cola con 4 pacientes; ficha/historia/config/reportes → 403. mqn_efp queda con
> **0 usuarios rol 6** (MQN intacto).
> **Nota**: el sistema no permite borrar usuarios con actividad registrada (FK de
> `system_activity_logs`); el de prueba quedó sin rol y bloqueado.
P2 (los 7 puntos + refactor `isClinicScopedRole` derivado del mapa + payload de enfermería acotado). Landing provisional = cola del día.
**Criterio**: rol 6 loguea → landing enfermería con payload propio (sin ficha médica completa); NO cobra, NO atiende, NO entra a settings; MQN sin usuarios rol 6 = cero cambios; roles 1-5 idénticos (regresión de la matriz).

### F7 — Vitales exclusivos de enfermería — ✅ COMPLETO (commit 540c13ba)

> `clinicCan()` consulta la config del tenant, que **RESTRINGE, nunca amplía** (probado: la
> config no le da vitales al laboratorio ni cobro a enfermería). Con `nursing_only_vitals` ON:
> solo Enfermería registra; el **Doctor conserva el permiso** (los corrige en la atención) y la
> Recepción queda fuera. OFF (default) = MQN sin cambios.
> **`clinicEffectiveCan()`**: el blade de Citas, `getCollectionData` y el runtime de F4 ahora
> publican permisos EFECTIVOS (antes el mapa crudo → un botón podía verse habilitado y el
> backend rechazarlo). El botón "Vitales" pasa a consultar el permiso (estaba sin gate: `can.vitals`
> existía y no lo leía nadie — deuda de la auditoría).
> 5 tests (45 verdes). E2E con la recepcionista real: OFF → `can.vitals=true` + POST 200;
> ON → `can.vitals=false` + POST 403, con `charge` intacto. mqn_efp restaurado a defaults.
P1: consumir el permiso efectivo de vitales en front+backend; interruptor por tenant.
**Criterio**: tenant nuevo ON → recep sin botón Vitales y backend la rechaza; enfermera sí; doctor ve/corrige; MQN OFF → recep intacta.

### F8 — Receta clínica separada (2 sesiones) — `rx_details`
P11: `clinic_prescriptions`/`clinic_prescription_items` (dominio 2, §0.4), `users.cmp`, editor de líneas en el tab, receta PDF (clon Vet), FUA con productos. El carrito viejo sigue para MQN.
**Criterio**: receta PDF con días/indicación y CMP; flags `administer_in_clinic`/`send_to_pharmacy` por línea; MQN (flag off) usa el carrito de siempre y su FUA sigue vacío.

### F9 — Motor de planes y aplicaciones (2–3 sesiones) — `treatment_plans`
P3/P4/P9: tablas planes/aplicaciones + `clinic_treatment_application_vitals`, generación atómica desde receta (`administer_in_clinic`), cola de enfermería (landing definitivo rol 6, secciones Hoy/Atrasadas/Próximas/Completadas/Omitidas), botón "Aplicaciones (n/m)" en citas AT.
- **La 1.ª dosis se cobra (con la cita/tratamiento); las aplicaciones SIGUIENTES NO se cobran** (pedido del usuario 2026-08-24): son atención de enfermería, no una cita nueva → confirmar aplicación + vitales, SIN pasar por linkSale/markFree. El cobro es propiedad del plan (se pagó una vez), no de cada aplicación.
**Criterio**: receta ampolla ×3 con "administrar en clínica" → 3 aplicaciones (transacción única); enfermera confirma la de hoy + dosis/vía + vitales de la visita; día 2 y 3 en su cola SIN re-cobro; plan → done al completar; historia muestra las aplicaciones.

### F10 — Cita libre + servicios TTO (2 sesiones) — `free_appointments`
P6/P7/P8: migración type/treatment_kind/service_item_id, catálogo "Servicios de enfermería", modal 1 paso, receta obligatoria (upload temporal de F1), cobro por servicio, derivación al motor (F9).
**Criterio**: recep registra sutura en <1 min: servicio → precio auto → receta obligatoria → paga → PA → aparece en la cola de enfermería; MQN no ve "Libre".

### F11 — Laboratorio cobrable (1 sesión) — `lab_billing`
P18: snapshot comercial completo en `medical_laboratory_items`, precio en el modal, "Cobrar lab" → circuito POS → venta vinculada → reporte (F2).
**Criterio**: solicitud de lab muestra precios; cobro genera venta que aparece en Ingresos; snapshot preserva precio/moneda aunque cambie el catálogo.

### F12 — Pedido persistente a farmacia (2 sesiones) — `pharmacy_dispatch`
P5: `clinic_pharmacy_orders`/`_items` (dominio 3, estado + compare-and-set), pantalla "Pedidos de clínica" en POS para rol 4, link idempotente, precarga DIGEMID.
**Criterio**: doctor guarda receta con `send_to_pharmacy` → cajero de farmacia (otra máquina) ve el pedido → reclama → cobra → estado charged; 2.º cajero no puede cobrarlo; MQN (flag off) sin cambios.

**Total estimado: 18–26 sesiones** (revisado con Codex: incluye tests, seguridad, archivos privados,
máquina de estados, 4 dominios separados, y verificación por tenant). F0–F3 (base, ~5–8 sesiones)
no tienen flags y benefician a MQN de inmediato. F12 puede ir en paralelo a F10 pero solo después
de F8 (receta) y del contrato transaccional de F2.

---

## 4. Verificaciones previas en MQN prod (read-only, antes de F1)

**Ya verificado en `mqn_efp` local (2026-08-23)** — patient_status guarda CÓDIGOS: T02=342, NULL=56, T01=24, T03/T04/T05=0. recital_date usados=4. Citas=443, con nota de venta=438, con boleta/factura=0, items carrito=1, órdenes lab=3, adjuntos=8, archivos de resultado=0. Cero duplicados de consultas y de vitales. → Las migraciones UNIQUE (I9, B12) son triviales HOY en mqn_efp, pero **hay que re-auditar cada tenant real antes de aplicarlas en prod**.

```sql
-- Correr en CADA tenant real antes de las migraciones UNIQUE / de storage:
SELECT patient_status, COUNT(*) FROM medical_consultations GROUP BY 1;   -- ¿códigos vs sucio?
SELECT COUNT(*) FROM appointment_documents; SELECT COUNT(*) FROM laboratory_results WHERE file_url IS NOT NULL;  -- dimensiona migración storage S1
SELECT appointment_id, vital_sign_id, COUNT(*) c FROM vital_signs_values GROUP BY 1,2 HAVING c>1;   -- dedup antes de UNIQUE (I9)
SELECT appointment_id, COUNT(*) c FROM medical_consultations GROUP BY 1 HAVING c>1;                  -- dedup antes de UNIQUE (B12)
```

### 4.1 ⚠️ DRIFT DE MIGRACIONES en tenants creados por ETL (hallazgo 2026-08-24)

**Un tenant migrado por ETL no tiene historial de migraciones.** Verificado en `mqn_efp`:

| | tablas reales | filas en `migrations` |
|---|---|---|
| `mqn_efp` (ETL desde MQN legacy) | 341 | **3** |
| `tenancy_demo` (nacido por migraciones) | 346 | 1062 |

Consecuencia: `tenancy:migrate` **falla al primer archivo** ("Table 'cat_document_types' already
exists") y **se detiene**, dejando pendientes todas las posteriores. Por eso a `mqn_efp` le faltan
tablas/columnas de features nuevas aunque el vertical clínico esté completo.

**Faltantes reales detectados en `mqn_efp`** (ninguna es de Clínica — las 15 tablas clínicas están OK):
`bot_messages`, `bot_sessions` (WhatsApp Bot) · `received_payments` (Yape Inbox) ·
`item_warehouse_prices` (precios por almacén) · `weighted_average_costs` (costo promedio) ·
`seller_bonus_ledger` (rename de `seller_point_ledger`, que sí tiene) · más columnas sueltas
(p.ej. faltaban `atc_codes` + `items.atc_id`, que rompían con 500 CUALQUIER búsqueda de
`search-sale-items` — se aplicaron a mano para validar F3/B3).

**Acción obligatoria ANTES del deploy final** (no urgente para F4-F12, que son locales):
1. Correr el audit por tenant y comparar el set de tablas contra un tenant sano (`tenancy_demo`).
2. Para cada tenant con drift: **marcar como corridas** las migraciones cuyo efecto ya existe
   (`INSERT INTO migrations`) y **aplicar** solo las que realmente faltan, en orden. Alternativa
   más segura: script que recorra los archivos, detecte con `hasTable`/`hasColumn` si el efecto
   ya está, y registre o aplique según corresponda.
3. Verificar que `tenancy:migrate` corre limpio de punta a punta ANTES de deployar las
   migraciones de F2 (UNIQUE) y las de features nuevas.
> Sin esto, un `tenancy:migrate` en prod se corta en la primera colisión y deja el tenant a medias.

**Decisión (2026-08-24): se hace UNA VEZ, en la fase de deploy, no ahora.** Motivo: cada fase
(F4, F5, F8, F9…) agrega migraciones nuevas; reconciliar antes obliga a re-verificar igual al
final. No bloquea el desarrollo local (las tablas clínicas están completas). Si durante una fase
falta una tabla/columna en el tenant local, se aplica puntual solo para validar y se anota acá.

**Entregable de la fase de deploy — `clinica:reconcile-migrations`** (comando nuevo, idempotente):
1. `--dry-run` (default): recorre `database/migrations/tenant/*.php` en orden; por cada una que NO
   esté en la tabla `migrations`, detecta con `hasTable`/`hasColumn`/`SHOW INDEX` si su efecto YA
   existe. Clasifica en: **YA APLICADA** (solo falta registrarla) · **FALTA** (hay que correrla) ·
   **INDETERMINADA** (revisar a mano). Reporta por tenant, con `--tenant=` y `--format=json`.
2. `--apply`: registra en `migrations` las YA APLICADAS (sin ejecutarlas) y ejecuta las que FALTAN,
   en orden, dentro de transacción por migración. Nunca toca las INDETERMINADAS: las lista para
   decisión manual.
3. Verificación final: `tenancy:migrate` debe correr limpio de punta a punta en TODOS los tenants
   antes de deployar las migraciones de F2 (UNIQUE) y las de features nuevas.
> Correr con backup previo. La clasificación "YA APLICADA" es la parte delicada: ante la duda, la
> migración va a INDETERMINADA y se revisa a mano, nunca se registra a ciegas.

## 5. Decisiones abiertas (del usuario)

1. **¿Bloquear vitales a la recep del tenant nuevo?** RESUELTO por tu pedido: SÍ en el tenant nuevo (`nursing_only_vitals=ON`); MQN queda OFF. No es recomendación, es tu requisito.
2. **Catálogo TTO**: ¿flag en items (`is_clinic_nursing_service`) o categoría configurable? Recomendado: categoría (cero migración de items, patrón Vet probado).
3. **I20 homónimos**: ¿relajar UNIQUE de nombre para pacientes? Afecta también POS — dejar para cuando muerda.
4. **¿Ofrecer `rx_details`/`extended_history` a MQN?** Construidos con flag, demo al Dr. Quezada después; él decide.
5. **Posología completa** (dosis/vía/frecuencia además de días+indicación): no pedida; el esquema Vet queda de molde si se pide.
6. **Reorden de la ficha (P19)**: ¿opción (A) flujo único scrolleable o (B) insertar en tabs actuales? (ver P19).

---

## 6. Verificación visual en vivo — jena = `mqn_efp` (2026-08-23, Playwright)

Recorrido del tenant estable real (443 citas) confirmando que el plan coincide con la UI.
Capturas: `clinica-01-citas-lista.png`, `clinica-02-ficha-medica.png`.

| Pantalla | Confirmado | Impacto en el plan |
|----------|-----------|--------------------|
| **Citas** (lista) | Columnas y botones por estado exactos: RE→Pagar/Sin cobro/Reprogramar/Ticket · GR→Vitales/Atender · AT→Ticket/FUA · cita #435 "Cobrar prod. S/170.00". Hora 12h en UI. Botón "Nuevo" único en header. | P1/P5/P13 anclados. El botón **"Libre"** (P7) va junto a "Nuevo" (header). |
| **Wizard Nueva cita** | 3 pasos: Especialidad+Profesional → Fecha/hora (turnos mañana/tarde) → Confirmación. Paso 3: Paciente(+Nuevo), apoderado, **"Motivo de consulta *"**, **"Descripción"** (placeholder "Detalle de síntomas / observaciones…"), estudios previos + adjuntar. | **P13 = renombrar "Descripción"→"Enfermedad actual"** (campo `symptom_details`, confirmado). Nota **"máx 2MB"** ← confirma la inconsistencia 2MB(cita)/5MB(ficha) del plan. |
| **Ficha doctor** (5 tabs) | Tabs: Atención Médica · Ayuda Dx · Procedimientos y Tratamientos · Ventas · Historial. | Estructura del plan correcta. |
| — Tab Atención Médica | EVA (0-10) → Evaluación Clínica (11 vitales, IMC auto) → Diagnóstico [+Nuevo] CIE-10. Sidebar: paciente "46 años · Masculino", Última Visita, Ver historia completa; Resumen; "Solicitar examen de laboratorio". | **P14 gap CONFIRMADO en vivo**: el doctor NO ve Motivo ni Enfermedad actual. **P12 CONFIRMADO**: cero antecedentes. |
| — Tab Procedimientos y Tratamientos | Procedimientos [+Nuevo] + "Días de tratamiento"; sub-tabs Evaluación / Tratamiento / Plan de Trabajo / Resultado. | **P11**: "días" ya existe para procedimientos, falta para medicamentos (tab Ventas). Relato clínico enterrado acá → refina **P19**. |
| — sub-tab Resultado | Estado del Paciente (5 opciones: **Alta Médica · Requiere Recita · Derivación a Especialista · Hospitalización · En Observación**), Fecha de recita (opcional), Observaciones adicionales. | **P15 (quitar Hospitalización)** y **P16 (Requiere Recita→Reevaluación)** CONFIRMADOS. |
| — Tab Ventas | "Productos de la consulta — No se cobra aquí: se cobra después en caja (POS)". | **P11 receta** va acá (rename + columnas días/indicación). |
| — Tab Ayuda Dx | Heading "Archivos Adjuntos — Estudios médicos e imágenes (ecografías, laboratorios, radiografías…)", botón Subir archivo. | **P10**: rename tab "Ayuda Dx"→"Estudios complementarios" encaja con el contenido real. |

**Pendiente de recorrer en próxima pasada** (los agentes ya los cubrieron por código, falta verlos en vivo si querés): Laboratorio (rol 5), Pacientes+Historia, Reportes, Configuración (Personal/Signos vitales/Servicio de cita), y el POS (cobro producto). No cambian el plan; son confirmación.

---

## 7. Cierre de F10–F12 + revisión visual (2026-08-24)

**Todas las fases del plan están implementadas.** 82 tests, 364 aserciones, 0 saltados.
Commits: `0a1541fc` (F10) · `8160ecba` (F11) · `109d8bc9` (F12+S14) · `8bb18ba1` (reconciliación
de migraciones + categoría de servicios) · `a2a6ad98` (5 bugs de la revisión visual).

### Lo que quedó construido

| Fase | Entregable | Flag |
|---|---|---|
| F10 | Atención libre: botón "Libre", receta OBLIGATORIA, servicio con su propio precio, deriva sola a enfermería al cobrar | `free_appointments` |
| F11 | Laboratorio cobrable: snapshot de precio/moneda por examen, total antes de pedir, "Cobrar lab" → POS → comprobante | `lab_billing` |
| F12 | Pedido persistente a farmacia: `clinic_pharmacy_orders` con pending→claimed→charged, cola en el POS del cajero, compare-and-set contra doble cobro | `pharmacy_dispatch` |
| S14 | POS bloqueado en cobros de clínica para el perfil de recepción (guardia también en `clickAddItem`) | — |

Decisión de diseño en F11/F12: las rutas de cobro cuelgan del prefijo que YA alcanza quien cobra
(`appointments` para el lab, `pos` para farmacia). No se ensanchó ningún perfil.

### Bugs encontrados por la revisión visual (no por los tests)

1. **Recepción no podía cobrar** — el guard S3 devolvía 403 en `POST /store/get_igv` y moría la
   inicialización del POS. Era un bug propio de F1, invisible para los tests unitarios porque
   ninguno ejercía el arranque del POS. Fijado con `RedirectModule::POS_CHECKOUT_SEGMENTS` + test.
2. **Líneas de pedido perdidas en silencio** — un producto sin stock no entra al carrito; el
   cajero cobraba de menos y el pedido igual quedaba `charged`.
3. Buscador de paciente del modal "Libre" apuntaba a un método inexistente.
4. Los renombrados (P12/P15) no aparecían: el default estático ganaba siempre al fallback por
   función. Resuelto con `clinicLabelDefaultFor()` + `label_defaults` en el runtime.
5. "Pedidos de clínica" seguía visible durante un cobro bloqueado.

> Lección: los tests de caracterización cubren el backend, no el arranque de una pantalla.
> Un flujo que atraviesa middleware + inicialización Vue solo se prueba abriéndolo.

### Estado de la regla NO DEPLOY

Sigue vigente: **nada a producción**. Antes del deploy, en este orden:

1. `php artisan clinica:reconcile-migrations --tenant=<uuid>` (dry-run) en CADA tenant, revisar
   las INDETERMINADAS a mano, y recién ahí `--apply`.
2. Verificar que `tenancy:migrate` corre limpio de punta a punta.
3. Las 13 migraciones `2026_08_24_*` del vertical van después de eso.

### Pendientes conocidos (no bloquean, decisión del usuario)

- **S13** — Registro de accesos a la historia clínica (NTS 139-MINSA exige auditoría informática).
  Es el control que reemplaza al siloing doctor-a-doctor que se decidió NO implementar (§S2).
- Orden de secciones de la ficha (P19): se insertaron en el orden pedido dentro de la pestaña
  existente, pero EVA sigue arriba y los signos vitales no están en la columna principal. Se eligió
  la opción de menor riesgo para MQN; mover EVA/vitales requiere decisión.
- `full_description` del catálogo compartido termina en `" - "` y se ve como guion suelto en el
  selector de exámenes. Es del buscador global de ítems, no del vertical.

---

## Pendientes de consulta con el cliente (2026-08-25)

Tres decisiones de negocio que bloquean código ya diseñado. **No inventar la respuesta**: cada
una cambia la implementación, y elegir mal cuesta más que esperar.

### 1. ¿Qué medicamentos se aplican en la clínica?

**Por qué importa.** Hoy el doctor tilda "En clínica" línea por línea, y eso alimenta la cola de
enfermería (`TreatmentPlanner` filtra por `administer_in_clinic`). Pero la respuesta no depende del
paciente ni del caso: **depende del producto**. Una ampolla siempre se aplica, una pastilla nunca.
El doctor está tildando lo mismo una y otra vez, y el día que se olvide, enfermería no se entera de
que ese paciente tiene que volver.

**Lo que se descartó y por qué.** Marcar por categoría de producto sería un clic para cientos de
ítems, pero **no sirve con los datos actuales**: `mqn_efp` no tiene NINGUNA categoría con productos,
y las de demo (Procedimientos, Servicios de enfermería, Vacunas) no separan forma farmacéutica.
Habría que reorganizar el catálogo entero, y las categorías ya se usan en reportes.

**Lo que hay que preguntar.** ¿Cómo van a organizar el catálogo? Si aparecen categorías por forma
farmacéutica, se marca por categoría. Si no, el flag va en el producto, con acción masiva en el
listado. **El momento es ahora**: están cargando el stock, y una casilla más mientras cargan es
gratis; después son 800 productos.

Cuando esto se resuelva, la columna "En clínica" desaparece de la ficha.

### 2. ¿Cómo se cobran las sesiones de una atención libre?

**El bug.** `WalkInController@store` valida `sessions` (nullable|integer|min:1|max:30) y **lo
descarta**: `deriveToNursing` crea el plan siempre con `total_sessions => 1` y `duration_days => 1`.
El paciente externo que trae 3 ampollas para 3 días queda agendado UNA vez; al día siguiente vuelve
y enfermería no tiene nada en cola.

**Por qué no se arregló todavía.** El arreglo necesita persistir el número en la cita (columna
nueva), pero antes hay que saber **cómo se cobra**, porque decide si el cargo se genera por plan o
por visita:

- **3 aplicaciones cobradas juntas al registrar** → un solo cargo de S/15, el paciente pasa por
  caja una vez. Simple, pero si abandona después de la primera ya pagó las tres.
- **S/5 cada vez que vuelve** → el paciente pasa por caja las 3 veces. Más justo, pero son 3
  pasos por caja y hay que definir qué pasa si viene y no paga.

Lo dicho hasta ahora: *"5 soles por aplicación"*, lo que sugiere la segunda — pero sin confirmar.

### 3. Los 8 tipos de tratamiento: ¿son productos o solo clasificación?

**Cómo está hoy.** Son DOS cosas separadas y recepción elige las dos:

- **El tipo** (IM, VEV, NEB, SUTURA, CURA, RET_PUNTOS, RET_IMPLANTES, MED) es una lista **fija en
  el código**: `ClinicTreatmentPlan::KINDS`. No es un producto, no tiene precio, y agregar uno
  exige tocar código y desplegar.
- **El servicio** es un producto del catálogo (categoría de enfermería) con su precio. Es el que
  hace el trabajo real: va al POS y genera el cobro.

El tipo casi no hace nada: se guarda en el plan y enfermería lo ve como etiqueta. No afecta precio,
cobro ni cantidad de sesiones.

**Los dos problemas.** (a) El cliente no puede agregar un tipo nuevo sin nosotros. (b) Recepción
elige lo mismo dos veces y nada garantiza que coincidan — puede marcar tipo `SUTURA` y cobrar el
producto `Nebulización`; el sistema lo acepta.

**Qué preguntar.** *"¿Cobran distinto según sea IM, endovenosa o nebulización? ¿Una sutura chica
cuesta lo mismo que una grande?"*

- **Si cada uno tiene su precio** → cada tipo ES un producto. Se elimina la lista fija: recepción
  elige solo el servicio y el tipo viene con él. Agregar tipos pasa a ser crear productos.
- **Si todas cuestan igual** → son clasificación, las dos listas se justifican, pero el tipo debe
  salir igual del producto para que no puedan contradecirse.

**Decidir junto con el punto 1**: si se agrega un campo al producto para "se aplica en clínica",
el tipo de tratamiento va en el mismo lugar. Un cambio en vez de dos.

### 4. Detalles de la receta impresa (80mm)

- **El nombre lleva el código interno**: sale `60958 - PARACETAMOL Tableta 500 mg -`, con el código
  adelante y un guion colgando. Al paciente no le sirve, y si lleva el papel a otra farmacia menos.
  No se cambió porque modifica lo que queda escrito en un documento médico.
- **Sobra ~25% de papel** al final. El alto se calcula generoso a propósito (si falta, DomPDF corta
  la firma y una receta sin firma no sirve), pero en térmica es papel tirado en cada impresión.
- **El CMP no sale** si el médico no lo tiene cargado. Hay que confirmar que los médicos reales de
  producción lo tengan: una receta sin CMP no es válida.

### Reglas ya cerradas con el cliente (NO volver a discutir)

- El pedido a farmacia lleva la **receta entera**; la farmacia despacha lo que hay. El doctor no
  rutea (no le compete, y para decidirlo necesitaba un stock que ya no vale cuando el paciente
  llega a la caja).
- El doctor **no ve precios** (comercial) pero **sí ve stock** (clínico: "¿puedo tratar a este
  paciente con esto?").
- La receta impresa avisa **solo donde no alcanza**: `Le damos 4 · compre 1 afuera`, o
  `Cómprelo afuera`. Si la farmacia cubre todo, el papel no dice nada.
- La **atención libre siempre se cobra**. El paciente compró su medicamento afuera: ese acto es el
  único ingreso de la visita. La regla no depende de la feature `allow_free`.
- **Dos cajas**: recepción cobra citas, farmacia vende al público y despacha pedidos.
- `stock_control` apagado mientras cargan inventario; prendido después. Es el mismo flag que ya
  gobierna el POS.
