import { Cohort } from '../models/Cohort.js'; import { OnboardedPatient } from '../models/OnboardedPatient.js'; import { addPatientToWhatsapp, removePatientFromWhatsapp, syncWhatsappMembership } from './whatsapp.js'; import { applyOnboardedPii, hashMobile, isMobileNumber } from './cryptoPii.js'; import { toYmd, weekdayOf } from './schedule.js'; import Boom from '@hapi/boom'; async function safeWhatsapp(label, fn) { try { await fn(); } catch (err) { console.error(`[whatsapp] ${label} failed: ${err?.message || err}`); } } export async function syncCohortPatients(cohort, previousPatientIds = []) { const nextIds = (cohort.patients || []).map((id) => String(id)); const prevIds = (previousPatientIds || []).map((id) => String(id)); if (nextIds.length) { await OnboardedPatient.updateMany( { _id: { $in: nextIds } }, { $set: { assignedEducator: cohort.virtualPrt, cohort: cohort._id, matchingStatus: 'assigned' } }, ); } const removed = prevIds.filter((id) => !nextIds.includes(id)); for (const id of removed) { const other = await Cohort.findOne({ _id: { $ne: cohort._id }, patients: id }); if (other) { await OnboardedPatient.updateOne( { _id: id }, { $set: { assignedEducator: other.virtualPrt, cohort: other._id, matchingStatus: 'assigned' } }, ); } else { await OnboardedPatient.updateOne( { _id: id }, { $set: { assignedEducator: null, cohort: null, matchingStatus: 'unassigned' } }, ); } } if (cohort._id && cohort.virtualPrt) { await OnboardedPatient.updateMany( { cohort: cohort._id }, { $set: { assignedEducator: cohort.virtualPrt } }, ); } await safeWhatsapp('membership sync', () => syncWhatsappMembership(cohort, previousPatientIds)); } export async function addPatientToCohort(cohort, patientId) { const id = String(patientId); const current = (cohort.patients || []).map((item) => String(item._id || item)); if (!current.includes(id)) { cohort.patients.push(patientId); await cohort.save(); } await OnboardedPatient.updateOne( { _id: patientId }, { $set: { assignedEducator: cohort.virtualPrt, cohort: cohort._id, matchingStatus: 'assigned' } }, ); await safeWhatsapp('add patient', () => addPatientToWhatsapp(cohort, patientId)); } export async function removePatientFromCohort(cohortId, patientId) { if (!cohortId || !patientId) return; await Cohort.updateOne({ _id: cohortId }, { $pull: { patients: patientId } }); await safeWhatsapp('remove patient', () => removePatientFromWhatsapp(cohortId, patientId)); } export async function releaseCohortPatients(cohort) { const previous = (cohort.patients || []).map((p) => String(p._id || p)); await syncCohortPatients({ ...cohort.toObject?.() || cohort, patients: [] }, previous); } // Cohort placement copied onto each patient so the record matches the cohort. export function cohortPatientFields(cohort) { const start = toYmd(cohort.startDate); return { matchingStatus: 'assigned', assignedEducator: cohort.virtualPrt?._id || cohort.virtualPrt, cohort: cohort._id, slot: cohort.slot || '', preferredWeekday: start ? weekdayOf(start) : null, sessionDate: cohort.startDate || null, sessionLanguage: cohort.language || '', diseaseType: cohort.diseaseType || '', }; } export async function applyCohortToPatients(cohort) { const ids = (cohort.patients || []).map((item) => item._id || item).filter(Boolean); if (!ids.length || !cohort._id) return; await OnboardedPatient.updateMany({ _id: { $in: ids } }, { $set: cohortPatientFields(cohort) }); } // Creates confirmed patients for a manual cohort and returns their ids. export async function createManualCohortPatients(cohort, rows, userId) { const list = (Array.isArray(rows) ? rows : []).map((row) => ({ name: String(row?.name || '').trim(), mobile: String(row?.mobile || '').trim(), age: row?.age ?? '', gender: String(row?.gender || '').trim(), whatsappOptOut: Boolean(row?.whatsappOptOut), })).filter((row) => row.name || row.mobile); if (!list.length) return []; if (list.some((row) => !row.name || !isMobileNumber(row.mobile))) throw Boom.badRequest('Each patient needs a name and a 10 digit mobile.'); const hashes = list.map((row) => hashMobile(row.mobile)); if (hashes.some((hash) => !hash)) throw Boom.badRequest('Each patient needs a mobile number.'); if (new Set(hashes).size !== hashes.length) throw Boom.badRequest('Each patient needs a different mobile.'); const taken = await OnboardedPatient.find({ mobileHash: { $in: hashes } }).select('_id'); if (taken.length) throw Boom.badRequest('A patient with that mobile is already in the system.'); const placement = cohortPatientFields(cohort); const created = []; try { for (const row of list) { const doc = await OnboardedPatient.create({ ...applyOnboardedPii({}, row), ...placement, paymentStatus: 'Paid', onboardedBy: userId, reviewStatus: 'confirmed', reviewedBy: userId, reviewedAt: new Date(), whatsappOptOut: row.whatsappOptOut, }); created.push(doc._id); } } catch (err) { if (created.length) await OnboardedPatient.deleteMany({ _id: { $in: created } }); throw err; } return created; }