Booking — Common cases
How to read each case: start with the symptom/question, then read the expected behavior, how to verify it in the UI/API/data/code, the source anchor, and the Real-world example. These cases prioritize running behavior; open the linked spec/code when you need the original contract.
Real-world situations and how the system responds.
Double-booking
- The same attendee cannot book the same TermProduct on overlapping date/time.
- Checked at create, submit, and confirm.
- Admin can bypass with
IgnoreDoubleBooking = true. - Records of type
Subscription/WaitingListare not counted as a normal booking conflict.
Real-world example: A parent books two children into the same Monday morning session. → Avoid accidentally recording the same child twice. → The system blocks the second child at submit unless admin enables
IgnoreDoubleBooking.
How does Subscription Enrichment create Booking lines?
Subscription Booking does not create a normal TermBookingOrder. On confirm, the previewed and server- revalidated occurrence creates a priced/itemized TermBookingLine and a corresponding TermAttendance. The base Subscription session remains a zero-priced line; Enrichment does not go into TermBookingExtra.
Real-world example: Select Morning Care Mon–Fri at a
$5rate. → There are five$5Enrichment lines and five corresponding attendance records. → The Booking total is$25in addition to the base session, with no fake extra.
Subscription Enrichment is duplicated after a retry
Compare the semantic key TermBookingId + TermProductId + Date, not only the Product name. The operation/replay guard must converge to at most one active line and one attendance record per key; the Booking total must not increase.
Real-world example: The confirm queue times out and the worker runs again. → The key for 03/10 already exists. → The retry skips the existing line and does not invoice the occurrence a second time.
Manage Enrichments cannot edit an occurrence
Past, invoiced, credited, attended, or Booking-locked occurrences are locked. A request containing a duplicate date, a date outside the Term, outside the BU/site/attendee/subscription scope, or otherwise invalid must fail atomically; it must not partially add/remove the remaining dates.
Real-world example: Staff removes an invoiced occurrence in the same request as a future date. → Preserve the audit trail and collected amount. → The API returns validation/adjustment-required and does not mutate the whole request.
Height / weight (measurement)
- A course may configure height/weight bounds. An attendee out of bounds → invalid selection, blocks submit.
- A missing required value (e.g. weight) → submit fails for that selection.
- Admin overrides each rule: turning off Height still lets Weight block, and vice versa.
Real-world example: A swimming class sets a minimum weight threshold; the child has no weight recorded. → Ensure the child meets safety conditions before joining the class. → Submit fails for that selection until weight is entered, or admin turns off the Weight rule only.
Booking Cut-off (selecting past sessions)
- By default sessions past the cut-off cannot be selected.
- Admin turns off Booking Cut-off in Booking Rule Options → past sessions become selectable (all categories: Term Care, Holiday, Class).
Real-world example: A child attended Tuesday but registration was forgotten and needs to be back-booked. → Let ops record a session that already happened for billing/attendance. → Admin turns off Booking Cut-off, selects that past session again, then confirms.
Pay Later & auto-cancel timeout
- PayNow / Deposit / Direct Debit may arm a timeout: if unpaid past the deadline, auto-cancel.
- PayLater has no timeout but must pass eligibility validation at submit.
Real-world example: A parent chooses Deposit, then leaves without paying. → Avoid holding a slot indefinitely and blocking capacity. → When the timeout expires, the booking auto-cancels and the slot opens for someone else.
Capacity
- Limited by
TermProduct.CapacityandTermProgramSetCapacity(limited weeks). ValidateCapacityForConfirmBookingruns before confirm.
Real-world example: One place remains, and two parents click confirm almost at the same time. → Do not exceed class capacity. →
ValidateCapacityForConfirmBookingblocks the later confirm and reports no availability.
Add-on capacity (required add-on capacity)
- Add-ons (e.g. bus route) have their own capacity; when an order line fills up → confirm is blocked with
FULLY_ADDON_CAPACITY_BOOKING_FOUND, unless admin enablesIgnoreDoubleBooking. - Different from session capacity: this is the capacity of the add-on attached to the booking, not the session headcount.
Real-world example: A new booking includes a bus route that is already fully subscribed. → Avoid taking on more children than the bus can carry. → Confirm is blocked at the plan-build step; admin enables
IgnoreDoubleBookingto allow it deliberately.
Confirm only when the order is in an allowed state
- Only
TypeId ∈ {Booking, TrialSession}andStatusId ∈ {Submitted, Submitted_Change, Attendance_Approved, Quote}can be confirmed; other orders fail the confirm. Confirm_Booking→Approved,Confirm_Attendance→Attendance_Approved(skips Billing Difference),Confirm_Invoice→Quote.
Real-world example: Admin clicks Confirm on a
WaitingListorder or an alreadyCanceledbooking. → Avoid creating a runtime booking from an invalid state. → Confirm is blocked, the booking does not change state.
Quote → Accept
- Instead of confirming immediately, you can
GenerateQuotes(statusQuote) → customerAcceptQuote(Accept) → confirm (Approved).
Real-world example: Admin finalizes a holiday program for a family but lets them approve the price themselves. → Let the customer review and accept the quote before committing to payment. →
GenerateQuotessends the quote, the customer runsAcceptQuote, then the booking is confirmed.
Cancel → credit note
- Cancelling a confirmed booking: status →
Canceled, reverses invoices via credit note, deactivatesTermAttendance— even if some lines previously leftApproved.
Real-world example: A family cancels a course that was already paid and attended. → Keep the ledger correct and remove sessions that are no longer valid. → Booking becomes
Canceled, the system issues a credit note to reverse the invoice, andTermAttendanceis deactivated.
Stale attendance
- A dry-run process detects
TermAttendancestill active while its parentTermBookingisCanceled(returns a count + sample, without modifying data) so ops can repair safely.
Real-world example: An old issue left attendance behind for a canceled booking, making the attendance report wrong. → Detect bad data before deleting anything automatically. → Dry-run lists the count + sample records so ops can inspect and handle them manually.
Combined edit
- A category with
CombinedBookingOptionId = 1: editing one booking loads same-ActionLogIdbookings across courses in the category. Removing one from the group on reconfirm → cancel + deactivate its attendance. Details: Combine booking rule.
Real-world example: A parent changes one child's schedule that was booked as part of a combined multi-course group. → Keep the whole group in sync instead of drifting booking by booking. → Editing one booking loads bookings with the same
ActionLogId; removing one from the group cancels it and deactivates its attendance.
Confirm Attendance (Submit & Confirm Attendance)
- After submit → calls confirm with action type
Confirm_Attendanceimmediately → statusAttendance_Approved(not yet fully confirmed), does not open the Billing Difference preview. - Submit fails, or does not return the complete
TermBookingOrderIds→ reports an error, does not confirm.
Real-world example: Admin needs to lock in roll call for a booking that already attended but was not financially settled. → Avoid forcing a meaningless financial-delta review. →
Submit & Confirm Attendancesubmits then locksAttendance_Approvedimmediately, no preview.
Editing a confirmed booking (Billing Difference)
The full flow, conditions and token behavior are in Edit Booking & Preview Billing Diff. This page keeps only the operational cases that are easy to confuse.
Editing part of a booking that shares a BillingId
- The Preview scope is the attendees belonging to the
TermBookingOrderIdsreturned by the current Submit. - Other booking groups sharing the same
BillingIdare grouped intoOther booking groups; this row hasIsOutsideSelectedScope = trueand does not increase the selected-attendee count. Summary.Differenceremains the total for the whole billing scope; the breakdown is not an independent per-attendee calculation.
Real-world example: Booking Manager has two groups with the same name,
After School Caress; the upper group has 1 attendee, the lower group has 2 attendees, and only the lower group is edited. → The Confirm popup lists 2 attendees. → Preview shows 2 separate rows and oneOther booking groupsrow for the upper group, while the final total still reconciles to the whole billing record.
Distinguishing the form estimate from Preview billing impact
Estimated fee per attendeeat the bottom of the booking form is a reference amount for the current form, expressed per attendee.Difference/Net billing impactin Preview is the net impact for the whole billing scope:new invoices − credit notes − scheduled-delete invoices.- Because the scope and unit differ, the two values may differ without indicating a billing error. For Confirm, use the net impact and the invoice/credit-note rows in Preview as the authoritative source.
Real-world example: The form shows
$65/attendee but Preview shows+$68. → Do not conclude that Preview is wrong just because$68is higher than$65; first check which selected-attendee invoices, other booking groups, and shared/unallocated bucket the Preview includes.
Archived booking items and finance transactions
TermBookingLine/Extra.StatusId = Archivedis a removed booking item with historical value; if active invoice/credit-note logs remain partially uncredited, that amount must enter reversal/re-billing.Invoice/CreditNote.IsActive = falseis a deactivated/soft-archived transaction; it is excluded from current billed, paid/outstanding, and Preview actions.- The current baseline accepts finance status
Generated,Submitted,Approved, andPaid.Void,Deleted, andKeptare historical/non-current statuses and are not added to the current billing total. - An active credit note with an allowed status is subtracted from current net billed; therefore current net billed may be lower than
Paid to date + Open invoice balance, and the UI must explain the existing credit.
Real-world example: An active
Submittedcredit note reduces current billed by$1.50, while the invoice balance remains$125.00. → Keep the credit because it is current financial credit. → Preview shows current net$123.50without a false warning; aVoidor inactive credit note is excluded.
No original transaction
- No invoice/credit-note baseline → do not build an unreliable Preview amount.
- Open direct-confirm only when the error code is allowed by policy; do not create a
PreviewSnapshotToken.
Real-world example: A booking has never produced an invoice. → Do not show an invented difference. → Admin may direct-confirm under policy, while a flow that already has a token must block if Preview becomes unavailable.
Booking Manager bulk Confirm with edited bookings
- Bulk
Confirm_Bookingqueues only selections containing new bookings. - If at least one edit/pending-edit order is present → reject the entire selection before locks and schedulers.
- Mixed new/edit selections are not split automatically; staff confirms the edited booking separately through Billing Difference.
- Bulk
Confirm_Attendanceremains available because it does not confirm billing.
Real-world example: Admin selects a new booking group together with an edited group for Confirm. → Avoid queueing the new booking while skipping billing review for the edited group. → The system rejects the whole selection; staff confirms the edited group separately through Preview, then can reselect the new booking for bulk Confirm.
Real-world example: A booking has never produced an invoice, so there is no baseline to compare. → Avoid showing an invented difference. → The system offers explicit direct confirmation without a
PreviewSnapshotToken.
Keeping roll call on edit (roll call preserve)
- Edit confirm recreates
TermBookingLineand replacesTermAttendancebecause price/discount changed → keeps the roll call from the old attendance when it hasRollCallStatusId > 0— even if the old one was already deactivated in the flow. - Copies the full
RollCallHistory(action id, timestamps, signed-by, signature, absent reason, note); a confirm retry does not duplicate history. RollCallStatusId = 0(not yet signed) → normal new attendance, no copy. A removed/cancelled session → roll call is not copied elsewhere.
Real-world example: A booking already has Monday's roll call, then admin changes that session's price and confirm recreates the attendance. → Avoid losing the attendance/leaving traces. → The new attendance copies the roll call + history from the old one (invariant
INV-BOOK-27).
Submit & Confirm Attendance (admin)
- Admin Mode has a Submit & Confirm Attendance action: submit first, then call confirm with action type
Confirm_Attendanceusing theTermBookingOrderIdsthe submit returned. - The booking ends at
Attendance_Approved— roll call locked, not a full confirm. - Not gated by Billing Difference (invariant
INV-BOOK-26). - Submit ok but confirm-attendance fails → reports an error so admin can retry or finish in Booking Manager; the booking stays at
Submitted. Submit returns noTermBookingOrderIds→ reports an error, no confirm call.
Real-world example: A swimming class already ran but admin forgot to lock the first session and needs roll call right away. → No fee-delta review needed because this action changes no money. →
Submit & Confirm Attendancebrings the booking toAttendance_Approvedso roll-call staff can work, without a full confirm.
Pending notification suppression (Submit & Confirm)
POST /BookingOrdersin admin mode withStatusId= Confirmed (Submit & Confirm) → does not enqueue/send the pending-booking notification for that request.- A request submit that does not direct-confirm (booking stays pending) → the pending-booking notification runs as usual.
- Backend-only behavior: the
POST /BookingOrderscontract is not changed. - Mute Notifications (admin confirmation popup) → suppresses the action's eligible notification jobs:
- Submit → suppress submitted; Confirm → suppress confirmed.
- Submit & Confirm → submitted already suppressed (direct-confirm) + confirmed suppressed.
- Cancel → suppress cancelled.
- Mute not selected → existing notification behavior stays unchanged.
Real-world example: Admin bulk-finalizes with
Submit & Confirmbut does not want to spam parents. → Avoid unnecessary intermediate-state emails. → A direct-confirm request sends no pending notification; with Mute Notifications on, the confirmed job is not sent either.
“Clicking a calendar event shows no detail”
Booking Calendar uses a new read-only popup. It opens only for a real event, not an empty cell or +N more. The content includes TPS, session, time and every attendee; the library defaults are suppressed.
Real-world example: A tile shows
...and the user needs to know who booked. → Click the event. → The popup renders one attendee per line with the same time zone/format as the tile.
“A special-day programme is still bookable”
Check that the booking surface groups the session with the correct sessionGroupKey/bucket. A special-day programme must be non-bookable; do not infer this state from capacity.
Real-world example: A special session shares a time with a normal class and appears in the same row. → Do not expose the wrong slot. → Match product and time slot within the bucket, then filter the special-day programme.