Booking
A booking puts a patient into a concrete time slot generated from a Schedule. TokenSlot is a bookable unit of availability; TokenBooking is a patient's appointment against that slot, carrying its status through the lifecycle. You touch both whenever an integration creates, cancels, reschedules, or reads an appointment.
Source:
- Model:
care/emr/models/scheduling/booking.py - Resource spec:
care/emr/resources/scheduling/slot/spec.py - Viewset (side effects):
care/emr/api/viewsets/scheduling/booking.py
These models live in two layers. The Django model is storage: status is an unconstrained CharField and several relations are opaque at the DB level. The Pydantic resource specs (care/emr/resources/scheduling/slot/) are the API layer — they pin status to a fixed enum, validate writes, and define the read schemas that embed slot, resource, facility, token, charge item, encounter, and patient sub-objects.
Models
| Model | Purpose |
|---|---|
TokenSlot | A concrete, bookable time window generated from a schedulable resource's availability |
TokenBooking | A patient's appointment against a TokenSlot, with status, notes, and optional links to an encounter, token, and charge item |
Both extend EMRBaseModel, the shared Care EMR base providing external_id, created_date/modified_date, soft-delete via deleted, created_by/updated_by, and history/meta JSON.
TokenBooking fields
| Field | Type | Notes |
|---|---|---|
token_slot | FK → TokenSlot (PROTECT) | The slot this appointment occupies; required. PROTECT prevents deleting a slot that has bookings |
patient | FK → emr.Patient (CASCADE) | The booked patient; required |
booked_on | DateTimeField | auto_now_add — stamped once at creation, never accepted from clients |
booked_by | FK → User (CASCADE) | User who created the booking; nullable for self-service or system-created bookings |
status | CharField (storage) → BookingStatusChoices (API) | Appointment lifecycle status. Unbounded on the model; the write spec restricts it to the BookingStatusChoices enum |
note | TextField | Free-text note; nullable in storage but required (str) on write |
tags | ArrayField[int] | Tag IDs, default empty list. Managed via SingleFacilityTagManager; reads return rendered tag objects, not raw IDs |
associated_encounter | FK → emr.Encounter (PROTECT) | Encounter linked to the appointment; nullable, defaults to None. Surfaced only by TokenBookingRetrieveSpec |
token | FK → emr.Token (PROTECT) | Queue token issued for this booking; nullable, defaults to None, related_name="token_booking". Created by the generate_token action |
charge_item | FK → emr.ChargeItem (CASCADE) | Billing charge item for the appointment; nullable. Auto-created on booking when the schedule has a charge_item_definition |
BookingStatusChoices values
BookingStatusChoices (str, Enum) is the full set of status values the write spec accepts:
| Value | Notes |
|---|---|
proposed | |
pending | |
booked | Default applied server-side when a booking is created via lock_create_appointment |
arrived | |
fulfilled | Terminal; in COMPLETED_STATUS_CHOICES |
cancelled | Set only via the cancel endpoint; in CANCELLED_STATUS_CHOICES + COMPLETED_STATUS_CHOICES |
noshow | Terminal; in COMPLETED_STATUS_CHOICES |
entered_in_error | Set only via the cancel endpoint; in CANCELLED_STATUS_CHOICES + COMPLETED_STATUS_CHOICES |
checked_in | |
waitlist | |
in_consultation | Cannot be cancelled — the cancel handler rejects it |
rescheduled | Set only via the reschedule endpoint; in CANCELLED_STATUS_CHOICES + COMPLETED_STATUS_CHOICES |
Two derived sets (from slot/spec.py) drive the lifecycle rules:
| Set | Values |
|---|---|
CANCELLED_STATUS_CHOICES | entered_in_error, cancelled, rescheduled |
COMPLETED_STATUS_CHOICES | fulfilled, noshow, entered_in_error, cancelled, rescheduled |
TokenBookingWriteSpec.perform_extra_deserialization rejects any write whose status is in CANCELLED_STATUS_CHOICES with "Cannot cancel a booking. Use the cancel endpoint". Cancellation and rescheduling go through dedicated endpoints (see below), never a plain update.
Related models
TokenSlot
A bookable window derived from a resource's availability. Bookings reference it via PROTECT, so a slot with active bookings cannot be deleted.
| Field | Type | Notes |
|---|---|---|
resource | FK → SchedulableResource (CASCADE) | The schedulable resource — practitioner, location, or healthcare service — the slot belongs to; required |
availability | FK → Availability (CASCADE) | The availability rule that generated this slot; nullable |
start_datetime | DateTimeField | Slot start; required, tz-aware |
end_datetime | DateTimeField | Slot end; required, tz-aware |
allocated | IntegerField | Count of bookings currently allocated to the slot, used to enforce capacity. Defaults to 0. Platform-maintained — never written by clients |
SchedulableResource and Availability live in the Schedule module. SchedulableResource.resource_type is one of SchedulableResourceTypeOptions: practitioner, location, healthcare_service.
Resource specs (API schema)
Every spec extends EMRResource (base.py): serialize (DB → pydantic, read) and de_serialize (pydantic → DB, write), with perform_extra_serialization / perform_extra_deserialization hooks.
| Spec class | Role | Exposes / behaviour |
|---|---|---|
TokenBookingBaseSpec | shared | __model__ = TokenBooking, __exclude__ = ["token_slot", "patient"] |
TokenBookingWriteSpec | write · create + update | Fields: status: BookingStatusChoices, note: str. Rejects status in CANCELLED_STATUS_CHOICES (use the cancel/reschedule endpoints). Serves as both pydantic_model and pydantic_update_model |
TokenBookingMinimumReadSpec | read · embed | Lightweight read for when a booking is embedded in another resource, such as a token. Fields: id, token_slot (embedded TokenSlotBaseSpec), booked_on, status: str, note, created_date, modified_date |
TokenBookingBaseReadSpec | read · shared | Adds booked_by: UserSpec, resource_type, resource (serialized resource), facility, created_by/updated_by, token: TokenReadSpec, tags: list[dict] (rendered), charge_item: dict |
TokenBookingReadSpec | read · list/default | TokenBookingBaseReadSpec + patient: PatientRetrieveSpec. The viewset's pydantic_read_model for list responses |
TokenBookingOTPReadSpec | read · OTP flow | TokenBookingBaseReadSpec + patient: PatientOTPReadSpec for the public/OTP booking flow |
TokenBookingRetrieveSpec | read · detail | TokenBookingReadSpec + associated_encounter: dict (serialized EncounterListSpec). The viewset's pydantic_retrieve_model |
TokenSlotBaseSpec | read · embed | __model__ = TokenSlot, __exclude__ = ["resource", "availability"]. Fields: id, availability (embedded as {name, tokens_per_slot, id, schedule:{name,id}}), start_datetime, end_datetime, allocated |
Read serialization details (perform_extra_serialization)
idalways comes fromobj.external_id(UUID), not the integer PK.token_slotis embedded viaTokenSlotBaseSpec.serialize(...).resource_typecomes fromtoken_slot.resource.resource_type;resourceis built byserialize_resource()— aUserSpecforpractitioner,HealthcareServiceReadSpecforhealthcare_service,FacilityLocationListSpecforlocation.facilityis a cachedFacilityBareMinimumSpec;booked_by/created_by/updated_byare cachedUserSpecs.tagsare rendered throughSingleFacilityTagManager().render_tags(obj)as objects, not raw IDs.token(TokenReadSpec),charge_item(ChargeItemReadSpec), andassociated_encounter(EncounterListSpec) appear only when present.
Methods & save behaviour
status has no model-level choices; the lifecycle lives entirely in the spec/viewset layer (TokenBookingViewSet). The server-maintained behaviour:
- Creation (
lock_create_appointment) — runs under a per-resource lock and a transaction. Rejects past slots (end_datetime < now) and full slots (allocated >= availability.tokens_per_slot), rejects a duplicate active booking for the same patient/slot (any status not inCOMPLETED_STATUS_CHOICES), then incrementstoken_slot.allocated, creates the booking withstatus="booked", and — if the schedule has acharge_item_definition— auto-creates the linkedcharge_item. - Cancel (
cancelaction) — acceptsCancelBookingSpec { reason: cancelled | entered_in_error | rescheduled, note?: str }. Rejects cancelling anin_consultationbooking. Decrementstoken_slot.allocated(unless already cancelled), setsstatus = reason, optionally overwritesnote, setsupdated_by, and aborts any linkedcharge_item(handle_charge_item_cancel+ statusaborted). - Reschedule (
rescheduleaction) — acceptsRescheduleBookingSpec { new_slot: UUID, new_booking_note: str, previous_booking_note?: str, tags: list[UUID] }and requirescan_reschedule_booking. Cancels the existing booking with reasonrescheduled, then runslock_create_appointmentto create a fresh booking onnew_slotfor the same patient. Rejects rescheduling to the same slot. - Generate token (
generate_tokenaction) — acceptsTokenGenerationSpec { category: UUID, note?: str, queue?: UUID }. Rejects if a token already exists. Resolves or creates aTokenQueuefor the slot's date and resource, allocates the nextnumberunder a queue lock, creates aToken(status="CREATED"), and links it tobooking.token. booked_onisauto_now_add— never set by clients.
API integration notes
TokenSlotandTokenBookingare exposed through Care's scheduling REST API and align with the FHIRSlotandAppointmentresources; payload field names may differ from Django model names.- Writes use
TokenBookingWriteSpec(statusconstrained toBookingStatusChoices,noterequired).cancelled/entered_in_error/rescheduledcannot be set through a plain update — use thecancel/rescheduleactions. - List responses use
TokenBookingReadSpec; detail responses useTokenBookingRetrieveSpec, which addsassociated_encounter. allocatedonTokenSlotis a capacity counter maintained bylock_create_appointmentand cancel; do not write it directly.tagsis stored as an array of integer tag IDs but returned as rendered tag objects on read.- List requires a
resource_typequery param and authorization scoped to organizations or resource IDs.
Related
- Reference: Schedule
- Reference: Token
- Reference: Encounter
- Reference: Patient
- Reference: Charge item
- Source: booking.py (model)
- Source: slot/spec.py (resource spec)
- Source: booking.py (viewset)