145 lines
4.7 KiB
JavaScript
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,
|
|
};
|