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