Scheduling
Packages: packages/domain/scheduling, packages/application/scheduling
Purpose
The Scheduling context manages the allocation of aircraft and instructors over time. It is responsible for all booking creation, conflict detection, and grounded-aircraft enforcement. It coordinates across People and Aircraft without owning their data — it references them by identifier only.
Entities
Booking (aggregate root)
A scheduled allocation of an aircraft for a person, covering a defined time range.
Key attributes:
id— unique identifierpersonId— the person who owns the booking (PersonId)aircraftId— the aircraft allocated (AircraftId)instructorId— optional instructor assigned (PersonId | null)timeRange— start and end time of the bookingstatus—provisional|confirmed|checked_out|completed|cancelled(see Booking status lifecycle below)createdAt— creation timestampcancelledAt— timestamp if cancelled; null otherwise
Flight (entity, owned here — not by Training)
The actual operational record of an aircraft’s use, distinct from the Booking’s planned intent. Created when a Booking is checked out; completed (or marked as a no-flight cancellation) at Return.
Key attributes:
id— unique identifierbookingId— the Booking this Flight was created from (BookingId, one Flight per Booking)aircraftId— the aircraft flown (AircraftId)pilotPersonId— the pilot (PersonId)instructorPersonId— optional instructor (PersonId | null)dispatcherPersonId— who released the aircraft at Checkout (PersonId)status—active|completed|cancelled_no_flightcheckedOutAt/returnedAt— timestampscheckedInByPersonId— who checked the aircraft back in at Return (PersonId | null)hasDefect/defectDescription— defect reporting captured at ReturnstartHobbs/startTach— meter readings captured at Checkout (used to compute hours flown at Return)
Value objects
- BookingId — typed identifier wrapping a UUID.
- FlightId — typed identifier wrapping a UUID.
- TimeRange — start and end
Datepair; validated so end > start; used for all overlap calculations.
Domain rules
- A Booking cannot be created for an aircraft that is
grounded. The aircraft status must be checked before the Booking is accepted. - A Booking cannot overlap with another Booking occupying the same aircraft over the same time range — see Conflict detection for which statuses count as occupying.
- If an instructor is assigned, the instructor cannot already have an occupying booking in the same time range.
- The booking owner (
personId) must be an active Person. - A Booking with
status: cancelledcannot be modified or reassigned;checked_out/completedBookings also cannot be cancelled. - Bookings cannot be backdated beyond a configurable threshold.
- A Flight can only be created from a
confirmedBooking with no existing Flight; both the pilot and (if assigned) instructor must be active People at Checkout. - A defect reported at Return requires a non-empty description; a reported defect auto-grounds the aircraft — no severity triage in this MVP.
Booking status lifecycle
provisional → confirmed → checked_out → completed
↓ ↓
cancelled
provisional is reserved for a future self-service/planning flow — createBooking produces a
confirmed Booking directly in the current MVP. checked_out and completed are produced by
the Checkout and Return transactions (below), not by any direct status-setting use case.
Key use cases
createBooking— validate availability, detect conflicts, persist a new confirmed Booking.cancelBooking— transition a confirmed Booking to cancelled.updateBooking— amend time range or instructor assignment (subject to re-validation of all rules).listBookings— return Bookings filterable by aircraft, person, instructor, and date range.getBooking— return a single Booking by ID.checkAvailability— query whether an aircraft (and optional instructor) is free in a given time range.checkoutBooking— the Checkout transaction: validates the confirmed Booking and no existing Flight, verifies the pilot/instructor are active, dispatches the aircraft viaAircraftDispatcher(recording optional start meter readings), creates the Flight, and transitions the Booking tochecked_out.returnBooking— the Return transaction: validates the checked-out Booking and its Flight, requires a defect description if a defect is reported, releases the aircraft viaAircraftDispatcher(recording optional end meter readings; a defect auto-grounds the aircraft), completes the Flight, transitions the Booking tocompleted, and records a Billing Usage Event viaUsageEventRecorder.
Not yet built: no UI screen calls
checkoutBooking/returnBookingyet — both are fully implemented and D1-persisted (FlightRepositoryD1,AircraftDispatcherD1), but only callable programmatically today. Wiring a screen to them is the next UI-focused issue on the roadmap.
Conflict detection
Conflict detection is a first-class domain rule, not an infrastructure concern. The domain checks for overlapping TimeRange values across occupying bookings before accepting a new one — confirmed, checked_out, and completed all occupy the aircraft/instructor’s time (provisional and cancelled do not); see OCCUPYING_BOOKING_STATUSES in booking.ts, the single source of truth both the domain check and BookingRepositoryD1’s query derive from. The check must cover:
- Aircraft conflicts: same
aircraftId, overlappingtimeRange. - Instructor conflicts: same
instructorId, overlappingtimeRange.
An overlap exists when newBooking.start < existing.end AND newBooking.end > existing.start.
Cross-context write access: the AircraftDispatcher and UsageEventRecorder ports
Checkout/Return need to write to Aircraft (dispatch/release/ground) and Billing (record a
Usage Event) — a stronger coupling than the read-only ID references elsewhere in this doc.
Both are handled the same way: a port defined here in application/scheduling
(AircraftDispatcher, UsageEventRecorder), implemented in packages/infrastructure/repositories
(AircraftDispatcherD1, UsageEventRecorderD1). Scheduling never imports domain/aircraft’s
mutation functions or domain/billing directly — only these two ports, keeping the
identifiers-only cross-context rule intact even for writes.
Resolved:
AircraftDispatcherD1’s concurrency guard between two near-simultaneous dispatch/release calls on the same aircraft — see aircraft.md (issue #40).checkoutBooking/returnBooking’s Aircraft-write-before-Flight/Booking-write ordering gap — Flight+Booking now persist as a single atomic D1 batch (FlightBookingWriterport); if that batch fails, the Aircraft write is automatically compensated, with a distinctpartial_failure_needs_recoveryerror if compensation itself also fails. Full design and rejected alternatives in ADR-007 (issue #31).UsageEventRecorder’s separate post-write failure mode is explicitly not covered by this — left as a distinct follow-up.
Operating hours
A single, organisation-wide Operating Hours value (openHour, closeHour) constrains when
Bookings may be scheduled. Owned by the OperatingHoursRepository port in
packages/application/scheduling, implemented by OperatingHoursRepositoryD1. Not per-aircraft
or per-person in v1.
Cross-context relationships
| Context | Usage |
|---|---|
| People | Reads active Person status and Instructor type via port before allowing a booking |
| Aircraft | Reads aircraft status (grounded/available) via port before allowing a booking |
| Training | A Lesson may reference a BookingId; references the Scheduling-owned Flight by FlightId |
| Billing | A Charge Item may reference a BookingId; returnBooking writes a Usage Event via the UsageEventRecorder port (write, not just a read reference — see below) |
| Notifications | Booking confirmation and cancellation events trigger Notifications |
Scheduling depends on People and Aircraft by port — it calls repository interfaces that return minimal read-models, not full domain objects.
Booking confirmation email
When a booking is successfully created via /schedule/new, the server sends a confirmation email to the booking person. The email includes aircraft registration, type, date and time, duration, and instructor name (if assigned). Sending is fire-and-forget and does not block the redirect to the booking detail page.
See email architecture for the full workflow diagram.