Booking Flow — Design¶
This page describes the end-to-end booking flow from "patient browses available times" through "appointment booked." It is informational, not normative — the contract is enforced by individual profile constraints in NexusEmrCoreSchedule, NexusEmrCoreSlot, and NexusEmrCoreAppointment; this page narrates how the pieces fit together.
Actors¶
- Source EMR (Phase 1: OscarPro) — owns the recurring templates (
scheduletemplatefamily) and the booked appointments (appointmenttable). - Oscar-side converter — reads the source tables and emits
Schedule+Slotresources to a FHIR server (Medplum / HAPI). - Booking UI — patient-facing or staff-facing app; reads Slots, creates Appointments, manages hold flows.
- Hold-timeout reconciler — background job that downgrades stale
busy-tentativeSlots back tofree.
Data flow¶
┌──────────────────────────────────────────────────────────────────────┐
│ OscarPro │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ scheduletemplate │ │ scheduletemplatecode│ │
│ │ provider_no │ │ code (1 char) │ │
│ │ name (PK pt 2) │ │ description │ │
│ │ timecode (string) │ │ duration │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ scheduledate │ │ appointment │ │
│ │ (provider, date, │ │ (start_time, │ │
│ │ template_name) │ │ end_time, etc.) │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
│ │ │ │
│ ▼ │ │
│ Oscar-side converter: │ │
│ - Walks timecode chars │ │
│ - Looks up scheduletemplatecode │ │
│ - Overlays appointment rows for │ │
│ busy/free status │ │
└─────────────┼──────────────────────────┼──────────────────────────────┘
│ │
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ NexusEmrCoreSchedule │ │ NexusEmrCoreAppointment │
│ .actor[PractitionerRole│ │ .slot[] → Slot │
│ , Location, │ │ .start/.end │
│ HealthcareSvc] │ │ .participant[…] │
│ .planningHorizon │ │ │
│ .serviceType[] │ │ │
└──────────────┬───────────┘ └──────────────┬───────────┘
│ │
▼ │
┌──────────────────────────┐ │
│ NexusEmrCoreSlot │◄────────────────┘
│ .schedule (1..1) │ Appointment.slot resolves
│ .status │ to NexusEmrCoreSlot.
│ .start / .end │
│ .serviceType[] │
│ .appointmentType │
└──────────────┬───────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Booking UI (Patient app / Provider app) │
│ 1. GET Schedule?actor=PractitionerRole/X │
│ 2. GET Slot?schedule=Schedule/Y&status= │
│ free&start=ge2026-06-01 │
│ 3. User selects free slot │
│ 4. POST Appointment with .slot=Slot/Z │
│ 5. Backend flips Slot.status to busy │
│ (or busy-tentative → busy after confirm) │
└─────────────────────────────────────────────┘
Status state machine¶
Slot.status (per FHIR R4) takes one of these values:
converter converter
│ │
│ default │ appointment overlay
▼ ▼
┌───────┐ booking client ┌──────┐
│ free │──────────────────────▶ │ busy │
└───┬───┘ PATCH (hold start) └──┬───┘
│ │
▲ │ Appointment.status →
│ hold-timeout job │ cancelled
│ PATCH busy-tentative │
│ → free (after N min) ▼
│ (back to "free" via converter
│ re-emission, OR booking client
│ explicit PATCH)
│
│ ┌─────────────────┐
└──────▶│ busy-tentative │
└─────────┬───────┘
│ booking client (on user confirm)
│ PATCH → busy + POST Appointment
▼
busy (see above)
converter only
│
▼
┌──────────────────┐
│ busy-unavailable │ (within-day blockout: ScheduleDate.available != '1')
└──────────────────┘
Holidays (rows in OscarPro's scheduleholiday table) produce no Slots on the holiday date rather than busy-unavailable Slots — the absence of Slots is the signal.
Writer ownership¶
| Writer | Allowed values |
|---|---|
| Oscar-side converter | free, busy, busy-unavailable |
| Booking system | busy-tentative, busy (on confirm), free (on cancel) |
| Hold-timeout reconciler | free (downgrades busy-tentative after timeout) |
The Oscar-side converter NEVER writes busy-tentative. That status is the booking system's exclusively. The contract lives in NexusEmrCoreSlot's intro markdown so it ships with the IG.
Concurrency¶
If two writers race on the same slot (e.g. the converter re-emits at the same moment the booking system writes busy-tentative), the IG-level mitigation is If-Match optimistic concurrency on the booking system's update. This is not currently enforced at the profile level — surfaced as a follow-up if production data shows races.
Hold timeouts¶
Hold timeout duration is a booking-system concern, not an IG concern. A common pattern:
- 5-minute hold: user is shown a slot's confirm screen with a countdown
- On confirm: PATCH
Slot.status = busy+ POST Appointment - On timeout: hold-timeout reconciler PATCHes
Slot.status = free
The IG doesn't define a Slot.heldUntil extension or hold duration field. If a future booking client needs the hold duration carried on the resource (rather than in the booking system's own state), that's an additive extension in a future minor version.
Walk-ins¶
Walk-in appointments — where a patient arrives without a prior Slot reservation — produce an Appointment with .slot omitted entirely. The Appointment.slot cardinality is 0..* (not 1..*) precisely to support this case alongside historical appointments imported from EMRs that predate the Slot model.
See NexusEmrAppointment-walk-in-example.json for a worked example.
Overbooking¶
Slot.overbooked is set by the booking system (not the converter) when multiple Appointments are intentionally booked into the same slot window — e.g. a walk-in is squeezed in over a held slot. Informational only; the IG doesn't enforce a maximum.