marmar-context / web / context / test / fixture / data.ts
data.ts
Raw
// Local browser QA only. Every person, clinic, number and record here is fictional.
import type * as T from "../../src/api/types";
import type { Source } from "../../src/types";
import { ADAEZE_RECORDS, MARIAM_APP_RECORD, MARIAM_RECORDS, TUNDE_RECORDS } from "./records";

export const FIXTURE_EMAIL = "emeka.okafor@example.test";
/** The fixture's email code and second-factor code. Fixture-only values. */
export const FIXTURE_EMAIL_CODE = "246810";
export const FIXTURE_TOTP_CODE = "135790";
export const FIXTURE_TOTP_SECRET = "JBSWY3DPEHPK3PXP";

/** clarifies: the original line a clarification record is linked to (REV-5). */
export type StoredRecord = T.ContextRecord & { text: string | null; clarifies?: Source["clarification"] };
export type Shared = {
  medicines: Omit<T.SharedMedicines, "asOf" | "lastLookedAt">;
  doses: Omit<T.SharedDoses, "asOf" | "lastLookedAt">;
  daily: T.DailyCheckIn[];
  records: Omit<T.SharedRecords, "asOf" | "lastLookedAt">;
};
export type StoredPatient = {
  base: Omit<
    T.PatientDetail,
    "medicines" | "doses" | "attention" | "checkInLimits" | "activeCheckInId" | "lastCheckIn"
  >;
  shared: Shared | null;
  lastDaily: string | null;
  lastLooked: Record<string, string | null>;
  newSinceLastCheck: boolean;
  usedThisWeekBefore: number;
  variant: "film" | "urgent";
  activity: T.ActivityEvent[];
};

const DAY = 86_400_000;
export const iso = (ms: number) => new Date(ms).toISOString();
let counter = 0;
export const nextId = (prefix: string) => `${prefix}-${Date.now().toString(36)}${(counter++).toString(36)}`;

const weekdays = (open: string, close: string, saturday?: [string, string]) => [
  ...[1, 2, 3, 4, 5].map((day) => ({ day, open, close })),
  ...(saturday ? [{ day: 6, open: saturday[0], close: saturday[1] }] : []),
];
const allDay = [0, 1, 2, 3, 4, 5, 6].map((day) => ({ day, open: "00:00", close: "24:00" }));

export function clinicianSeed(): T.ClinicianProfile {
  return {
    id: "clin-okafor",
    email: FIXTURE_EMAIL,
    title: "Dr",
    fullName: "Dr Emeka Okafor",
    firstName: "Emeka",
    lastName: "Okafor",
    spokenName: "Doctor Okafor",
    pronoun: "he",
    profession: "doctor",
    licensingBody: "MDCN",
    licenceNumber: "MDCN/R/104233",
    licenceExpiresAt: "2027-03-31",
    verifiedAt: "2026-10-01T09:00:00Z",
    photoUrl: null,
    mobilePhone: "+234 803 555 0101",
    idleLockMinutes: 15,
    notifications: { browser: false, email: true },
    termsAcceptedAt: null,
  };
}

export function clinicSeed(alwaysOpen: boolean): T.ClinicProfile {
  return {
    id: "clinic-surulere",
    role: "owner",
    version: 1,
    name: "Surulere Family Clinic",
    address: "12 Example Close, Surulere, Lagos",
    phone: "+234 1 555 0142",
    country: "NG",
    timeZone: "Africa/Lagos",
    openingHours: alwaysOpen ? allDay : weekdays("08:00", "18:00", ["09:00", "14:00"]),
    alertWatchHours: alwaysOpen ? allDay : weekdays("08:00", "18:00", ["09:00", "14:00"]),
    backupContact: { name: "Nurse Bisi Ade", phone: "+2348035550199", email: "bisi.ade@surulere-clinic.example.test" },
    emergencyNumbers: [
      { number: "767", label: "Lagos State emergency", testedAt: "2026-10-01T10:00:00Z", source: "clinic" },
      { number: "112", label: "National emergency", testedAt: "2026-10-01T10:05:00Z", source: "clinic" },
    ],
  };
}

