Reescribe el handoff de WhatsApp al modelo por EP + smoke test de los bot EPs
El bot de Luisa puebla la BD vía los 4 EPs HTTP (no SQL directo): conversacion, perfil, calificacion, intento. Actualiza el handoff de Simón en consecuencia (qué EP usa para cada cosa, enums/tipos a alinear, ya no necesita acceso a BD). Añade api-docs/smoke-bot-eps.mjs: crea un lead de prueba, ejerce los 4 EPs por HTTP, verifica en BD y limpia. Verificado end-to-end en produccion. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,10 @@ Cómo integra el bot de WhatsApp con la app Reformix. **Una sola base de datos**
|
||||
Postgres). El **lead se crea siempre desde el form web**, así que cuando el cliente elige WhatsApp
|
||||
el lead **ya existe** y te pasamos su `leadId`. No creas leads tú.
|
||||
|
||||
> **Modelo de integración (decidido):** el bot **no toca Postgres directamente**. Toda la escritura
|
||||
> va por **endpoints HTTP** autenticados (no necesitas credenciales de BD ni estar en la red de
|
||||
> Dokploy). Ya están **desplegados y probados** en `https://reformix.dv3.com.es`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Cómo arranca tu flujo
|
||||
@@ -17,77 +21,97 @@ Cuando el cliente elige "WhatsApp" en el funnel, la app hace `POST` al webhook
|
||||
|
||||
A partir de ahí Luisa escribe al `telefono` y trabaja **siempre con ese `leadId`**.
|
||||
|
||||
## 2. Reparto: qué escribes en la DB directamente vs qué mandas por el EP
|
||||
## 2. Cómo escribes en la BD: por API, no SQL
|
||||
|
||||
| Acción | Cómo |
|
||||
| Qué guardas | Endpoint |
|
||||
| --- | --- |
|
||||
| Historial del chat, calificación, intentos, jobs, estado del bot | **DB directa** (tus tablas, ver §3) |
|
||||
| Subir **fotos** del cliente + **notas/datos** que extraes | **EP** `POST /api/leads/:id/ingesta` |
|
||||
| Señalar "perfil completo" / devolver renders / cerrar y entregar | **EP** (flags `perfilCompleto` / `finalizar`) |
|
||||
| Cada turno del chat (+ estado del mensaje y paso del bot) | `POST /api/leads/:id/conversacion` |
|
||||
| Lo que vas extrayendo del lead (espacio, m², estilo, urgencia, presupuesto, viabilidad…) | `POST /api/leads/:id/perfil` |
|
||||
| Calificación del lead (score/nivel/criterios) | `POST /api/leads/:id/calificacion` |
|
||||
| Intento de contacto (resultado de cada intento) | `POST /api/leads/:id/intento` |
|
||||
| **Fotos** del cliente + **notas/datos** por zona | `POST /api/leads/:id/ingesta` |
|
||||
| Señalar "perfil completo" / devolver renders / cerrar y entregar | `POST /api/leads/:id/ingesta` (flags `perfilCompleto` / `finalizar`) |
|
||||
|
||||
**Por qué el EP para fotos/cierre:** ahí se dispara nuestra lógica (motor de presupuesto, PDF,
|
||||
email, señales). Si escribieras `lead_fotos` a mano, esa lógica no corre. Doc del EP:
|
||||
[`mvp/b2c/api-docs/README.md`](../mvp/b2c/api-docs/README.md). Auth: `Authorization: Bearer <FUNNEL_API_KEY>`.
|
||||
**Auth (todos):** header `Authorization: Bearer <FUNNEL_API_KEY>` (te paso la clave aparte; es la
|
||||
misma para todos los EPs). `Content-Type: application/json`. `:id` = el `leadId` del §1.
|
||||
|
||||
## 3. Tablas que escribes directamente (ya creadas en la DB única)
|
||||
**Por qué por EP y no SQL:** así se dispara nuestra lógica (motor de presupuesto, PDF, email,
|
||||
señales) y el esquema queda blindado (validación + tipos). Si escribieras las tablas a mano, esa
|
||||
lógica no corre y un valor inválido rompería la fila. **Doc completa con campos y ejemplos curl:**
|
||||
[`mvp/b2c/api-docs/README.md`](../mvp/b2c/api-docs/README.md).
|
||||
|
||||
- **`conversacion_whatsapp`** — un registro por turno: `lead_id`, `rol` (`user`/`assistant`/`system`), `mensaje`, `media_type`, `media_url`, `transcripcion_audio`.
|
||||
- **`intentos_contacto`** — `lead_id`, `canal` (`whatsapp`), `resultado`, `completado`, `numero_intento`, `duracion_seg`.
|
||||
- **`lead_calificacion`** — 1 por lead: `lead_id` (único), `score` (0-100), `nivel` (`A`/`B`/`C`/`D`), `criterios` (jsonb).
|
||||
- **`worker_jobs`** — cola async si la usas: `tipo` (`analisis_fotos`/`render`/`presupuesto_ia`), `estado_job`, `payload`, `webhook_url`.
|
||||
- **`leads`** (UPDATE sobre el lead existente), columnas que te tocan:
|
||||
- `bot_step` (TEXT) — paso actual de la conversación (ver §5).
|
||||
- `estado_wa` — entrega del **mensaje** (`sin_enviar`/`enviado`/`entregado`/`leido`/`fallido`). **No** es el paso de la conversación.
|
||||
- Extracción en crudo: `espacio`, `rango_m2`, `estilo`, `presupuesto_declarado` (TEXT), `viable` (boolean), `fotos_solicitadas_at` (timestamp), `canal_origen`.
|
||||
- Datos normalizados para el presupuesto: `tipo_reforma`, `m2_suelo`, `calidad_global`, `urgencia`, `presupuesto_target`, `taste_text`.
|
||||
> `visitas` y `worker_jobs` quedan **fuera** de tu integración por ahora (son cola interna / panel
|
||||
> del reformista). Si los necesitas, lo hablamos y abrimos EP.
|
||||
|
||||
> Los `id` se autogeneran (`gen_random_uuid()`); **no** llames a `uuid_generate_v4()` en SQL (esta DB no tiene la extensión `uuid-ossp`).
|
||||
## 3. Resumen de los 4 EPs del bot
|
||||
|
||||
Campos completos y curls en api-docs; aquí el mínimo de cada uno.
|
||||
|
||||
- **`conversacion`** — `{ rol: user|assistant|system, mensaje, [mediaType, mediaUrl, transcripcionAudio, estadoWa, botStep] }` → `{ ok, id }`.
|
||||
- **`perfil`** (update parcial, solo lo que mandes) — cualquier subconjunto de: `botStep, estadoWa, canalOrigen, viable, espacio, rangoM2, estilo, presupuestoDeclarado, fotosSolicitadasAt, tipoReforma, m2Suelo, calidadGlobal, urgencia, presupuestoTarget, tasteText, estructural` → `{ ok, actualizado:[...] }`.
|
||||
- **`calificacion`** (upsert, 1 por lead) — `{ [score 0-100, nivel A|B|C|D, criterios{}, notasAgente] }` → `{ ok }`.
|
||||
- **`intento`** — `{ canal: formulario|whatsapp|llamada, numeroIntento, [resultado, completado, duracionSeg, notas, metadata{}] }` → `{ ok, id }`.
|
||||
|
||||
Errores comunes a todos: `401` (sin Bearer o clave mala), `404` (lead no existe), `422` (JSON o
|
||||
validación). Body de error: `{ ok:false, error:"..." }`.
|
||||
|
||||
## 4. Enums y tipos: usa los valores de la API (esto es lo que hay que alinear en el bot)
|
||||
|
||||
Tu esquema `reformix-full` tenía otros valores. En la DB única mandan estos:
|
||||
Tu esquema `reformix-full` tenía otros valores. En la API mandan estos (el EP rechaza con `422` lo
|
||||
que no encaje):
|
||||
|
||||
**`tipo_reforma`** → `cocina · bano · salon · comedor · integral · otro`
|
||||
**`tipoReforma`** → `cocina · bano · salon · comedor · integral · otro`
|
||||
`oficina`/`local`/`otros` → usa `otro`.
|
||||
|
||||
**`urgencia`** → `alta · media · baja` (tu `inmediata` → `alta`).
|
||||
|
||||
**`lead_estado`** → `nuevo · contactado · visita_agendada · presupuesto_enviado · ganado · perdido`
|
||||
`en_proceso`/`calificado` → `contactado`; `cliente` → `ganado`; `archivado` → `perdido`.
|
||||
**`estadoWa`** (entrega del **mensaje**) → `sin_enviar · enviado · entregado · leido · fallido`.
|
||||
|
||||
**`pipeline_stage`** → **no lo escribas**. Lo gestiona nuestro funnel/EP automáticamente.
|
||||
**`canalOrigen`** → `formulario_web · whatsapp · llamada · referido · anuncio`.
|
||||
|
||||
**Tipos:** `estructural` = **boolean** (no texto). `calidad_global` = enum **`basica`/`media`/`premium`** (no 1-10; la extracción cruda de calidad va en `estilo`/`taste_text`).
|
||||
**`calificacion.nivel`** → `A · B · C · D`. **`intento.canal`** → `formulario · whatsapp · llamada`.
|
||||
**`intento.resultado`** → `exitoso · no_contesta · ocupado · rechaza · error_tecnico`.
|
||||
|
||||
## 5. `bot_step` (estado de la conversación de Luisa) — NUEVO, persistido
|
||||
**Tipos:** `estructural` = **boolean** (no texto). `calidadGlobal` = enum **`basica`/`media`/`premium`**
|
||||
(no 1-10; la extracción cruda de calidad va en `estilo`/`tasteText`). `m2Suelo` = número (>0).
|
||||
`presupuestoTarget` = entero en **céntimos**. `fotosSolicitadasAt` = string ISO datetime.
|
||||
|
||||
Columna `leads.bot_step` (TEXT, nullable). Guarda el paso actual para verlo en el panel y poder
|
||||
retomar si el chat se corta. Valores sugeridos (puedes ajustar el vocabulario, es TEXT):
|
||||
**`pipeline_stage` / `estado`** → **no los escribas**. Los gestiona nuestro funnel/EP.
|
||||
|
||||
## 5. `bot_step` (estado de la conversación de Luisa) — persistido
|
||||
|
||||
Texto libre (lo mandas en `conversacion.botStep` o `perfil.botStep`). Lo guardamos en
|
||||
`leads.bot_step` para verlo en el panel y poder retomar si el chat se corta. Valores sugeridos
|
||||
(puedes ajustar el vocabulario, es TEXT):
|
||||
|
||||
`apertura → espacio → tamano → estilo → urgencia → presupuesto → pide_fotos → fotos_recibidas → completado`
|
||||
Terminales: `no_viable`, `abandonado`.
|
||||
|
||||
> Ojo: `estadoWa` es la **entrega del mensaje** (enviado/leído…), **no** el paso de la conversación.
|
||||
> El paso es `botStep`.
|
||||
|
||||
## 6. Webhooks salientes de la app (los recibes/encadenas tú)
|
||||
|
||||
- `WHATSAPP_START_WEBHOOK_URL` — inicio (§1).
|
||||
- `PERFIL_WEBHOOK_URL` — cuando se marca `perfilCompleto`, te llega toda la data por zona para generar renders/agente (payload en api-docs §webhooks).
|
||||
- `WHATSAPP_WEBHOOK_URL` — entrega: cuando el PDF está listo, te llega `{ pdfBase64, telefono, ... }` para mandarlo por WhatsApp.
|
||||
- `PERFIL_WEBHOOK_URL` — cuando marcas `perfilCompleto` en ingesta, te llega toda la data por zona para generar renders/agente (payload en api-docs §webhooks).
|
||||
- `WHATSAPP_WEBHOOK_URL` — entrega: cuando el PDF está listo (`finalizar`), te llega `{ pdfBase64, telefono, ... }` para mandarlo por WhatsApp.
|
||||
|
||||
Pásanos las **3 URLs** y las ponemos en producción (Dokploy).
|
||||
|
||||
## 7. Lo que hace la app sola (no lo dupliques)
|
||||
|
||||
`pipeline_stage`, cálculo del **presupuesto** orientativo, generación del **PDF** y envío del
|
||||
**email** los hace la app cuando llamas al EP con `finalizar`. Tú aportas fotos/notas y el estado
|
||||
de la conversación; la api producimos el entregable.
|
||||
**email** los hace la app cuando llamas a ingesta con `finalizar`. Tú aportas fotos/notas, el
|
||||
historial del chat y el estado de la conversación; la app produce el entregable.
|
||||
|
||||
## 8. Conectividad (ops)
|
||||
## 8. Estado: probado y en producción
|
||||
|
||||
La DB es el Postgres de la app en Dokploy (host interno `reformix-db-…`). Si tu bot corre en la
|
||||
misma red de Dokploy puede conectarse directo; si es externo, hay que exponer credenciales/host.
|
||||
Decidir con Carlos. Para el EP solo necesitas la URL pública + `FUNNEL_API_KEY`.
|
||||
Los 4 EPs están desplegados en `https://reformix.dv3.com.es` y verificados end-to-end (lead real →
|
||||
`200` con id de fila insertada, perfil reflejado en el panel, upsert de calificación correcto).
|
||||
Smoke test reutilizable: [`mvp/b2c/api-docs/smoke-bot-eps.mjs`](../mvp/b2c/api-docs/smoke-bot-eps.mjs).
|
||||
|
||||
---
|
||||
|
||||
**Resumen de lo que necesito de ti (Simón):** (1) las 3 URLs de webhook, (2) confirmar que el bot
|
||||
usa nuestros enums/tipos (§4), (3) cómo te conectas a la DB (directo en red Dokploy o externo).
|
||||
**Resumen de lo que necesito de ti (Simón):** (1) las **3 URLs de webhook** (§6), (2) confirmar que
|
||||
el bot usa nuestros **enums/tipos** (§4). La conexión a la BD ya no hace falta: trabajas solo con la
|
||||
URL pública + `FUNNEL_API_KEY`.
|
||||
|
||||
Reference in New Issue
Block a user