- Dockerfiles multistage (node:20-alpine) + .dockerignore para mvp/Whatsapp-bot
(expone 3000+3001, persiste prompts) y mvp/image-worker (expone 3001).
- Añade los 2 GET que el bot necesita y faltaban en la API:
GET /api/leads/:id (estado del lead) y GET /api/leads/:id/conversacion (historial).
- Limpia el package.json/lock de la raíz (baileys colado por error; no es workspace).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
El bot (Luisa) es externo y no toca Postgres directamente. Cuatro endpoints
autenticados con Bearer FUNNEL_API_KEY, validados con zod:
- POST /api/leads/:id/conversacion → turno de chat (+ estado_wa/bot_step)
- POST /api/leads/:id/perfil → update parcial del lead (extracción)
- POST /api/leads/:id/calificacion → upsert de lead_calificacion
- POST /api/leads/:id/intento → registro en intentos_contacto
Helpers compartidos lib/api/funnel-auth.ts (autorizado + jsonResponse) y
lib/api/bot-request.ts (validarBotRequest: auth + JSON + zod + lead existe).
La ruta de ingesta se refactoriza para reutilizar funnel-auth (DRY).
Schemas puros en lib/funnel/bot-schemas.ts con tests, y doc en api-docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Arregla 2 problemas del flujo de subir fotos desde el email:
- El enlace iba a /formulario (form completo) y al enviarlo re-ejecutaba
procesarLead, que VOLVÍA a llamar. Ahora el email apunta a /solicitud/[id]/fotos,
una página ligera (SubirFotos): solo sube fotos (+ nota opcional) al lead de la
URL, re-señala perfilCompleto y NO llama.
- Guarda en procesarLead: si el lead ya tiene llamada_completada, no se vuelve a
llamar (ni se pisa la transcripción real del webhook).
Copy de la página en COPY-GUIDE §3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
El gate del allowlist miraba allow.length>0 (tras parsear), así que un typo en
RETELL_ALLOWED_NUMBERS que no dejara ningún número válido abría las llamadas a
todos (fail-open). Ahora mira la presencia de la variable: si está puesta, se
enforce; un typo restringe a nadie (fail-closed), acorde a la intención.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guarda en iniciarLlamadaSaliente: si RETELL_ALLOWED_NUMBERS tiene valor (CSV de
E.164), solo se lanza la llamada a esos números; el resto se omite (devuelve
null, funnel en simulado). Vacío = se llama a todos. Protege tanto el form
(procesarLead) como el canal llamada (pedirLlamada). En prod = +34651194617.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Si la llamada no lleva metadata.lead_id (llamadas manuales/antiguas), el webhook
busca el lead más reciente con ese teléfono (últimos 9 dígitos) y le asocia la
transcripción + grabación. Responde JSON informativo (matched/leadId) para poder
re-procesar llamadas a mano.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- retell.ts: la llamada saliente manda metadata.lead_id; helpers obtenerLlamada
(GET /v2/get-call, dato autoritativo) y descargarGrabacion (guarda el audio
en nuestro sistema como data URI).
- /api/retell/webhook: en call_analyzed/call_ended relee la llamada por call_id,
guarda la transcripción real en lead.transcripcion, descarga la grabación a
lead.audio_url y deja el análisis + duración en un evento de pipeline. Seguro
por re-fetch (no se fía del body del webhook).
- orchestrator/pedirLlamada: pasan leadId; procesarLead ya no guarda transcript
simulado cuando la llamada es real (lo rellena el webhook).
- La ficha del panel ya mostraba transcripción + audio: ahora se pueblan solos.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Añadida la herramienta end_call al Retell LLM + instrucción en el prompt para
colgar cuando el cliente se despide o tras la despedida del agente (y si no
consiente la grabación). Guía sincronizada.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Prompt del agente de voz (Retell, en caliente + docs/retell-setup.md):
recuerda al cliente que envíe fotos del espacio por WhatsApp o por el enlace
del email, para que el render sea de su reforma.
- pedirLlamada: envía automáticamente el email con el enlace al formulario al
solicitar la llamada (antes solo con un botón manual), para que la frase del
agente "te enviamos un email" sea cierta. Best-effort.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pasos en el panel de Retell, prompt del agente listo para pegar (con las
variables dinámicas {{empresa_nombre}}/{{cliente_nombre}}/{{tipo_reforma}}/
{{provincia}}), variables que envía la app, compliance pendiente y cómo se
conecta (RETELL_API_KEY/FROM_NUMBER/AGENT_ID en Dokploy).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- docs/copy-b2b-estudio.md: estrategia (audiencia nivel 2, trigger map, tono),
7 opciones de H1 con ángulo/trigger/valoración, opciones de subhead y de CTA
(orientado a registro→demo, no trial), combo recomendado y checks de
autenticidad. Aprendizaje de compramostucoche aplicado.
- plan-accion: decisión "nada de 14 días → registro+demo" marcada como tomada;
enlazado el estudio.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Plan vivo con checkboxes desglosado por áreas: canal WhatsApp, medida del m²,
landing B2B (copys/vídeo/oferta/demo vs trial/competencia), onboarding del
reformista, dominio propio y producción de vídeo. Incluye decisiones de
producto pendientes. Incluye también el ajuste del handoff de Simón.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- 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>
Debajo del hero, nueva sección #demo: título + placeholder de vídeo 16:9
(sustituible por iframe/video cuando lo haya) + CTA "Empezar ahora". El botón
"Ver una demo en vivo" del hero apunta aquí (se movió el id #demo del mock del
hero a esta sección). Copy añadido a COPY-GUIDE §2.
Aplicado en public/b2b.html (servido en / y /b2b) y en mvp/b2b/index.html.
Nota: ambos ficheros ya tenían el hero divergido (public con copy más nuevo);
no toco ese copy aquí.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Referencia visual que separa las 4 dimensiones de estado (pipeline_stage,
lead_estado, estado_wa, estado_conversacion del bot) y dibuja el flujo de
formulario/WhatsApp/llamada sobre ellas, para decidir el modelado del estado
de conversación de Luisa.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Integra el esquema "reformix-full" del equipo de forma ADITIVA, sin tocar los
enums ni columnas existentes de la app (una sola DB, Drizzle es el dueño):
- Enums nuevos: estado_wa, canal_contacto, canal_origen, resultado_contacto,
rol_mensaje, job_tipo, job_estado, nivel_calificacion, visita_estado.
- Tablas nuevas: conversacion_whatsapp, intentos_contacto, lead_calificacion
(score 0-100 + nivel A/B/C/D), visitas, worker_jobs (cola async de los
workers de fotos/render/presupuesto). Referencian nuestros leads/users/tenants.
- Columnas nuevas en leads (nullable, las rellena el bot/Luisa): estado_wa,
canal_origen, espacio, rango_m2, estilo, presupuesto_declarado, viable,
fotos_solicitadas_at.
- Migración 0010 + db-schema/schema.sql regenerado.
El bot/n8n escribe estas tablas en la DB única y usa nuestros leads (creados
solo desde el form web). Pendiente: alinear valores de lead_estado/pipeline_stage.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Une la punta de Gitea (8ef99b5, que solo añadía la línea del .gitignore de la
colección Postman, ya presente aquí) con la rama integrada que incluye el
trabajo de Goyo (motor de presupuesto: impermeabilización, extras, zonas) y los
emails transaccionales + logo. Estrategia 'ours': se conserva el árbol actual
(no se pierde nada) y ambos remotos quedan en fast-forward.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- prepararLogo: el logo (data URI base64, URL http o ruta /public con APP_URL)
se adjunta como imagen inline con Content-ID y se referencia con cid:, que
es la forma fiable de mostrarlo en Gmail/Apple Mail/Outlook (el data URI
directo lo bloquea Gmail). Fallback al nombre de la empresa si no hay logo.
- El acento (barra, botón) ya usa el color de marca del tenant (resolveTheme).
Probado: envío real con el logo de Reformas Ejemplo embebido y tema pizarra.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reescribe los emails del funnel (entrega del presupuesto + enlace al
formulario) con plantilla HTML mobile-first de una columna, dark mode, botón
"bulletproof" en tabla, tipografías de sistema y versión en texto plano.
- mailer.ts: construirEmailHtml/construirEmailText compartidos; acento en el
color de marca del reformista (resolveTheme) + logo si es URL absoluta; copy
con jerarquía y CTA orientado a resultado.
- finalizar.ts: pasa la marca del tenant y un CTA de contacto (wa.me del
reformista o mailto) al email de entrega.
- actions.ts: pasa la marca al email con el enlace al formulario.
- COPY-GUIDE §6.b: asuntos (+alternativas), preheader, headline y cuerpo
mejorados.
Probado: render visual (light) de ambos emails y envío real por SMTP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Acerca el cálculo a tarifas de mercado sin rehacer el modelo lineal €/m²:
- Impermeabilización como partida propia en zonas húmedas (cocina/baño/integral)
- Extras fijos que no escalan con m²: boletín (siempre), tuberías (piso anterior
a 2000) y cambio de distribución (mover inodoro/ducha/bañera)
- Intensidad por tipo en fontanería/electricidad (baseline cocina) para que un
integral no escale como un baño
- Factor de zona por provincia en tramos (Madrid/BCN 1.40, islas 1.30, capitales
1.20, rural 0.85, resto 1.00)
- 2 preguntas nuevas en el formulario del cliente para disparar los extras
- Panel de precios: campo de impermeabilización + sección de extras fijos
- Seed recalibrado (mano de obra, extras, catálogo suelo/pared)
- Migración 0009 (leads.anterior_a_2000, leads.cambio_distribucion, pricing_config.extras)
- Tests del motor ampliados (impermeabilización, extras, intensidad por tipo)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- agruparPorZona (lib/funnel/fotos.ts): helper puro que agrupa fotos
(antes/después) y notas por zona, con fallback al tipo del lead. 5 tests.
- getLead trae también lead_notas.
- LeadFotosGaleria: galería por zona (Antes/Después + notas) que sustituye el
grid plano de la ficha del panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Llamada (/llamada): "Llamar ahora" dispara la llamada saliente de Retell;
"Programar" registra fecha/hora. Tras pedir, ofrece enviar por email el
enlace al formulario para subir las imágenes.
- WhatsApp (/whatsapp): arranca la conversación vía webhook al flujo externo
(con el teléfono del lead) y pide confirmación; al confirmar, agradece y
sigue por WhatsApp.
- actions: pedirLlamada, enviarEnlaceFormularioEmail, iniciarWhatsapp,
confirmarWhatsapp.
Verificado en navegador: las 3 pantallas y sus transiciones.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Paso intermedio /solicitud/[id]: el cliente elige llamada, WhatsApp o
formulario (crearLead ahora redirige aquí, no a /fotos).
- /formulario: FormularioZonas permite añadir varias zonas, cada una con tipo,
m², acabado, notas y fotos; /fotos queda como redirect.
- guardarDetallesYFotos: guarda fotos (antes, por zona) y notas (por zona),
agrega los campos del lead (m² suma, tipo único o 'integral', calidad más
alta, tasteText concatenado) para el presupuesto orientativo inmediato, y
señala perfilCompleto al flujo externo.
- Elimina FotosUploader (sustituido por FormularioZonas).
Verificado en navegador: 2 zonas → presupuesto al instante + notas por zona +
evento de perfil en DB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
COPY-GUIDE §3: paso 2 "Elige cómo seguir" con las 3 tarjetas, pantalla del
canal llamada (ahora/programar + nota de fotos por email/WhatsApp) y pantalla
del canal WhatsApp (contacto directo + confirmación). Texto literal para B1–B4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
api-docs/README.md: método/URL, auth Bearer, esquema del cuerpo (items
foto/texto, zona, momento, flags), respuesta y errores (401/404/422), ejemplos
curl de los 5 casos, y los payloads de los 3 webhooks salientes (perfil,
entrega WhatsApp, arranque WhatsApp) con cómo el flujo externo devuelve las
"después" por el mismo EP.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
POST /api/leads/:id/ingesta (Bearer FUNNEL_API_KEY): acepta items foto/texto
etiquetados por zona y momento, más flags perfilCompleto y finalizar.
- ingesta-schema.ts: zod del cuerpo (union discriminada foto|texto), exportado
para test; rechaza llamadas vacías.
- route.ts: auth 401, valida lead (404), inserta fotos (orden continúa el máx)
y notas, traza fotos_subidas; perfilCompleto→señalarPerfilCompleto,
finalizar→finalizarYEntregar.
- 10 tests del schema. Verificado por HTTP: 401/200/422/404 y finalizar genera
el pdf_url y avanza el lead a whatsapp_entregado.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- webhooks.ts: postWebhook best-effort + señalarGeneracionPerfil,
notificarFlujoWhatsapp (entrega) e iniciarConversacionWhatsapp (arranque).
- perfil.ts: señalarPerfilCompleto arma el JSON por zona (notas + fotos
antes/después) y lo manda al flujo externo; deja traza render_generado.
- finalizar.ts: finalizarYEntregar construye el PDF, persiste pdf_url, envía
el email (siempre) y la señal de WhatsApp, y avanza el lead a entregado.
- orchestrator: comentario en Paso 7 apuntando a la entrega real en finalizar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- mailer.ts: transport nodemailer perezoso desde env; enviarPresupuestoEmail
(adjunta el PDF) y enviarEnlaceFormulario. Best-effort: sin SMTP configurado
o ante error devuelven false sin lanzar.
- COPY-GUIDE §6.b: copy literal de ambos emails al cliente final.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- build-presupuesto.ts: construirPresupuestoPdf(leadId) agrupa fotos y notas
por zona (fallback al tipoReforma del lead), convierte las imágenes con
resolverImagenPdf y arma el PDF. Carga el lead por id sin scoping (uso
interno desde el route del panel y desde la finalización pública).
- tenant-queries: getTenantPerfilById(tenantId) sin auth; getTenantPerfil lo
reutiliza con el tenant de la sesión.
- PresupuestoDoc: prop zonas + sección "Imágenes de tu reforma" (antes/después
lado a lado + notas por zona).
- route del panel: refactor para reutilizar construirPresupuestoPdf (DRY),
manteniendo getLead como guardia de auth/404.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Variables nuevas (todas opcionales vía zod, sin romper build/demo):
- FUNNEL_API_KEY: clave Bearer del EP de ingesta.
- SMTP_* + EMAIL_FROM: envío de email del presupuesto/enlace.
- PERFIL/WHATSAPP/WHATSAPP_START webhook URLs: señales al flujo externo.
- APP_URL: base para enlaces absolutos.
Helpers emailConfigurado()/perfilWebhookConfigurado()/whatsappWebhookConfigurado()/
whatsappStartConfigurado(). nodemailer como dep directa (stack: Email SMTP).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Prepara el modelo de datos para la ingesta multicanal del perfil del lead:
- lead_fotos: columnas momento (foto_momento antes/despues, default antes) y
zona (tipo_reforma, nullable con fallback al tipoReforma del lead).
- lead_notas: tabla append-only de datos de texto por zona (ej. "suelo
premium"), con origen (ep|funnel|panel) para auditar quién los aportó.
- Migración 0008 + regenerado db-schema/schema.sql.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Genera db-schema/schema.sql (DDL completo: 12 enums, 14 tablas, FKs e
índices) a partir del schema Drizzle, para que el equipo pueda consultar y
proponer cambios sobre el modelo de datos sin leer las migraciones una a una.
- db-schema/schema.sql: foto del esquema actual (drizzle-kit export)
- db-schema/README.md: qué es cada tabla, cómo cambiar el modelo y cómo
regenerar/levantar la BD desde este SQL
- package.json: script db:export para regenerar la foto
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Incluye una imagen WebP optimizada (48caf530-fa5f-476b-8120-195cfce7a9ec.webp) y una imagen PNG de ejemplo de reformas (reformas-ejemplo.png) para usar en la galería de trabajos y demostraciones del funnel.