Bot: enviar apertura proactiva al recibir /whatsapp-start + fix clave teléfono

El funnel promete "te escribimos por WhatsApp" pero el bot solo registraba la
sesión y esperaba a que el cliente escribiera primero → no llegaba nada. Ahora
WhatsappService escucha un startEmitter y manda el mensaje de apertura de Luisa
al teléfono (verifica el número con onWhatsApp), persiste estadoWa/botStep y el
intento. Además normaliza la clave de teléfono a solo-dígitos en leadSessions
(antes "+34..." no casaba con los dígitos del jid entrante → ignoraba al cliente).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Carlos Narro
2026-06-09 17:18:10 +02:00
parent a740d08863
commit 0a00d42553
2 changed files with 75 additions and 4 deletions

View File

@@ -143,20 +143,29 @@ export class WebhookListener implements OnApplicationBootstrap {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private leadSessions = new Map<string, { leadId: string; telefono: string; nombre: string; jid: string | null }>();
private normTel(t: string): string {
return (t || '').replace(/\D/g, '');
}
private async handleWhatsappStart(payload: { leadId: string; telefono: string; nombre: string; empresa: string }) {
const { leadId, telefono, nombre } = payload;
const { leadId, nombre, empresa } = payload;
const telefono = this.normTel(payload.telefono);
this.logger.log(`[START] leadId=${leadId}, telefono=${telefono}, nombre=${nombre}`);
this.leadSessions.set(telefono, { leadId, telefono, nombre, jid: null });
this.logger.log(`Lead ${leadId} registrado en sesiones.`);
// Dispara la apertura proactiva (la envía WhatsappService, evitando dependencia circular).
const { startEmitter } = await import('../whatsapp/whatsapp.service');
startEmitter.emit('start', { leadId, telefono, nombre, empresa });
this.logger.log(`Lead ${leadId} registrado; apertura disparada.`);
}
getLeadIdByTelefono(telefono: string): string | null {
return this.leadSessions.get(telefono)?.leadId ?? null;
return this.leadSessions.get(this.normTel(telefono))?.leadId ?? null;
}
registerJid(telefono: string, jid: string) {
const session = this.leadSessions.get(telefono);
const session = this.leadSessions.get(this.normTel(telefono));
if (session) {
session.jid = jid;
}

View File

@@ -27,6 +27,7 @@ import { ApiClient } from '../api/api-client.service';
import { wrapSocket } from 'baileys-antiban';
export const pdfEmitter = new EventEmitter();
export const startEmitter = new EventEmitter();
interface LeadContext {
leadId: string;
@@ -62,6 +63,7 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.conectar();
this.escucharPdf();
this.escucharStart();
}
async onModuleDestroy() {
@@ -98,6 +100,66 @@ export class WhatsappService implements OnModuleInit, OnModuleDestroy {
});
}
// Apertura proactiva: cuando el funnel dispara /whatsapp-start, Luisa escribe ella el primer
// mensaje (el bot ya no es solo reactivo).
private escucharStart() {
startEmitter.on(
'start',
async (p: { leadId: string; telefono: string; nombre: string; empresa: string }) => {
try {
await this.enviarApertura(p);
} catch (err: any) {
this.logger.error(`[APERTURA] Error: ${err.message}`);
}
},
);
}
private async enviarApertura(p: { leadId: string; telefono: string; nombre: string; empresa: string }) {
if (!this.sock) {
this.logger.warn(`[APERTURA] WhatsApp no conectado; no se envía a ${p.telefono}`);
return;
}
const tel = (p.telefono || '').replace(/\D/g, '');
let jid = `${tel}@s.whatsapp.net`;
try {
const res = await this.sock.onWhatsApp(tel);
if (res && res[0]?.exists && res[0]?.jid) jid = res[0].jid;
else if (!res || !res[0]?.exists) this.logger.warn(`[APERTURA] ${tel} no parece estar en WhatsApp`);
} catch {
/* seguimos con el jid por defecto */
}
const primerNombre = (p.nombre || '').trim().split(' ')[0] || 'hola';
const empresa = p.empresa || 'Reformix';
const apertura =
`¡Hola ${primerNombre}! Soy Luisa, del equipo de ${empresa}. 😊\n\n` +
`Acabas de pedir presupuesto para tu reforma y te ayudo a prepararlo (con un render de cómo ` +
`quedaría incluido). Para empezar, cuéntame: ¿qué espacio quieres reformar? (cocina, baño, salón…)`;
// Contexto para los siguientes mensajes del cliente.
this.jidToLeadId.set(jid, p.leadId);
this.webhookListener.registerJid(tel, jid);
this.leadCache.set(p.leadId, {
leadId: p.leadId,
telefono: tel,
nombre: p.nombre || '',
botStep: 'apertura',
viable: null,
});
await this.enviarMensaje(jid, apertura);
this.logger.log(`[APERTURA] Enviada a ${jid} (lead ${p.leadId})`);
try {
await this.api.actualizarPerfil(p.leadId, { estadoWa: 'enviado', botStep: 'apertura', canalOrigen: 'whatsapp' });
await this.conversacionService.guardarMensaje(p.leadId, 'assistant', apertura, { botStep: 'apertura' });
await this.api.registrarIntento(p.leadId, 'whatsapp', 1, 'exitoso', true);
} catch (err: any) {
this.logger.warn(`[APERTURA] No se pudo persistir en la app: ${err.message}`);
}
}
private normalizarTelefono(jid: string): string {
return jid.split('@')[0].replace(/\D/g, '');
}