VirtualPR/backend/src/services/cohorts.js

136 lines
5.2 KiB
JavaScript

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