import type { MyHistoryEvent } from '../history/collapseHistory'; // WHERE A PATIENT'S OWN RECORD CAME FROM, READ FROM THE FEED A PATIENT IS ALLOWED TO READ. // // The staff chart answers this from `GET /R4/Provenance`, and that route excludes patients by design: // a patient has their own activity feed, and two org-trimmed views of one record would disagree about // the same events. Measured 2026-09-14 in the portal - the provenance read comes back 403 and the // origin line, which fails silent on a refusal, said nothing at all. So the sentence was correct code // that no patient could ever see. // // The same door the field-history overlay came through: `my-history` carries the actor, the // organization and the moment, and the patient may read it. // // WHAT THIS DOES NOT DO, deliberately: it never names the creation METHOD. That coding lives on the // Provenance and is not in this feed, so "registered at the front desk" versus "imported from a patient // list" cannot be told apart here - and guessing from the actor's type would be exactly the inference // the staff describer refuses to make. What IS a fact is WHO did it, and that is what this reports. /** A creation event on the patient's own record, as the patient's feed reports it. */ export interface SelfRecordOrigin { /** The person the feed names. Already resolved server-side, never a bare id. */ actorName: string; /** How the feed names whoever created the record, e.g. `PractitionerRole/pr-1`. */ actorReference: string | undefined; organizationName: string | undefined; recorded: string | undefined; } const CREATE = 'C'; /** * The creation of THIS patient's record, if the feed reaches back that far. * * NEWEST-FIRST, so the last match is the earliest one the page holds. A record older than the feed's * window simply is not in it, and the caller says nothing rather than guessing - the same silence the * self-facing describer already keeps for a record whose method was never recorded. */ export function findSelfRecordOrigin( events: readonly MyHistoryEvent[], patientId: string, ): SelfRecordOrigin | undefined { const creations = events.filter( (event) => event.action === CREATE && event.resourceType === 'Patient' && event.resourceId === patientId, ); const creation = creations[creations.length - 1]; if (!creation) return undefined; return { actorName: creation.actorName, actorReference: creation.actorReference, organizationName: creation.organizationName, recorded: creation.recorded, }; } const dateText = (iso: string | undefined): string | undefined => { if (!iso) return undefined; const parsed = new Date(iso); if (Number.isNaN(parsed.getTime())) return undefined; return parsed.toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric' }); }; /** * The sentence a patient reads on their own profile. * * THE CHECK PROMPT TURNS ON WHETHER THE READER IS THE ONE WHO TYPED IT, which is the only thing that * decides whether anybody has ever been asked. Somebody else's typing is worth checking; your own is * not, and being asked to audit it reads as the product doubting you. * * SO IT COMPARES AGAINST THE READER, NOT AGAINST THE RECORD'S SUBJECT. A parent who created their * child's record is reading their own work and is asked nothing - keying this on `Patient/` * alone would have prompted them, on a record only they have ever touched. * * `viewerReferences` is every reference that IS the reader: their own patient record, and the related * person they are acting through when they are acting for somebody. Absent or empty means the reader * cannot be identified, and the prompt stays - the safe direction, because the question it asks is * harmless and its absence is what leaves a typo unchecked. */ export function describeSelfRecordOrigin( origin: SelfRecordOrigin | undefined, viewerReferences: readonly (string | undefined)[] = [], ): string | undefined { if (!origin) return undefined; const on = dateText(origin.recorded); const when = on ? ` on ${on}` : ''; const at = origin.organizationName ? ` at ${origin.organizationName}` : ''; const byTheReader = Boolean( origin.actorReference && viewerReferences.some((reference) => reference === origin.actorReference), ); if (byTheReader) { return `This record was created${when} from what you entered.`; } const by = origin.actorName ? ` by ${origin.actorName}` : ''; return ( `This record was created${by}${at}${when}.` + ' Please check the details below are right, and change anything that is not.' ); } const UPDATE = 'U'; /** * The latest change somebody else made to the patient's own record, so the patient knows the per-field * history has something to see. Newest first; the reader's own edits are skipped, and with no reader * references nothing is reported rather than guessing that somebody else changed it. */ export function findLatestChangeByOthers( events: readonly MyHistoryEvent[], patientId: string, viewerReferences: readonly (string | undefined)[] = [], ): SelfRecordOrigin | undefined { const readers = viewerReferences.filter(Boolean); if (!readers.length) return undefined; const change = events.find( (event) => event.action === UPDATE && event.resourceType === 'Patient' && event.resourceId === patientId && Boolean(event.actorReference) && !readers.includes(event.actorReference), ); if (!change) return undefined; return { actorName: change.actorName, actorReference: change.actorReference, organizationName: change.organizationName, recorded: change.recorded, }; } /** One page of `my-history`, as the feed answers it. */ export interface MyHistoryPageResponse { events?: MyHistoryEvent[]; mayHaveOlder?: boolean; /** The cursor to hand back as `before` for the next older page. */ olderThan?: string; } /** * The same search across the feed's pages: a patient's own edits can fill the first page, so older pages * are read until a match, the end, or a cursor that stops moving. `maxPages` guards a runaway and * `isCancelled` stops the walk once the reader has left the profile. */ export async function findLatestChangeByOthersPaged( fetchPage: (before?: string) => Promise, patientId: string, viewerReferences: readonly (string | undefined)[] = [], maxPages = 50, isCancelled: () => boolean = () => false, ): Promise { if (!viewerReferences.some(Boolean)) return undefined; let before: string | undefined; for (let page = 0; page < maxPages; page += 1) { const response = await fetchPage(before); const events = response?.events ?? []; const found = findLatestChangeByOthers(events, patientId, viewerReferences); if (found) return found; // The feed's own cursor, else the oldest moment this page held. const next = response?.olderThan ?? events[events.length - 1]?.recorded; if (!response?.mayHaveOlder || !next || next === before) return undefined; before = next; if (isCancelled()) return undefined; } return undefined; } export function describeLatestChangeByOthers(change: SelfRecordOrigin | undefined): string | undefined { if (!change) return undefined; const on = dateText(change.recorded); const who = change.actorName || 'Someone at your practice'; const at = change.organizationName ? ` at ${change.organizationName}` : ''; return `${who}${at} updated your record${on ? ` on ${on}` : ''}. Open History on a section to see what changed.`; }