Booking — Core concepts
Domain objects to know before booking
Normal booking does not create courses or schedules from scratch. It reads configuration from the product hierarchy, lets the user select sessions, then writes that selection into the order/booking runtime.
ProgramCategory -> Program -> TermProgramSet -> TermProduct / Session
^
|
Term| Object | What it is | Role in booking |
|---|---|---|
| Term | Operating period/term with StartDate, EndDate, status, and usually a site/org scope. | Sets the time boundary for courses and sessions; one Term contains many TermProgramSet records. |
| ProgramCategory / Category | Program type, such as TermCare, Holiday, or Class. | Holds many base rules for booking, payment, discount, invoice, cut-off, and combine booking. |
| Program | A concrete program under a category, usually tied to a site/org. | Acts as the course/template source used to create term-specific offerings. |
| TermProgramSet / TPS | A concrete offering/course within a Term. | Defines schedule, price, capacity, weekdays, discounts, and booking display metadata. |
| TermProduct / Session | A concrete session/slot generated from a TPS, with date, time, capacity, price, and ProductType. | The smallest selectable unit recorded on booking lines. If someone says "Section" in this booking context, read it as Session/TermProduct, not a separate entity. |
| Holiday | A special ProgramType/category (Holiday = 12). | Uses booking modes Casual/Standard, FullTime/Full Term, and PartTime/Full Week; also supports day activities. |
| Activity | ProductType.Activity, used only by Holiday. | A holiday day theme/activity; it has no price and is not the main time slot like a Session. |
Runtime objects connect the user to that setup:
| Object | What it is | When it appears |
|---|---|---|
| Account | Family/payer responsible for finance. | Selected manually in admin booking, or resolved from the authenticated customer flow. |
| Attendee | Child/participant being booked into sessions. | Each order/booking targets one attendee; validation uses profile fields such as age, school year, height, and weight. |
| TermBookingOrder | In-flight booking request, like a cart/request. | Created when sessions are selected and submitted before confirmation. |
| TermBooking | Confirmed booking, the official runtime record. | Created when a TermBookingOrder is confirmed. |
| TermAttendance | Roll-call/attendance record for each confirmed session. | Created during booking confirmation. |
| Billing / Invoice | Booking finance ledger and invoice. | Billing is created on submit; invoice generation is triggered on confirm according to payment/finance rules. |
Order vs Booking: request vs confirmed
The most important distinction:
| TermBookingOrder | TermBooking | |
|---|---|---|
| What | An in-flight booking request (cart) | A confirmed booking (actual runtime) |
| When | From creation until confirmation | After confirmation |
| Child rows | TermBookingOrderLine/Extra/Discount | TermBookingLine/Extra/Discount |
| Finance | Submit → creates Billing | Confirm → triggers Invoice + TermAttendance |
The system models the flow using
TermBookingOrderfor in-flight requests andTermBookingfor confirmed bookings.
Subscription Enrichment is a Booking-owned charge
Subscription Booking does not go through the normal booking TermBookingOrder. When the Enrollment is confirmed, each valid Enrichment occurrence creates one active TermBookingLine and one corresponding TermAttendance.
| Concept | Rule |
|---|---|
| Occurrence identity | TermBookingId + TermProductId + Date; no two active lines may share the key. |
| Price | Read the current TermProduct.RateStandard on the server; do not accept client price/count/amount. |
| Accounting item | Read TermProduct.RateStandardItemId; if missing or stale, fail closed before mutation. |
| Base Subscription session | Remains a zero-priced line under the existing semantics; it is not replaced by Enrichment. |
| Booking total | BookingAmount/TotalAmount includes priced Enrichment lines and current adjustments. |
| Subscription Billing | Do not copy the amount from TermBookingLine into the schedule or Subscription invoice. |
Generation and Manage Enrichments are both retry-safe; replay only converges to one active line per occurrence identity.
Real-world example: A Booking has Morning Care on 01/10 at $5 plus a zero-priced base session. → The Booking total is $5; a retry worker does not create a second line; the Subscription invoice still uses its own schedule.
Status lifecycle
TermBookingStatus (Tux/src/Tux.Core/Enums/TermBookingStatus.cs):
| Status | Code | Meaning |
|---|---|---|
Removed | 0 | Deleted request / change request. |
Draft | 100 | Draft. |
Created | 200 | Saved to cart (after CreateBookingOrder). |
Submitted | 300 | Submitted new request / saved change request. |
Submitted_Change | 350 | Affected by other submits — not displayed. |
Attendance_Approved | 450 | Attendance-only confirmation. |
Quote | 475 | Quote generated. |
Accept | 485 | Quote accepted. |
Approved | 500 | Confirmed (final). |
Archived/Suspended/Closed | 600/700/800 | Archived/suspended/closed states. |
Canceled | 900 | Cancelled. |
Processing(400) is NOT IN USE.
Confirm — target status and conditions
ActionType decides the target status on confirm:
Action (ActionType) | Target status | Meaning |
|---|---|---|
Confirm_Booking | Approved (500) | Full confirm: creates TermAttendance, triggers Invoice. |
Confirm_Attendance | Attendance_Approved (450) | Confirms attendance only, not gated by Billing Difference (INV-BOOK-26). |
Confirm_Invoice | Quote (475) | Creates a quote only. |
An order is eligible for confirm when:
IsActiveandTypeId∈ {Booking(0),TrialSession(2)} —WaitingList/Subscription/EoIare not confirmed through this path.StatusId∈ {Submitted(300),Submitted_Change(350),Attendance_Approved(450),Quote(475)}.- One confirm pass only groups orders with the same
OrgId/AccountId/StatusId/TypeId/ProgramCategoryId/BillingId(INV-BOOK-03);Confirm_BookingonQuotestatus uses a narrow order set only (INV-BOOK-16).
Gates before confirm:
- Add-on capacity: an order line fills add-on capacity → confirm blocked, unless
IgnoreDoubleBooking. - Session capacity:
ValidateCapacityForConfirmBookingchecks session headcount before confirm. - Billing Difference: editing a confirmed booking (original status
Approved/Attendance_Approved/Quote) requires a preview before confirm;Confirm_Attendancealways skips this step.
Order type — booking kinds
TermBookingOrderType (Tux/src/Tux.Core/Enums/TermBookingOrderType.cs):
| Type | Code | Note |
|---|---|---|
| Booking | 0 | Normal booking (scope of this page). |
| WaitingList | 1 | Waiting list. |
| TrialSession | 2 | Trial session (shares submit/confirm path with Booking). |
| Subscription | 4 | Subscription booking (Enrollment path). |
| EoI | 5 | Expression of Interest. |
⚠️ In the normal booking double-booking guard, records with
TypeId = SubscriptionorWaitingListare not treated as conflicts.MakeupSession/ClosureDayare used only on Line/Extra, not as TypeId.
Booking type — FULL_TIME / PART_TIME / CASUAL
Each line is classified by rate type, then the order type is derived by priority. Enum BookingType: {0,1,2} = {CASUAL, FULL_TIME, PART_TIME} (INV-BC-001).
| Level | Where it is determined | How it is derived |
|---|---|---|
| Line | TermBookingOrderLine.BookingRateTypeId / TermBookingLine.BookingRateTypeId | A saved value → used directly; not saved → compare AppliedItemId against the term product's rate items: RateFullTimeItemId → FULL_TIME, RatePartTimeItemId → PART_TIME, otherwise → CASUAL (INV-BC-003) |
| Order | Derived at use time (BookingTypeHelper.DetermineBookingType), no dedicated column stored | FULL_TIME > PART_TIME > CASUAL: a FULL_TIME line → FULL_TIME; a PART_TIME line → PART_TIME; otherwise → CASUAL (INV-BC-002) |
Line resolve order: TermBookingOrderLine.BookingRateTypeId → TermBookingLine.BookingRateTypeId → ItemId vs TermProduct.RateFtItemId/RatePtItemId → null (falls back to CASUAL). An order with no lines → CASUAL.
Classification affects:
- Payment / eligibility: Direct Debit (
DirectDebitEligibilityService), PayNow (DirectPaymentService.ValidatePayNowEligibilityAsync), deposit (DepositEligibilityService). - Discount:
DiscountEligibilityService+BookingHelper; the PartTime discount scope also includesFULL_TIMElines (INV-BC-004). - Pricing:
BillingServiceresolves rate/item and enriches the preview. - Subsidy gating: shared in the predicate with subsidy flags.
- The family-discount flow uses a separate day-count classifier (
TermBookingService.GetBookingType), not the same item-mapping algorithm; preview and commit semantics differ.
⚠️
TermBookingOrder.IsCasualOrSpecialOnlyonly appears in the DTO projection; the model is marked[NotMapped].
Real-world example: An order has one full-time line (matching
RateFullTimeItemId) and one casual line. → Determine the booking type to compute pricing/discount/payment eligibility. → The order ranksFULL_TIMEby the priorityFULL_TIME > PART_TIME > CASUAL.
Confirmation type — confirm tied to payment
TermBookingConfirmationType (...Enums/TermBookingConfirmationType.cs):
| Type | Code | When |
|---|---|---|
Confirm_Without_Payment | 101 | Confirm with no payment. |
Pay_Now_Pay_All | 201 | Pay in full now. |
Pay_Now_Pay_Deposit | 202 | Pay deposit now. |
Direct_Debit | 301 | Set up recurring debit. |
Admin_Confirm | 401 | Admin confirm (used by subscription direct-confirm). |
PayNow / Deposit / Direct Debit may arm an auto-cancel timeout; PayLater has no timeout but must pass eligibility validation.
The backend fixes ConfirmationTypeId itself if it differs from the paying PaymentOptionId and logs a warning (invariant INV-BOOKING-CONFIRM-001): PayNow/PayNowDeposit → 201; DirectDebit → 301; PayLater (no PaymentOptionId) keeps 101. Checkout does not call ConfirmBookingOrders for PayLater.
Account resolution: admin vs customer
| Context | Target account |
|---|---|
| Admin — new booking | Empty, must select account/attendee manually (not from login/profile/subscription). |
| Admin — edit booking/order | Account of the edited booking/order, locked. |
| Customer — new/edit | Authenticated account, locked. |
Billing Difference review
Billing Difference is a projected financial impact calculation before changes are committed. It applies to Admin Edit of a confirmed booking (original status Approved/Attendance_Approved/Quote). Submit & Confirm Attendance never previews.
- When enabled and eligible: Submit → Preview Billing Difference → Confirm + Mute Notifications.
- Preview uses all
TermBookingOrderIdsreturned by Submit and calculates the whole billing scope; attendee breakdowns explain the result but are not separate per-person calculations. - Preview is read-only. Confirm uses the same IDs/options, checks
PreviewSnapshotToken, and only then writes. - When disabled or ineligible, the flow skips Preview/token and confirms directly according to the existing policy.
Submit & Confirm Attendanceis not gated by Billing Difference because it does not commit a billing change.
For conditions, token behavior, amounts and error codes, see Edit Booking & Preview Billing Diff. For the controller/service trace, see Preview Billing Diff — code flow. For cases such as shared BillingId, archived lines and bulk edits, see Common cases.
Real-world example: Admin reduces a confirmed booking from $65 to $55. → Preview shows the -$10 impact. → Confirm writes billing; if the token is stale, the system requires Preview again.
Next: Architecture & data.
Calendar event popup and special-day programmes
- Booking Calendar maps an event once and reuses the same object for the tile and popup, so the displayed time does not drift. The popup is read-only and keeps the complete attendee list.
+N more, quick-info, editor and alert popups from the library are disabled in Booking Calendar. Transportation and Makeup-session calendars do not use this policy.- Special-day programmes are grouped/matched by their own
sessionGroupKey/bucket and are not bookable; this is a booking-surface rule, not a capacity fallback.
Real-world example: Two special sessions share a bucket but have different products/time slots. → They are not merged. → The matcher uses both product and time slot, while the popup renders the mapped event.