/** * WHAT A PATIENT IS TOLD WHEN THE SELF-BOOKING GUARD REFUSES THEM. * * The guard allows one upcoming self-booked appointment per practitioner, and it is enforced on both * sides - the client disables the picker, and the server answers 409. That scope is not decided here and * this file does not change it. * * WHAT IT CHANGES IS THE ONLY THING THE REFUSAL OFFERED: a phone number. MEASURED 2026-08-30 at a * practice with two practitioners, on a patient holding one appointment with each: every route through * the flow was closed, and the advice was to telephone the practice - on the screen whose whole purpose * is saving them that call. * * TWO STATES THAT LOOK THE SAME AND ARE NOT, WHICH IS THE POINT. * * somebody else is free the patient is one click from booking, and nothing said so. The picker is * right there, already populated, and the message talked about the telephone. * nobody is free the phone number IS the answer, and saying so plainly is better than * leaving them to try each practitioner and find out one at a time. * * The old message could not tell those apart, so it gave the second answer in both cases - which reads * as a dead end in the case that is not one. */ export interface SelfBookingGuardMessage { /** The refusal itself, always shown. */ readonly refusal: string; /** What to do about it. Never empty: there is always either a colleague or a telephone. */ readonly remedy: string; } export interface SelfBookingGuardMessageInput { readonly practitionerName: string; /** The existing appointment's start, already formatted for the patient's own clock. */ readonly existingStartLabel?: string; readonly organizationName: string; readonly organizationPhone?: string; /** * How many OTHER practitioners offering this visit the patient could still book with. * * Counted from the same list the picker renders, so the sentence cannot promise a choice the screen * does not offer. A negative or non-finite count is treated as none: over-promising an alternative * that is not there sends the patient hunting, which is worse than the telephone. */ readonly otherPractitionersAvailable: number; } const available = (count: number): number => Number.isFinite(count) && count > 0 ? Math.floor(count) : 0; export const buildSelfBookingGuardMessage = ( input: SelfBookingGuardMessageInput, ): SelfBookingGuardMessage => { const when = input.existingStartLabel ? ` on ${input.existingStartLabel}` : ''; const refusal = `You already have an upcoming appointment with ${input.practitionerName}${when}.`; const contact = input.organizationPhone ? `call ${input.organizationName} at ${input.organizationPhone}` : `contact ${input.organizationName}`; const others = available(input.otherPractitionersAvailable); if (others === 0) { // Said plainly rather than left to be discovered. Without this sentence the patient tries each // name in turn and meets the same refusal, which is how a two-practitioner practice reads as // broken rather than as full. return { refusal, remedy: `Everyone who offers this visit already has an appointment booked with you, so ` + `${contact} to arrange another.`, }; } return { refusal, remedy: others === 1 ? `Choose the other practitioner above to book this visit, or ${contact} to book another ` + `with ${input.practitionerName}.` : `Choose a different practitioner above to book this visit, or ${contact} to book another ` + `with ${input.practitionerName}.`, }; }; /** The two sentences as one line, for the places that have room for a string and not for a layout. */ export const selfBookingGuardText = (input: SelfBookingGuardMessageInput): string => { const { refusal, remedy } = buildSelfBookingGuardMessage(input); return `${refusal} ${remedy}`; };