marmar-context / web / context / test / fixture / routes-patients.ts
routes-patients.ts
Raw
// Fixture routes for invitations, patients, shared data, records, reading and checks.
import { randomUUID } from "node:crypto";
import type * as T from "../../src/api/types";
import { modelProvider, transcribe } from "../../server/model";
import { exportLines, printableHtml, readRecords, runCheckRun, toSources, type StoredExtraction } from "./clinical";
import { connectMariam, iso, nextId, type StoredPatient, type StoredRecord } from "./data";
import { clinicianName, HttpError, invalid, need, route, text, type Ctx, type Route } from "./http";
import { logActivity, patientDetail, recordsFor, renamedDeep, type Store } from "./store";
import { fingerprint } from "../../server/context";
import { wireCheck, wireDetail, wireExtraction, wireInvitation, wireItem, wireRecord, wireTile } from "./wire";

const MB = 1024 * 1024;
const ACCEPT: Record<string, string> = {
  "text/plain": "TXT",
  "application/pdf": "PDF",
  "image/jpeg": "JPG",
  "image/png": "PNG",
  "image/heic": "HEIC",
};

const patientOf = (ctx: Ctx): StoredPatient => need(ctx.store.patients.get(decodeURIComponent(ctx.params[0])), "That patient");
const isStale = (store: Store, ex: StoredExtraction) => fingerprint(toSources(recordsFor(store, ex.patientId))) !== ex.fingerprint;
const publicExtraction = (store: Store, ex: StoredExtraction) => wireExtraction(ex, isStale(store, ex));
const lastCheckAt = (store: Store, p: StoredPatient) => (p.base.lastCheckId ? store.checks.get(p.base.lastCheckId)?.ranAt ?? null : null);
const latestCheck = (store: Store, check: T.CheckRun) => (store.patientChecks.get(check.patientId) ?? []).at(-1) === check.id;
// The backend refuses (409 PATIENT_NOT_VISIBLE / 403) shared reads for a patient that is not connected to this clinician.
const sharedPatient = (ctx: Ctx) => {
  const p = patientOf(ctx);
  if (p.base.kind !== "connected") throw new HttpError(404, "PATIENT_NOT_VISIBLE", "This patient is not shared with you.");
  return p;
};

function shared(ctx: Ctx, section: T.SharedSection | null) {
  const p = sharedPatient(ctx);
  const s = p.base.sharing;
  if (!s || s.status === "stopped" || !p.shared) throw new HttpError(403, "SHARING_STOPPED", "This patient stopped sharing with you.");
  if (s.status === "paused") throw new HttpError(403, "SHARING_PAUSED", "This patient paused sharing. New information is hidden until they resume.");
  if (section && !s.scopes.includes(section)) throw new HttpError(403, "SCOPE_NOT_SHARED", "This patient does not share this with you.");
  // As the backend: the previous look at this section, then this one is recorded.
  const lastLookedAt = section ? p.lastLooked[`shared.${section}`] ?? null : null;
  if (section) p.lastLooked[`shared.${section}`] = new Date().toISOString();
  return { p, asOf: new Date().toISOString(), lastLookedAt };
}

function addRecord(store: Store, p: StoredPatient, fields: Omit<StoredRecord, "id" | "patientId" | "addedAt" | "addedBy">): StoredRecord {
  const rec: StoredRecord = { ...fields, id: nextId("rec"), patientId: p.base.id, addedAt: new Date().toISOString(), addedBy: clinicianName(store) };
  store.records.set(rec.id, rec);
  return rec;
}

