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:
Carlos Narro
2026-06-09 17:28:56 +02:00
parent 0a00d42553
commit a8b6d62dd6
4 changed files with 69 additions and 1 deletions

View File

@@ -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',

View File

@@ -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');

View File

@@ -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.`);

View 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,
);
}