function base(
  id: string,
  name: string,
  kind: T.PatientKind,
  dob: string | null,
  now: number,
  change: Partial<StoredPatient["base"]> = {},
): StoredPatient["base"] {
  const age = dob ? Math.floor((now - Date.parse(dob)) / (365.25 * DAY)) : null;
  return {
    id,
    kind,
    name,
    firstName: name.split(" ")[0],
    age,
    sharing: null,
    lastActivityAt: iso(now - 3 * DAY),
    dateOfBirth: dob,
    birthYear: dob ? Number(dob.slice(0, 4)) : null,
    sex: null,
    phone: null,
    pronoun: null,
    preferredTitle: null,
    language: "en",
    hasOwnAccount: kind === "connected",
    identityOnFile: dob ? "date_of_birth" : "none",
    timeZone: "Africa/Lagos",
    quietHours: { start: "22:00", end: "07:00" },
    blockedTimes: [],
    lastCheckId: null,
    newInformation: null,
    ...change,
  };
}

const sharing = (
  sharedBy: string[],
  scopes: T.ShareScope[],
  calls: boolean,
  changedAt: string,
  status: T.SharingStatus = "connected",
): T.Sharing => ({ status, sharedBy, scopes, allowsCheckInCalls: calls, withheldSome: false, changedAt });

const counts = (onTime: number, late: number, skipped: number, missed: number, expected: number): T.DoseCounts => ({
  onTime,
  late,
  skipped,
  missed,
  expected,
});

/** Mariam's shared data once she is connected (SHR-2 to SHR-5). */
export function mariamShared(now: number): Shared {
  const by = (who: string, daysAgo: number) => ({ addedBy: who, addedAt: iso(now - daysAgo * DAY), changedAt: iso(now - daysAgo * DAY) });
  return {
    medicines: {
      withheldSome: false,
      medicines: [
        { id: "m-metformin", name: "Metformin", strength: "500 mg", schedule: "1 tablet twice a day", startDate: "2024-03-10", purpose: "Diabetes", origin: "prescribed", status: "current", stoppedAt: null, prescriber: "Dr Emeka Okafor", ...by("Funke", 28) },
        { id: "m-amlodipine", name: "Amlodipine", strength: "5 mg", schedule: "1 tablet once a day", startDate: "2023-11-01", purpose: "Blood pressure", origin: "prescribed", status: "current", stoppedAt: null, prescriber: null, ...by("Mariam", 60) },
        { id: "m-amoxicillin", name: "Amoxicillin", strength: "500 mg", schedule: "1 capsule three times a day", startDate: "2026-10-23", purpose: "Chest infection", origin: "prescribed", status: "current", stoppedAt: null, prescriber: "Dr Emeka Okafor", ...by("Funke", 7) },
      ],
      allergies: [
        { id: "a-penicillin", substance: "Penicillin", reaction: "Hives", onset: "Within a day", severity: null, year: 2019, treatment: null, recordedWhere: "Ikeja hospital", ...by("Funke", 28) },
      ],
      conditions: [
        { id: "c-diabetes", name: "Type 2 diabetes", ...by("Funke", 28) },
        { id: "c-bp", name: "Hypertension", ...by("Funke", 28) },
      ],
      profile: { dateOfBirth: "1959-03-04", age: 67, sex: "female", weightKg: null, weightRecordedAt: null, pregnancy: null },
    },
    doses: {
      week: { taken: 12, expected: 14, onTime: 12 },
      perMedicine: [
        { medicineId: "m-metformin", name: "Metformin", schedule: "Morning and evening", week: counts(12, 0, 2, 0, 14), last30: counts(51, 3, 6, 0, 60), changedAt: iso(now - 0.4 * DAY) },
      ],
      patterns: [{ text: "Evening Metformin skipped 2 times this week", medicineId: "m-metformin", changedAt: iso(now - 0.4 * DAY) }],
    },
    daily: [
      { id: "d1", date: iso(now - DAY).slice(0, 10), mood: "Okay", energy: "Low", sleep: "Fair", sideEffects: ["Loose stools"], note: "Stomach running since Sunday.", changedAt: iso(now - DAY) },
      { id: "d2", date: iso(now - 3 * DAY).slice(0, 10), mood: "Good", energy: "Fair", sleep: "Good", sideEffects: [], note: null, changedAt: iso(now - 3 * DAY) },
    ],
    records: {
      labs: [
        { id: "l-egfr", name: "eGFR", value: "38", unit: "mL/min/1.73m2", range: "Above 60", flag: "low", date: "2026-10-12", ...by("Funke", 17) },
        { id: "l-egfr-aug", name: "eGFR", value: "47", unit: "mL/min/1.73m2", range: "Above 60", flag: "low", date: "2026-08-14", ...by("Funke", 78) },
        { id: "l-egfr-apr", name: "eGFR", value: "58", unit: "mL/min/1.73m2", range: "Above 60", flag: "low", date: "2026-04-03", ...by("Funke", 211) },
        { id: "l-hba1c", name: "HbA1c", value: "7.9", unit: "%", range: "Below 7.0", flag: "high", date: "2026-09-15", ...by("Funke", 40) },
        { id: "l-hba1c-jun", name: "HbA1c", value: "7.4", unit: "%", range: "Below 7.0", flag: "high", date: "2026-06-10", ...by("Funke", 143) },
      ],
      documents: [
        { id: "doc-yaba", title: "Yaba lab kidney report", kind: "Lab report", date: "2026-10-12", ...by("Funke", 17) },
        { id: "doc-ikeja", title: "Ikeja hospital discharge letter", kind: "Discharge letter", date: "2026-09-02", ...by("Funke", 40) },
      ],
    },
  };
}

