import { Organization } from 'fhir/r4'; import { joinNames, type OrgCollabTeam } from '../utils/orgCollabTeams'; /* * THREE INSTRUMENTS, NOT ONE AGREEMENT WITH THREE FLAGS - the client half of the table in * `aws/sam/services/api/src/utils/consentInstruments.ts`. * * Spencer, 2026-09-01: "It's consent to treat, authorization to access clinical records and HIPAA * privacy agreement. These are the 3 things every practice requires from a new patient." * * ONLY THE PARTS A SCREEN NEEDS ARE MIRRORED. The FHIR category, scope and platform uri stay on the * server, because a client that knows how a Consent is shaped is a client that will eventually build * one. What the screens need is the id to send, the NAME to render, and whether the document is agreed * to or merely read. * * THE IDS AND NAMES MUST MATCH THE SERVER BYTE FOR BYTE. The ids are sent on the wire; the names are * the legal identity of the document. While this flow was being drawn the same instrument picked up * three different names across booking, the admin editor and the attestation - and a legal instrument * named three ways is three documents to anyone reading the evidence later. */ export const CONSENT_INSTRUMENTS = ['consent-to-treat', 'hipaa-authorization', 'hipaa-privacy'] as const; export type ConsentInstrumentId = (typeof CONSENT_INSTRUMENTS)[number]; /** * A GRANT IS GIVEN OR REFUSED; AN ACKNOWLEDGEMENT IS ONLY EVER MADE. * * You cannot decline having been told something, so the privacy notice is not a third boolean of the * same kind - modelling it as one would put a Decline control on the one document that cannot have one. */ export type ConsentInstrumentKind = 'grant' | 'acknowledgement'; export interface ConsentInstrument { id: ConsentInstrumentId; /** The final name. Rendered verbatim wherever this instrument is named, in every workspace. */ name: string; kind: ConsentInstrumentKind; /** One line telling the practice what this document is for, shown above its editor. */ purpose: string; } export const CONSENT_INSTRUMENT_DEFINITIONS: Record = { 'consent-to-treat': { id: 'consent-to-treat', name: 'Consent to Treat', kind: 'grant', purpose: 'Permission to provide care. Without it this practice cannot treat the patient.', }, 'hipaa-authorization': { id: 'hipaa-authorization', name: 'HIPAA Authorization', kind: 'grant', purpose: 'Permission to see and share the patient’s clinical record.', }, 'hipaa-privacy': { id: 'hipaa-privacy', name: 'HIPAA Privacy', kind: 'acknowledgement', // Read, never agreed to - so it has no Decline anywhere, and its prompt is a different sentence. purpose: 'Notice of how this practice handles the patient’s information. It is read, not agreed to.', }, }; export const isConsentInstrumentId = (value: unknown): value is ConsentInstrumentId => typeof value === 'string' && (CONSENT_INSTRUMENTS as readonly string[]).includes(value); /** * THE SENTENCE THE PATIENT TICKS, and it is not the same sentence for all three. * * Spencer, 2026-09-02: "'I have read' for privacy, keep 'I agree' for the other two." * * IT LIVES HERE RATHER THAN ON THE SCREENS BECAUSE THERE ARE THREE SCREENS - the booking flow, the * check-in capture and the admin editor. Wording a screen composes is wording that drifts; wording it * is handed cannot. The distinction is legal rather than stylistic: rendering the notice as "I agree" * alongside two real grants invites a Decline control on the one document that cannot have one. */ export const consentPrompt = (instrument: ConsentInstrumentId): string => { const definition = CONSENT_INSTRUMENT_DEFINITIONS[instrument]; return definition.kind === 'acknowledgement' ? `I have read the ${definition.name}.` : `I agree to the ${definition.name}.`; }; /** * WHAT THE FRONT DESK IS RECORDING WHEN IT ATTESTS - the same distinction as `consentPrompt`, for the * staff side of the same three documents. * * Spencer, 2026-09-02: *"Just remember that they need to attest to all 3 documents, hipaa privacy, * hipaa authorization and consent to treat."* * * THE WARNING ON THE CAPTURE DIALOG SAID "that this patient agreed" FOR ALL THREE. For the two grants * that is exactly right and is the point of the warning. For the privacy notice it records something a * patient cannot meaningfully give: an acknowledgement is that they WERE TOLD, and attesting it as a * grant misdescribes the one document whose whole distinction is that it is not one. * * It lives beside `consentPrompt` for the reason that one states: there are several screens, and * wording a screen composes is wording that drifts. */ export const attestationClaim = (instrument: ConsentInstrumentId): string => { const definition = CONSENT_INSTRUMENT_DEFINITIONS[instrument]; return definition.kind === 'acknowledgement' ? 'that this patient was given this notice and has read it' : 'that this patient agreed'; }; /** * The instruments that can be declined - derived from `kind` rather than listed, so an acknowledgement * added later cannot accidentally gain a Decline control. */ export const declinableInstruments = (): ConsentInstrumentId[] => CONSENT_INSTRUMENTS.filter((id) => CONSENT_INSTRUMENT_DEFINITIONS[id].kind === 'grant'); /* * PER-INSTRUMENT PAPER REQUIREMENT. The server WRITES these exact urls and the plugin READS them back - * the literals must match `organization-consent-settings.ts` byte for byte, exactly as the single * `organization-require-signed-auth-form` reader in organizationExtensions.ts already does. */ export const ORG_REQUIRE_SIGNED_FORM_EXTENSION_URL_BASE = 'https://akashichealthrecords.com/fhir/extensions/organization-require-signed-form'; export const orgRequireSignedFormExtensionUrl = (instrument: ConsentInstrumentId): string => `${ORG_REQUIRE_SIGNED_FORM_EXTENSION_URL_BASE}/${instrument}`; export const ORG_REQUIRE_SIGNED_AUTH_FORM_EXTENSION_URL = 'https://akashichealthrecords.com/fhir/extensions/organization-require-signed-auth-form'; /** * Whether this practice requires a signed form on file before staff may record this document. * * FALLS BACK TO THE SUPERSEDED ORG-WIDE SETTING, mirroring the server. A practice that turned the old * one on meant "we want paper on file", and reading absent-as-false here would show the admin an * unticked box for a requirement that is still being enforced - the screen and the gate disagreeing * about what the practice's policy is. */ export const getOrgRequireSignedFormFor = ( org: Organization | null | undefined, instrument: ConsentInstrumentId, ): boolean => { const perInstrument = org?.extension?.find((e) => e.url === orgRequireSignedFormExtensionUrl(instrument)); if (typeof perInstrument?.valueBoolean === 'boolean') return perInstrument.valueBoolean; return org?.extension?.find((e) => e.url === ORG_REQUIRE_SIGNED_AUTH_FORM_EXTENSION_URL)?.valueBoolean ?? false; }; /** * The instrument whose grant governs reading and adding to the record. Exactly one, by definition. * * It is exported under this name because it is what every surface built before the split actually * meant. The old welded "Authorization to Treat" BEHAVED as records access - an unsigned authorization * meant the practice could not see the record - so a surface asking "is this practice authorized?" is * asking about this one, whatever the object it was reading used to be called. */ export const RECORDS_INSTRUMENT: ConsentInstrumentId = 'hipaa-authorization'; /** * WHAT THE PATIENT'S OWN PRACTICE PAGE SAYS ABOUT EACH DOCUMENT * * THE DEFECT THIS CLOSES: the outstanding-agreements notice renders only while something is * OUTSTANDING, and a declined document is answered. So a patient who refused Consent to Treat saw the * notice disappear - taking with it the only route to the document - while the confirmation they had * just read promised "you can agree later if you change your mind". There was no later, and the booking * gate now refuses them. * * AND THE OBVIOUS FIX IS THE WRONG ONE. Re-showing the notice for a declined document would break a * settled rule: a decline is never re-prompted. VOLUNTEERING IS NOT THE SAME AS BEING ASKED. A nag * arrives; an affordance waits. So the door lives here, on a page the patient opens by choice, and * nothing goes looking for them. * * THE WORDING IS PER DOCUMENT BECAUSE THE DOCUMENTS ARE NOT ALIKE. The HIPAA Authorization governs * whether the practice may READ the record; Consent to Treat governs whether they may TREAT; the privacy * notice is an acknowledgement that cannot be granted or refused at all. One sentence covering three * would have to be vague enough to be true of all of them, and vague is the opposite of what a patient * looking at their own decision needs. * * THE AUTHORIZATION'S SENTENCES ARE MOVED HERE UNCHANGED, word for word. They were written against * Spencer's forward-only ruling and this file's own header calls them load-bearing rather than a draft. * Rewriting them while relocating them would have buried a copy change inside a refactor. */ export interface AgreementStateCopy { /** What the patient answered, stated back to them. */ active: string; withdrawn: string; declined: string; none: string; proposed: string; /** The label on the door back. Absent where there is no door - an acknowledgement has none. */ restoreLabel?: string; restoreAgainLabel?: string; /** * The verb for giving it up. Per document because the verbs are not interchangeable: you withdraw * ACCESS to a record, and you withdraw CONSENT to be treated - "withdraw access" on the consent * document would name the wrong thing. Absent for the acknowledgement, which cannot be given up. */ withdrawLabel?: string; /** The confirmation's own question. Whole sentences rather than an assembled one - see the note. */ withdrawTitle?: string; /** * WHAT THE CONFIRMATION SAYS WILL HAPPEN, and it lives here for the same reason the title does. * * MEASURED IN A BROWSER 2026-09-03, and it is why this field exists. The confirmation took its * TITLE from this table and left its BODY hardcoded to the records-access wording. Withdrawing * Consent to Treat therefore asked "Withdraw your consent to be treated by X?" and then explained, * underneath, that the practice "will not see anything NEW in your record" and that "your * appointments are not cancelled" - a promise about a DIFFERENT document, on the one screen where * a patient is deciding whether to stop their own care. * * A half-migrated surface is worse than an un-migrated one: the title made it look instrument- * aware, so nothing about the screen invited a second look. Keeping the two halves in one entry is * what stops them disagreeing again. */ withdrawBody?: string; /** The consequences, each led by its plain-words headline; the body then says what they keep. */ withdrawPoints?: Array<{ lead: string; detail: string }>; /** The confirmation's way out, in the patient's words. */ keepLabel?: string; } // Who from this practice leaves which of the patient's teams; generic while that is not known. const collabTeamsDetail = (practice: string, teams?: OrgCollabTeam[] | null): string => { if (!teams) return `Anyone from ${practice} leaves them and stops seeing those conversations.`; if (teams.length === 0) return `Nobody from ${practice} is in one right now.`; const people = [...new Set(teams.flatMap((team) => team.fromHere))]; const one = people.length === 1; return `${joinNames(people)} ${one ? 'leaves' : 'leave'} ${joinNames(teams.map((team) => team.name))}, ` + `and ${one ? 'stops' : 'stop'} seeing those conversations.`; }; export const agreementStateCopy = ( instrument: ConsentInstrumentId, practice: string, collabTeams?: OrgCollabTeam[] | null, ): AgreementStateCopy => { if (instrument === 'hipaa-authorization') { return { active: `${practice} can see your health record and add to it, and can include you in messages and collaboration teams.`, withdrawn: `You withdrew ${practice}'s access.`, declined: `You declined ${practice}'s access to your health record. You can change this whenever you like.`, none: `${practice} does not have access to your health record.`, proposed: `${practice} has asked for access to your health record. They cannot see it until you agree.`, restoreLabel: 'Allow access', restoreAgainLabel: 'Allow access again', withdrawLabel: 'Withdraw access', withdrawTitle: `Withdraw ${practice}'s access?`, /* * SPENCER, 2026-09-03: *"withdrawing access has to cancel scheduled appointments. Give * them a confirmation stating that will happen. It will also remove any practitioners from * the org from any collab teams they're in."* * * THE SENTENCE THAT USED TO SIT HERE SAID *"Your appointments are not cancelled"* - true * when it was written and reversed by that ruling. It is the most load-bearing line on this * screen, so it is corrected rather than left standing. * * THE ORDER IS THE POINT. The two irreversible consequences come FIRST, in plain words, * because they are what somebody would be surprised by afterwards. What the practice keeps * follows, because it is reassurance rather than a decision. */ withdrawPoints: [ { lead: `Everyone at ${practice} loses access`, detail: '— every practitioner and every member of their front desk, not just the person ' + 'you see. They cannot message you, and your visits already booked with them will be ' + 'cancelled. Those visits will not come back if you allow access again.', }, { lead: 'They are removed from your collaboration teams.', detail: collabTeamsDetail(practice, collabTeams) }, ], withdrawBody: `They keep what they already have. ${practice} can still see your record as it ` + 'stands at the moment you withdraw — they are required to keep it, and withdrawing is not ' + 'erasing — but nothing you or anyone else adds after that will reach them. We will tell ' + 'them you withdrew; we will not tell them why.', keepLabel: 'Keep their access', }; } if (instrument === 'consent-to-treat') { return { active: `You agreed to be treated by ${practice}.`, withdrawn: `You withdrew your consent to be treated by ${practice}. They cannot treat you or book you a visit until you agree again.`, // IT NAMES THE CONSEQUENCE, and that is not a nudge. This is the one document whose refusal // stops care outright, and a patient looking at it deserves to know why they cannot be booked // - the screen that says nothing is the defect this card was raised for. declined: `You declined to be treated by ${practice}. They cannot treat you or book you a visit while that stands. You can change this whenever you like.`, none: `You have not answered whether ${practice} may treat you.`, proposed: `${practice} has asked you to agree to be treated. They cannot treat you until you do.`, restoreLabel: 'Agree to be treated', restoreAgainLabel: 'Agree again', withdrawLabel: 'Withdraw consent', keepLabel: 'Keep my consent', withdrawTitle: `Withdraw your consent to be treated by ${practice}?`, /* * REWRITTEN 2026-09-03 ON SPENCER'S RULING, and the previous version is worth recording * because it was CORRECT when written. It said a booked visit *"is not cancelled * automatically - contact the practice"*, which is exactly what the system did: measured in * a browser, the appointment survived the withdrawal. * * The ruling changed the behaviour, so the copy follows the behaviour. It ships in the * same change as the cancellation itself and must never lead it: a promise that visits are * cancelled, over code that leaves them standing, sends somebody away from a visit that is * still on the practice's calendar. * * AND IT SAYS THE PART THAT CANNOT BE UNDONE. Re-agreeing restores the grant; it does not * restore the appointments. A patient who reads "you can agree again at any time" and * nothing else will reasonably assume it is all reversible. */ withdrawBody: `From the moment you withdraw, ${practice} cannot treat you. Your visits already ` + 'booked with them will be cancelled, and their clinicians will be removed from any ' + `collaboration team you have. This does not change what ${practice} can already see in your record; ` + 'that is a separate agreement. You can agree again at any time, but your cancelled ' + 'visits will not come back.', }; } /* * THE PRIVACY NOTICE, AND IT HAS NO DOOR BECAUSE IT HAS NO DECISION. * It is an acknowledgement - what is recorded is that the patient was told. There is nothing to * withdraw and nothing to refuse, which is why `declinableInstruments()` excludes it and why the * modal shows "I Have Read" instead of "I Agree". Offering a way to change it would invent a choice * the document does not carry. */ return { active: `You have read ${practice}'s privacy notice.`, withdrawn: `You have read ${practice}'s privacy notice.`, declined: `You have read ${practice}'s privacy notice.`, none: `${practice} has a privacy notice for you to read.`, proposed: `${practice} has a privacy notice for you to read.`, }; };