# Estado del proyecto — Chatbot WhatsApp Policlínica

**Fecha de análisis:** 26 de abril de 2026  
**Analista:** Incorporación nueva  
**Archivo principal:** `backend/app/Http/Controllers/PoliclinicaChatbotController.php` (2.365 líneas)  
**Base de datos:** MariaDB · esquema `aunnait`  
**Integración externa:** Axon (sistema HIS) · WhatsApp vía MessageBird/Nest API

---

## 1. Contexto del análisis

Este documento recoge el estado técnico real del chatbot de WhatsApp en el momento de la incorporación al proyecto. El análisis fue provocado por un incidente reportado por un paciente (DNI `00822685K`, 22/04/2026) que no pudo modificar su cita desde el recordatorio automático y cuya conversación terminó por inactividad sin ningún mensaje de error al usuario.

La investigación forense reveló un fallo crítico con impacto en producción y, al extender el análisis al código completo, se identificaron **15 fallos adicionales** de diversa gravedad que se documentan a continuación.

---

## 2. Datos de producción

| Métrica | Valor |
|---|---|
| Conversaciones totales en BD | 71.571 |
| Conversaciones finalizadas | 67.374 |
| Primera conversación registrada | 24/04/2024 |
| Última conversación registrada | 26/04/2026 |
| Finalizadas correctamente | 7.955 |
| Recordatorios enviados | 31.436 |
| Recordatorios respondidos por paciente | 21.753 |
| DNI no encontrado en sistema | 2.559 |
| Sin citas disponibles para operar | 538 |
| Sin huecos disponibles | 1.903 |
| Modificar cita incompleto | 1.443 |
| Cancelar cita incompleto | 840 |
| Reserva caducada | 325 |

### Catálogo de motivos (`his_medicos_pruebas`)

| Métrica | Valor |
|---|---|
| Registros totales | 98 |
| Agendas cubiertas en catálogo | 28 |
| Agendas activas en producción (2026) | 31 |
| Agendas SIN entrada en catálogo | mínimo 3 (confirmadas: 2985, 3860, 3938) |
| Fecha de creación del catálogo | 21/02/2025 |
| Última actualización del catálogo | **Nunca** |

---

## 3. Incidente detonante

**Paciente:** `00822685K`  
**Fecha:** 22/04/2026 · 18:21:02  
**Acción:** Pulsó "Modificar cita" desde recordatorio automático  
**Error registrado:** `Trying to get property 'id_aseguradora' of non-object` en línea 2256  
**Impacto sobre el paciente:** La conversación murió en silencio. El bot no envió ningún mensaje de error. La sesión acabó por inactividad.

**Estadística del mismo fallo (abril 2026):**  
9 conversaciones crashadas / 154 intentos de "Modificar cita" = **5,84% de tasa de fallo**

---

## 4. Inventario de fallos

---

### CRÍTICOS — Producción afectada hoy

---

#### Fallo 1 · Crash activo: "Modificar cita" desde recordatorio
**Líneas:** 2254–2256  
**Descripción:** `getDatosModificarRecordatorio()` devuelve `null` cuando la agenda del paciente no existe en el catálogo `his_medicos_pruebas` o cuando el nombre del acto médico enviado por Axon no coincide exactamente con el almacenado (variantes de texto o collation diferente). La línea siguiente accede a `$ids->id_aseguradora` sin ninguna comprobación previa.

```php
// CÓDIGO ACTUAL (línea 2254):
$ids = $this->getDatosModificarRecordatorio($id_bot, $cita->acto_medico);
$this->enviarMensaje(..., "Buscando los huecos disponibles...", $id_bot);
$huecos = $this->obtenerFechas($ids->id_aseguradora, ...); // CRASH si $ids es null
```

**Consecuencia:** Excepción PHP fatal. La conversación muere en silencio sin mensaje al paciente.  
**Casos confirmados en producción:** 9 (13–22 de abril de 2026)  
**Agendas afectadas:** 2978, 2985, 3503, 3860, 3874, 3938  
**Doctores afectados:** DR. LUJÁN, CATALINA NUERO CRUZ, DR. NIETO CARRIZOSA, DR. BROCCI, GINECOLOGÍA DR. ÁLVAREZ, TRAUMA DR. FELIPE VELASCO

---

#### Fallo 2 · Inyección SQL (OWASP A03)
**Líneas:** ~1.418 y ~1.460 (`obtenerEstadoBot()` y `crearRegistro()`)  
**Descripción:** El número de teléfono del remitente, recibido directamente del webhook de WhatsApp sin sanitización, se concatena en una query SQL:

```php
// CÓDIGO ACTUAL:
$query = "SELECT c.id, ... FROM chatbot c ...
          WHERE telefono = '" . $telefono . "' AND finalizado = 0 ...";
$datos = DB::select($query);
```

**Consecuencia:** Exfiltración de datos de pacientes, modificación o borrado de registros. Vulnerabilidad OWASP Top 10 A03:2021.

---

