import { CONSENT_INSTRUMENTS, type ConsentInstrumentId, } from '../../../config/consentInstruments.ts'; import { orgAgreementName } from '../../../components/OrgAgreements/orgAgreements.ts'; import { checkInPanelCopy, declinedGuidance } from '../../../components/RecordAuthorizationDialog/recordAuthorizationDialog.helpers.ts'; import type { OrgConsentStatusesResponse } from '../../../services/OrgAuthorizationService.tsx'; // WHAT THE BOOKING DIALOG SAYS ABOUT THE THREE AGREEMENTS, AND WHY IT IS A FUNCTION RATHER THAN // THREE TERNARIES IN THE JSX. // // THE DEFECT IT EXISTS TO CLOSE WAS SILENCE, NOT WRONG WORDING. The notice read ONE document - the // HIPAA Authorization - and hid itself whenever that one was active. A patient can agree to the // records authorization and REFUSE consent to treat; that combination is not a corner case, it is the // asymmetry the consent model was specified around. For that patient the read came back active, the // panel hid, and the desk booked a visit for somebody who had refused to be treated and was told // nothing at all. // // A wrong sentence is read and doubted. A missing panel is indistinguishable from a patient with // nothing outstanding, so there is no moment at which anybody could notice - not the desk, not the // patient, not an audit afterwards. The first sign is somebody arriving for a visit they cannot // lawfully be given. // // AND THE EASY VERSION OF THIS FIX IS WORSE THAN THE DEFECT. Showing a consent panel on every // booking trains the desk to scroll past it, and a gate that is always on is decoration. Returning // `null` for a patient with all three agreed is the load-bearing case here, which is why it is a // return value the caller cannot forget rather than a condition written at the call site. export interface BookingAuthorizationNoticeLine { instrument: ConsentInstrumentId; /** One sentence naming the document and what the patient did about it. */ heading: string; } export interface BookingAuthorizationNotice { title: string; /** One line per document that is not agreed. Empty when the read itself failed. */ lines: BookingAuthorizationNoticeLine[]; consequence: string; } /** * The notice for a patient being booked, or `null` when there is nothing to say. * * `'unknown'` means the read FAILED, which is not the same as a patient who has answered nothing, and * it is the one state in this dialog that refuses the booking. It keeps its own sentences. * * THE PER-DOCUMENT LINES COME FROM `checkInPanelCopy` RATHER THAN FROM NEW COPY. Check-in already had * this exact defect and it was fixed there: a refusal drawn as an absence, because the sentence was * derived from a boolean that cannot tell a blank from a decision. Three surfaces describing one * refusal three ways is how the desk and the patient stop agreeing about what they are looking at. */ export const bookingAuthorizationNotice = ( consent: OrgConsentStatusesResponse | 'unknown', subject: string, // A patient registered with no email has no login, so the portal is not a way back for them. hasLogin = true, ): BookingAuthorizationNotice | null => { if (consent === 'unknown') { return { title: 'We couldn’t check this patient’s authorization', lines: [], consequence: 'The appointment can’t be booked until we can confirm it. Try again in a moment.', }; } const unmet = CONSENT_INSTRUMENTS.filter((id) => consent.instruments[id] !== 'active'); // CONTROL 1, AND IT IS THE ONE THAT PROTECTS THIS FROM ITSELF. Everything agreed means nothing to // say. A permanent panel would be the same defect wearing the opposite face. if (unmet.length === 0) return null; const lines = unmet.map((instrument) => ({ instrument, heading: checkInPanelCopy(consent.instruments[instrument], subject, orgAgreementName(instrument)).heading, })); const refused = unmet.filter((id) => consent.instruments[id] === 'declined'); const withdrawn = unmet.filter((id) => consent.instruments[id] === 'withdrawn'); // A REFUSAL OUTRANKS A WITHDRAWAL OUTRANKS AN ABSENCE, because the panel gets one headline and the // strongest thing the patient did is the one the desk has to act on. The lines below still name // every document individually, so nothing is hidden by the ordering. // THE GATE IS JOINT. ANY REFUSAL COSTS THE APPOINTMENT, AND THIS SENTENCE HAS ALREADY BEEN // WRITTEN THE OTHER WAY ONCE - READ THIS BEFORE "CORRECTING" IT AGAIN. // // The settled ruling, in Spencer's quoted words: "A patient can not be treated without all 3 things // agreed to. Declining any of them would prevent treatment. This means on decline any appointments // need cancelled with that org." Its own decision table answers the tempting case directly: // declines records access -> may they be treated? NO. // // THE PLAUSIBLE-SOUNDING OPPOSITE IS A SUPERSEDED DRAFT, and it reads convincingly because it // came with reasoning: "refusing records access leaves the practice treating a patient whose history // it cannot see - degraded care, and their right." That was overruled by name, in a later section // headed as correcting it. The reasoning it was overruled for is the trap: governing different // things does not make them separately sufficient, and a practice that may not read a patient's // chart cannot safely treat them either. // // The asymmetry that DOES survive is about GRANTS, not appointments: declining consent to treat // leaves the records grant standing. That is not this panel's subject - this panel is about whether // the visit will happen. if (refused.length > 0) { return { title: refused.length > 1 ? 'Two of the agreements were declined for this patient' : 'An agreement was declined for this patient', lines, // NOT "you can book this" - the booking is REFUSED. Spencer, 2026-09-02: "What we won't do is // allow a booking to happen unless there's record of agreement either way." // // A decline is not the desk's to record over, login or not: the server refuses it, so the way back // named is the one person entitled to answer, the same guidance the desk dialog gives. consequence: 'This appointment cannot be booked. ' + declinedGuidance(subject), }; } if (withdrawn.length > 0) { return { title: 'This practice’s access was withdrawn for this patient', lines, // A withdrawal is a DECISION the patient made, not paperwork nobody did, and saying so is what // stops the front desk re-capturing it as a matter of routine. It refuses the booking for the // same reason a decline does, and it names the same way back rather than only saying no. consequence: hasLogin ? 'This appointment cannot be booked until they agree again. It has to come from someone entitled to answer for them — in their portal, or on a form signed here and recorded at check-in.' : 'This appointment cannot be booked until they agree again. They have no login of their own, so it has to come from someone entitled to answer for them: an established guardian from their own account, or a form signed here and recorded at check-in.', }; } return { title: 'Agreements outstanding', lines, // AN OUTSTANDING AGREEMENT NO LONGER REFUSES THE BOOKING. A ruling superseded the answer // this sentence used to give: the receptionist books over the phone, and check-in - not // the booking call - is where an unanswered agreement still has to be resolved before care. // The panel is informational here, not a refusal: it tells the desk the patient hasn't // answered yet, and where to send them, so it is never wallpaper the desk stops reading. // // IT NAMES THE PORTAL FIRST, for the same reason the blocking branches above do, and the // order is fixed: the portal first, attestation at check-in as the fallback for the // incapacitated, the elderly and the tech-averse, never because it is quicker. // // NOT SPLIT MID-SENTENCE, and that is a real constraint rather than formatting. A sibling // test asserts this phrase against the source text to prove the panel names the portal // path rather than offering the desk a shortcut, and a concatenation across "answer in" / // "their portal" makes that assertion unmatchable - it then fails looking exactly like the // sentence was removed. consequence: hasLogin ? 'They have not answered yet. They can answer in their portal, or have it recorded at check-in.' // No login of their own is not no way to answer: an established guardian answers from their own account. : 'They have not answered yet. They have no login of their own, so an established guardian can answer from their own account, or it is recorded at check-in.', }; };