Persiste bot_step + handoff de WhatsApp para Simón
- Migración 0011: leads.bot_step (TEXT) = paso actual de la conversación del bot (Luisa), para verlo en el panel y poder retomar chats cortados. TEXT (no enum) para que el bot evolucione su vocabulario sin migración. - docs/handoff-whatsapp-simon.md: spec de integración del bot (DB única, lead desde el form, reparto DB-directa vs EP, tablas que escribe, alineación de enums/tipos a los nuestros, bot_step, webhooks y conectividad). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
93
docs/handoff-whatsapp-simon.md
Normal file
93
docs/handoff-whatsapp-simon.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Handoff WhatsApp (Luisa) — para Simón
|
||||||
|
|
||||||
|
Cómo integra el bot de WhatsApp con la app Reformix. **Una sola base de datos** (la de la app;
|
||||||
|
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ú.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Cómo arranca tu flujo
|
||||||
|
|
||||||
|
Cuando el cliente elige "WhatsApp" en el funnel, la app hace `POST` al webhook
|
||||||
|
**`WHATSAPP_START_WEBHOOK_URL`** (lo configuras tú y nos lo pasas) con:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "leadId": "uuid", "telefono": "+34...", "nombre": "...", "empresa": "Reformas Ejemplo" }
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
| Acción | Cómo |
|
||||||
|
| --- | --- |
|
||||||
|
| 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`) |
|
||||||
|
|
||||||
|
**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>`.
|
||||||
|
|
||||||
|
## 3. Tablas que escribes directamente (ya creadas en la DB única)
|
||||||
|
|
||||||
|
- **`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`.
|
||||||
|
|
||||||
|
> Los `id` se autogeneran (`gen_random_uuid()`); **no** llames a `uuid_generate_v4()` en SQL (esta DB no tiene la extensión `uuid-ossp`).
|
||||||
|
|
||||||
|
## 4. Enums y tipos: usa NUESTROS valores (esto es lo que hay que alinear en el bot)
|
||||||
|
|
||||||
|
Tu esquema `reformix-full` tenía otros valores. En la DB única mandan estos:
|
||||||
|
|
||||||
|
**`tipo_reforma`** → `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`.
|
||||||
|
|
||||||
|
**`pipeline_stage`** → **no lo escribas**. Lo gestiona nuestro funnel/EP automáticamente.
|
||||||
|
|
||||||
|
**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`).
|
||||||
|
|
||||||
|
## 5. `bot_step` (estado de la conversación de Luisa) — NUEVO, persistido
|
||||||
|
|
||||||
|
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):
|
||||||
|
|
||||||
|
`apertura → espacio → tamano → estilo → urgencia → presupuesto → pide_fotos → fotos_recibidas → completado`
|
||||||
|
Terminales: `no_viable`, `abandonado`.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
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; nosotros producimos el entregable.
|
||||||
|
|
||||||
|
## 8. Conectividad (ops)
|
||||||
|
|
||||||
|
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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**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).
|
||||||
@@ -149,6 +149,7 @@ CREATE TABLE "leads" (
|
|||||||
"taste_text" text,
|
"taste_text" text,
|
||||||
"preferences_snapshot" jsonb,
|
"preferences_snapshot" jsonb,
|
||||||
"estado_wa" "estado_wa",
|
"estado_wa" "estado_wa",
|
||||||
|
"bot_step" text,
|
||||||
"canal_origen" "canal_origen",
|
"canal_origen" "canal_origen",
|
||||||
"espacio" text,
|
"espacio" text,
|
||||||
"rango_m2" text,
|
"rango_m2" text,
|
||||||
|
|||||||
1
mvp/b2c/drizzle/0011_warm_post.sql
Normal file
1
mvp/b2c/drizzle/0011_warm_post.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "leads" ADD COLUMN "bot_step" text;
|
||||||
2452
mvp/b2c/drizzle/meta/0011_snapshot.json
Normal file
2452
mvp/b2c/drizzle/meta/0011_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -78,6 +78,13 @@
|
|||||||
"when": 1780584271011,
|
"when": 1780584271011,
|
||||||
"tag": "0010_square_vulture",
|
"tag": "0010_square_vulture",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780593183911,
|
||||||
|
"tag": "0011_warm_post",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -250,6 +250,10 @@ export const leads = pgTable(
|
|||||||
// --- Flujo WhatsApp/llamada (esquema reformix-full; aditivos, los rellena el bot/Luisa).
|
// --- Flujo WhatsApp/llamada (esquema reformix-full; aditivos, los rellena el bot/Luisa).
|
||||||
// estadoWa nullable: null = aún sin enviar (el "nuevo" del diagrama).
|
// estadoWa nullable: null = aún sin enviar (el "nuevo" del diagrama).
|
||||||
estadoWa: estadoWa('estado_wa'),
|
estadoWa: estadoWa('estado_wa'),
|
||||||
|
// Paso actual de la conversación del bot (Luisa). TEXT (no enum) para que el bot evolucione
|
||||||
|
// su vocabulario sin migración. Valores sugeridos: apertura · espacio · tamano · estilo ·
|
||||||
|
// urgencia · presupuesto · pide_fotos · fotos_recibidas · completado · no_viable · abandonado.
|
||||||
|
botStep: text('bot_step'),
|
||||||
canalOrigen: canalOrigen('canal_origen'),
|
canalOrigen: canalOrigen('canal_origen'),
|
||||||
espacio: text('espacio'), // extracción en crudo de Luisa (se normaliza a tipoReforma)
|
espacio: text('espacio'), // extracción en crudo de Luisa (se normaliza a tipoReforma)
|
||||||
rangoM2: text('rango_m2'), // crudo (se normaliza a m2Suelo)
|
rangoM2: text('rango_m2'), // crudo (se normaliza a m2Suelo)
|
||||||
|
|||||||
Reference in New Issue
Block a user