Files
reformix-hackaton/docs/arquitectura-integracion.md

574 lines
28 KiB
Markdown

# 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 },
"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)
```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 <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:
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 <FUNNEL_API_KEY>`.
```json
{
"items": [
{
"tipo": "foto",
"zona": "cocina",
"momento": "despues",
"imagen": "data:image/..."
}
],
"finalizar": true
}
```
5. **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 `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