require('dotenv').config(); const API_BASE = (process.env.RAPPORT_API_BASE || 'https://rapportapi.click/').replace(/\/?$/, '/'); const ACCESS_TOKEN = process.env.RAPPORT_ACCESS_TOKEN || ''; const PARTITION = process.env.RAPPORT_PARTITION || 'rapport-lifemankind'; /** * Fetch a contact from Rapport contact/get_one. * Returns a normalized doctor object or null. */ async function getContactById(contactId) { if (!contactId) return null; if (!ACCESS_TOKEN) { const err = new Error('RAPPORT_ACCESS_TOKEN is not configured'); err.statusCode = 503; throw err; } const url = new URL('contact/contact/get_one', API_BASE); url.searchParams.set('id', contactId); url.searchParams.set('application', 'rapport'); const response = await fetch(url.toString(), { method: 'GET', headers: { Authorization: ACCESS_TOKEN, Accept: 'application/json', }, }); const body = await response.json().catch(() => ({})); if (!response.ok) { const err = new Error(body.message || `Rapport contact lookup failed (${response.status})`); err.statusCode = response.status === 401 ? 503 : 502; throw err; } const item = body?.data?.item || body?.item || null; if (!item || !item.id) { return null; } // Optional partition check when present on the contact if (item.partition && item.partition !== PARTITION) { return null; } return item; } function isTgicsEnrolled(contact) { const flag = contact?.tgics_enrolled_doctor; if (flag === undefined || flag === null) return false; return String(flag).toLowerCase() === 'yes' || flag === true || flag === 1 || flag === '1'; } function toDoctorPayload(contact, userId) { return { contact_id: contact.id, user_id: userId || null, name: contact.name || '', wfsa_code: contact.msl_code || '', phone: contact.phone || '', email: contact.email || '', age: contact.age || '', years_of_practice: contact.years_of_practice || '', type_of_practice: contact.type_of_practice || '', city: contact.city || contact.city_town || '', state: contact.state || '', qualification: contact.qualification || '', tgics_enrolled_doctor: contact.tgics_enrolled_doctor || '', }; } async function resolveTgicsDoctor(userId, contactId) { const contact = await getContactById(contactId); if (!contact) { return { ok: false, statusCode: 404, message: 'Doctor not found.' }; } if (!isTgicsEnrolled(contact)) { return { ok: false, statusCode: 403, message: 'Doctor is not enrolled for TGICS.', }; } return { ok: true, doctor: toDoctorPayload(contact, userId), contact }; } function normalizePhone(phone) { const digits = String(phone || '').replace(/\D+/g, ''); if (digits.length >= 10) { return digits.slice(-10); } return digits; } async function searchContactsByKeyword(keyword) { if (!ACCESS_TOKEN) { const err = new Error('RAPPORT_ACCESS_TOKEN is not configured'); err.statusCode = 503; throw err; } const url = new URL('contact/contact/get_all', API_BASE); url.searchParams.set('keyword', keyword); url.searchParams.set('start', '0'); url.searchParams.set('length', '100'); url.searchParams.set('search', JSON.stringify({})); const response = await fetch(url.toString(), { method: 'GET', headers: { Authorization: ACCESS_TOKEN, Accept: 'application/json', }, }); const body = await response.json().catch(() => ({})); if (!response.ok) { const err = new Error(body.message || `Rapport contact search failed (${response.status})`); err.statusCode = response.status === 401 ? 503 : 502; throw err; } return Array.isArray(body?.data?.items) ? body.data.items : []; } /** * Resolve TGICS doctor by MR userId + WhatsApp phone (last 10 digits). * Prefers contacts owned by that user_id. */ async function resolveTgicsDoctorByPhone(userId, phone) { const needle = normalizePhone(phone); if (needle.length < 10) { return { ok: false, statusCode: 400, message: 'Please enter a valid 10-digit mobile number.', }; } const items = await searchContactsByKeyword(needle); const matches = items.filter((contact) => { if (!contact || !contact.id) return false; if (!isTgicsEnrolled(contact)) return false; if (normalizePhone(contact.phone) !== needle) return false; if (contact.partition && contact.partition !== PARTITION) return false; return true; }); if (matches.length === 0) { return { ok: false, statusCode: 404, message: 'No enrolled doctor found for this mobile number.', }; } const forUser = matches.filter((c) => String(c.user_id || '') === String(userId)); const unscoped = matches.filter((c) => !c.user_id); const contact = forUser[0] || unscoped[0]; if (!contact) { return { ok: false, statusCode: 404, message: 'No enrolled doctor found for this mobile number under this link.', }; } return { ok: true, doctor: toDoctorPayload(contact, userId), contact }; } async function rapportGet(service, path, params = {}) { if (!ACCESS_TOKEN) { const err = new Error('RAPPORT_ACCESS_TOKEN is not configured'); err.statusCode = 503; throw err; } const url = new URL(`${service}/${path}`, API_BASE); url.searchParams.set('application', 'rapport'); for (const [key, value] of Object.entries(params)) { if (value === undefined || value === null) continue; url.searchParams.set(key, typeof value === 'object' ? JSON.stringify(value) : String(value)); } const response = await fetch(url.toString(), { method: 'GET', headers: { Authorization: ACCESS_TOKEN, Accept: 'application/json', }, }); const body = await response.json().catch(() => ({})); const apiStatus = Number(body?.statusCode || response.status); if (!response.ok || apiStatus >= 400) { const err = new Error(body.message || `Rapport ${service}/${path} failed (${apiStatus})`); err.statusCode = apiStatus === 401 ? 503 : 502; throw err; } return body?.data || {}; } const CACHE_TTL_MS = Number(process.env.RAPPORT_CACHE_TTL_MS || 10 * 60 * 1000); const cache = new Map(); async function cached(key, loader, refresh = false) { const hit = cache.get(key); if (!refresh && hit && hit.expiresAt > Date.now()) { return hit.value; } const value = await loader(); cache.set(key, { value, expiresAt: Date.now() + CACHE_TTL_MS }); return value; } /** * Active users holding any of the given role ids (ums/users/dropdown, de-duplicated by id). */ async function getUsersByRoles(roleIds, { refresh = false } = {}) { const ids = [...new Set((roleIds || []).map((r) => String(r).trim()).filter(Boolean))]; return cached(`users:${ids.join(',')}`, async () => { const byId = new Map(); for (const roleId of ids) { const data = await rapportGet('ums', 'users/dropdown', { filter: { roles: roleId } }); for (const user of Array.isArray(data.items) ? data.items : []) { if (user?.id && !byId.has(user.id)) { byId.set(user.id, user); } } } return [...byId.values()]; }, refresh); } /** * Map of owner user id -> position details (ums/hierarchy/get_all with positions + export). */ async function getPositionsByOwner({ refresh = false } = {}) { return cached('positions', async () => { const data = await rapportGet('ums', 'hierarchy/get_all', { positions: 1, export: 1 }); const rows = Array.isArray(data.hierarchys) ? data.hierarchys : Array.isArray(data.items) ? data.items : []; const byOwner = new Map(); for (const row of rows) { const owner = row?.owner; if (!owner || owner === 'vacant' || byOwner.has(owner)) continue; const info = row.owner_info || {}; byOwner.set(owner, { position_id: row.id || '', position_name: row.name || '', position_code: row.code || '', designation: row.designation || '', name: `${info.firstname || ''} ${info.lastname || ''}`.trim(), employee_code: info.employee_code || '', }); } return byOwner; }, refresh); } const ENROLLED_VALUES = ['Yes', 'yes', 'YES']; const ES_PAGE_SIZE = 1000; const ES_MAX_WINDOW = 10000; /** * All TGICS-enrolled contacts (full get_all items). * The ES term filter is case-sensitive, so each spelling of "yes" is queried and merged. */ async function getEnrolledContacts({ refresh = false } = {}) { return cached('enrolled-contacts', async () => { const byId = new Map(); for (const value of ENROLLED_VALUES) { for (let start = 0; start < ES_MAX_WINDOW; start += ES_PAGE_SIZE) { const data = await rapportGet('contact', 'contact/get_all', { start, length: ES_PAGE_SIZE, keyword: '', search: { attribute_tgics_enrolled_doctor: value }, }); const items = Array.isArray(data.items) ? data.items : []; for (const item of items) { if (item?.id && !byId.has(item.id)) { byId.set(item.id, item); } } const total = Number(data.count) || 0; if (items.length < ES_PAGE_SIZE || start + ES_PAGE_SIZE >= total) break; } } return [...byId.values()]; }, refresh); } module.exports = { rapportGet, getUsersByRoles, getPositionsByOwner, getEnrolledContacts, getContactById, isTgicsEnrolled, toDoctorPayload, resolveTgicsDoctor, resolveTgicsDoctorByPhone, normalizePhone, PARTITION, };