Skip to main content

Scheduling & Appointments — How it works

Overview

The scheduling engine (M13-001…003) owns provider and service calendars and the appointment lifecycle. An administrator defines a schedule template — the weekly pattern a provider or a facility service works — and schedule blocks carve absences out of it. Bookable slots are then computed on read from template minus blocks minus existing appointments, and an appointment is booked into one. Arrival at the desk hands the patient over to the clinical and workflow engines.

Data model & ownership

TablePurpose
schedule_templateA weekly pattern for one provider_id or one facility_service_id; slot duration, capacity, effective-from/to.
schedule_template_dayOne working window (day_of_week, start_time, end_time) on a template.
schedule_blockAn absence: a provider's leave, or (with no provider) the whole facility closed for a window. Carries a mandatory reason.
appointmentThe booking: patient, optional provider/service, starts_at/ends_at, relational status, appointment_type_id.

Migrations: scheduling/001 (templates + blocks), scheduling/002 (appointments), scheduling/003 (appointment visit type).

There is no slot table, by design. A stored slot is a second source of truth that drifts from the template the moment anybody edits one, and a year of empty slots for every provider is a table nobody reads. AvailabilityQueryService computes them per query instead.

Key rules & invariants

  • appointment.appointment_type_id is a visit_type id. It is the kind of visit that was booked, and it is what arrival opens — and therefore what decides which patient journey the patient walks (visit_type_journey_mapping). A null falls through to the clinical engine's default rather than inventing intent for appointments booked before the column existed.
  • Status is never taken from the request. Every appointment starts BOOKED; a client that could post COMPLETED would be writing a clinical event that never happened. Statuses are BOOKED, CONFIRMED, CHECKED_IN, COMPLETED, CANCELLED, NO_SHOW; the first four occupy slot capacity, and the last three are terminal (arrival and cancellation refuse them).
  • Arrival is idempotent and does not touch the queue. arrive resolves the visit through EncounterContextService ("the named visit, else the patient's open one here, else open one"), marks the appointment CHECKED_IN, and stops. The workflow engine starts the journey and the M28-003 bridge does the enqueueing. A receptionist clicking twice must not open a second stay.
  • Emergency cover at arrival (M13-003B): the booked provider can be swapped at check-in, and the replacement is refused if they are already blocked or booked for that window.
  • Blocking is refused over live appointments. Creating a block across a window that already has occupying appointments fails rather than silently stranding patients.
  • Booking is patient-scoped. book() consults the patient access guard — holding the booking permission says nothing about whether this caller may act on this patient. ⚠ It currently calls authorize rather than authorizeOrNotFound, so a denial answers 403 where an unknown patient answers 404. That asymmetry is an existence oracle and is a known open item.

API

See the API Reference. Endpoints under /api/v1/scheduling:

VerbPath
POST / GET / PUT / DELETE/schedule-templates[/{id}]
POST / GET / DELETE/schedule-blocks[/{id}]
GET/availability?providerId=&from=&to= — computed slots
POST / GET/appointments (search by provider, patient, date range)
POST/appointments/{id}/arrive[?providerId=] — check-in
POST/appointments/{id}/cancel?reason=
POST/appointments/{id}/reschedule

Note that arrive and cancel take query parameters, not a body.

Permissions

Declared on SchedulingModuleDescriptor so the catalog seeder registers them: scheduling.template.read / .write, scheduling.calendar.read, scheduling.appointment.read / .write / .manage.

Configuration & feature flags

Domain-module Feign client exchange.client.scheduling.SchedulingClient; URL property scheduling.service.url (SCHEDULING_SERVICE_URL).