Añade canales de llamada y WhatsApp al funnel B2C
- 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>
This commit is contained in:
@@ -8,8 +8,13 @@ import { db } from '@/db';
|
||||
import { leads, leadFotos, leadNotas, leadPipelineEventos } from '@/db/schema';
|
||||
import type { NewLeadFoto, NewLeadNota } from '@/db/schema';
|
||||
import { getTenantBySlug } from '@/lib/funnel/public-queries';
|
||||
import { getTenantPerfilById } from '@/db/tenant-queries';
|
||||
import { procesarLead } from '@/lib/funnel/orchestrator';
|
||||
import { señalarPerfilCompleto } from '@/lib/funnel/perfil';
|
||||
import { iniciarLlamadaSaliente, construirVariablesLlamada } from '@/lib/voice/retell';
|
||||
import { iniciarConversacionWhatsapp } from '@/lib/webhooks';
|
||||
import { enviarEnlaceFormulario } from '@/lib/email/mailer';
|
||||
import { env } from '@/lib/env';
|
||||
|
||||
const MAX_ZONAS = 6;
|
||||
const MAX_FOTOS_ZONA = 6;
|
||||
@@ -207,3 +212,78 @@ export async function guardarDetallesYFotos(leadId: string, formData: FormData):
|
||||
revalidatePath('/panel');
|
||||
redirect(`/solicitud/${leadId}/estado`);
|
||||
}
|
||||
|
||||
// Canal llamada: el cliente pide que le llamen ahora o programa la llamada. "Ahora" dispara la
|
||||
// llamada saliente de Retell; "programar" registra la fecha y la señala (el dialing en hora lo
|
||||
// hace el flujo externo, la app no monta cron). Best-effort.
|
||||
export async function pedirLlamada(
|
||||
leadId: string,
|
||||
cuando: 'ahora' | string,
|
||||
): Promise<{ ok: boolean; programada?: string }> {
|
||||
const [lead] = await db.select().from(leads).where(eq(leads.id, leadId)).limit(1);
|
||||
if (!lead) return { ok: false };
|
||||
const tenant = await getTenantPerfilById(lead.tenantId);
|
||||
|
||||
if (cuando === 'ahora') {
|
||||
const llamada = await iniciarLlamadaSaliente({
|
||||
telefono: lead.telefono,
|
||||
variables: construirVariablesLlamada({ nombreEmpresa: tenant.nombreEmpresa }, lead),
|
||||
});
|
||||
await db.insert(leadPipelineEventos).values({
|
||||
leadId,
|
||||
stage: 'prellamada_enviada',
|
||||
metadata: { via: 'llamada', cuando: 'ahora', real: Boolean(llamada), simulado: !llamada },
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const fecha = new Date(cuando);
|
||||
const programadaAt = Number.isNaN(fecha.getTime()) ? null : fecha.toISOString();
|
||||
await db.insert(leadPipelineEventos).values({
|
||||
leadId,
|
||||
stage: 'prellamada_enviada',
|
||||
metadata: { via: 'llamada', cuando: 'programada', programadaAt },
|
||||
});
|
||||
return { ok: true, programada: programadaAt ?? undefined };
|
||||
}
|
||||
|
||||
// Canal llamada: envía al cliente un email con el enlace a su formulario para subir las imágenes.
|
||||
export async function enviarEnlaceFormularioEmail(leadId: string): Promise<boolean> {
|
||||
const [lead] = await db.select().from(leads).where(eq(leads.id, leadId)).limit(1);
|
||||
if (!lead) return false;
|
||||
const tenant = await getTenantPerfilById(lead.tenantId);
|
||||
const url = `${env.APP_URL ?? ''}/solicitud/${leadId}/formulario`;
|
||||
return enviarEnlaceFormulario({
|
||||
to: lead.email,
|
||||
nombre: lead.nombre,
|
||||
empresa: tenant.nombreEmpresa,
|
||||
url,
|
||||
});
|
||||
}
|
||||
|
||||
// Canal WhatsApp: arranca la conversación con el lead a través del flujo externo (que manda el
|
||||
// primer mensaje a su teléfono) y deja traza. El cliente confirma luego en la UI.
|
||||
export async function iniciarWhatsapp(leadId: string): Promise<{ ok: boolean; telefono: string }> {
|
||||
const [lead] = await db.select().from(leads).where(eq(leads.id, leadId)).limit(1);
|
||||
if (!lead) return { ok: false, telefono: '' };
|
||||
const tenant = await getTenantPerfilById(lead.tenantId);
|
||||
const ok = await iniciarConversacionWhatsapp({
|
||||
leadId,
|
||||
telefono: lead.telefono,
|
||||
nombre: lead.nombre,
|
||||
empresa: tenant.nombreEmpresa,
|
||||
});
|
||||
return { ok, telefono: lead.telefono };
|
||||
}
|
||||
|
||||
// Canal WhatsApp: el cliente confirma que ha recibido el mensaje; seguimos por WhatsApp.
|
||||
export async function confirmarWhatsapp(leadId: string): Promise<{ ok: boolean }> {
|
||||
const [lead] = await db.select({ id: leads.id }).from(leads).where(eq(leads.id, leadId)).limit(1);
|
||||
if (!lead) return { ok: false };
|
||||
await db.insert(leadPipelineEventos).values({
|
||||
leadId,
|
||||
stage: 'prellamada_enviada',
|
||||
metadata: { via: 'whatsapp', confirmado: true },
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user