28 KiB
Arquitectura de Integración — Reformix
Índice
- Visión general del sistema
- Landing pública y captura del lead (sitio web)
- El funnel B2C — pipeline de 7 pasos
- Elección de canal: formulario, llamada o WhatsApp
- Sistema de webhooks salientes (app → bot/worker)
- Sistema de endpoints de bot (app ← bot/worker)
- El agente de WhatsApp (Luisa)
- Workers: render de imágenes y presupuesto
- El presupuesto como entregable final
- Diagrama de flujo completo
- Estado actual vs lo que falta
- Guía de conexión: cómo integrar Luisa + Workers con la app
1. Visión general del sistema
Reformix es un SaaS multi-tenant para empresas de reformas. Tiene dos caras:
- B2B (reformista): el reformista se registra, configura su perfil, catálogo de materiales, precios, y pone un widget en su web.
- B2C (cliente final): el cliente llega a la landing del reformista, pide presupuesto, sube fotos, y recibe un presupuesto con render "antes/después" en < 7 minutos.
Componentes del sistema
┌─────────────────────────────────────────────────────────────────┐
│ SITIO WEB (Next.js) │
│ Landing → Formulario → Elección canal → Pipeline → Entrega │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ App Reformix (mvp/b2c/) │ │
│ │ - Landing pública /[slug] │ │
│ │ - Funnel B2C /solicitud/[id]/* │ │
│ │ - Panel reformista /panel/* │ │
│ │ - API endpoints para bot (mvp/b2c/src/app/api/leads/) │ │
│ │ - Motor de presupuesto (mvp/b2c/src/budget/) │ │
│ │ - Generación de PDF (mvp/b2c/src/lib/pdf/) │ │
│ │ - Envío de email (mvp/b2c/src/lib/email/) │ │
│ │ - Webhooks salientes (mvp/b2c/src/lib/webhooks.ts) │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ AGENTE WHATSAPP (Luisa) — EXTERNO │
│ (mvp/Whatsapp-bot/) │
│ - Conexión WhatsApp via Baileys │
│ - Pipeline Claude 4-capas para cualificar leads │
│ - DEBE usar API HTTP de la app (hoy escribe directo a BD) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ WORKERS (Render + Análisis) — NO IMPLEMENTADO AÚN │
│ - Generación de renders "después" (Nano Banana 2 / Image 2) │
│ - Análisis de fotos con IA │
│ - DEBE recibir webhook PERFIL_WEBHOOK_URL y devolver vía API │
└─────────────────────────────────────────────────────────────────┘
2. Landing pública y captura del lead (sitio web)
Flujo de captura
Usuario llega a /[slug] (landing del reformista)
│
▼
Rellena formulario Hero:
- nombre + email + teléfono
- consentimiento privacidad + contratación (RGPD obligatorio)
│
▼
crearLead(slug, data) → Server Action
│
├── Valida con Zod schema
├── Busca tenant por slug
├── INSERT en leads (pipelineStage: 'form_completado', estado: 'nuevo')
└── INSERT en leadPipelineEventos (stage: 'form_completado')
Lo que crea la base de datos
El lead se crea con:
id(UUID) — este es el leadId que usará todo el sistematenantId(UUID) — referencia al reformistanombre,email,telefono— datos del clientepipelineStage: 'form_completado'— dónde está en el pipelineestado: 'nuevo'— estado comercial
Importante: El lead NO tiene aún tipoReforma, m2Suelo, calidadGlobal, etc. Esos se rellenan después, cuando el cliente pasa por el canal elegido.
3. El funnel B2C — pipeline de 7 pasos
Definido por el enum pipelineStage en src/db/schema.ts:
| # | Stage | Qué ocurre | Quién lo dispara |
|---|---|---|---|
| 1 | form_completado |
Lead creado con datos básicos | Server Action crearLead() |
| 2 | fotos_subidas |
Cliente describe reforma + sube fotos por zona | Server Action guardarDetallesYFotos() o API ingesta |
| 3 | prellamada_enviada |
Notificación SMS/WhatsApp previa a llamada | Orchestrator procesarLead() o bot |
| 4 | llamada_completada |
Agente IA (Retell) cualifica al lead | Orchestrator (simulado) o webhook Retell (real) |
| 5 | render_generado |
Render "después" generado por IA | Orchestrator (simulado con imagen demo) |
| 6 | presupuesto_generado |
Presupuesto calculado con motor real | Orchestrator procesarLead() |
| 7 | whatsapp_entregado |
PDF entregado al cliente | finalizarYEntregar() |
Pipeline automático (procesarLead)
Cuando el cliente completa el formulario detallado (con zonas, fotos, etc.), la app ejecuta:
guardarDetallesYFotos(leadId, formData)
├── Guarda fotos (momento: 'antes') en leadFotos
├── Guarda notas en leadNotas
├── Calcula tipoReforma, m2Suelo, calidadGlobal desde las zonas
├── UPDATE lead (pipelineStage: 'fotos_subidas')
│
└── procesarLead(leadId) ← ORQUESTRADOR
├── Paso 4: Evento prellamada_enviada
├── Paso 5: Llamada Retell (real si configurado, sino simulada con transcript ficticio)
├── Paso 6a: Render demo (imagen estática, NO IA real)
├── Paso 6b: Presupuesto REAL con motor (computeBudget)
├── UPDATE lead (pipelineStage: 'presupuesto_generado')
└── Paso 7: Si envio=automatico → lead pasa a 'whatsapp_entregado'
4. Elección de canal: formulario, llamada o WhatsApp
Después de crear el lead, el cliente llega a /solicitud/[id]/ donde elige cómo continuar:
Canal formulario (/solicitud/[id]/formulario)
El cliente rellena un formulario multi-zona: tipo de reforma, m², calidad, notas, sube fotos. Esto dispara guardarDetallesYFotos() que ejecuta el pipeline completo (incluyendo llamada simulada, render demo, presupuesto real).
Canal llamada (/solicitud/[id]/llamada)
El cliente pide que le llamen (ahora o programado). Se le envía un email con enlace para subir fotos después. Dispara pedirLlamada() que inicia llamada Retell saliente (si configurado). El cliente recibe la llamada del agente IA, y después puede subir fotos via el enlace del email.
Canal WhatsApp (/solicitud/[id]/whatsapp)
El cliente elige continuar por WhatsApp. La app dispara iniciarWhatsapp() que:
iniciarWhatsapp(leadId)
└── POST a WHATSAPP_START_WEBHOOK_URL
Payload: { leadId, telefono, nombre, empresa }
→ El bot de WhatsApp (Luisa) recibe esto y empieza la conversación
Este es el punto de entrada del bot de WhatsApp. El bot recibe el leadId y a partir de ahí debe escribir los datos extraídos de la conversación usando los endpoints de la app.
5. Sistema de webhooks salientes (app → bot/worker)
La app envía 3 señales HTTP a sistemas externos. Todas son best-effort (nunca lanzan error, devuelven boolean).
5.1 WHATSAPP_START_WEBHOOK_URL — Arranque de conversación WhatsApp
Disparado por: iniciarWhatsapp() (cuando el lead elige canal WhatsApp)
POST {url}
{
"leadId": "uuid",
"telefono": "+34...",
"nombre": "...",
"empresa": "Reformas Ejemplo"
}
La app espera que: el bot de WhatsApp reciba esto y comience la conversación con el lead. El bot debe usar el leadId para escribir los datos vía los endpoints de la app.
5.2 PERFIL_WEBHOOK_URL — Perfil completo para generar renders
Disparado por:
señalarPerfilCompleto()enguardarDetallesYFotos()(formulario)señalarPerfilCompleto()ensubirFotos()(fotos por email)- API
ingestacon flagperfilCompleto: true(bot/worker)
POST {url}
{
"leadId": "uuid",
"cliente": { "nombre": "...", "telefono": "...", "email": "...", "provincia": "..." },
"reforma": { "tipo": "cocina", "m2Suelo": 12, "calidad": "media",
"estructural": false, "urgencia": "media", "presupuestoTarget": 800000 },
"empresa": { "tenantId": "uuid", "nombre": "Reformas Ejemplo" },
"zonas": [
{ "zona": "cocina",
"notas": ["encimera de cuarzo"],
"fotos": { "antes": ["data:image/..."], "despues": [] } }
]
}
La app espera que: el worker externo genere renders "después" a partir de las fotos "antes", y los devuelva haciendo POST al endpoint /api/leads/:id/ingesta con momento: "despues" y opcionalmente finalizar: true.
5.3 WHATSAPP_WEBHOOK_URL — Entrega del PDF
Disparado por: finalizarYEntregar() (cuando el PDF está listo)
POST {url}
{
"leadId": "uuid",
"telefono": "+34...",
"nombre": "...",
"empresa": "Reformas Ejemplo",
"pdfBase64": "JVBERi0xLj...",
"filename": "presupuesto-nombre.pdf"
}
La app espera que: el bot de WhatsApp reciba esto y envíe el PDF al cliente por WhatsApp.
6. Sistema de endpoints de bot (app ← bot/worker)
La app expone 5 endpoints bajo /api/leads/:id/. Todos requieren Authorization: Bearer <FUNNEL_API_KEY>.
| Endpoint | Qué hace | Tabla que escribe |
|---|---|---|
POST /api/leads/:id/conversacion |
Guarda un turno del chat | conversacion_whatsapp |
POST /api/leads/:id/perfil |
Actualiza datos extraídos del lead | leads (campos: espacio, rangoM2, estilo, tipoReforma, m2Suelo, etc.) |
POST /api/leads/:id/calificacion |
Upsert de calificación | lead_calificacion |
POST /api/leads/:id/intento |
Registra intento de contacto | intentos_contacto |
POST /api/leads/:id/ingesta |
Sube fotos/notas + flags de perfilCompleto/finalizar | lead_fotos, lead_notas |
Flujo de uso típico del bot
1. Bot recibe WHATSAPP_START_WEBHOOK → leadId, telefono, nombre
2. Bot inicia conversación por WhatsApp
3. Por cada interacción:
a. Bot llama POST /conversacion (guarda el turno)
b. Bot llama POST /perfil (actualiza datos extraídos: espacio, m2, estilo, etc.)
c. Cuando tiene datos suficientes, llama POST /calificacion
d. Cuando pide fotos, el cliente las envía, bot las guarda vía POST /ingesta
4. Cuando el perfil está completo:
- Bot marca perfilCompleto: true en POST /ingesta
- Esto dispara PERFIL_WEBHOOK_URL → worker genera renders
- Worker devuelve renders vía POST /ingesta con momento: "despues" + finalizar: true
- finalizar:true dispara WHATSAPP_WEBHOOK_URL → bot recibe PDF y lo envía al cliente
7. El agente de WhatsApp (Luisa)
Estado actual (código en mvp/Whatsapp-bot/)
El bot de Luisa es un servicio NestJS independiente que:
- Se conecta a WhatsApp usando la librería Baileys (WebSocket no oficial)
- Orquesta un pipeline Claude de 4 capas:
- Capa 1 — Clasificador (Haiku): extrae intención y valor del mensaje
- Capa 2 — Validador: valida contra valores permitidos
- Capa 3 — Generador (Sonnet): produce el borrador de respuesta
- Capa 4 — Reglas (Haiku): corrige tono e identidad
- Mantiene una máquina de estados de 7 pasos para cualificar al lead:
nuevo → apertura → espacio → tamano → estilo → urgencia → presupuesto → fin - Tiene un scheduler que cada 5 minutos busca leads "nuevos" en BD y les envía el mensaje de apertura
- Soporta multimedia: transcripción de audio (Gemini), análisis de imágenes (Claude Vision)
Problema actual
El bot escribe directamente a Postgres usando TypeORM con sus propias entidades (Lead y Conversacion), en lugar de usar los endpoints HTTP de la app. Esto causa:
- Incompatibilidad de IDs: el bot usa
idnumérico autoincremental, la app usa UUID - Tablas duplicadas: el bot tiene su propia tabla
conversacion, la app tieneconversacion_whatsapp - Enums desalineados: el bot usa
urgente/medio_plazo/frio, la app usaalta/media/baja synchronize: trueen TypeORM puede alterar el schema de la BD real- El scheduler crea leads desde cero, pero según la arquitectura los leads ya existen (creados desde el form web)
Lo que DEBE hacer el bot
- No crear leads. Recibirlos vía
WHATSAPP_START_WEBHOOK_URLcon elleadIdUUID. - No escribir a BD directamente. Usar los 5 endpoints HTTP (
conversacion,perfil,calificacion,intento,ingesta). - No tener scheduler propio. El arranque lo hace la app vía webhook.
- Usar los enums correctos según la app (
alta/media/baja,cocina/bano/salon/comedor/integral/otro, etc.). - No mantener estado propio. El estado de la conversación (
botStep) se persiste vía/perfil.
Dónde está el código
| Elemento | Ruta |
|---|---|
| Código del bot (NestJS) | mvp/Whatsapp-bot/src/ |
| Prompts de Luisa | mvp/Whatsapp-bot/prompts/ |
| Configuración (.env) | mvp/Whatsapp-bot/.env |
| Documentación del bot | mvp/Whatsapp-bot/README.md |
8. Workers: render de imágenes y presupuesto
Estado actual
Los workers no están implementados. Existe:
- La tabla
worker_jobsen la BD (mvp/b2c/src/db/schema.ts:515-537) con tipos:analisis_fotos,render,presupuesto_ia - El webhook
PERFIL_WEBHOOK_URLlisto para enviar el perfil completo al worker - El endpoint
ingestalisto para recibir los renders de vuelta - Renders simulados con imágenes estáticas en
procesarLead()(usa/despues.webp,/despues-bano.webp, etc.)
Lo que DEBE hacer el worker de renders
1. Recibe POST a PERFIL_WEBHOOK_URL con:
{ leadId, cliente, reforma, empresa, zonas: [{ zona, notas, fotos: { antes: [...], despues: [] } }] }
2. Para cada zona:
a. Toma las fotos "antes" del cliente
b. Genera render "después" usando modelo de IA (Nano Banana 2, Image 2, Stable Diffusion, etc.)
c. Convierte el render a data URI base64
3. Devuelve los renders haciendo POST a /api/leads/:id/ingesta:
{ items: [{ tipo: "foto", zona: "cocina", momento: "despues", imagen: "data:image/..." }],
finalizar: true }
(finalizar:true dispara la construcción del PDF + email + señal WhatsApp)
Stack planeado para renders
Según la documentación:
- Nano Banana 2 o Image 2 (Google Gemini) — modelos image-to-image
- Alternativa: Replicate SDXL + ControlNet (~0,02€/imagen)
- Alternativa: DALL-E 3 HD (~0,08€/imagen)
Dónde implementar los workers
Los workers son servicios externos independientes. Pueden ser:
- n8n workflows (orquestación visual)
- Un worker Node.js/Python que escucha webhooks y se comunica con APIs de IA
- Cloud Functions (Vercel, Cloudflare Workers, AWS Lambda)
No hay código de workers en este repositorio. El repositorio solo define el contrato (webhooks + API).
9. El presupuesto como entregable final
El entregable final es un PDF de presupuesto que incluye:
- Cabecera con datos del reformista (logo, nombre, CIF, dirección)
- Datos del cliente y tipo de reforma
- Tabla de presupuesto con partidas calculadas por el motor:
- Demolición, impermeabilización, alicatado, fontanería, electricidad, carpintería, mano de obra, extras, licencia
- Render "después" generado por IA
- Galería por zona con fotos "antes" y "después"
- Footer legal
El motor de presupuesto (mvp/b2c/src/budget/)
Es real, no simulado. Calcula presupuestos basado en:
- Catálogo de materiales del reformista (precios por calidad: básica/media/premium)
- Configuración de precios (factores por zona, mano de obra, extras fijos)
- Inputs del lead: tipo de reforma, m², calidad, urgencia, estructural, provincia
Flujo de entrega
finalizarYEntregar(leadId)
├── construirPresupuestoPdf(leadId)
│ ├── Carga lead completo + fotos + notas + catálogo + config
│ ├── Renderiza PDF con @react-pdf
│ └── Devuelve buffer PDF + filename
│
├── UPDATE lead (pdfUrl)
│
├── enviarPresupuestoEmail() → SMTP (opcional)
│
├── notificarFlujoWhatsapp() → WHATSAPP_WEBHOOK_URL
│ └── Bot recibe pdfBase64 y lo envía por WhatsApp
│
└── UPDATE lead (pipelineStage: 'whatsapp_entregado', estado: 'presupuesto_enviado')
10. Diagrama de flujo completo
LANDING PÚBLICA /[slug]
│
crearLead()
│
▼
┌───────────────────────┐
│ Lead CREADO │
│ pipelineStage: │
│ form_completado │
│ estado: nuevo │
└───────────┬───────────┘
│
▼
┌────────────────────────┐
│ ELECCIÓN DE CANAL │
│ /solicitud/[id]/ │
└──────┬────────┬───────┘
│ │
┌────────┘ └────────┐
▼ ▼
┌──────────────┐ ┌──────────────┐
│ FORMULARIO │ │ WHATSAPP │
│ + fotos │ │ │
└──────┬───────┘ │ iniciar │
│ │ Whatsapp() │
│ └──────┬───────┘
│ guardarDetalles │
│ YFotos() │ POST WHATSAPP
│ │ START WEBHOOK
├── Guarda fotos │
├── Guarda notas ▼
│ ┌──────────────┐
└── procesarLead() │ BOT WHATSAPP │
│ │ (Luisa) │
│ │ │
├── Simula │ Inicia │
│ llamada │ conversación │
│ │ │
├── Render │ Por cada msg: │
│ demo │ POST /perfil │
│ (img fija)│ POST /conv. │
│ │ │
├── Presup. │ Cuando listo: │
│ REAL │ POST /ingesta │
│ │ perfilCompleto│
│ │ │
└── Estado └──────┬────────┘
intermedio │
│ │
▼ │
┌────────────────────┘
│ PERFIL_WEBHOOK_URL
▼
┌──────────────────┐
│ WORKER RENDER │
│ Genera imágenes │
│ "después" │
└────────┬─────────┘
│ POST /api/leads/:id/ingesta
│ { items: [{tipo:"foto", momento:"despues",...}],
│ finalizar: true }
▼
┌──────────────────────────────┐
│ finalizarYEntregar() │
│ - Construye PDF │
│ - Envía email │
│ - WHATSAPP_WEBHOOK_URL │
└────────┬─────────────────────┘
│
▼
┌──────────────────┐
│ BOT WHATSAPP │
│ Recibe pdfBase64│
│ y lo envía al │
│ cliente │
└──────────────────┘
11. Estado actual vs lo que falta
| Componente | Estado | Notas |
|---|---|---|
| Landing pública / formulario | ✅ Implementado | Multi-zona, fotos, notas |
| Motor de presupuesto | ✅ Implementado | Real, con catálogo y config |
| ✅ Implementado | Con @react-pdf | |
| ✅ Implementado | SMTP, best-effort | |
| Endpoints API del bot | ✅ Implementado y en producción | 5 endpoints en dv3.com.es |
| Webhooks salientes | ✅ Implementado | 3 webhooks listos |
| Autenticación bot | ✅ Implementado | Bearer token con FUNNEL_API_KEY |
| Bot WhatsApp (Luisa) | ❌ Por reconectar | Hoy escribe directo a BD, debe usar APIs HTTP |
| Workers render | ❌ No implementado | Existe solo tabla y webhook, sin worker real |
| Renders IA | ❌ Simulado | Usa imágenes estáticas /despues.webp |
| Llamada Retell real | ⚠️ Parcial | Código listo, depende de config de vars de entorno |
| n8n workflows | ❌ No existe | Mencionado en docs pero sin implementar |
12. Guía de conexión: cómo integrar Luisa + Workers con la app
12.1 Lo que necesita el bot de WhatsApp (Luisa)
El bot DEBE:
- Recibir leads por webhook (
WHATSAPP_START_WEBHOOK_URL), no buscarlos en BD. - Usar los endpoints HTTP de la app en lugar de TypeORM:
POST /api/leads/:id/conversacion— para cada turno del chatPOST /api/leads/:id/perfil— para actualizar datos extraídosPOST /api/leads/:id/calificacion— para calificar al leadPOST /api/leads/:id/intento— para registrar intentosPOST /api/leads/:id/ingesta— para subir fotos y marcar perfil completo
- Usar los enums correctos de la app:
urgencia:alta,media,bajatipoReforma:cocina,bano,salon,comedor,integral,otrocalidadGlobal:basica,media,premiumestadoWa:sin_enviar,enviado,entregado,leido,fallidocanalOrigen:formulario_web,whatsapp,llamada,referido,anuncio
- Trabajar con UUIDs (el
leadIdque recibe del webhook). - No tener scheduler interno — la app controla cuándo arrancar.
12.2 Lo que necesita el worker de renders
El worker DEBE:
- Escuchar en la URL que configures como
PERFIL_WEBHOOK_URL(POST /perfil-completo). - Recibir el payload con
leadId,cliente,reforma,zonas(con fotos "antes"). - Generar renders para cada zona: Etapa 1 (Claude Haiku → prompt), Etapa 2 (Gemini Flash → imagen), Etapa 3 (Claude Haiku Vision → validación). Todo via OpenRouter.
- Devolver los renders llamando a
POST /api/leads/:id/ingestacon: - Autenticarse con
Authorization: Bearer <FUNNEL_API_KEY>.{ "items": [ { "tipo": "foto", "zona": "cocina", "momento": "despues", "imagen": "data:image/..." } ], "finalizar": true } - Autenticarse con
Authorization: Bearer <FUNNEL_API_KEY>.
12.3 Variables de entorno necesarias en la app
# Para que el bot pueda escribir en la BD:
FUNNEL_API_KEY=<clave-compartida>
# URLs donde escuchan el bot y el worker:
WHATSAPP_START_WEBHOOK_URL=https://url-del-bot/whatsapp-start
PERFIL_WEBHOOK_URL=https://url-del-worker/perfil-completo
WHATSAPP_WEBHOOK_URL=https://url-del-bot/whatsapp-pdf
12.4 Secuencia de integración recomendada
Paso 1 — Reconectar Luisa a los endpoints HTTP (prioridad alta)
- Eliminar TypeORM y las entidades propias del bot
- Implementar llamadas HTTP a los 5 endpoints de la app
- Ajustar enums y tipos para que coincidan con la app
- Eliminar el scheduler interno
- Configurar las 3 URLs de webhook en la app
Paso 2 — Implementar worker de renders (prioridad media)
- Crear servicio que escuche
PERFIL_WEBHOOK_URL - Elegir modelo de IA para image-to-image
- Integrar con el endpoint
ingestapara devolver resultados
Paso 3 — Conectar llamada Retell real (prioridad baja)
- Configurar variables de entorno de Retell
- El bot de WhatsApp o el formulario pueden complementar la llamada