Bot responde a mensajes entrantes: recupera lead por teléfono + fix matching
El bot no respondía a las réplicas del cliente: la sesión lead↔teléfono vivía solo en memoria y no casaba (se pierde al reiniciar el contenedor). Ahora si no está en memoria, getOrCreateContext busca el lead en la BD por teléfono vía un EP nuevo GET /api/leads/by-phone (match por últimos 9 dígitos) y re-registra la sesión. Aparte, los ids de modelo de Claude del bot estaban mal (-4-5 vs 4.5). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,20 @@ export class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async buscarLeadPorTelefono(telefono: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${this.baseUrl}/api/leads/by-phone`, {
|
||||||
|
headers: this.headers,
|
||||||
|
params: { telefono },
|
||||||
|
});
|
||||||
|
return data?.leadId ?? null;
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.response?.status === 404) return null;
|
||||||
|
this.logger.error(`buscarLeadPorTelefono error: ${err.message}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async guardarConversacion(
|
async guardarConversacion(
|
||||||
leadId: string,
|
leadId: string,
|
||||||
rol: 'user' | 'assistant' | 'system',
|
rol: 'user' | 'assistant' | 'system',
|
||||||
|
|||||||
@@ -171,6 +171,14 @@ export class WebhookListener implements OnApplicationBootstrap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-registra una sesión recuperada de la BD (cuando no estaba en memoria, p. ej. tras reinicio).
|
||||||
|
ensureSession(telefono: string, leadId: string, nombre = '') {
|
||||||
|
const tel = this.normTel(telefono);
|
||||||
|
if (!this.leadSessions.has(tel)) {
|
||||||
|
this.leadSessions.set(tel, { leadId, telefono: tel, nombre, jid: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async handleWhatsappPdf(payload: { leadId: string; telefono: string; pdfBase64: string; filename: string }) {
|
private async handleWhatsappPdf(payload: { leadId: string; telefono: string; pdfBase64: string; filename: string }) {
|
||||||
this.logger.log(`[PDF] leadId=${payload.leadId}, filename=${payload.filename}`);
|
this.logger.log(`[PDF] leadId=${payload.leadId}, filename=${payload.filename}`);
|
||||||
const { pdfEmitter } = await import('../whatsapp/whatsapp.service');
|
const { pdfEmitter } = await import('../whatsapp/whatsapp.service');
|
||||||
|
|||||||
@@ -263,7 +263,16 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getOrCreateContext(telefono: string, jid: string): Promise<LeadContext | null> {
|
private async getOrCreateContext(telefono: string, jid: string): Promise<LeadContext | null> {
|
||||||
const leadId = this.webhookListener.getLeadIdByTelefono(telefono);
|
let leadId = this.webhookListener.getLeadIdByTelefono(telefono);
|
||||||
|
|
||||||
|
// Fallback: si no está en memoria (reinicio del bot), recuperarlo de la BD por teléfono.
|
||||||
|
if (!leadId) {
|
||||||
|
leadId = await this.api.buscarLeadPorTelefono(telefono);
|
||||||
|
if (leadId) {
|
||||||
|
this.webhookListener.ensureSession(telefono, leadId);
|
||||||
|
this.logger.log(`Lead ${leadId} recuperado por teléfono ${telefono} (sin sesión en memoria).`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!leadId) {
|
if (!leadId) {
|
||||||
this.logger.log(`Mensaje ignorado de ${telefono}: lead no registrado. Debe iniciarse desde la web.`);
|
this.logger.log(`Mensaje ignorado de ${telefono}: lead no registrado. Debe iniciarse desde la web.`);
|
||||||
|
|||||||
37
mvp/b2c/src/app/api/leads/by-phone/route.ts
Normal file
37
mvp/b2c/src/app/api/leads/by-phone/route.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { desc, like } from 'drizzle-orm';
|
||||||
|
import { db } from '@/db';
|
||||||
|
import { leads } from '@/db/schema';
|
||||||
|
import { autorizado, jsonResponse } from '@/lib/api/funnel-auth';
|
||||||
|
|
||||||
|
export const runtime = 'nodejs';
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
// Busca el lead más reciente por teléfono (comparando los últimos 9 dígitos, ignorando prefijos y
|
||||||
|
// formato). Lo usa el bot de WhatsApp para recuperar el leadId de un mensaje entrante cuando no
|
||||||
|
// tiene la sesión en memoria (p. ej. tras un reinicio del contenedor).
|
||||||
|
export async function GET(req: Request) {
|
||||||
|
if (!autorizado(req)) return jsonResponse({ ok: false, error: 'No autorizado.' }, 401);
|
||||||
|
|
||||||
|
const tel = (new URL(req.url).searchParams.get('telefono') ?? '').replace(/\D/g, '');
|
||||||
|
if (tel.length < 6) return jsonResponse({ ok: false, error: 'telefono inválido.' }, 422);
|
||||||
|
const last9 = tel.slice(-9);
|
||||||
|
|
||||||
|
const [lead] = await db
|
||||||
|
.select({
|
||||||
|
id: leads.id,
|
||||||
|
nombre: leads.nombre,
|
||||||
|
telefono: leads.telefono,
|
||||||
|
botStep: leads.botStep,
|
||||||
|
viable: leads.viable,
|
||||||
|
})
|
||||||
|
.from(leads)
|
||||||
|
.where(like(leads.telefono, `%${last9}%`))
|
||||||
|
.orderBy(desc(leads.createdAt))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!lead) return jsonResponse({ ok: false, error: 'Lead no encontrado.' }, 404);
|
||||||
|
return jsonResponse(
|
||||||
|
{ leadId: lead.id, nombre: lead.nombre, telefono: lead.telefono, botStep: lead.botStep, viable: lead.viable },
|
||||||
|
200,
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user