import type { Reference } from 'fhir/r4'; // - WHICH Location a new Appointment records, given the block the slot came from. // // TELEHEALTH IS A LOCATION on this project (, ). That makes // this function the point where a booking becomes a video visit or an in-person one - it is not a label. // An Appointment whose Location participant is a physical site IS an in-person appointment as far as // every downstream consumer is concerned, however the booking was described in the UI. // // THE DEFECT THIS EXISTS TO FIX: the receptionist flow never consulted the Schedule at all. Its // resolution chain had three candidates - a `selectedLocationFilter` match against the ROLE's locations, // the chosen org location, and `practitionerRole.location[0]` - and the block the slot actually came from // was not among them. Booking through the "Dr. X - Video Visit" practitioner option (which is a whole // Schedule whose only Location actor is the virtual one) therefore recorded the org's primary PHYSICAL // site, because with no location filter set the chain collapsed to that last fallback. The UI proved it // knew the block was a video block - it rendered the words "Video Visit" in the option label, resolved // from the Schedule's own Location actor - and that knowledge was thrown away before the payload was // built. The comment above the staff chain even claimed it "matches the patient booking path"; it did // not, and that claim is what made the gap easy to miss on review. // // THE FIX IS NOT "stamp the virtual Location at write time". own fence, and it is right: a // block can carry BOTH actors ( keeps the org's virtual Location as a CO-ACTOR on the physical // block precisely so a video option does not fork the slot pool), so "the block's location" is genuinely // ambiguous and something has to disambiguate it. The selected location IS that something - the picker // offers "Video visit" as a location, so choosing the modality and choosing the site are the same act. // // Pure and standalone so it can actually be tested: both booking flows live in ~2900-line components that // import the services barrel, which cannot be imported by a test (see // - the barrel is the 2.1 GB type-check trap). /** * The Location a booking off this block should record, or `undefined` when the block names no location * at all and the caller's own fallbacks should decide. * * @param blockLocationRefs every `Location/` actor on the chosen Schedule, in actor order - already * falling back to the PractitionerRole's locations for blocks saved before location moved onto the * Schedule. Pass `getScheduleLocationRefs(schedule, role)`. * @param selectedLocationId the site the booker explicitly chose, or `undefined` for "no preference". * Callers normalise their own "any location" sentinel to `undefined` before calling - the sentinel is * declared twice in this codebase (`utils/booking/staffBookabilityContext.ts` and a second literal * inside `PatientBookAppointment`), so this function refuses to know about it rather than becoming a * third place that has to agree. */ export const resolveBookingLocationRef = ( blockLocationRefs: readonly Reference[], selectedLocationId: string | undefined, ): Reference | undefined => { if (blockLocationRefs.length === 0) return undefined; // An explicit choice wins, but ONLY against this block's own actors. A block that does not offer the // chosen site falls back to what it does offer rather than recording a site the practitioner is not // at - the slot is real and belongs to this block, so the block is the authority on where it happens. if (selectedLocationId) { const matched = blockLocationRefs.find( (ref) => ref.reference?.split('/').pop() === selectedLocationId, ); if (matched) return matched; } // ACTOR ORDER IS THE TIE-BREAK, and it is load-bearing rather than arbitrary. appends the // virtual Location as a CO-ACTOR to an existing physical block, so on a block offering both, `[0]` is // the physical site. That is the correct default: a block that merely *also* offers video is an // in-person block unless the booker asked for video, and asking for video means selecting it above. return blockLocationRefs[0]; }; /** * Backfill a display name so downstream readers that print `locationRef.display` with no fallback of * their own do not render a blank site. A `Location/` actor on a Schedule is not guaranteed to carry one. * * The order mirrors what the pickers already do: the ref's own display, then the org location list by id, * then the bare id - never the organization's name, which would tell someone booked into a Video Visit to * attend a physical building ( is the same family: * do not substitute a confident wrong answer for a missing one). */ export const withLocationDisplay = ( ref: Reference | undefined, locations: readonly { id?: string; name?: string }[], ): Reference | undefined => { if (!ref?.reference) return undefined; const id = ref.reference.split('/').pop(); return { reference: ref.reference, display: ref.display || locations.find((loc) => loc.id === id)?.name || id, }; };