export const patientRoutes: Route[] = [
  route("GET", "/invitations", "patients", ({ store }) => ({
    invitations: [...store.invitations.values()].map(({ token: _t, connect: _c, ...inv }) => wireInvitation(inv)),
  })),
  route("POST", "/invitations/:id/accept", "patients", (ctx) => {
    const inv = need(ctx.store.invitations.get(ctx.params[0]), "That invitation");
    if (ctx.body?.confirmedMyPatient !== true) throw invalid("Check the highlighted fields.", [{ path: "confirmedMyPatient", message: "Invalid input: expected true" }]);
    const link = typeof ctx.body.linkToPatientId === "string" ? ctx.body.linkToPatientId : null;
    let patient: StoredPatient;
    if (inv.id === "inv-mariam") {
      const existing = ctx.store.patients.get("p-mariam")!;
      if (link === "p-mariam") patient = existing;
      else {
        patient = { ...existing, base: { ...existing.base, id: "p-mariam-app" }, activity: [] };
        ctx.store.patients.set(patient.base.id, patient);
      }
      const appRecord = renamedDeep(connectMariam(patient, Date.now()));
      appRecord.id = `rec-${patient.base.id}-app`;
      appRecord.patientId = patient.base.id;
      ctx.store.records.set(appRecord.id, appRecord);
      patient.newSinceLastCheck = true;
      patient.base.newInformation = { since: new Date().toISOString(), items: ["Medicines, allergies and doses from MARMAR Health"] };
    } else {
      patient = inv.connect();
      ctx.store.patients.set(patient.base.id, patient);
    }
    const by = inv.withPerson ? `${inv.patientName.split(" ")[0]} and ${inv.withPerson.name}` : inv.patientName.split(" ")[0];
    logActivity(patient, by, "patient", `${by} invited Dr Okafor from MARMAR Health.`);
    logActivity(patient, clinicianName(ctx.store), "clinician", `Dr Okafor accepted and confirmed ${inv.patientName} is his patient${link ? ", linked to the clinic record" : ""}.`);
    ctx.store.invitations.delete(inv.id);
    return { patientId: patient.base.id, shareId: inv.id };
  }),
  route("POST", "/invitations/:id/decline", "patients", (ctx) => {
    const inv = need(ctx.store.invitations.get(ctx.params[0]), "That invitation");
    const reason = ctx.body?.reason ?? null;
    if (reason !== null && !["not_my_patient", "not_for_context", "not_using_context", "other"].includes(reason)) throw invalid("Check the highlighted fields.", [{ path: "reason", message: "Invalid option" }]);
    ctx.store.invitations.delete(inv.id);
    return { declined: true };
  }),

  route("GET", "/patients", "patients", ({ store }) => ({
    items: [...store.patients.values()].map((p) => wireItem(patientDetail(store, p), lastCheckAt(store, p))),
    asOf: new Date().toISOString(),
  })),
  route("POST", "/patients/search", "patients", (ctx) => {
    const q = text(ctx.body?.query, 120).toLowerCase();
    if (!q) throw invalid("Check the highlighted fields.", [{ path: "query", message: "Too small" }]);
    return {
      items: [...ctx.store.patients.values()].filter((p) => p.base.name.toLowerCase().includes(q)).map((p) => wireItem(patientDetail(ctx.store, p), lastCheckAt(ctx.store, p))),
      asOf: new Date().toISOString(),
    };
  }),
  route("POST", "/patients", "patients", (ctx) => {
    const name = text(ctx.body?.displayName, 120);
    if (name.length < 2) throw invalid("Check the highlighted fields.", [{ path: "displayName", message: "Too small" }]);
    const a = ctx.body?.attestation;
    if (a?.careRelationship !== true || a?.patientAgreement !== true || a?.version !== "2026-10-30")
      throw invalid("Check the highlighted fields.", [{ path: "attestation", message: "Confirm the care relationship and the patient's agreement" }]);
    const dob = typeof ctx.body.dateOfBirth === "string" ? ctx.body.dateOfBirth : null;
    if (!dob === (ctx.body.ageYears === undefined)) throw invalid("Check the highlighted fields.", [{ path: "dateOfBirth", message: "Give a date of birth or an age" }]);
    const id = `p-${randomUUID().slice(0, 8)}`;
    const p: StoredPatient = {
      base: {
        id, kind: "clinic_added", name, firstName: name.split(" ")[0],
        age: dob ? Math.floor((Date.now() - Date.parse(dob)) / (365.25 * 86_400_000)) : Number(ctx.body.ageYears),
        sharing: null, lastActivityAt: new Date().toISOString(), dateOfBirth: dob, birthYear: dob ? Number(dob.slice(0, 4)) : null,
        sex: ctx.body.sex === "female" || ctx.body.sex === "male" ? ctx.body.sex : null, phone: null, pronoun: null, preferredTitle: null,
        language: "en", hasOwnAccount: false, identityOnFile: dob ? "date_of_birth" : "none", timeZone: "Africa/Lagos",
        quietHours: { start: "22:00", end: "07:00" }, blockedTimes: [], lastCheckId: null, newInformation: null,
      },
      shared: null, lastDaily: null, lastLooked: {}, newSinceLastCheck: false, usedThisWeekBefore: 0, variant: "film", activity: [],
    };
    logActivity(p, clinicianName(ctx.store), "clinician", `Dr Okafor added ${name} and confirmed a care relationship and the patient's agreement.`);
    ctx.store.patients.set(id, p);
    // As the backend: 201 with the bare patient row, not the detail.
    const { attention: _a, lastCheckId: _l, newInformation: _n, sharing: _s, medicines: _m, doses: _d, lastCheckIn: _c, name: _name, age: _age, ...row } = wireItem(patientDetail(ctx.store, p));
    return { patient: row };
  }, 201),
  route("GET", "/patients/:id", "patients", (ctx) => {
    const p = patientOf(ctx);
    return { patient: wireDetail(patientDetail(ctx.store, p), lastCheckAt(ctx.store, p)) };
  }),
  route("GET", "/patients/:id/shared", "patients", (ctx) => wireTile(patientDetail(ctx.store, shared(ctx, null).p))),
  route("POST", "/patients/:id/viewed", "patients", (ctx) => {
    const p = patientOf(ctx);
    const section = text(ctx.body?.section, 20);
    if (!["medicines", "doses", "check_ins", "records", "overview"].includes(section)) throw invalid("Check the highlighted fields.", [{ path: "section", message: "Invalid option" }]);
    const lastLookedAt = p.lastLooked[`viewed.${section}`] ?? null;
    p.lastLooked[`viewed.${section}`] = new Date().toISOString();
    return { lastLookedAt };
  }),
  route("GET", "/patients/:id/activity", "patients", (ctx) => ({ events: [...patientOf(ctx).activity].reverse() })),

  route("GET", "/patients/:id/shared/medicines", "patients", (ctx) => {
    const { p, asOf, lastLookedAt } = shared(ctx, "medicines");
    return { ...p.shared!.medicines, asOf, lastLookedAt };
  }),
  route("GET", "/patients/:id/shared/doses", "patients", (ctx) => {
    const { p, asOf, lastLookedAt } = shared(ctx, "doses");
    return { ...p.shared!.doses, asOf, lastLookedAt, complete: true };
  }),
  route("GET", "/patients/:id/shared/check-ins", "patients", (ctx) => {
    const { p, asOf, lastLookedAt } = shared(ctx, "check_ins");
    return { daily: p.shared!.daily, requested: [], asOf, lastLookedAt };
  }),
  route("GET", "/patients/:id/shared/records", "patients", (ctx) => {
    const { p, asOf, lastLookedAt } = shared(ctx, "records");
    return { ...p.shared!.records, asOf, lastLookedAt };
  }),

  route("GET", "/patients/:id/records", "patients", (ctx) => ({
    records: recordsFor(ctx.store, patientOf(ctx).base.id).map(wireRecord),
  })),
  route("GET", "/records/:id/lines", "patients", (ctx) => {
    const rec = need(ctx.store.records.get(ctx.params[0]), "That record");
    return {
      recordId: rec.id,
      lines: (rec.text ?? "").split("\n").map((line, i) => ({ number: i + 1, text: line, page: null, box: null, confidence: null, hardToRead: false, handwritten: false })),
    };
  }),
  route("PATCH", "/records/:id", "patients", (ctx) => {
    const rec = need(ctx.store.records.get(ctx.params[0]), "That record");
    if (rec.origin === "app" || rec.origin === "check_in") throw new HttpError(403, "READ_ONLY", "Records from MARMAR Health and check-ins cannot be edited.");
    const title = text(ctx.body?.title, 120);
    if (ctx.body?.title !== undefined && !title) throw invalid("A record needs a title.");
    if (title) rec.title = title;
    if (ctx.body?.type) rec.type = ctx.body.type;
    if (ctx.body?.place !== undefined) rec.place = text(ctx.body.place, 120) || null;
    if (ctx.body?.recordDate !== undefined) rec.recordDate = text(ctx.body.recordDate, 10) || null;
    return wireRecord(rec);
  }),
  route("POST", "/records/:id/confirm-patient", "patients", (ctx) => {
    const rec = need(ctx.store.records.get(ctx.params[0]), "That record");
    if (!rec.heldBack) throw new HttpError(409, "NOT_HELD_BACK", "This record is not held back.");
    rec.heldBack = null;
    return wireRecord(rec);
  }),
  route("POST", "/records/:id/read-again", "patients", (ctx) => {
    const rec = need(ctx.store.records.get(ctx.params[0]), "That record");
    if (rec.reading !== "failed") throw new HttpError(409, "NOT_FAILED", "Only a record that could not be read is read again.");
    return wireRecord(rec);
  }),
  route("DELETE", "/records/:id", "patients", (ctx) => {
    const rec = need(ctx.store.records.get(ctx.params[0]), "That record");
    if (rec.origin === "app") throw new HttpError(403, "READ_ONLY", "Records from MARMAR Health are removed in the app.");
    ctx.store.records.delete(rec.id);
    return { deleted: true, removedAt: new Date().toISOString(), reading: "unchanged" };
  }),
  route("POST", "/patients/:id/records/uploads", "patients", (ctx) => {
    const p = patientOf(ctx);
    const fileName = text(ctx.body?.fileName, 200);
    const size = Number(ctx.body?.sizeBytes);
    const type = text(ctx.body?.contentType, 100);
    if (!ACCEPT[type]) throw invalid(`${fileName} is not a file Context reads. Use TXT, PDF, JPG, PNG or HEIC.`);
    if (size > 15 * MB)
      throw new HttpError(400, "FILE_TOO_LARGE", `${fileName} is ${(size / MB).toFixed(1)} MB, ${((size - 15 * MB) / MB).toFixed(1)} MB over the 15 MB limit per file.`);
    const uploadId = randomUUID();
    ctx.store.uploads.set(uploadId, { patientId: p.base.id, fileName, contentType: type, sizeBytes: size, data: null });
    return { uploadId, uploadUrl: `${ctx.url.origin}/__fixture/uploads/${uploadId}`, method: "PUT", headers: { "Content-Type": type }, expiresAt: iso(Date.now() + 15 * 60_000), maxBytes: 15 * MB };
  }, 201),
  route("POST", "/patients/:id/records", "patients", async (ctx) => {
    const p = patientOf(ctx);
    if (recordsFor(ctx.store, p.base.id).length >= 20)
      throw new HttpError(400, "TOO_MANY_RECORDS", "A check can use up to 20 records. Remove one before adding another.");
    const title = text(ctx.body?.title, 120) || "Untitled record";
    const type = ["prescription", "lab_result", "discharge_summary", "clinic_note", "other"].includes(ctx.body?.type) ? ctx.body.type : "other";
    const common = { title, type, place: text(ctx.body?.place, 120) || null, recordDate: text(ctx.body?.recordDate, 10) || null, pages: null };
    let rec: StoredRecord;
    if (ctx.body?.uploadId) {
      const up = need(ctx.store.uploads.get(ctx.body.uploadId), "That upload");
      if (!up.data) throw invalid("The file has not finished uploading. Try again.");
      const isText = up.contentType === "text/plain";
      // Photos and PDFs: Meta Muse Spark transcribes them when it is the configured model.
      const byModel = !isText && modelProvider() === "meta";
      const body = isText ? up.data.toString("utf8").replace(/\r\n/g, "\n").trim() : byModel ? await transcribe(up.data, up.contentType) : null;
      const read = isText || byModel;
      rec = addRecord(ctx.store, p, {
        ...common, origin: ctx.body.origin === "camera" ? "camera" : "upload", fileName: up.fileName, contentType: up.contentType, sizeBytes: up.sizeBytes,
        reading: read ? "read" : "failed",
        readingError: read ? null : "The fixture reads text files only. Scans and photos are read on MARMAR's servers.",
        readingErrorCode: read ? null : "OCR_NOT_CONFIGURED", heldBack: null, clarification: null,
        lineCount: body ? body.split("\n").length : 0, text: body,
      });
      ctx.store.uploads.delete(ctx.body.uploadId);
    } else {
      const body = typeof ctx.body?.text === "string" ? ctx.body.text.replace(/\r\n/g, "\n").trim() : "";
      if (!body) throw invalid("Paste or type the record's text.");
      if (body.length > 12000) throw invalid("This note is longer than 12,000 characters. Split it into smaller records.");
      rec = addRecord(ctx.store, p, { ...common, origin: "pasted", fileName: null, contentType: "text/plain", sizeBytes: body.length, reading: "read", readingError: null,
        readingErrorCode: null, heldBack: null, clarification: null, lineCount: body.split("\n").length, text: body });
    }
    logActivity(p, clinicianName(ctx.store), "clinician", `Dr Okafor added the record "${rec.title}".`);
    return wireRecord(rec);
  }, 201),

  route("POST", "/patients/:id/extractions", "patients", async (ctx) => {
    const p = patientOf(ctx);
    const records = recordsFor(ctx.store, p.base.id);
    if (!records.some((r) => r.reading === "read")) throw new HttpError(400, "NO_READABLE_RECORDS", "Add at least one readable record first.");
    const ex = await readRecords(p.base.id, records);
    const previous = ctx.store.extractions.get(ctx.store.latestExtraction.get(p.base.id) ?? "");
    if (previous) // Keep decisions for facts that read exactly the same.
      for (const f of ex.facts) {
        const old = previous.facts.find((o) => o.id === f.id && o.label === f.label && o.detail === f.detail && o.quote === f.quote);
        if (old && previous.decisions[old.id]) ex.decisions[f.id] = previous.decisions[old.id];
      }
    ctx.store.extractions.set(ex.id, ex);
    ctx.store.latestExtraction.set(p.base.id, ex.id);
    return publicExtraction(ctx.store, ex);
  }, 201),
  route("GET", "/patients/:id/extractions/latest", "patients", (ctx) => {
    const ex = ctx.store.extractions.get(ctx.store.latestExtraction.get(patientOf(ctx).base.id) ?? "");
    return { extraction: ex ? publicExtraction(ctx.store, ex) : null };
  }),
  route("GET", "/extractions/:id", "patients", (ctx) => publicExtraction(ctx.store, need(ctx.store.extractions.get(ctx.params[0]), "That reading"))),
  route("PUT", "/extractions/:id/decisions", "patients", (ctx) => {
    const ex = need(ctx.store.extractions.get(ctx.params[0]), "That reading");
    if (ctx.body?.revision !== ex.revision)
      throw new HttpError(409, "STALE_REVISION", "This review was changed in another tab. Load the latest version before you go on.");
    const confirmed: string[] = Array.isArray(ctx.body?.confirmedHardToRead) ? ctx.body.confirmedHardToRead : [];
    const decisions = ctx.body?.decisions ?? {};
    if (confirmed.some((id) => !ex.facts.some((f) => f.id === id && f.hardToRead) || decisions[id] !== "include"))
      throw invalid("Only an included, hard-to-read fact can be confirmed.");
    if (Object.entries(decisions).some(([id, d]) => !ex.facts.some((f) => f.id === id) || (d !== "include" && d !== "exclude")))
      throw invalid("A decision names a fact outside this reading.");
    ex.decisions = decisions;
    ex.coverageReview = Object.fromEntries(
      Object.entries(ctx.body?.coverageReview ?? {}).map(([id, v]) => [id, { ...(v as object), confirmedBy: clinicianName(ctx.store) }]),
    ) as T.Extraction["coverageReview"];
    ex.acknowledgedOriginals = ctx.body?.acknowledgedOriginals === true;
    ex.confirmedHardToRead = confirmed;
    ex.revision += 1;
    return { revision: ex.revision, savedAt: new Date().toISOString() };
  }),

  route("POST", "/extractions/:id/clarifications", "patients", async (ctx) => {
    const ex = need(ctx.store.extractions.get(ctx.params[0]), "That reading");
    const p = need(ctx.store.patients.get(ex.patientId), "That patient");
    const original = need(ctx.store.records.get(text(ctx.body?.recordId, 80)), "That record");
    const line = Number(ctx.body?.line);
    const quote = (original.text ?? "").split("\n")[line - 1]?.trim();
    const value = text(ctx.body?.text, 500);
    const reason = text(ctx.body?.reason, 500);
    if (!quote || quote !== text(ctx.body?.quote, 12000)) throw invalid("The original line changed. Read the records again.");
    if (!value) throw invalid("Write the fact you are adding.");
    if (reason.length < 3) throw invalid("Say how you confirmed it.");
    addRecord(ctx.store, p, {
      title: `Clarification · line ${line} of ${original.title}`, type: "clinic_note", place: null, recordDate: original.recordDate, pages: null,
      origin: "pasted", fileName: null, contentType: "text/plain", sizeBytes: value.length, reading: "read", readingError: null, readingErrorCode: null,
      heldBack: null, clarification: { clarificationId: nextId("clar") }, lineCount: 1, text: value,
      clarifies: { sourceId: original.id, line, quote, reason, confirmedBy: clinicianName(ctx.store) },
    });
    logActivity(p, clinicianName(ctx.store), "clinician", `Dr Okafor added a clarification to "${original.title}".`);
    const next = await readRecords(p.base.id, recordsFor(ctx.store, p.base.id));
    for (const f of next.facts) if (ex.decisions[f.id] && ex.facts.some((o) => o.id === f.id && o.label === f.label && o.detail === f.detail)) next.decisions[f.id] = ex.decisions[f.id];
    ctx.store.extractions.set(next.id, next);
    ctx.store.latestExtraction.set(p.base.id, next.id);
    return publicExtraction(ctx.store, next);
  }, 201),
  route("POST", "/patients/:id/checks", "patients", (ctx) => {
    const p = patientOf(ctx);
    const ex = need(ctx.store.extractions.get(text(ctx.body?.extractionId, 80)), "That reading");
    if (ex.recordsNotReady.length)
      throw new HttpError(409, "RECORDS_NOT_READY", `Some records are not ready for the check: ${ex.recordsNotReady.map((r) => `${r.title} (${r.status.replace("_", " ")})`).join(", ")}.`);
    if (isStale(ctx.store, ex)) throw new HttpError(409, "STALE_EXTRACTION", "The records changed after they were read. Read them again.");
    if (ex.facts.some((f) => f.hardToRead && ex.decisions[f.id] === "include" && !ex.confirmedHardToRead.includes(f.id)))
      throw new HttpError(400, "HARD_TO_READ_NOT_CONFIRMED", "Check each included Hard to read value against the original first.");
    if (ctx.body?.revision !== ex.revision) throw new HttpError(409, "STALE_REVISION", "This review was changed in another tab. Load the latest version first.");
    let check: T.CheckRun;
    const newMeds = (p.shared?.medicines.medicines ?? []).filter((m) => m.startDate && Date.now() - Date.parse(m.startDate) < 21 * 86_400_000).map((m) => m.name.toLowerCase());
    try {
      const reactions = Object.fromEntries((p.shared?.medicines.allergies ?? []).map((a) => [a.substance.toLowerCase(), { reaction: a.reaction, onset: a.onset, year: a.year }]));
      check = runCheckRun(ex, recordsFor(ctx.store, p.base.id), clinicianName(ctx.store), !!p.base.sharing?.withheldSome, newMeds, reactions);
    } catch (error) {
      throw invalid(error instanceof Error ? error.message : "The check could not run.");
    }
    ctx.store.checks.set(check.id, check);
    ctx.store.patientChecks.set(p.base.id, [...(ctx.store.patientChecks.get(p.base.id) ?? []), check.id]);
    p.base.lastCheckId = check.id;
    p.newSinceLastCheck = false;
    p.base.newInformation = null;
    logActivity(p, clinicianName(ctx.store), "clinician", `Dr Okafor ran the check: ${check.assessment.contextual.findings.length} findings for review.`);
    return wireCheck(check, true);
  }, 201),
  route("GET", "/patients/:id/checks", "patients", (ctx) => ({
    checks: (ctx.store.patientChecks.get(patientOf(ctx).base.id) ?? [])
      .map((id) => ctx.store.checks.get(id)!)
      .map(({ id, ranAt, ranBy, assessment }) => ({ id, ranAt, ranBy, findings: assessment.contextual.findings.length, added: assessment.addedRuleIds.length }))
      .reverse(),
  })),
  route("GET", "/checks/:id", "patients", (ctx) => {
    const check = need(ctx.store.checks.get(ctx.params[0]), "That check");
    return wireCheck(check, latestCheck(ctx.store, check));
  }),
  route("POST", "/checks/:id/findings/:finding/mark", "patients", (ctx) => {
    const check = need(ctx.store.checks.get(ctx.params[0]), "That check");
    const findingId = decodeURIComponent(ctx.params[1]);
    if (!check.assessment.contextual.findings.some((f) => f.id === findingId)) throw new HttpError(404, "FINDING_NOT_FOUND", "That finding is not part of this check.");
    const kind = ctx.body?.kind;
    if (!["reviewed", "not_relevant", "problem"].includes(kind)) throw invalid("Choose Reviewed, Not relevant or Report a problem.");
    const reason = text(ctx.body?.reason, 500) || null;
    const note = text(ctx.body?.note, 1000) || null;
    if (kind === "not_relevant" && (!reason || reason.length < 3)) throw new HttpError(400, "REASON_REQUIRED", "Say why this finding is not relevant.");
    if (kind === "problem" && !note && !reason) throw new HttpError(400, "DESCRIPTION_REQUIRED", "Describe the problem for MARMAR's clinical team.");
    const mark: T.FindingMark = { findingId, kind, note, reason, by: clinicianName(ctx.store), at: new Date().toISOString() };
    check.marks.push(mark);
    const p = ctx.store.patients.get(check.patientId);
    if (p) logActivity(p, mark.by, "clinician", `Dr Okafor marked a finding ${kind.replace("_", " ")}.`);
    return mark;
  }, 201),
  route("GET", "/checks/:id/export", "patients", (ctx) => {
    const check = need(ctx.store.checks.get(ctx.params[0]), "That check");
    const p = need(ctx.store.patients.get(check.patientId), "That patient");
    const format = ctx.url.searchParams.get("format") ?? "json";
    if (!["json", "html", "pdf"].includes(format)) throw invalid("Check the highlighted fields.", [{ path: "format", message: "Invalid option" }]);
    logActivity(p, clinicianName(ctx.store), "clinician", `Dr Okafor exported the check as ${format.toUpperCase()}.`);
    const name = `marmar-context-${p.base.name.toLowerCase().replace(/\s+/g, "-")}-${check.ranAt.slice(0, 10)}`;
    // As the backend: pdf and html both answer the printable page.
    if (format !== "json") {
      const html = printableHtml("MARMAR Context: check evidence", exportLines(check, p.base.name, check.ranBy, recordsFor(ctx.store, p.base.id)));
      ctx.res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Disposition": `inline; filename="${name}.html"`, "X-Export-Format": "html",
        "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" });
      ctx.res.end(html);
      return undefined;
    }
    ctx.res.writeHead(200, { "Content-Type": "application/json", "Content-Disposition": `attachment; filename="${name}.json"` });
    ctx.res.end(JSON.stringify({ product: "MARMAR Context", schemaVersion: 1, exportedAt: new Date().toISOString(), exportedBy: clinicianName(ctx.store), patient: { name: p.base.name, age: p.base.age, dateOfBirth: p.base.dateOfBirth },
      clinic: { name: ctx.store.clinic.name }, check: wireCheck(check, latestCheck(ctx.store, check)), informationTool: check.informationTool }, null, 2));
    return undefined;
  }),
];