Paso de fotos + flujo cross-canal llamada→WhatsApp para el render

Bot: al cerrar la cualificación, Luisa pide una foto y entra en modo recogida;
al llegar la foto la sube como "antes" con perfilCompleto:true → dispara render +
presupuesto + entrega del PDF. Nuevo webhook /whatsapp-fotos para que, tras una
llamada, Luisa escriba al lead, referencie lo hablado y le pida las fotos
(reutiliza el mismo modo).

App: el webhook de Retell, tras el análisis de la llamada, llama a pedirFotosWhatsapp
(WHATSAPP_FOTOS_WEBHOOK_URL) con el contexto de la reforma.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-06-10 12:50:33 +02:00
parent c5d4a9296a
commit b0871b733c
5 changed files with 173 additions and 6 deletions

View File

@@ -1,8 +1,9 @@
import { desc, eq, like } from 'drizzle-orm';
import { db } from '@/db';
import { leads, leadPipelineEventos } from '@/db/schema';
import { leads, leadPipelineEventos, tenants } from '@/db/schema';
import { obtenerLlamada, descargarGrabacion } from '@/lib/voice/retell';
import { analizarTranscripcion } from '@/lib/funnel/analizar-conversacion';
import { pedirFotosWhatsapp } from '@/lib/webhooks';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
@@ -81,7 +82,34 @@ export async function POST(req: Request): Promise<Response> {
// Mismo cerebro de captura que WhatsApp: extrae los campos clave de la transcripción de la llamada.
const analisis = detalle.transcript
? await analizarTranscripcion(leadId, detalle.transcript, 'llamada')
: { ok: false, error: 'sin transcripción' };
: { ok: false as const, error: 'sin transcripción' };
// Cross-canal: tras la llamada, Luisa escribe al lead por WhatsApp, referencia lo hablado y le
// pide las fotos (el agente de voz le dijo que las enviara por ahí).
let fotosPedidas = false;
if (analisis.ok) {
const [info] = await db
.select({ nombre: leads.nombre, telefono: leads.telefono, tenantId: leads.tenantId })
.from(leads)
.where(eq(leads.id, leadId))
.limit(1);
if (info) {
const [t] = await db
.select({ nombre: tenants.nombreEmpresa })
.from(tenants)
.where(eq(tenants.id, info.tenantId))
.limit(1);
const tipo = (analisis.perfil?.tipoReforma as string | undefined) ?? null;
const contexto = tipo ? `la reforma de tu ${tipo}` : 'tu reforma';
fotosPedidas = await pedirFotosWhatsapp({
leadId,
telefono: info.telefono,
nombre: info.nombre,
empresa: t?.nombre ?? 'Reformix',
contexto,
});
}
}
return ok({
matched: true,
@@ -89,5 +117,6 @@ export async function POST(req: Request): Promise<Response> {
transcript: Boolean(detalle.transcript),
grabacion: Boolean(audioUrl),
analizado: analisis.ok,
fotosPedidas,
});
}

View File

@@ -26,6 +26,8 @@ const schema = z.object({
PERFIL_WEBHOOK_URL: opcional,
WHATSAPP_WEBHOOK_URL: opcional,
WHATSAPP_START_WEBHOOK_URL: opcional,
// Cross-canal: tras una llamada, pedir al lead las fotos por WhatsApp.
WHATSAPP_FOTOS_WEBHOOK_URL: opcional,
// Base pública de la app, para construir enlaces (ej. el enlace al formulario en el email).
APP_URL: opcional,
// LLM (OpenRouter) para el post-análisis de la conversación de WhatsApp.
@@ -47,6 +49,7 @@ export const env = schema.parse({
PERFIL_WEBHOOK_URL: process.env.PERFIL_WEBHOOK_URL,
WHATSAPP_WEBHOOK_URL: process.env.WHATSAPP_WEBHOOK_URL,
WHATSAPP_START_WEBHOOK_URL: process.env.WHATSAPP_START_WEBHOOK_URL,
WHATSAPP_FOTOS_WEBHOOK_URL: process.env.WHATSAPP_FOTOS_WEBHOOK_URL,
APP_URL: process.env.APP_URL,
OPENROUTER_API_KEY: process.env.OPENROUTER_API_KEY,
OPENROUTER_MODEL_ANALISIS: process.env.OPENROUTER_MODEL_ANALISIS,
@@ -76,3 +79,7 @@ export function whatsappWebhookConfigurado(): boolean {
export function whatsappStartConfigurado(): boolean {
return Boolean(env.WHATSAPP_START_WEBHOOK_URL);
}
export function whatsappFotosConfigurado(): boolean {
return Boolean(env.WHATSAPP_FOTOS_WEBHOOK_URL);
}

View File

@@ -3,6 +3,7 @@ import {
perfilWebhookConfigurado,
whatsappWebhookConfigurado,
whatsappStartConfigurado,
whatsappFotosConfigurado,
} from '@/lib/env';
// POST JSON best-effort: nunca lanza. Devuelve true solo si el destino respondió 2xx.
@@ -55,3 +56,16 @@ export async function iniciarConversacionWhatsapp(payload: {
if (!whatsappStartConfigurado()) return false;
return postWebhook(env.WHATSAPP_START_WEBHOOK_URL!, payload);
}
// Cross-canal: tras una llamada, que Luisa escriba al lead por WhatsApp, referencie lo hablado
// (contexto) y le pida las fotos para completar el render + presupuesto.
export async function pedirFotosWhatsapp(payload: {
leadId: string;
telefono: string;
nombre: string;
empresa: string;
contexto?: string;
}): Promise<boolean> {
if (!whatsappFotosConfigurado()) return false;
return postWebhook(env.WHATSAPP_FOTOS_WEBHOOK_URL!, payload);
}