#### Fallo 3 · Crash: `reply_to` inválido en flujo de recordatorio
**Línea:** ~125  
**Descripción:** `getMensajeRespondido2($reply_to)` puede devolver `null` (mensaje reenviado, caducado). La siguiente línea accede directamente a `$mensaje_respondido->id_chatbot` sin comprobar.

**Consecuencia:** Crash PHP en cualquier respuesta de recordatorio con `reply_to` no registrado en BD.

---

### GRAVES — Degradación funcional o riesgo de datos

---

#### Fallo 4 · Crash: "Modificar cita" desde flujo normal
**Línea:** ~360 (caso `lista_citas_modificar`)  
Mismo patrón que el Fallo 1. `getDatosModificar()` puede devolver `null` y se usa sin comprobar.  
**Consecuencia:** Crash si el texto del mensaje no coincide exactamente con el registro en `citas_paciente`.

---

#### Fallo 5 · `var_dump()` y `echo` activos en producción
**Líneas:** 71, 80, 107, 117, 120, 198, 202 y más (más de 10 ocurrencias)  
Vuelcan al response HTTP datos internos: IDs de pacientes, teléfonos, estructuras de objetos.  
**Consecuencia:** Filtración de datos personales (GDPR). Respuesta HTTP contaminada que puede provocar reintentos automáticos del webhook de WhatsApp.

---

#### Fallo 6 · `die` como control de flujo en controlador Laravel
**Ubicaciones:** `crearCita()` y `guardarInfoAgendas()`  
Laravel no ejecuta su ciclo de respuesta. WhatsApp recibe respuesta vacía o truncada.  
**Consecuencia:** Los webhooks de WhatsApp reenvían el mensaje si no reciben `200 OK`. Riesgo de doble creación o doble cancelación de cita.

---

#### Fallo 7 · Variables no inicializadas tras excepción silenciosa
**Afecta a:** `getDatosModificarRecordatorio()`, `getDatosModificar()`, `getActosBBDD()`, `getIdFromHueco()`  
La variable de resultado se declara dentro del `try`. Si la query falla, el `catch` está vacío y el `return` referencia una variable inexistente.  
**Consecuencia:** En error de BD, el método devuelve un valor indefinido que propaga el error de forma impredecible.

---

#### Fallo 8 · Bug de precedencia de operadores en condición de recordatorio
**Línea:** 119  
```php
if ($recordatorio === 1 && (...) || $texto == "Baja recordatorios" && $tipoMensaje == 11)
```
Por precedencia de PHP (`&&` antes que `||`), "Baja recordatorios" no requiere `$recordatorio === 1`.  
**Consecuencia:** Cualquier usuario en flujo normal que envíe "Baja recordatorios" es redirigido al handler de recordatorios con datos de contexto incorrectos.

---

#### Fallo 9 · Copy-paste bug en filtro de huecos de tarde
**Línea:** ~1.725 (`mismoRangoHorario()`)  
Se comprueba `$hora1` dos veces en la condición de tarde, en lugar de `$hora1` y `$hora2`.  
**Consecuencia:** Lógica de agrupación de huecos de tarde incorrecta. Huecos duplicados u omitidos.

---

### MEDIOS — Comportamiento incorrecto silencioso

---

#### Fallo 10 · `array_filter` sobre objeto en `obtenerCitasPaciente()`
**Línea:** ~2.008  
`array_filter()` se aplica al objeto de Axon completo, sobreescribiendo `$citas`. El `foreach` posterior accede a `$citas->cita` que ya no existe.  
**Consecuencia:** Filtro de citas nocturnas no funciona. Citas de madrugada pueden mostrarse al paciente.

---

#### Fallo 11 · `citas_paciente` sin limpieza previa por sesión
**Método:** `obtenerCitasPaciente()`  
INSERT de citas sin DELETE previo por `id_chatbot`. En ejecución doble del mismo flujo, los registros se duplican.  
**Consecuencia:** Consultas posteriores pueden retornar el registro de una ejecución anterior, operando la cita equivocada.

---

#### Fallo 12 · Catálogo `his_medicos_pruebas` obsoleto y sin sincronización
Creado el 21/02/2025. Nunca actualizado. No existe ningún job, cron ni proceso que lo sincronice con Axon.  
**Consecuencia:** Fallo estructural garantizado para agendas o actos médicos incorporados desde feb/2025. Raíz del Fallo 1.

---

#### Fallo 13 · Collation mismatch entre tablas
- `chatbot.acto_medico` → `utf8mb3_general_ci`
- `his_medicos_pruebas.activity_name` → `utf8mb3_uca1400_ai_ci`

**Consecuencia:** Textos visualmente idénticos no hacen match sin ningún error visible. Contribuye al Fallo 1.

---

### BAJOS — Deuda técnica estructural

---

#### Fallo 14 · Código muerto en `getActosBBDD()`
**Línea:** ~1.148  
`return $array_actos;` seguido de ~25 líneas de paginación que nunca se ejecutan.

---

