# Arquitectura de Integración — Reformix ## Índice 1. [Visión general del sistema](#1-visión-general-del-sistema) 2. [Landing pública y captura del lead (sitio web)](#2-landing-pública-y-captura-del-lead-sitio-web) 3. [El funnel B2C — pipeline de 7 pasos](#3-el-funnel-b2c--pipeline-de-7-pasos) 4. [Elección de canal: formulario, llamada o WhatsApp](#4-elección-de-canal-formulario-llamada-o-whatsapp) 5. [Sistema de webhooks salientes (app → bot/worker)](#5-sistema-de-webhooks-salientes-app--botworker) 6. [Sistema de endpoints de bot (app ← bot/worker)](#6-sistema-de-endpoints-de-bot-app--botworker) 7. [El agente de WhatsApp (Luisa)](#7-el-agente-de-whatsapp-luisa) 8. [Workers: render de imágenes y presupuesto](#8-workers-render-de-imágenes-y-presupuesto) 9. [El presupuesto como entregable final](#9-el-presupuesto-como-entregable-final) 10. [Diagrama de flujo completo](#10-diagrama-de-flujo-completo) 11. [Estado actual vs lo que falta](#11-estado-actual-vs-lo-que-falta) 12. [Guía de conexión: cómo integrar Luisa + Workers con la app](#12-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 sistema** - `tenantId` (UUID) — referencia al reformista - `nombre`, `email`, `telefono` — datos del cliente - `pipelineStage: 'form_completado'` — dónde está en el pipeline - `estado: '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: ```typescript 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: ```typescript 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) ```json 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()` en `guardarDetallesYFotos()` (formulario) - `señalarPerfilCompleto()` en `subirFotos()` (fotos por email) - API `ingesta` con flag `perfilCompleto: true` (bot/worker) ```json POST {url} { "leadId": "uuid", "cliente": { "nombre": "...", "telefono": "...", "email": "...", "provincia": "..." }, "reforma": { "tipo": "cocina", "m2Suelo": 12, "calidad": "media", "estructural": false, "urgencia": "media", "presupuestoTarget": 800000 }, "preferencias": { "estilo": "nórdico", "gustos": "tonos azules, muebles de madera, encimera clara" }, "empresa": { "tenantId": "uuid", "nombre": "Reformas Ejemplo" }, "zonas": [ { "zona": "cocina", "notas": ["encimera de cuarzo"], "fotos": { "antes": ["data:image/..."], "despues": [] } } ] } ``` **`preferencias`** (opcional): gustos estéticos del cliente capturados en la conversación (`estilo` = campo `estilo` del lead; `gustos` = `tasteText`, resumen en texto libre de colores/materiales/acabados que pidió). Cada clave se omite si está vacía. El worker los inyecta como bloque dedicado en el prompt de imagen para que el render los represente; si no llegan, infiere un estilo neutro. **La app espera que:** el worker externo genere renders "después" a partir de las fotos "antes" respetando las `preferencias` del cliente, 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) ```json 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 `. | 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: 1. **Se conecta a WhatsApp** usando la librería Baileys (WebSocket no oficial) 2. **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 3. **Mantiene una máquina de estados** de 7 pasos para cualificar al lead: `nuevo → apertura → espacio → tamano → estilo → urgencia → presupuesto → fin` 4. **Tiene un scheduler** que cada 5 minutos busca leads "nuevos" en BD y les envía el mensaje de apertura 5. **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 `id` numérico autoincremental, la app usa UUID - Tablas duplicadas: el bot tiene su propia tabla `conversacion`, la app tiene `conversacion_whatsapp` - Enums desalineados: el bot usa `urgente/medio_plazo/frio`, la app usa `alta/media/baja` - `synchronize: true` en 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 1. **No crear leads.** Recibirlos vía `WHATSAPP_START_WEBHOOK_URL` con el `leadId` UUID. 2. **No escribir a BD directamente.** Usar los 5 endpoints HTTP (`conversacion`, `perfil`, `calificacion`, `intento`, `ingesta`). 3. **No tener scheduler propio.** El arranque lo hace la app vía webhook. 4. **Usar los enums correctos** según la app (`alta/media/baja`, `cocina/bano/salon/comedor/integral/otro`, etc.). 5. **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_jobs`** en la BD (`mvp/b2c/src/db/schema.ts:515-537`) con tipos: `analisis_fotos`, `render`, `presupuesto_ia` - El **webhook `PERFIL_WEBHOOK_URL`** listo para enviar el perfil completo al worker - El **endpoint `ingesta`** listo 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: 1. **Cabecera** con datos del reformista (logo, nombre, CIF, dirección) 2. **Datos del cliente** y tipo de reforma 3. **Tabla de presupuesto** con partidas calculadas por el motor: - Demolición, impermeabilización, alicatado, fontanería, electricidad, carpintería, mano de obra, extras, licencia 4. **Render "después"** generado por IA 5. **Galería por zona** con fotos "antes" y "después" 6. **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 | | PDF | ✅ Implementado | Con @react-pdf | | Email | ✅ 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: 1. **Recibir leads por webhook** (`WHATSAPP_START_WEBHOOK_URL`), no buscarlos en BD. 2. **Usar los endpoints HTTP de la app** en lugar de TypeORM: - `POST /api/leads/:id/conversacion` — para cada turno del chat - `POST /api/leads/:id/perfil` — para actualizar datos extraídos - `POST /api/leads/:id/calificacion` — para calificar al lead - `POST /api/leads/:id/intento` — para registrar intentos - `POST /api/leads/:id/ingesta` — para subir fotos y marcar perfil completo 3. **Usar los enums correctos** de la app: - `urgencia`: `alta`, `media`, `baja` - `tipoReforma`: `cocina`, `bano`, `salon`, `comedor`, `integral`, `otro` - `calidadGlobal`: `basica`, `media`, `premium` - `estadoWa`: `sin_enviar`, `enviado`, `entregado`, `leido`, `fallido` - `canalOrigen`: `formulario_web`, `whatsapp`, `llamada`, `referido`, `anuncio` 4. **Trabajar con UUIDs** (el `leadId` que recibe del webhook). 5. **No tener scheduler interno** — la app controla cuándo arrancar. ### 12.2 Lo que necesita el worker de renders El worker DEBE: 1. **Escuchar en la URL que configures como `PERFIL_WEBHOOK_URL`** (POST /perfil-completo). 2. **Recibir el payload** con `leadId`, `cliente`, `reforma`, `zonas` (con fotos "antes"). 3. **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. 4. **Devolver los renders** llamando a `POST /api/leads/:id/ingesta` con: 5. **Autenticarse** con `Authorization: Bearer `. ```json { "items": [ { "tipo": "foto", "zona": "cocina", "momento": "despues", "imagen": "data:image/..." } ], "finalizar": true } ``` 5. **Autenticarse** con `Authorization: Bearer `. ### 12.3 Variables de entorno necesarias en la app ``` # Para que el bot pueda escribir en la BD: FUNNEL_API_KEY= # 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 `ingesta` para 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