310 lines
9.3 KiB
JavaScript
310 lines
9.3 KiB
JavaScript
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,
|
|
};
|