Baseleg Docs
Domain · v1 spine / Scheduling · Baseleg Docs
v1 primary

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 identifier
  • personId — 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 booking
  • statusprovisional | confirmed | checked_out | completed | cancelled (see Booking status lifecycle below)
  • createdAt — creation timestamp
  • cancelledAt — 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 identifier
  • bookingId — 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)
  • statusactive | completed | cancelled_no_flight
  • checkedOutAt / returnedAt — timestamps
  • checkedInByPersonId — who checked the aircraft back in at Return (PersonId | null)
  • hasDefect / defectDescription — defect reporting captured at Return
  • startHobbs / 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 Date pair; validated so end > start; used for all overlap calculations.

Domain rules

  1. A Booking cannot be created for an aircraft that is grounded. The aircraft status must be checked before the Booking is accepted.
  2. 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.
  3. If an instructor is assigned, the instructor cannot already have an occupying booking in the same time range.
  4. The booking owner (personId) must be an active Person.
  5. A Booking with status: cancelled cannot be modified or reassigned; checked_out/completed Bookings also cannot be cancelled.
  6. Bookings cannot be backdated beyond a configurable threshold.
  7. A Flight can only be created from a confirmed Booking with no existing Flight; both the pilot and (if assigned) instructor must be active People at Checkout.
  8. 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 via AircraftDispatcher (recording optional start meter readings), creates the Flight, and transitions the Booking to checked_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 via AircraftDispatcher (recording optional end meter readings; a defect auto-grounds the aircraft), completes the Flight, transitions the Booking to completed, and records a Billing Usage Event via UsageEventRecorder.

Not yet built: no UI screen calls checkoutBooking/returnBooking yet — 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, overlapping timeRange.
  • Instructor conflicts: same instructorId, overlapping timeRange.

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 (FlightBookingWriter port); if that batch fails, the Aircraft write is automatically compensated, with a distinct partial_failure_needs_recovery error 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

ContextUsage
PeopleReads active Person status and Instructor type via port before allowing a booking
AircraftReads aircraft status (grounded/available) via port before allowing a booking
TrainingA Lesson may reference a BookingId; references the Scheduling-owned Flight by FlightId
BillingA Charge Item may reference a BookingId; returnBooking writes a Usage Event via the UsageEventRecorder port (write, not just a read reference — see below)
NotificationsBooking 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.