# Análisis: chat interno por laboratorio (cross-sucursal)

> **Estado:** análisis de implicaciones — **sin implementación**.  
> **Fecha:** 2026-07-31  
> **Relacionado:** `PLAN_MULTITENANT.md` (tenant vs sucursal), `ANALISIS_CONTEXTO.md`,
> `PLAN_ACTUALIZACION.md` (roadmap).

Documento de contexto: qué implica un chat básico **dentro de cada laboratorio**
(tenant), usable entre sucursales. No es un plan de PRs listo para codificar hasta
cerrar las decisiones de la §7.

---

## Resumen


Un **chat básico a nivel de tenant** (laboratorio), **sin filtrar por sucursal**, encaja bien con el modelo mental actual del SaaS:

- **Tenant** = unidad de aislamiento y de “quién puede hablar con quién”.
- **Sucursal** = punto operativo (caja, folio, reportes), no un silo de comunicación.

Hoy un usuario de sucursal A ya comparte el mismo `tenant_id` con la sucursal B; solo difiere `session('idsucursal')` (vía `empleado.idsucursal`). Un chat **tenant-wide** no rompe multi-tenant; al contrario, es el scope natural.

**No existe** infraestructura de chat/realtime en el repo (ni WebSocket, ni SSE, ni colas). El patrón más cercano es el **feedback** (mensaje asíncrono con adjuntos hacia superadmin), no un chat entre peers.

---

## 1. Implicaciones de producto

### 1.1 Lo que implica “sin importar sucursales”

| Aspecto | Implicación |
|--------|-------------|
| Quién ve a quién | Todo el personal **activo** del mismo laboratorio (admin, empleado, laboratorista), con etiqueta de sucursal opcional en el directorio |
| No es chat entre labs | Un usuario del tenant A **nunca** puede escribir al tenant B (regla de oro multi-tenant) |
| Superadmin | Fuera del chat de labs (salvo soporte futuro / impersonación auditada) |
| Valor operativo | Coordinación caja ↔ lab, “¿ya salió el resultado de X?”, cobertura entre sedes, avisos internos |
| Riesgo de abuso | Canal informal para **datos clínicos / PHI** (nombres, folios, resultados) si no se acota el uso |

### 1.2 Decisiones de producto que hay que cerrar antes de implementar

1. **Modelo de conversación**
   - **DM 1:1** (más simple, suficiente para “A escribe a B”).
   - **Canales / salas** (p. ej. `#general`, `#sucursal-norte`) — más UX y moderación.
   - **Híbrido v1**: solo DM + opcional sala “General del lab”.

2. **Alcance de participantes**
   - ¿Todos los roles del tenant, o excluir algún perfil?
   - ¿Usuarios desactivados (`active=0` / `banned`) salen del directorio y de conversaciones activas?

3. **Persistencia y retención**
   - ¿Historial indefinido o purga a N días? (storage, privacidad, NOM/datos personales).
   - ¿Soft delete de mensajes (como el resto del dominio) o hard delete?

4. **Adjuntos**
   - Texto puro en v1 (recomendado).
   - Imágenes/PDF más adelante (misma disciplina que `feedback` + uploads por tenant).

5. **Notificaciones**
   - Solo badge en la app mientras hay sesión.
   - Email/push fuera de alcance v1 (complejidad + costos).

6. **Moderación**
   - ¿Puede el admin borrar mensajes ajenos?
   - ¿Mute / bloquear entre usuarios? (probablemente no en v1).

---

## 2. Encaje con la arquitectura actual

### 2.1 Aislamiento: tenant sí, sucursal no

```
Tenant (laboratorio)
├── Sucursal A  ── usuario empleado   ──┐
├── Sucursal B  ── usuario lab        ──┼── mismo chat scope (tenant_id)
└── Admin (todas)                     ──┘
```

- **Scoping obligatorio:** todas las tablas de chat con `tenant_id` + `BaseTenantModel` (o SQL crudo con `tenant_id()` fail-closed).
- **No filtrar por `idsucursal`** en listado de usuarios ni en envío/recepción.
- **Metadato útil (no filtro):** mostrar `nombre_sucursal` del empleado en el directorio y en el header del DM para contexto operativo.

Validaciones en servidor al enviar un mensaje:

1. Emisor autenticado y con `session('tenant_id')` válido.
2. Destinatario existe, `active`, mismo `tenant_id`, no superadmin.
3. Conversación pertenece al mismo tenant (si se reutiliza `conversation_id`).

Cualquier omisión aquí es un **bug de aislamiento** del mismo nivel de gravedad que cruzar ventas entre labs.

### 2.2 Identidad: user vs empleado

