// The fixture's question bank and plan template (YCI-2 to YCI-6, YCI-20). Fictional wording
// drawn from the PRD; the real bank is signed off by the clinical safety lead.
import type * as T from "../../src/api/types";
import { pronouns } from "../../src/logic/format";
type Patient = Pick<T.PatientDetail, "id" | "kind" | "name" | "firstName" | "phone" | "pronoun" | "timeZone" | "quietHours" | "blockedTimes" | "checkInLimits"> & {
doses: { taken: number; expected: number } | null;
};
const greeting = (timeZone: string) => {
const hour = Number(new Intl.DateTimeFormat("en-GB", { timeZone, hour: "2-digit", hourCycle: "h23" }).format(new Date()));
return hour < 12 ? "Good morning" : hour < 17 ? "Good afternoon" : "Good evening";
};
const suggestion = (
id: string,
text: string,
reason: string,
source: T.QuestionSource,
suggested: boolean,
followUps: string[] = [],
): T.QuestionSuggestion => ({ id: `s-${id}`, bankId: id, text, reason, source, followUps, suggested });
/** Questions from the findings, the missing factors and the bank, each with why it is there. */
export function suggestions(patient: Patient, check: T.CheckRun | null): T.QuestionSuggestion[] {
const her = pronouns(patient.pronoun).possessive;
const out: T.QuestionSuggestion[] = [];
const symptom = ["When did it start?", "How often?", "How bad is it?"];
for (const finding of check?.assessment.contextual.findings ?? []) {
const detail = check!.details[finding.id];
const title = detail?.clinicianTitle ?? finding.title;
const from = `From the finding: ${title}`;
const source: T.QuestionSource = { kind: "finding", findingId: finding.id, title };
const medicine = finding.medicines[0]?.name ?? "the medicine";
if (finding.ruleId.startsWith("allergy-")) {
const allergen = check!.assessment.includedFacts.find((f) => f.kind === "allergy")?.label.toLowerCase() ?? "it";
out.push(
suggestion("medicine-bought", `Have you bought the ${medicine}, the new antibiotic?`, from, source, true),
suggestion("allergy-history", `Your records say ${allergen} gave you a reaction. What happened?`, from, source, true, [
"How soon after?",
"Did your face or lips swell, or was breathing hard?",
"Did you go to hospital?",
]),
suggestion("medicine-started", "Have you started it?", from, source, false),
suggestion("medicine-pack", "What name is on the pack?", from, source, false),
);
} else if (finding.ruleId.startsWith("lab-metformin")) {
out.push(
suggestion("metformin-stomach", "Since starting Metformin, have you had stomach upset or loose stools?", from, source, false, symptom),
suggestion("kidney-test", "Have you had a kidney test since October?", from, source, false, ["Where?"]),
);
} else if (finding.ruleId.startsWith("lab-potassium")) {
out.push(suggestion("potassium-symptoms", "Have you had any muscle weakness, or a fast or uneven heartbeat?", from, source, true, symptom));
}
}
for (const factor of check?.missingFactors ?? [])
if (factor.id === "weight")
out.push(suggestion("weight", "Do you know your weight?", `Missing: weight · ${factor.reason.toLowerCase()}`, { kind: "missing", factor: "weight" }, false));
if (patient.doses && patient.doses.taken < patient.doses.expected)
out.push(
suggestion(
"missed-doses",
"Many people miss doses. Did you miss any this week?",
`From ${her} doses: ${patient.doses.taken} of ${patient.doses.expected} taken this week`,
{ kind: "bank", topic: "doses" },
false,
),
);
out.push(
suggestion("other-medicines", "Do you take anything from the chemist or the market, or any herbs?", "From the question bank: other medicines", { kind: "bank", topic: "other medicines" }, true, [
"For pain, like Alabukun?",
"Which agbo, and how often?",
]),
);
const seen = new Set<string>();
return out.filter((s) => !seen.has(s.bankId) && seen.add(s.bankId));
}
/** GET /patients/:id/check-ins/plan. */
export function planTemplate(
patient: Patient,
clinician: T.ClinicianProfile,
clinic: T.ClinicProfile,
check: T.CheckRun | null,
alwaysOpen = false,
eligibility: T.CheckInPlanTemplate["eligibility"] = { ok: true, code: null, message: null },
used = 0,
): T.CheckInPlanTemplate {
const doctor = clinician.spokenName;
const he = pronouns(clinician.pronoun);
const emergency = clinic.emergencyNumbers.map((n) => n.number).join(" or ");
return {
patientId: patient.id,
protocolVersion: "yci-protocol-2026-10-30-draft",
bankVersion: "yci-bank-2026-10-30-draft",
eligibility,
urgentFirst: check?.assessment.contextual.findings.some((f) => f.tier === "get_help_now")
? { phone: patient.phone, confirmLine: `I will speak to ${patient.firstName} today.` }
: null,
fairUse: { used, cap: 30 },
opening: [`${greeting(patient.timeZone)}. I'm Yasmar, MARMAR's computer assistant. I'm not a person. I'm calling for ${doctor}'s clinic.`],
identity: [`Am I speaking with ${patient.name}?`, "Please tell me your date of birth."],
consent: `Your answers go to ${doctor}'s clinic, which may read them while we talk. MARMAR keeps the text of this call for 12 months. I can't give medical advice, and this isn't an emergency line. You can skip any question or stop at any time. Shall we go on?`,
feeling: "How have you been feeling?",
feelingFollowUps: ["When did it start?", "How often?", "How bad is it?"],
suggestions: suggestions(patient, check),
alwaysLast: `Anything else ${doctor} should know?`,
closings: [
{ id: "sent", text: `Thank you. I've sent your answers to ${doctor}'s clinic.`, requires: "none", fixed: false },
{ id: "read_today", text: `I'll let ${doctor} know today.`, requires: "read_today", fixed: false },
{ id: "see_in_clinic", text: `${he.subject[0].toUpperCase()}${he.subject.slice(1)} may want to see you in clinic soon.`, requires: "none", fixed: false },
{ id: "call_by_5", text: "The clinic will call you by 5 pm.", requires: "reminder", fixed: false },
{ id: "safety_net", text: `If anything gets worse, call ${he.possessive} clinic on ${clinic.phone}; if it's closed, go to a hospital.`, requires: "none", fixed: true },
],
safetyNet: `If you have trouble breathing, chest pain, a swollen face or lips, if you faint, or feel much worse, call ${emergency} or go to the nearest hospital now.`,
window: {
patientTimeZone: patient.timeZone,
clinicTimeZone: clinic.timeZone,
clinicHours: clinic.openingHours,
alertWatchHours: clinic.alertWatchHours,
dayStart: alwaysOpen ? "00:00" : "08:00",
dayEnd: alwaysOpen ? "24:00" : "20:00",
quietHours: alwaysOpen ? { start: "00:00", end: "00:00" } : patient.quietHours,
blockedTimes: patient.blockedTimes,
defaultExpiryHours: 24,
maxExpiryHours: 72,
known: patient.kind === "connected",
},
limits: patient.checkInLimits,
maxQuestions: 10,
maxOwnQuestions: 3,
maxQuestionLength: 200,
};
}