import type { Appointment } from "fhir/r4"; /** * May a PATIENT cancel this appointment from their own portal? R6. * * THIS SHIPPED ONLY ONCE THE SERVER BOUNDARY DID. `PUT /R4/Appointment/{id}` used to check nobody - a * patient token could update any appointment, in any org, by id. The hole predated any button, * but a Cancel control is what would have made it reachable in one click by design, so the button waited. * The patient boundary is merged and deployed; this is the client half arriving second, on purpose. * * WHAT THIS FUNCTION IS *NOT*: a cancellation-window policy. Whether a practice allows a patient to * cancel two hours before a visit is a rule nobody has set, and inventing one here would quietly make a * business decision inside a predicate. Everything below is CORRECTNESS - states in which the word * "cancel" has no meaning - and the window is flagged on the card instead. */ /** Statuses from which a patient cancelling still means something. */ const CANCELLABLE = new Set(["proposed", "pending", "booked", "waitlist"]); export const patientMayCancel = ( appointment: Appointment | undefined, now: Date = new Date(), ): boolean => { if (!appointment?.id) return false; // Already over. Cancelling history is not a thing a patient can do, and offering it invites them to // try and then wonder what happened. const start = appointment.start ? new Date(appointment.start).getTime() : NaN; if (Number.isNaN(start) || start <= now.getTime()) return false; // `arrived` and `checked-in` are deliberately NOT here even though the queue counts them as upcoming: // the patient is physically at the practice, and a self-service cancel at that point is a conversation // with the front desk, not a button. return CANCELLABLE.has(appointment.status ?? ""); }; /** * The cancelled form of the appointment, carrying no reason. * * SPREAD FROM THE STORED RESOURCE, one field changed. The server replaces the whole resource on a PUT *, so building a fresh object here would silently drop * `meta.tag` - the org affiliation - and every participant. One field moves; nothing else is retyped. * * NOTHING CALLS THIS DIRECTLY, and that is the point. It is the shared base the two reason-carrying * forms below are built from, so that adding a reason can never mean retyping the resource. A caller * that reaches for it is a caller that has not decided why the visit went, which is the state this * whole file exists to prevent. */ const cancelledWithoutReason = (appointment: Appointment): Appointment => ({ ...appointment, status: "cancelled", }); /** * WHY A VISIT WENT, WHEN THE PATIENT CANCELLED IT THEMSELVES * * THIS IS AN HL7 CODE, and here it is the right one. The note below explains why `pat` is the wrong * story for a refusal - the practice called that visit off. When a patient presses Cancel in their own * portal, `pat` is not the nearest available code, it is exactly what happened. * * THE SYSTEM MATCHES WHAT THE SERVER ALREADY WRITES. `endCareOnConsentWithdrawal.ts` stamps * `http://terminology.hl7.org/CodeSystem/appointment-cancellation-reason` with `pat`, so a practice * reading its own cancellations sees one coding system rather than two depending on which side of the * wire ended the visit. * * WITHOUT THIS, EVERY CANCELLATION A PATIENT CAN MAKE IS INDISTINGUISHABLE FROM EVERY OTHER. The bare * cancel used to send no `cancelationReason` at all, so a row of them said a patient kept dropping * visits and could not say more than that. */ export const HL7_CANCELLATION_REASON_SYSTEM = "http://terminology.hl7.org/CodeSystem/appointment-cancellation-reason"; export const PATIENT_CANCELLED = "pat"; export const cancelledByPatient = (appointment: Appointment): Appointment => ({ ...cancelledWithoutReason(appointment), cancelationReason: { coding: [{ system: HL7_CANCELLATION_REASON_SYSTEM, code: PATIENT_CANCELLED, display: "Patient", }], text: "Cancelled by the patient", }, }); export const PRACTICE_CANCELLED = "prov"; // Staff cancelling from the patient record: the practice called the visit off, not the patient. export const cancelledByPractice = (appointment: Appointment): Appointment => ({ ...cancelledWithoutReason(appointment), cancelationReason: { coding: [{ system: HL7_CANCELLATION_REASON_SYSTEM, code: PRACTICE_CANCELLED, display: "Provider", }], text: "Cancelled by the practice", }, }); /** * WHY A VISIT WENT, WHEN IT WENT BECAUSE A PATIENT REFUSED AN AGREEMENT * * NOT AN HL7 CODE, and deliberately so. `appointment-cancellation-reason` has no member meaning * this; the nearest, `pat`, says the PATIENT cancelled, which is the wrong story - the patient refused a * document, and the practice cannot lawfully treat them, so the practice is what called the visit off. * A near-enough standard code here would read as fact to anyone consuming it later. */ export const AKASHIC_CANCELLATION_REASON_SYSTEM = "https://akashichealthrecords.com/fhir/CodeSystem/appointment-cancellation-reason"; export const REFUSED_CONSENT = "refused-consent"; /** * WHY A VISIT WENT, WHEN THE PRACTICE SAID NO TO A REQUEST FOR IT * * NOT `prov`, AND NOT AN HL7 CODE AT ALL. `prov` means the provider cancelled - a visit that existed * and was called off. This is the other thing: a booking that was never confirmed, refused while it was * still a request. A patient who is told *"the practice cancelled your appointment"* about a time they * were never given is being told something that did not happen. * * AND THE DISTINCTION IS LOAD-BEARING RATHER THAN TIDY. Without it a decline is indistinguishable from * every other cancellation INCLUDING ONE THE PATIENT MADE THEMSELVES, and telling somebody the practice * refused a booking they cancelled is a worse defect than saying nothing at all. */ export const DECLINED_REQUEST = "declined-request"; /** * WHAT A PATIENT IS TOLD WHEN THE PRACTICE ANSWERED NO, FROM ONE PLACE. * * The sentence lived in two copies - the reason text this module writes onto the record, and the * Appointments page's own literal - and the practice card that also needs it would have made a third. * * A REWORDED COPY IS HOW TWO SURFACES STOP AGREEING ABOUT THE SAME APPOINTMENT. The awaiting-practice * marker is a shared constant for exactly this reason; the outcome deserves the same treatment as the * wait, and the outcome is the half a patient cannot check anywhere else. */ export const DECLINED_BY_PRACTICE_LABEL = "The practice could not confirm this time"; /** * The cancelled form, carrying the reason a refusal produced it. * * THE REASON IS THE DIFFERENCE BETWEEN THIS AND A PATIENT CHANGING THEIR MIND, and the practice has to * be able to tell them apart afterwards. A row of bare cancellations says a patient kept dropping visits; * these say the practice was not permitted to hold them. */ /** * The cancelled form of a booking the practice declined while it was still awaiting approval. * * THE TEXT IS WHAT IS TRUE AND NOTHING MORE. The desk gives no reason when it declines, and the product * must not invent one: anything warmer is a guess about a decision the patient cannot see, and anything * colder reads as a fault. */ export const cancelledAsDeclinedRequest = (appointment: Appointment): Appointment => ({ ...cancelledWithoutReason(appointment), cancelationReason: { coding: [{ system: AKASHIC_CANCELLATION_REASON_SYSTEM, code: DECLINED_REQUEST, display: "Declined request", }], text: DECLINED_BY_PRACTICE_LABEL, }, }); /** * Did the PRACTICE decline this request, as opposed to anything else that cancels an appointment? * * Reads the coding rather than the text, because the text is a sentence somebody may reword and the * code is the fact. Anything without this exact coding answers no - including a bare cancellation, * which is what every decline looked like before the reason existed. */ export const wasDeclinedByPractice = (appointment: Appointment | undefined): boolean => (appointment?.cancelationReason?.coding ?? []).some( (coding) => coding.system === AKASHIC_CANCELLATION_REASON_SYSTEM && coding.code === DECLINED_REQUEST, ); /** What a cancelled visit says at the desk: declined or cancelled, then its recorded reason when there is one. */ export const cancellationExplanation = (appointment: Appointment): string => { const reason = appointment.cancelationReason?.text || appointment.cancelationReason?.coding?.find((coding) => coding.display)?.display; return (wasDeclinedByPractice(appointment) ? "Request declined" : "Cancelled") + (reason ? ": " + reason : ""); }; /** * Did this cancellation come from the patient's own act - their Cancel, or their refusal of an * agreement? The patient was there when it happened, so nothing should tell them the practice did it. */ export const wasCancelledByThePatientsOwnAnswer = (appointment: Appointment | undefined): boolean => (appointment?.cancelationReason?.coding ?? []).some( (coding) => (coding.system === HL7_CANCELLATION_REASON_SYSTEM && coding.code === PATIENT_CANCELLED) || (coding.system === AKASHIC_CANCELLATION_REASON_SYSTEM && coding.code === REFUSED_CONSENT), ); export const cancelledForRefusedConsent = (appointment: Appointment): Appointment => ({ ...cancelledWithoutReason(appointment), cancelationReason: { coding: [{ system: AKASHIC_CANCELLATION_REASON_SYSTEM, code: REFUSED_CONSENT, display: "Refused consent", }], text: "Refused consent", }, });