| Concepto | Uso en chat |
|----------|-------------|
| `users.id` | **Participante canónico** (auth, presencia de login, ban) |
| `empleado` | Nombre para mostrar, sucursal, foto futura |
| Admin sin empleado | Debe poder chatear (hoy a veces solo username en sesión) |

Recomendación: FK de participantes a **`users.id`**, no a `idempleado`. Join a `empleado` solo para UI.

### 2.3 Rutas y roles

Rutas planas con filtros Shield, p. ej.:

```
['auth', 'group:admin,empleado,laboratorista']
```

- Superadmin **no** entra (no tiene `tenant_id` de lab).
- UI: widget/drawer en layouts de backoffice (`layouts` compartidos), no en landing/login.

### 2.4 Auditoría

El dominio usa `BaseAuditModel` + soft delete + `auditoria`. Para chat:

| Opción | Pros | Contras |
|--------|------|---------|
| Extender `BaseTenantModel` / `BaseAuditModel` | Consistente | Bitácora ruidosa (cada mensaje = fila en `auditoria`) |
| Modelo ligero sin auditoría de cada mensaje | Menos I/O | Excepción documentada |
| Auditar solo acciones admin (borrar, exportar) | Equilibrio | Hay que implementarlo a mano |

**Recomendación v1:** tablas con `tenant_id` + timestamps; **no** auditar cada `INSERT` de mensaje; sí soft-delete y `deleted_by` si un admin borra.

---

## 3. Consideraciones técnicas críticas

### 3.1 Entrega “en tiempo real” (el punto más delicado)

Stack actual: **CI4 + jQuery + sin SPA**, producción en **Hostinger** (Apache/PHP-FPM típico), local con **FrankenPHP**. No hay broker (Redis/Pusher), ni colas de jobs para push.

| Enfoque | Complejidad | Hosting Hostinger | UX | Notas |
|---------|-------------|-------------------|----|--------|
| **Polling AJAX** (`GET /chat/poll?since=id`) cada 3–5 s | Baja | Compatible | “Casi live” | **Mejor v1**. CSRF + sesión ya resueltos |
| **SSE (Server-Sent Events)** | Media | Flaky en shared hosting (timeouts, buffers) | Mejor | Requiere long-lived connection; FrankenPHP lo tolera mejor que PHP-FPM clásico |
| **WebSockets** (Ratchet, Swoole, Mercure, Pusher, Ably) | Alta | Suele requerir servicio aparte / plan VPS | Mejor | Fuera de encaje “sin build tooling / hosting simple” en v1 |
| **Cola + push nativo** | Alta | Poco natural en este stack | App-like | Premature |

**Recomendación técnica v1:**

1. Persistencia en MySQL.
2. Cliente: polling corto cuando el panel de chat está abierto; intervalo más largo (o pausa) con pestaña oculta (`document.visibilityState`).
3. Badge de no leídos con poll ligero (solo contador) en el shell.
4. Dejar la API lista para sustituir el poll por SSE/WebSocket sin cambiar el esquema.

### 3.2 Modelo de datos (propuesta mínima)

```sql
-- Conversación 1:1 (o grupo simple)
chat_conversacion (
  id, tenant_id,
  tipo ENUM('dm','canal') DEFAULT 'dm',
  titulo NULL,              -- solo canales
  created_at, updated_at, deleted_at
)

chat_participante (
  id, tenant_id,
  conversacion_id, user_id,
  last_read_mensaje_id NULL,
  muted TINYINT DEFAULT 0,
  UNIQUE(conversacion_id, user_id)
)

chat_mensaje (
  id, tenant_id,
  conversacion_id, user_id,   -- autor
  body TEXT,                   -- texto plano o sanitizado
  created_at, deleted_at, deleted_by
)
```

Índices clave:

- `(tenant_id, conversacion_id, id)` en mensajes (poll `id > last_id`).
- `(tenant_id, user_id)` en participantes (inbox del usuario).
- DM: evitar duplicar conversaciones (orden canónico `min(user_a,user_b)` + unique lógico en app o tabla `chat_dm_pair`).

**No** poner `idsucursal` en mensaje como filtro de acceso; como máximo denormalizar para analytics (“mensajes enviados desde sede X”).

### 3.3 Seguridad

| Riesgo | Mitigación |
|--------|------------|
| Cross-tenant leak | Siempre `tenant_id` en WHERE; tests de aislamiento (como `BaseTenantModelTest`) |
| IDOR (`conversacion_id` de otro lab / conversación ajena) | Verificar membresía del user en la conversación |
| XSS en mensajes | Escape en vista (`esc()`); no renderizar HTML; si Markdown, whitelist estricta |
| CSRF | POST/PUT con token sesión (ya global); endpoints AJAX rotan token como el resto de `app.js` |
| Flood / spam | Rate limit por user (p. ej. N msgs/min) en controller o filtro |
| PHI en chat | Copy de uso + política; **no** sustituir el flujo formal de resultados; opcional filtro de patrones (débil) |
| Enumeración de users | Listar solo users del mismo tenant activos |

