import type { Provenance } from 'fhir/r4'; // - where did this patient record come from? // // Design `identity/patient-record-provenance.md`, settled by Spencer 2026-08-14: *"use provenance for // all four creation paths."* The server records the method; this reads it back and says it in words a // receptionist would use. // // THIS FILE IS COUPLED TO THE SERVER ACROSS A REPO BOUNDARY, and nothing compiles that coupling. The // system URL and the four codes are declared in `aws/sam/services/api/src/utils/patientCreationProvenance.ts` // and duplicated here because the plugin cannot import from the infra repo. Change one and the other // silently stops matching - the symptom is every record reading "created before we started recording", // which looks like old data rather than a broken join. The test pins the exact strings for that reason. export const PATIENT_CREATION_METHOD_SYSTEM = 'https://akashichealthrecords.com/fhir/CodeSystem/patient-creation-method'; export const PATIENT_IMPORT_BATCH_SYSTEM = 'https://akashichealthrecords.com/fhir/identifiers/patient-import-batch'; export type PatientCreationMethod = | 'self-registration' | 'staff-registration' | 'bulk-import' | 'guardian-dependent'; export interface PatientCreationOrigin { method?: PatientCreationMethod; /** Who did it, as a display name if the server resolved one. Never used to identify them. */ actorName?: string; recordedAt?: string; /** Present only for an import - what lets a whole batch be found again. */ batchId?: string; } const isCreation = (provenance: Provenance): boolean => (provenance.activity?.coding ?? []).some((coding) => coding.code === 'CREATE'); const methodOf = (provenance: Provenance): PatientCreationMethod | undefined => (provenance.activity?.coding ?? []) .find((coding) => coding.system === PATIENT_CREATION_METHOD_SYSTEM) ?.code as PatientCreationMethod | undefined; /** * The one Provenance that says how this record came to exist. * * A patient accumulates a Provenance per UPDATE for the rest of their life, so the creation one is * found by its activity rather than by taking the oldest - an ordering assumption would quietly start * describing an edit as the origin the first time history was returned in a different order. */ export function findCreationProvenance(provenances: Provenance[]): Provenance | undefined { return provenances.filter(isCreation).find((provenance) => methodOf(provenance) !== undefined) ?? provenances.find(isCreation); } export function readPatientCreationOrigin(provenances: Provenance[]): PatientCreationOrigin { const creation = findCreationProvenance(provenances); if (!creation) return {}; const batchId = (creation.entity ?? []) .map((entity) => entity.what?.identifier) .find((identifier) => identifier?.system === PATIENT_IMPORT_BATCH_SYSTEM) ?.value; return { method: methodOf(creation), actorName: creation.agent?.[0]?.who?.display?.trim() || undefined, recordedAt: creation.recorded, ...(batchId ? { batchId } : {}), }; } 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' }); }; /** * WHAT THE LINE SAYS WHEN THERE IS NO CREATION PROVENANCE TO READ. * * It states the one thing that is true in every case that reaches it, and makes no claim about the * record's age in either direction. See the note on `describePatientOrigin` for why age is not * inferred here any more. */ export const ORIGIN_NOT_AVAILABLE = 'How this record was added is not available'; /** * How this record came to exist, in plain words. * * NOTHING IS EVER INFERRED: describing a record as staff-registered because it looked likely is a * false statement, and it is indistinguishable from a true one. * * AND AN ABSENCE IS NEVER A CLAIM ABOUT AGE, in either direction. Two walks found this line stating * a checkable falsehood at each end of a record's life, from one cause: it turned "we have no * provenance" into "this record is older than provenance". * * 2026-09-13, a record too NEW: `GET /R4/Provenance?target=Patient/` is a SEARCH and the index is * eventually consistent, so a patient imported minutes earlier read *"Created before we started * recording how records were added"*. Three minutes later the same URL read the truth. * * 2026-09-14, a record too OLD: the fix for that passed the record's `meta.lastUpdated` so a young * record could be told apart - but `lastUpdated` is the LAST WRITE, not the first, so a record from * March read *"not available yet"* the moment anybody edited it. Sound reasoning about a creation * timestamp, false about an update one. * * SO IT STOPPED INFERRING. The record's creation time is not on the resource - only a `_history` read * gives it, and a second read that can fail is a worse trade than a shorter sentence. What is left is * true whichever end of time the record sits at, and a sentence that says less is never false. */ export function describePatientOrigin(origin: PatientCreationOrigin): string { const by = origin.actorName ? ` by ${origin.actorName}` : ''; const on = dateText(origin.recordedAt); const when = on ? ` ยท ${on}` : ''; switch (origin.method) { case 'self-registration': // No "by": the patient IS the actor, and naming them back to a reader looking at their // own chart reads as a stranger having done it. return `Registered by the patient themselves${when}`; case 'staff-registration': return `Added at the front desk${by}${when}`; case 'bulk-import': return `Imported from a patient list${by}${when}`; case 'guardian-dependent': return `Added as a dependent${by}${when}`; default: // No method, and no guess at why. A record that predates the recording and a record whose // provenance has not been indexed yet are the same absence from here. return ORIGIN_NOT_AVAILABLE; } }