import type { Location, Reference, Schedule } from 'fhir/r4'; // S4 - whether a schedule block is opted INTO video, and how that is written. // // THE INVARIANT THIS PROTECTS: the virtual Location joins the practitioner's EXISTING Schedule as an // ADDITIONAL actor. Never a separate Schedule. Slots derive from Schedules, so a second Schedule would // give a second, independent slot pool and let a room appointment and a video appointment both take // 10:00. As a co-actor on one Schedule, capacity is shared BY CONSTRUCTION - no reservation code is // involved, and none should be. If a change here starts needing `acquireSlotReservation`, the approach // has gone wrong. // // Pure on purpose: ManageScheduleModal is a 1000-line react-hook-form component with two save paths, and // this decision is the part worth pinning independently of the form. /** * Does this block offer video? True only when the org's virtual Location is EXPLICITLY among its * actors. * * An org-wide block (no Location actors at all - the "all locations" convention) is deliberately * FALSE. That convention is a booking-time wildcard for physical sites; letting it imply video would * opt every org-wide block into telehealth the moment the org created a virtual Location, with no * practitioner agreeing to it. Same fail-open leak closed in `matchSchedules`, and it has to be closed * in both places or the writer and the reader disagree. */ export const scheduleOffersVideo = ( schedule: Schedule | undefined, virtualLocationId: string | undefined, ): boolean => { if (!virtualLocationId) return false; return (schedule?.actor ?? []).some( (ref) => ref.reference === `Location/${virtualLocationId}`, ); }; /** * The location options a booking picker should OFFER, with video removed when the practice does not do * video at all. * * THE ORG GATE HIDES; THE OTHER TWO EXPLAIN. Once a block carries the virtual Location it flows into * both pickers' option lists automatically (they build options from the schedules' own location refs). * That is right when the practice offers video. When it does not, "Video visit" is an option that exists * only to be refused - the resolver would answer ORG_TELEHEALTH_DISABLED, which is a true statement and * a useless one, because there is nothing the person choosing can do about it. The per-service and * per-block gates are the opposite: "why can't I book THIS by video" has an answer worth showing. * * Shared by both pickers on purpose. Two copies of this rule is how the receptionist and the patient end * up offering different things - which has already happened once in this file's neighbourhood. * *: `serviceAllowsVideo` - the SERVICE gate now HIDES here too. * * The paragraph above says the per-service gate should EXPLAIN rather than hide. That was the design, * and it was never reachable. `resolveBookability` carries a correct, correctly-BY-DESIGN-classified * `SERVICE_NOT_TELEHEALTH_ELIGIBLE` blocker - but it is guarded on * `criteria.location === virtualLocationId`, and its only three production callers * (`bookabilityScopes`, `providerSetupChain`, `practitionerBookingReadiness`) pass `{}` as criteria. * **Neither booking picker calls the resolver at all.** So the blocker was alive in its unit tests and * dead in the product: both pickers offered "Video visit" for a service that cannot be delivered by * video, and only SERVER gate refused it - at submit, after the form was filled. * Proven in a browser ( UI spec, S2 + S6). * * Hiding is the right answer HERE specifically because settled that the **receptionist gets * no why-not surface at all** (their bookable set is a superset, so the panel would mislead). With no * panel there is nothing to explain with, so an absent option is the only honest UI on that picker. * Wiring the resolver into the PATIENT picker so it can explain remains worth doing - this makes the * product correct meanwhile, rather than leaving it offering surgery by video. * * REQUIRED, NOT OPTIONAL, AND IT FAILS CLOSED. There is no working project-wide type-check in this * plugin, so nothing forces a future caller to pass it - and an omitted argument arrives as `undefined`. * `undefined` therefore HIDES. A gate that wrongly removes an option is visibly wrong and gets fixed; one * that wrongly offers video for surgery is invisibly wrong, which is the exact failure this card exists * for. Same posture as `buildScheduleLocationActors` below, which also fails closed rather than trusting * its caller. */ export const offerableLocationOptions = ( options: T[], virtualLocationId: string | undefined, orgTelehealthEnabled: boolean, serviceAllowsVideo: boolean, ): T[] => { if (!virtualLocationId) return options; // Two independent reasons to hide, deliberately not folded into one boolean by the caller: the org // not offering video and this service not being deliverable by video are different facts, and a // caller that had to AND them itself is a third place for the rule to drift. if (orgTelehealthEnabled && serviceAllowsVideo === true) return options; return options.filter((option) => option.id !== virtualLocationId); }; /** What a patient sees for the virtual Location: the deck draws telehealth as a place called this. */ export const VIDEO_VISIT_LABEL = 'Video visit'; /** * The patient picker's site options with the virtual Location shown as a video visit, first. * * The virtual Location carries whatever name the practice gave it, and one called "ZZWALK 1666 Control * Site" read as another building: a patient offered video saw no video option. The deck lists it as * "Video visit" right after "Any location". Only the label and position change; the id is still the * virtual Location, so everything that books against it is untouched. */ export const videoVisitFirst = ( options: T[], virtualLocationId: string | undefined, ): T[] => { const video = virtualLocationId ? options.find((option) => option.id === virtualLocationId) : undefined; if (!video) return options; return [{ ...video, display: VIDEO_VISIT_LABEL }, ...options.filter((option) => option !== video)]; }; /** * The `Location/` actors a save should write: the chosen physical site (if any) plus the virtual one * when the block offers video. The caller replaces all existing `Location/` actors with this. * * The two axes are independent: an org-wide block (no physical site) can still offer video, and a * site-specific block can decline it. */ export const buildScheduleLocationActors = ( physicalLocationRef: Reference | undefined, virtualLocation: Location | undefined, offerByVideo: boolean, ): Reference[] => { const actors: Reference[] = physicalLocationRef ? [physicalLocationRef] : []; // Fails closed rather than trusting the UI: with no virtual Location configured there is nothing to // reference, and inventing one would write a dangling reference that reads as "offers video". if (!offerByVideo || !virtualLocation?.id) return actors; const virtualRef = `Location/${virtualLocation.id}`; // The physical selection should never BE the virtual location (`onlySites` keeps it out of that // picker), but a duplicate actor would make the block look doubly-located to every reader, so guard // rather than assume the picker upstream. if (actors.some((ref) => ref.reference === virtualRef)) return actors; return [...actors, { reference: virtualRef, display: virtualLocation.name }]; };