import type { HealthcareService, Schedule } from 'fhir/r4'; import type { PractitionerWithRoles } from '../../services/PractitionerService'; import { holdsClinicalRole } from '../staffRoleCatalog'; import { isClinicianEnabled } from '../clinicianEnabled'; import { resolveBookability, type BookabilityContext } from './resolveBookability'; // The four things that must be true before a patient can book this provider, each with its CURRENT state. // // Why this exists: the onboarding hub has a checklist like this, but only ever for the FIRST provider in the // org - provider #4 gets nothing. And its version has three links, not four, which is precisely how the // desync hid: it conflated "services are assigned to this provider" with "the schedule block lists them". // Those are different facts and the second is the one booking actually matches on. // // This is the causal structure an operator cannot otherwise see. Everything else in the booking panel reports // state; this explains WHY. // // MIRROR, NEVER A GATE. export type ChainLinkId = | 'workspace' // Second, because it is the gate on the two links after it and the only one that is invisible on // the screens they point at. A practice that completed every OTHER link still ended with a provider // nobody could book, and the panel showed the failure at the LAST link - sending them to a schedule // block whose services field the missing seat had removed. | 'clinicianSeat' | 'servicesAssigned' | 'scheduleWithHours' | 'blockOffersServices'; export interface ChainLink { id: ChainLinkId; label: string; /** Plain-language explanation of what this link means and where it is set. Never empty. */ detail: string; done: boolean; } export function resolveProviderSetupChain( entry: PractitionerWithRoles, schedulesList: Schedule[], ctx: BookabilityContext, /** This practice's whole loaded catalogue, when the caller has it: then only a current active service counts. */ catalogue?: HealthcareService[], ): ChainLink[] { const hasWorkspace = holdsClinicalRole(entry.roles); // ANY active role of theirs holding a seat is enough, because a seat makes THAT role bookable and the // chain's question is whether this person can be booked at all. The seat lives on the ROLE and not on // the person - one human can hold two roles at one practice where only one is seated - so this must // never be rewritten as a property of the practitioner. const holdsSeat = entry.roles.some((r) => r.active !== false && isClinicianEnabled(r)); // This reads PractitionerRole.healthcareService - the SAME field whose misuse caused the readiness // desync. It is legitimate HERE and nowhere else: this link describes an AUTHORING STEP ("services are // assigned to this person"), not whether anything is bookable. The bookability answer is the last link. // Do not "simplify" by making this link stand in for bookability; that is the original bug. const current = catalogue && new Set(catalogue.filter((s) => s.active !== false).map((s) => s.id)); const servicesAssigned = entry.roles.some((r) => (r.healthcareService ?? []).some((ref) => !current || current.has(ref.reference?.split('/').pop()))); const practitionerId = entry.practitioner.id; const own = schedulesList.filter((s) => (s.actor ?? []).some((a) => a.reference === `Practitioner/${practitionerId}`), ); const results = own.length ? resolveBookability(own, {}, ctx) : []; const scheduleWithHours = results.some( (r) => !r.blockers.includes('BLOCK_NO_HOURS') && !r.blockers.includes('BLOCK_NO_PLANNING_HORIZON') && !r.blockers.includes('BLOCK_HORIZON_EXPIRED'), ); const blockOffersServices = results.some( (r) => !r.blockers.includes('BLOCK_DOES_NOT_OFFER_SERVICE'), ); return [ { id: 'workspace', label: 'Set up as a provider', detail: 'They hold the Practitioner workspace, which is what makes them someone patients can book.', done: hasWorkspace, }, { id: 'clinicianSeat', label: 'An active clinician seat', detail: 'Given on their Workspaces tab — “Enable as a clinician”. Without it they cannot be booked, and ' + 'a schedule block will not offer the services field at all. Your first clinician seat is free.', done: holdsSeat, }, { id: 'servicesAssigned', label: 'Services assigned to them', detail: 'The services this provider offers, chosen on their Services Offered tab.', done: servicesAssigned, }, { id: 'scheduleWithHours', label: 'A schedule with hours and dates', detail: 'At least one schedule block with weekly hours and a start and end date that has not passed.', done: scheduleWithHours, }, { id: 'blockOffersServices', label: 'That schedule lists those services', detail: 'Each schedule block says which services can be booked in it. This is the step most often missed — a provider can offer a service and still have no block that lists it.', done: blockOffersServices, }, ]; }