Eventbuzz_Concierge/src/services/whatsappInbound.service.js

145 lines
4.7 KiB
JavaScript

const ConciergeBot = require('../models/ConciergeBot');
const Event = require('../models/Event');
const WhatsAppChatSession = require('../models/WhatsAppChatSession');
const { getOrgWhatsAppCredentials } = require('../utils/whatsappCloud');
const { handleFirstInbound, handleUnlinkedMessage } = require('./whatsappReception.service');
const { handleFollowup, inboundText } = require('./whatsappFollowup.service');
const { logWhatsApp, maskPhone } = require('../utils/whatsappLog');
async function findReceptionBotByPhoneNumberId(phoneNumberId) {
const pid = String(phoneNumberId || '').trim();
if (!pid) {
logWhatsApp('inbound.no_phone_id', {});
return null;
}
// Match on reception.enabled only. Status ARCHIVED is a knowledge-purge
// marker (ended events) and must not block a reception desk that is still on.
const bots = await ConciergeBot.find({ 'reception.enabled': true })
.setOptions({ strictQuery: false })
.lean();
logWhatsApp('inbound.reception_bots', {
incomingPhoneNumberId: pid,
count: bots.length,
bots: bots.map((b) => ({
botId: String(b._id),
name: b.name || '',
status: b.status || '',
})),
});
for (const bot of bots) {
const creds = await getOrgWhatsAppCredentials(String(bot.organization));
const orgPid = creds ? String(creds.phoneNumberId || '').trim() : '';
logWhatsApp('inbound.bot_check', {
botId: String(bot._id),
botName: bot.name || '',
status: bot.status || '',
orgPhoneNumberId: orgPid || '(none)',
match: Boolean(creds && orgPid === pid),
});
if (creds && orgPid === pid) return bot;
}
return null;
}
async function processInboundMessage(jobData) {
const { phoneId, messageData, profileName } = jobData || {};
if (!messageData) {
logWhatsApp('inbound.skip', { reason: 'no_message' });
return;
}
const bot = await findReceptionBotByPhoneNumberId(phoneId);
if (!bot) {
logWhatsApp('inbound.skip', {
reason: 'no_reception_bot',
phoneNumberId: String(phoneId || ''),
});
return;
}
const eventId = bot.event;
const event = await Event.findById(eventId).select('name location locationType startDate endDate').lean();
const eventName = event?.name || 'Event';
const userPhone = String(messageData.from || '').replace(/\D/g, '');
if (!userPhone) {
logWhatsApp('inbound.skip', { reason: 'no_from', botId: String(bot._id) });
return;
}
logWhatsApp('inbound.process', {
botId: String(bot._id),
eventName,
from: maskPhone(userPhone),
type: messageData.type || '',
profileName: profileName || '',
dripCount: Array.isArray(bot.reception?.drips) ? bot.reception.drips.length : 0,
dripAudience: bot.reception?.dripAudience || 'matched',
});
let session = await WhatsAppChatSession.findOne({ botId: bot._id, phoneNumber: userPhone });
if (!session) {
try {
session = await WhatsAppChatSession.create({
botId: bot._id,
eventId,
organization: bot.organization,
phoneNumber: userPhone,
waProfileName: profileName || '',
receptionState: 'IDENTIFYING',
history: [],
lastSeenAt: new Date(),
});
} catch (err) {
if (err && err.code === 11000) {
session = await WhatsAppChatSession.findOne({ botId: bot._id, phoneNumber: userPhone });
} else {
throw err;
}
}
if (!session) return;
}
session.lastSeenAt = new Date();
const isFirst = !session.firstInboundAt;
logWhatsApp('inbound.session', {
sessionId: String(session._id),
first: isFirst,
receptionState: session.receptionState,
hasParticipant: Boolean(session.participant),
});
if (isFirst) {
await handleFirstInbound(bot, session, {
profileName: profileName || session.waProfileName,
eventId,
eventName,
messageData,
});
return;
}
// Drips are queued at first inbound / match. If the organiser added them later,
// pick them up on the next message (deduped) so a 1-minute test can still fire.
if (!(session.dripJobIds || []).length) {
const audience = bot.reception?.dripAudience === 'all_inbound' ? 'all_inbound' : 'matched';
if (session.participant || audience === 'all_inbound') {
const { scheduleDripsForSession } = require('./whatsappDrip.service');
await scheduleDripsForSession(session, bot, {
phase: session.participant ? 'match' : 'first_inbound',
});
}
}
if (session.receptionState !== 'ONBOARDED' || !session.participant) {
await handleUnlinkedMessage(bot, session, { messageData, eventId, eventName });
return;
}
await handleFollowup({ bot, event, session, messageData });
}
module.exports = {
processInboundMessage,
findReceptionBotByPhoneNumberId,
inboundText,
};