### 3.4 Rendimiento y escala multi-tenant

- Una BD compartida: el chat crece **por tenant**. Un lab grande + poll agresivo puede saturar MySQL.
- Poll: `WHERE tenant_id = ? AND conversacion_id = ? AND id > ? ORDER BY id ASC LIMIT 50`.
- Evitar `SELECT *` del historial completo en cada poll; historial inicial paginado hacia atrás.
- Contador no leídos: agregar en `chat_participante` o query `COUNT` con `id > last_read_mensaje_id`.
- Retención: job CLI futuro (`spark chat:purge --days=90`) si el volumen duele.

### 3.5 UX en el shell actual

- Layouts backoffice ya comparten topbar/navmenu: badge “mensajes” + drawer lateral es el camino de menor fricción.
- No meter chat en landing pública ni en portal de resultados del paciente.
- Mobile: el POS y lab se usan en pantallas chicas; drawer full-height y targets táctiles.
- `prefers-reduced-motion` si hay animaciones de entrada.

### 3.6 Lo que el chat **no** debe ser

- Canal formal de entrega de resultados (ya hay portal + PDF + tokens).
- Sustituto de tickets de soporte a la plataforma (eso es `feedback` → superadmin).
- Chat paciente–lab (otro producto, otro compliance).

---

## 4. Implicaciones operativas / legales (ligeras pero reales)

- Mensajes entre personal pueden contener **datos de pacientes**. Aunque el aislamiento es por lab, el lab es responsable del uso interno.
- Soft delete ≠ olvido: backups de MySQL siguen teniendo el texto.
- Si más adelante hay **exportación/auditoría** para el dueño del lab, el admin debería poder revisar o exportar (feature aparte).
- Retención alineada a política de privacidad del producto (cuando exista billing/legal formal).

---

## 5. Esfuerzo relativo (orden de magnitud)

| Capa | Esfuerzo v1 (DM + poll) |
|------|-------------------------|
| Migraciones + modelos `BaseTenantModel` | Bajo–medio |
| API REST (inbox, abrir DM, enviar, poll, mark read, directorio) | Medio |
| UI drawer + jQuery | Medio |
| Tests aislamiento + IDOR | Medio (imprescindible) |
| SSE/WebSocket / adjuntos / push | Alto — **fase 2** |

Comparable en “forma” a un CRUD + endpoint AJAX del POS, **más** el loop de polling y el cuidado de seguridad peer-to-peer.

---

## 6. Recomendación de diseño v1

1. **Scope:** chat **solo dentro del tenant**, visible entre todas las sucursales.
2. **Tipo:** DM 1:1 entre `users` activos del lab; directorio con nombre + rol + sucursal.
3. **Transporte:** polling AJAX; badge de no leídos en topbar.
4. **Contenido:** texto plano, sin adjuntos.
5. **Modelos:** `tenant_id` + membresía de conversación; sin filtro por sucursal.
6. **Fuera de v1:** canales, archivos, notificaciones email, superadmin en el chat, edición de mensajes, reacciones.

---

## 7. Preguntas abiertas (para cerrar si se implementa)

1. ¿Solo DM o también un canal “General” del laboratorio desde el día 1?
2. ¿Quién puede usar el chat? (todos los roles del lab vs solo admin+empleado, etc.)
3. ¿Historial ilimitado o retención (p. ej. 90 días)?
4. ¿El admin puede borrar mensajes de otros o solo los propios?
5. ¿Prioridad real vs otras piezas del roadmap (billing, onboarding, carrito en BD)?

---

## 8. Conclusión

| Pregunta | Respuesta corta |
|----------|-----------------|
| ¿Tiene sentido cross-sucursal? | **Sí** — la unidad natural es el tenant; la sucursal es operativa, no de comunicación |
| ¿Rompe multi-tenant? | **No**, si todo va con `tenant_id` y validación de destinatario |
| ¿Hay algo técnico gordo? | **Sí: realtime y hosting.** En este stack, **polling + MySQL** es lo realista; WebSockets/SSE son fase 2 o infra aparte |
| ¿Riesgo principal no técnico? | Uso del chat para **datos clínicos** informales |
| ¿Complejidad oculta? | IDOR, XSS, flood, crecimiento de tablas, no filtrar por error con `idsucursal` |

Este documento es **análisis de implicaciones**, no un plan de implementación listo para codificar. Cuando se decida el alcance v1 (preguntas de la §7), se puede bajar a migraciones, rutas y UI concretas.