function simpleShared(now: number, meds: [string, string, string][], taken: number, expected: number): Shared {
  const at = iso(now - 2 * DAY);
  return {
    medicines: {
      withheldSome: false,
      medicines: meds.map(([name, strength, schedule], i) => ({
        id: `m-${name.toLowerCase()}`, name, strength, schedule, startDate: "2025-01-10", purpose: null, origin: "prescribed" as const,
        status: "current" as const, stoppedAt: null, prescriber: null, addedBy: "Patient", addedAt: iso(now - (20 + i) * DAY), changedAt: iso(now - (20 + i) * DAY),
      })),
      allergies: [],
      conditions: [],
      profile: { dateOfBirth: null, age: null, sex: null, weightKg: null, weightRecordedAt: null, pregnancy: null },
    },
    doses: {
      week: { taken, expected, onTime: taken },
      perMedicine: meds.map(([name], i) => ({ medicineId: `m-${name.toLowerCase()}`, name, schedule: null, week: counts(i ? 7 : taken - 7 * (meds.length - 1), 0, 0, i ? 0 : expected - taken, i ? 7 : expected - 7 * (meds.length - 1)), last30: counts(28, 1, 1, 0, 30), changedAt: at })),
      patterns: [],
    },
    daily: [{ id: "d1", date: iso(now - DAY).slice(0, 10), mood: "Good", energy: "Good", sleep: "Good", sideEffects: [], note: null, changedAt: iso(now - DAY) }],
    records: { labs: [], documents: [] },
  };
}

export type Seed = {
  patients: StoredPatient[];
  records: StoredRecord[];
  invitations: (T.Invitation & { token: string; connect: () => StoredPatient })[];
};

const record = (patientId: string, r: { key: string; title: string; type: T.RecordType; place: string | null; recordDate: string | null; text: string }, addedAt: number, origin: T.RecordOrigin = "upload", addedBy = "Dr Emeka Okafor"): StoredRecord => ({
  id: `rec-${patientId.replace(/^p-/, "")}-${r.key}`,
  patientId,
  title: r.title,
  type: r.type,
  place: r.place,
  recordDate: r.recordDate,
  addedBy,
  addedAt: iso(addedAt),
  origin,
  fileName: origin === "upload" ? `${r.title.toLowerCase().replace(/\s+/g, "-")}.txt` : null,
  contentType: origin === "upload" ? "text/plain" : null,
  sizeBytes: r.text.length,
  pages: null,
  reading: "read",
  readingError: null,
  readingErrorCode: null,
  lineCount: r.text.split("\n").length,
  heldBack: null,
  clarification: null,
  text: r.text,
});

const event = (now: number, minutesAgo: number, actor: string, actorKind: T.ActivityEvent["actorKind"], action: string): T.ActivityEvent => ({
  id: nextId("act"),
  at: iso(now - minutesAgo * 60_000),
  actor,
  actorKind,
  action,
});