#### Fallo 15 · Protección anti-replay completamente desactivada
**Líneas:** 155–180  
Bloque íntegro comentado que bloqueaba respuestas a mensajes de conversaciones anteriores.  
**Consecuencia:** El bot procesa respuestas a mensajes de días atrás como si fueran válidos.

---

## 5. Resumen ejecutivo de fallos

| # | Criticidad | Descripción breve | Estado |
|---|---|---|---|
| 1 | CRITICO | Crash "Modificar cita" desde recordatorio | Activo. 9 casos confirmados. |
| 2 | CRITICO | Inyección SQL con teléfono del webhook | Activo. Vulnerabilidad de seguridad. |
| 3 | CRITICO | Crash si reply_to no existe en BD | Activo. Mensajes reenviados/caducados. |
| 4 | GRAVE | Crash "Modificar cita" desde flujo normal | Activo. |
| 5 | GRAVE | var_dump/echo volcando datos en producción | Activo. Filtración de datos + webhooks corruptos. |
| 6 | GRAVE | die en lugar de return, posible doble acción | Activo. Riesgo de citas duplicadas. |
| 7 | GRAVE | Variables no inicializadas tras excepción | Activo. Errores silenciosos. |
| 8 | GRAVE | Bug de precedencia en condición de recordatorio | Activo. Bypass de validación de flujo. |
| 9 | GRAVE | Copy-paste bug en lógica de huecos de tarde | Activo. Huecos incorrectos. |
| 10 | MEDIO | array_filter sobre objeto: filtro nocturno roto | Activo. |
| 11 | MEDIO | citas_paciente sin limpieza: datos duplicados | Activo. Riesgo de cita equivocada. |
| 12 | MEDIO | Catálogo his_medicos_pruebas sin actualizar desde feb/2025 | Activo. Raíz del Fallo 1. |
| 13 | MEDIO | Collation mismatch en JOIN entre tablas | Activo. Matches fallidos silenciosos. |
| 14 | BAJO | Código muerto en getActosBBDD() | Deuda técnica. |
| 15 | BAJO | Protección anti-replay comentada | Riesgo de acciones involuntarias. |

---

## 6. Estado del código

| Aspecto | Estado |
|---|---|
| Tests unitarios | NO existen |
| Tests de integración | NO existen |
| Código de depuración en producción | ACTIVO (var_dump, echo, print) |
| Validación de nulos en puntos críticos | AUSENTE en todos los flujos críticos |
| Uso de query builder parametrizado | PARCIAL (mezclado con SQL crudo sin sanitizar) |
| Gestión de errores estructurada | catch vacíos o con print en producción |
| Sincronización de catálogo con Axon | NUNCA ejecutada desde feb/2025 |
| Documentación técnica | NO existe |
| Entorno de staging/preproducción | No confirmado |

---

## 7. Plan de acción priorizado

### Inmediato (sin ventana de mantenimiento)

1. **Fix 1 — Null check en "Modificar cita" desde recordatorio** (Fallo 1): añadir `if (!$ids)` antes línea 2256.
2. **Fix 3 — Null check en respuesta de recordatorio** (Fallo 3): comprobar `if (!$mensaje_respondido)` antes línea 125.
3. **Fix 8 — Paréntesis en condición de recordatorio** (Fallo 8): añadir paréntesis explícitos.

### Corto plazo (con ventana planificada)

4. **Fix 2 — SQL Injection** (Fallo 2): reemplazar SQL crudo por Query Builder con binding parametrizado.
5. **Fix 5 — Limpiar debug de producción** (Fallo 5): eliminar todos los `var_dump`, `echo` y `print`.
6. **Fix 6 — Reemplazar `die` por `return`** (Fallo 6).
7. **Fix 11 — DELETE previo en `obtenerCitasPaciente()`** (Fallo 11).
8. **Poblar catálogo `his_medicos_pruebas`** (Fallo 12): llamar a Axon para agendas sin cobertura.

### Medio plazo (sprint planificado)

9. Corrección de collation (Fallo 13).
10. Corrección bug huecos de tarde (Fallo 9).
11. Corrección `array_filter` (Fallo 10).
12. Job de sincronización periódica del catálogo con Axon.
13. Restaurar protección anti-replay (Fallo 15).
14. Batería de tests de los flujos principales.

---

## 8. Conclusión

El chatbot fue entregado y puesto en producción en un estado de desarrollo incompleto. Los fallos identificados no son edge cases: son rutas de código principales que carecen de validación básica, con debug activo que expone datos de pacientes, con una vulnerabilidad de inyección SQL sobre el campo de entrada principal (teléfono), y con un catálogo de datos congelado desde febrero de 2025 que hace que la funcionalidad de "Modificar cita" desde recordatorio sea incapaz de funcionar para las agendas más activas de la clínica.

El sistema ha procesado más de 71.000 conversaciones en dos años y continúa en producción. La estabilidad observada se debe principalmente a que los flujos más usados (recordatorio "Aceptar"/"Cancelar") no pasan por el código más deficiente, y a que los crashes son silenciosos: el sistema no alerta, no registra métricas de error, y el paciente simplemente deja de recibir respuesta.
