# Handoff runtime del bot WhatsApp (Luisa) — para Simón Estado tras la sesión de depuración del 09-jun. El **flujo conversacional funciona end-to-end**; quedan **dos problemas de runtime del bot** que se diagnostican/cierran desde tu lado (tienes acceso a los logs en Dokploy). La parte de la app (EPs) está verificada y no es el problema. --- ## 1. Lo que está PROBADO funcionando - **EPs de la app** (en `mvp/b2c`): `conversacion`, `perfil`, `calificacion`, `intento`, `ingesta`, + `GET /api/leads/:id` y `GET /api/leads/:id/conversacion` y `GET /api/leads/by-phone`. Todos OK. Verificado: `POST /perfil` con el payload exacto del bot (`{espacio,rangoM2,estilo,botStep}`) → `200 {ok:true, actualizado:[...]}` y el lead se actualiza. **El EP no es el cuello de botella.** - **Bot:** apertura proactiva, **resolución del `@lid`**, matching del lead por teléfono, y la **conversación de cualificación de Luisa** (7 turnos reales: cocina → tamaño → estilo → urgencia). - **Worker de render** (`mvp/image-worker`): genera renders con `google/gemini-2.5-flash-image`. ## 2. Cambios que hice hoy en el bot (`mvp/Whatsapp-bot`) — para que no te pillen por sorpresa - **Apertura proactiva** al recibir `/whatsapp-start`: `WhatsappService` escucha un `startEmitter` y envía el primer mensaje (antes solo registraba la sesión y esperaba al cliente). Persiste `estadoWa/botStep` + intento. - **Resolución `@lid`** ([`resolverTelefono`](../mvp/Whatsapp-bot/src/whatsapp/whatsapp.service.ts)): WhatsApp entrega los mensajes desde una dirección `@lid` (p.ej. `239225534443615@lid`), no desde el número. Se resuelve a número vía `msg.key.remoteJidAlt` o el mapa LID→PN de Baileys. **Esto era la causa de que el bot ignorara los mensajes entrantes.** - **Recuperación del lead por teléfono:** si la sesión no está en memoria (reinicio), `getOrCreateContext` busca el lead en la BD vía `GET /api/leads/by-phone` y re-registra la sesión. - **`markOnlineOnConnect: true`**: con `false`, tras reconectar el dispositivo quedaba "no disponible" y WhatsApp **no entregaba** los mensajes. Con `true` empezó a recibir (la conversación de 7 turnos lo demuestra). - **Modelos de Claude corregidos** en el env: eran `claude-haiku-4-5` (guion) → inválidos en OpenRouter; ahora `anthropic/claude-haiku-4.5` y `anthropic/claude-sonnet-4.5` (punto). - **`BAILEYS_AUTH_DIR`** configurable (subcarpeta del volumen) para empezar una **sesión limpia** sin perder persistencia. Hoy apunta a `/app/auth_info_baileys/v2`. - **Endpoints de operación** (servidor de webhooks, puerto 3001): - `GET /qr` — QR de vinculación como **imagen** (HTTP Basic; usuario cualquiera, contraseña = `QR_TOKEN`, que está en el env del bot en Dokploy). - `GET /debug` — estado de conexión + anillo de los últimos eventos entrantes (mismo auth). Útil para ver `remoteJid`/`remoteJidAlt`, si llega algo, y el resultado del matching. - Subido el límite de body del worker a 30 MB (las fotos en data URI rompían el 100kb por defecto). ## 3. Problema A — conexión Baileys inestable (bucle de reconexión) **Evidencia en logs del contenedor (`docker logs`):** el socket se cae cada pocos minutos con `stream:error → conflict {type:"replaced"}` y `stream:error code 503`, y reconecta en bucle (`AwaitingInitialSync → Transitioning to Online → opened connection to WA → ✅ conectado`, repetido). El `conflict: replaced` indica que **otra sesión reclama la misma cuenta** — típico del **solapamiento en el deploy** (Swarm arranca el contenedor nuevo antes de matar el viejo y ambos usan la misma sesión del volumen). `markOnlineOnConnect: true` mejoró la recepción pero no arregla una conexión que se cae sola. **Mitigación parcial (Baileys):** configurar el deploy del bot como **stop-first / sin solapamiento** (1 réplica, recreate) para evitar el `conflict`. Pero los `503` son de WhatsApp y seguirán. **Vía robusta (acordada): Evolution API.** **Cómo reproducir/diagnosticar:** - `GET https://reformix-bot.dv3.com.es/debug` (Basic, contraseña `QR_TOKEN`): si `connection:open` pero `inbound` no crece cuando el cliente escribe → estás en el estado "zombi". - Para volver a recibir: sesión nueva → sube `BAILEYS_AUTH_DIR` a `/app/auth_info_baileys/v3`, redeploy, escanea el QR fresco en `/qr`. **Y evita redeployar después** (cada reconexión arriesga la recepción). **Camino robusto (acordado con Carlos): migrar el transporte de WhatsApp del bot a Evolution API** (ya está como primaria en el stack — NO la WhatsApp Cloud API oficial). Evolution gestiona la conexión y entrega los mensajes por **webhook**, sin un socket Baileys en-proceso que se vuelva zombi. El bot pasaría a: (1) recibir mensajes por webhook de Evolution, (2) enviar por su REST. La lógica de Claude + los EPs de la app se quedan igual; solo cambia la capa de transporte WhatsApp. ## 4. Problema B — el perfil se persiste solo a medias + máquina de estados errática **Verificado en vivo:** `persistirTurno` SÍ funciona cuando se llama — `Lead ... persistido via API: {"rangoM2":"10a20","botStep":"estilo"} → ok`. Pero en una conversación completa **solo apareció UN `persistido`** (rangoM2); `espacio`, `urgencia` y `presupuesto` no se guardaron. Y la conversación se descuadra (rechaza a 4500€ → con 8500€ vuelve a preguntar el tamaño → "preparo presupuesto"). Causa probable: `claudeService.llamarClaude` solo devuelve `entidad`/`nuevoEstado` en algunos turnos, y la lógica de estado/viabilidad no es determinista. **A revisar:** que cada turno con dato extraído llame a `persistirTurno`, que los valores encajen con los enums de la app (`urgencia` alta|media|baja, etc., o `POST /perfil` da 422 y no guarda nada), y endurecer la máquina de estados (no re-preguntar lo ya respondido; viabilidad estable). ## 4bis. Problema C (el que rompe el end-to-end) — el bot NUNCA dispara la generación **Verificado en logs:** Luisa termina diciendo *"en un momento recibes tu presupuesto"* pero **no hay ninguna llamada a `ingesta` / `perfilCompleto`** en toda la sesión (grep vacío). Es decir, al cerrar la cualificación el bot **no dispara nada**: ni render, ni PDF, ni entrega. Es una promesa vacía. **Qué falta (lado bot):** cuando la cualificación se completa (estado `presupuesto`/`fin_viable`), el bot debe (1) **pedir las fotos** del espacio por WhatsApp, (2) subirlas vía `POST /api/leads/:id/ingesta` (items `foto`, `momento:"antes"`), y (3) marcar `perfilCompleto:true` (y/o `finalizar`). Eso dispara en la app: `PERFIL_WEBHOOK` → worker genera renders → `ingesta finalizar` → PDF + email + entrega WhatsApp. **La app y el worker ya están listos para esto; solo falta que el bot llame al EP.** Contrato: [`mvp/b2c/api-docs/README.md`](../mvp/b2c/api-docs/README.md). ## 5. Infra (referencia rápida) | Servicio | App Dokploy | Dominio | | --- | --- | --- | | Bot | `reformix-bot` (`wY4F14fyEslU-4za_JIbi`) | `reformix-bot.dv3.com.es` (puerto 3001) | | Worker | `reformix-worker` (`sMQd9zwoyV14q1vm8Vs8U`) | `reformix-worker.dv3.com.es` | | App | `reformix-b2c` (`lzHDAuPuubbJu94OrkNS_`) | `reformix.dv3.com.es` | - Build Dockerfile desde Gitea, autodeploy en push a `main`. Volumen del bot: `/app/auth_info_baileys` (sesión WhatsApp). `OPENROUTER_API_KEY` y `FUNNEL_API_KEY` en el env de cada app. - Contrato de los EPs y enums: [`mvp/b2c/api-docs/README.md`](../mvp/b2c/api-docs/README.md). --- **Resumen:** la conversación de Luisa funciona (recibe, resuelve `@lid`, cualifica, responde). Quedan 3 cosas del bot: **A)** conexión inestable (`conflict`+`503`, bucle de reconexión) → Evolution API; **B)** persistencia parcial del perfil + estado errático; **C)** el bot **nunca dispara la generación** (no llama a `ingesta`/`perfilCompleto`), así que el presupuesto/render/entrega no llega — esto es lo que rompe el end-to-end y lo que más conviene cerrar. App y worker ya están listos esperando esa llamada.