import type { Appointment, Location } from 'fhir/r4'; import { isVirtualLocation } from '../roomLocation'; /** * The one thing the booking assist may say about a selection that contradicts what was asked for. * * Mirrors POST /R4/booking/check-intent. Kept beside the other two booking-assist shapes rather than * inside the service, because the surface that RENDERS this is not the one that fetches it. */ /** What the person is about to book, in the words the screen is already showing them. */ export interface BookingIntentSelection { serviceName?: string; locationName?: string; practitionerName?: string; /** `video` means a virtual location was selected. Omit it until the flow has answered. */ modality?: 'in-person' | 'video'; } export type BookingIntentAxis = 'modality' | 'location' | 'practitioner' | 'service'; export interface BookingIntentConflict { axis: BookingIntentAxis; /** * A VERBATIM SPAN OF THE PERSON'S OWN SENTENCE, checked server-side to really be in it. * * SHOW IT. It is the difference between "we think you wanted a video visit" and "you said 'as a * video visit'" - the first invites an argument about whether we understood, the second does not. * It is also the only evidence that the intent was STATED rather than guessed at, which is the * single rule this whole feature rests on. */ quote: string; /** What is actually selected on that axis, taken from the request rather than from the model. */ selected: string; /** One short sentence, patient-facing. Never a list - the server returns at most one conflict. */ message: string; } /** * What is about to be booked, read off the payload that is about to be POSTed. * * READ FROM THE PAYLOAD, NEVER FROM THE FORM A SECOND TIME. The booking dialog resolves the location * through `resolveBookingLocationRef`, which has its own tie-breaks - a block's own actors beat an * explicit filter that block does not offer, and actor order decides in-person over video. A second * reading of the form would miss all of that and could disagree with what actually gets booked, and then * the check would be checking something nobody is doing. * * MODALITY COMES FROM THE LOCATION, because that is where it lives. There is no modality field on an * appointment on this project: a visit is by video when the Location it records is the org's virtual one * (`physicalType` code `vi`). `undefined` when there is no location at all - the flow has not answered * yet, and asserting "in-person" from an absent value would invent a contradiction. * * Returns `undefined` when there is nothing to check, so the caller can skip the request entirely. */ export const describeSelectionForIntent = ( payload: Appointment | undefined, locations: readonly Location[], ): BookingIntentSelection | undefined => { if (!payload) return undefined; const actorOf = (type: string) => payload.participant ?.map((participant) => participant.actor) .find((actor) => actor?.reference?.startsWith(`${type}/`)); const locationActor = actorOf('Location'); const locationId = locationActor?.reference?.split('/').pop(); const location = locationId ? locations.find((candidate) => candidate.id === locationId) : undefined; const selection: BookingIntentSelection = { ...(payload.serviceType?.[0]?.text || payload.serviceType?.[0]?.coding?.[0]?.display ? { serviceName: payload.serviceType[0].text || payload.serviceType[0].coding?.[0]?.display } : {}), ...(locationActor?.display ? { locationName: locationActor.display } : {}), ...(actorOf('Practitioner')?.display ? { practitionerName: actorOf('Practitioner')?.display } : {}), ...(location ? { modality: isVirtualLocation(location) ? 'video' as const : 'in-person' as const } : {}), }; return Object.keys(selection).length > 0 ? selection : undefined; };