export function seed(now: number): Seed {
  const mariam: StoredPatient = {
    base: base("p-mariam", "Mariam Adeyemi", "clinic_added", "1959-03-04", now, { sex: "female", lastActivityAt: iso(now - 2 * 3_600_000), hasOwnAccount: false }),
    shared: null,
    lastDaily: null,
    lastLooked: {},
    newSinceLastCheck: false,
    usedThisWeekBefore: 0,
    variant: "film",
    activity: [event(now, 180, "Dr Emeka Okafor", "clinician", "Dr Okafor added Mariam Adeyemi as a clinic patient and confirmed a care relationship.")],
  };
  const adaeze: StoredPatient = {
    base: base("p-adaeze", "Adaeze Nwosu", "connected", "1968-05-11", now, {
      sex: "female", pronoun: null, phone: "+234 803 555 0161", lastActivityAt: iso(now - 50 * 60_000),
      sharing: sharing(["Adaeze"], ["medicines", "doses", "check_ins", "records"], true, iso(now - 50 * 60_000)),
      newInformation: { since: iso(now - 50 * 60_000), items: ["A new potassium result from MARMAR Health"] },
    }),
    shared: simpleShared(now, [["Lisinopril", "10 mg", "1 tablet once a day"], ["Spironolactone", "25 mg", "1 tablet once a day"]], 13, 14),
    lastDaily: iso(now - DAY),
    lastLooked: { medicines: iso(now - 2 * DAY), doses: iso(now - 2 * DAY), check_ins: iso(now - 2 * DAY), records: iso(now - 2 * DAY) },
    newSinceLastCheck: true,
    usedThisWeekBefore: 0,
    variant: "urgent",
    activity: [event(now, 50, "Adaeze", "patient", "Adaeze shared a new potassium result.")],
  };
  adaeze.shared!.records.labs.push({ id: "l-k", name: "Potassium", value: "6.8", unit: "mmol/L", range: "3.5 to 5.0", flag: "high", date: "2026-10-23", addedBy: "Adaeze", addedAt: iso(now - 50 * 60_000), changedAt: iso(now - 50 * 60_000) });
  const ngozi: StoredPatient = {
    base: base("p-ngozi", "Ngozi Eze", "connected", "1981-02-19", now, {
      sex: "female", pronoun: null, phone: "+234 803 555 0174", lastActivityAt: iso(now - 95 * 60_000),
      sharing: sharing(["Ngozi"], ["medicines", "doses", "check_ins", "records"], true, iso(now - 95 * 60_000)),
    }),
    shared: simpleShared(now, [["Amlodipine", "10 mg", "1 tablet once a day"]], 7, 7),
    lastDaily: iso(now - 4 * 3_600_000),
    lastLooked: {},
    newSinceLastCheck: false,
    usedThisWeekBefore: 0,
    variant: "film",
    activity: [],
  };
  const femi: StoredPatient = {
    base: base("p-femi", "Femi Balogun", "connected", "1977-08-02", now, {
      pronoun: null, phone: "+234 803 555 0187", lastActivityAt: iso(now - 26 * 3_600_000),
      sharing: sharing(["Femi"], ["medicines", "doses", "check_ins"], true, iso(now - 26 * 3_600_000)),
    }),
    shared: simpleShared(now, [["Metformin", "500 mg", "1 tablet twice a day"]], 11, 14),
    lastDaily: iso(now - 2 * DAY),
    lastLooked: {},
    newSinceLastCheck: false,
    usedThisWeekBefore: 1,
    variant: "film",
    activity: [],
  };
  const chidi: StoredPatient = {
    base: base("p-chidi", "Chidi Okeke", "connected", "1987-01-30", now, {
      sex: "male", pronoun: null, phone: "+234 803 555 0112", lastActivityAt: iso(now - 4 * DAY),
      sharing: sharing(["Chidi"], ["medicines", "doses"], false, iso(now - 4 * DAY)),
    }),
    shared: simpleShared(now, [["Losartan", "50 mg", "1 tablet once a day"]], 6, 7),
    lastDaily: null,
    lastLooked: {},
    newSinceLastCheck: false,
    usedThisWeekBefore: 0,
    variant: "film",
    activity: [event(now, 4 * 1440, "Chidi", "patient", "Chidi shared medicines and doses taken with Dr Okafor.")],
  };
  const halima: StoredPatient = {
    base: base("p-halima", "Halima Yusuf", "connected", "1955-06-14", now, {
      sex: "female", phone: "+234 803 555 0125", lastActivityAt: iso(now - 6 * DAY),
      sharing: sharing(["Halima"], ["medicines", "doses"], false, iso(now - DAY), "stopped"),
    }),
    shared: null,
    lastDaily: null,
    lastLooked: {},
    newSinceLastCheck: false,
    usedThisWeekBefore: 0,
    variant: "film",
    activity: [event(now, 1440, "Halima", "patient", "Halima stopped sharing with Dr Okafor. Live access ended.")],
  };
  const tunde: StoredPatient = {
    base: base("p-tunde", "Tunde Bakare", "clinic_added", "1981-04-09", now, { sex: "male", lastActivityAt: iso(now - 12 * DAY), hasOwnAccount: false }),
    shared: null,
    lastDaily: null,
    lastLooked: {},
    newSinceLastCheck: false,
    usedThisWeekBefore: 0,
    variant: "film",
    activity: [event(now, 12 * 1440, "Dr Emeka Okafor", "clinician", "Dr Okafor added Tunde Bakare as a clinic patient.")],
  };
  const records = [
    // FIXTURE_FILM=1 leaves out the hospital note, so the demo film can add it as a photo.
    ...MARIAM_RECORDS.filter((r) => !(process.env.FIXTURE_FILM === "1" && r.key === "hospital")).map((r, i) => record("p-mariam", r, now - (180 - i) * 60_000)),
    ...ADAEZE_RECORDS.map((r, i) => record("p-adaeze", r, now - (3 * 1440 - i) * 60_000, i ? "app" : "upload", i ? "Adaeze (MARMAR Health)" : "Dr Emeka Okafor")),
    ...TUNDE_RECORDS.map((r) => record("p-tunde", r, now - 12 * DAY)),
  ];
  const invitations: Seed["invitations"] = [
    {
      id: "inv-mariam",
      token: "mariam-invite-demo",
      patientName: "Mariam Adeyemi",
      patientAge: 67,
      patientPronoun: "she",
      withPerson: { name: "Funke", relation: "daughter" },
      scopes: ["medicines", "doses", "check_ins", "records"],
      allowsCheckInCalls: true,
      withheldSome: false,
      sentAt: iso(now - 25 * 60_000),
      expiresAt: iso(now + 14 * DAY - 25 * 60_000),
      possibleMatch: { patientId: "p-mariam", name: "Mariam Adeyemi", dateOfBirth: "1959-03-04" },
      connect: () => mariam,
    },
    {
      id: "inv-ibrahim",
      token: "ibrahim-invite-demo",
      patientName: "Ibrahim Musa",
      patientAge: 58,
      patientPronoun: "he",
      withPerson: null,
      scopes: ["medicines", "doses"],
      allowsCheckInCalls: false,
      withheldSome: false,
      sentAt: iso(now - 2 * DAY),
      expiresAt: iso(now + 12 * DAY),
      possibleMatch: null,
      connect: () => ({
        base: base("p-ibrahim", "Ibrahim Musa", "connected", "1968-09-21", now, {
          sex: "male", pronoun: null, phone: "+234 803 555 0133", lastActivityAt: iso(now),
          sharing: sharing(["Ibrahim"], ["medicines", "doses"], false, iso(now)),
        }),
        shared: simpleShared(now, [["Nifedipine", "30 mg", "1 tablet once a day"]], 7, 7),
        lastDaily: null,
        lastLooked: {},
        newSinceLastCheck: false,
        usedThisWeekBefore: 0,
        variant: "film",
        activity: [],
      }),
    },
  ];
  return { patients: [mariam, adaeze, ngozi, femi, chidi, halima, tunde], records, invitations };
}

/** Mariam once she accepts: her shares, Funke, and the app record (REC-8). */
export function connectMariam(p: StoredPatient, now: number): StoredRecord {
  p.base = {
    ...p.base,
    kind: "connected",
    hasOwnAccount: true,
    pronoun: null,
    preferredTitle: "Mrs Adeyemi",
    phone: "+234 803 555 0147",
    sharing: sharing(["Mariam", "Funke"], ["medicines", "doses", "check_ins", "records"], true, iso(now)),
    lastActivityAt: iso(now),
  };
  p.shared = mariamShared(now);
  p.lastDaily = iso(now - DAY);
  return {
    ...record("p-mariam", { key: "app", title: "From MARMAR Health", type: "other", place: "MARMAR Health app", recordDate: "2026-10-23", text: MARIAM_APP_RECORD }, now, "app", "Funke (MARMAR Health)"),
  };
}