A AppointmentType is a booking template. It says: "to book this
kind of appointment, the caller must provide one (or more) calendar
from each of these named slots, and they all have to be free at the
same time."
It is the right primitive whenever a booking needs to combine calendars that are picked from a pool at booking time — which is the dominant pattern in clinical scheduling.
Compared to a bundle: bundles fix the membership ("always Dr. Lee + this one room"); appointment types fix the roles and let you pick from a pool per role ("any cardiologist + any cath lab").
AppointmentType "Cardiology Outpatient"
│
├── AppointmentTypeSlot name="Physicians" order=0 required_count=1
│ └─ AppointmentTypeSlotMembership rows (the pool):
│ • Dr. Lee
│ • Dr. Patel
│ • Dr. Okafor
│
├── AppointmentTypeSlot name="Rooms" order=1 required_count=1
│ └─ Pool: { Exam Room 3, Exam Room 4, Exam Room 5 }
│
└── AppointmentTypeSlot name="Nurses" order=2 required_count=2
└─ Pool: { Nurse Ana, Nurse Ben, Nurse Cho, Nurse Dan }
Booking on this appointment type requires the caller to pick:
…and the system will only allow the booking if every picked calendar is free for the requested time window.
AppointmentTypeThe template aggregate. Per-organization unique by name. Has many
AppointmentTypeSlots (its slots) and many CalendarEvents (the
bookings made through it).
AppointmentTypeSlotA required role inside an appointment type. Carries:
name — human-meaningful ("Physicians", "Rooms", "Nurses").order — display/iteration order. The lowest-order slot is by
convention the primary slot for booking (its first selection
becomes the event's primary calendar).required_count — how many calendars from the pool must be picked at
booking time (default 1; 2 for the "two nurses" example above).calendars — many-to-many to Calendar through
AppointmentTypeSlotMembership. This is the pool.AppointmentTypeSlotMembershipThrough table linking a Calendar to an AppointmentTypeSlot. One row
per (slot, calendar). A given calendar may belong to multiple slots
across the same or different appointment types.
CalendarEventAppointmentTypeSelectionA row per (event, slot, calendar) recording which calendars satisfied
which slot for a particular booking. When CalendarEvent.appointment_type
is non-null, the event has one or more selection rows describing the
picks. The unique constraint enforces no duplicate (event, slot,
calendar) tuples.
CalendarEvent.calendar_fk (the event's "primary" calendar) is also
recorded inside the selections — it's the picked calendar from the
lowest-order slot.
AppointmentTypeService.create_appointment_type_eventThe service's create_appointment_type_event method:
>= required_count
calendars, all from that slot's pool, with no duplicates.(start_time, end_time) via
Calendar.objects.only_calendars_available_in_ranges. The whole
booking is rejected if even one calendar is busy.order slot.CalendarService.create_event on the primary calendar
so existing side-effects (external-provider sync, permissions,
attendee invites) all run unchanged.CalendarEventAppointmentTypeSelection rows for every (slot,
calendar) pick.BlockedTime on every non-primary selected calendar so
they appear busy. The service skips the BlockedTime for cases
where an external-provider invite will reliably create an equivalent
event natively (same provider on both sides, with an attendee link).AppointmentTypeQuerySet.only_appointment_types_bookable_in_ranges(ranges) —
returns appointment types where every slot has at least required_count
available calendars for every range. Use when listing which appointment types
a patient can book against.AppointmentTypeService.check_appointment_type_availability(appointment_type_id, ranges) —
for one appointment type, returns per-range, per-slot lists of which pool
calendars are available. Use when rendering "who's free?" UIs.AppointmentTypeService.find_bookable_slots(appointment_type_id, search_window, duration, slot_step) — walks the search window in fixed steps and
returns timestamps where every slot is satisfiable. Use to drive a
"show me bookable times this week" picker.The driving example. A physician + a room.
"Cardiology Outpatient".Physicians, pool of 4 cardiology personal calendars,
required_count=1.Rooms, pool of 3 exam-room resource calendars,
required_count=1.When a patient books a 2:30 PM Tuesday consult:
BlockedTime.Surgery for a patient typically needs a surgeon, an anaesthesiologist, an OR, and a circulating nurse. The pools are fluid: any qualified surgeon, any of the OR's matching that surgeon's specialty, etc.
"General Surgery".Surgeons (req 1), Anaesthesiologists (req 1), ORs
(req 1), Scrub Nurse (req 1), Circulating Nurse (req 1).find_bookable_slots does. Once a window is
picked, the user (or an algorithm) chooses one calendar from each
slot.A hospitalist on-call shift covers Friday 7 PM – Saturday 7 AM. The shift is satisfied by one hospitalist picked from a roster pool.
"Overnight Hospitalist On-Call".Hospitalist, pool = the hospitalist personal calendars,
required_count=1.The "pick any free one from the pool" semantics make AppointmentType
useful even for single-resource bookings where the caller wants a
generic "anyone" affordance instead of pre-selecting.
A complex chronic-care visit might require:
That's five slots, with required_count=2 on the medical-assistants
slot — exactly what the model is shaped to do.
For a telehealth visit that needs a third-party interpreter:
Physicians (pool of physicians on the platform),
required_count=1.Interpreters (pool of interpreter virtual calendars filtered
by language), required_count=1.Telehealth Endpoints (pool of virtual room calendars),
required_count=1.The same machinery handles purely-virtual bookings.
update_appointment_type reconciles slots and pool memberships against the
incoming spec. Slots missing from the incoming data are deleted, but
the service refuses to drop a slot, or evict a calendar from a
slot's pool, if a future booking selects that calendar in that
slot — to avoid orphaning live appointments.delete_appointment_type refuses if there are any past or future events
referencing the appointment type (the FK from CalendarEvent is PROTECT).required_countMost slots are 1-of-N ("one cardiologist"), but a few need N-of-M:
required_count lets the same model express both without a special
case. The pool size must always be at least required_count.
CalendarEvent — recurrence,
attendances, external attendees, and resource allocations all work as
documented in events.md and recurrence.md.only_calendars_available_in_ranges — see
availability.md. Improvements to that method
automatically improve appointment type bookability.