import type { HealthcareService, PractitionerRole, Reference, Schedule } from 'fhir/r4'; import { getRefSelfBookableOverride, getServiceSelfBookableDefault } from '../../config/serviceSelfBookable'; // The self-bookable services a schedule BLOCK offers, resolved through all three carriers. // // Extracted from PatientBookAppointment so the patient flow and every staff surface read it from ONE place. // Copying this resolution into a staff-side helper would create exactly the duplicate-truth defect this whole // program exists to remove - it is the same shape as readiness reading PractitionerRole.healthcareService // while booking matched Schedule.actor. // // The service LIST comes from the block's HealthcareService actors ("Services offered here" Schedule.actor), // so a patient only ever sees services bookable at that block/location. SELF-BOOKABLE then resolves in // precedence: block override (on the actor ref) practitioner override (on the role's healthcareService ref) // org default (the catalog entry), absent true. // // Name comes from the catalog when loaded, else the actor ref's display, else the bare id - so this does not // hard-depend on the caller having fetched the HealthcareService catalog. // // MIRROR, NEVER A GATE - the server does not enforce the self-bookable contract at all. export interface SelfBookableService { id: string; display: string; } /** A service on the block that patients cannot self-book, and the level that turned it off. */ export interface WithheldService extends SelfBookableService { blocker: 'NOT_SELF_BOOKABLE_BLOCK' | 'NOT_SELF_BOOKABLE_PRACTITIONER' | 'NOT_SELF_BOOKABLE_ORG'; } type Resolved = SelfBookableService & { selfBookable: boolean; level: WithheldService['blocker'] }; /** * `catalogueComplete`: the caller holds this practice's whole service catalogue, so a reference absent from it names a * service that was deleted or belongs elsewhere and offers nothing. Without it an absent entry is only not loaded yet. */ export interface ServiceCatalogueOptions { catalogueComplete?: boolean; } function resolveBlockServices( schedule: Schedule, role: PractitionerRole | undefined, serviceById: Map, options: ServiceCatalogueOptions = {}, ): Resolved[] { // The practitioner-level override lives on the role's healthcareService refs; index by service id. const practitionerRefById = new Map(); for (const ref of role?.healthcareService ?? []) { const id = ref.reference?.split('/').pop(); if (id) practitionerRefById.set(id, ref); } return (schedule.actor ?? []) .filter((ref) => ref.reference?.startsWith('HealthcareService/')) // A service the manager switched off is offered to nobody. No server check backs this yet (ruling asked). .filter((ref) => { const service = serviceById.get(ref.reference!.split('/').pop()); return options.catalogueComplete ? service !== undefined && service.active !== false : service?.active !== false; }) .map((ref) => { const id = ref.reference!.split('/').pop() as string; const display = serviceById.get(id)?.name ?? ref.display ?? id; const blockOverride = getRefSelfBookableOverride(ref); if (blockOverride !== undefined) return { id, display, selfBookable: blockOverride, level: 'NOT_SELF_BOOKABLE_BLOCK' as const }; const practitionerOverride = getRefSelfBookableOverride(practitionerRefById.get(id)); if (practitionerOverride !== undefined) { return { id, display, selfBookable: practitionerOverride, level: 'NOT_SELF_BOOKABLE_PRACTITIONER' as const }; } return { id, display, selfBookable: getServiceSelfBookableDefault(serviceById.get(id)), level: 'NOT_SELF_BOOKABLE_ORG' as const }; }); } export function resolveScheduleSelfBookableServices( schedule: Schedule, role: PractitionerRole | undefined, serviceById: Map, options: ServiceCatalogueOptions = {}, ): SelfBookableService[] { return resolveBlockServices(schedule, role, serviceById, options) .filter((s) => s.selfBookable) .map(({ id, display }) => ({ id, display })); } // The block's services that staff can book but patients cannot, so a why-not view can name them. export function resolveScheduleWithheldServices( schedule: Schedule, role: PractitionerRole | undefined, serviceById: Map, options: ServiceCatalogueOptions = {}, ): WithheldService[] { return resolveBlockServices(schedule, role, serviceById, options) .filter((s) => !s.selfBookable) .map(({ id, display, level }) => ({ id, display, blocker: level })); }