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:
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user