HandbookOperations22

Transport network.

Transport is the riskiest surface in a school after the nurse's office — a missed pickup means a child stranded; a missed authorization means a child on the wrong bus; a missed inspection means a vehicle on the road it shouldn't be. YESS treats Transport like the safety system it is. Every state transition writes a notification, every line of a driver's check-in passes through a boarder-safety trigger, every fuel litre and every field-trip pound posts itself to the finance ledger, and every assignment a substitute receives reaches the substitute's mobile app within the same transaction.

  • 8chapters
  • 5cross-cluster fanouts
  • ≈25 minto read end-to-end
  • EN/FR/ARevery surface

Prologue

What a transport-day actually looks like#

The transport day starts at 05:30 when the ops desk checks tomorrow's confirmed trips and the field-trip queue. By 06:00 drivers are walking around their vehicles with the pre-trip inspection checklist; vehicles that fail with a blocking defect disappear from the driver's mobile app until ops resolves the fail with a workshop reference. At 06:30 the first pickup run starts — and that's where the safety side of this cluster earns its name. Every student the driver marks present passes through a trigger: if the student is a boarder and lacks an active authorization for this route and trip type, an alarm fires within the same transaction and the parent + warden + school admin receive a notification before the driver can advance to the next stop.

Mid-trip incidents — flat tyres, route blockages, accidents — are reported from the driver's mobile app. Critical and high severity events fan out to every parent of every student on the route. If the driver flags injuries, an infirmary visit is auto-created so the nurse sees an inbound case before the bus arrives. Field-trip transports run their own lifecycle — confirmed, in-progress, completed — with cost actuals posting to the finance ledger (DR 5410 Transport-Trips / CR 1100 Cash) the moment ops marks the trip complete. Refuelling posts the same way (DR 5400 Fuel / CR 1100 Cash) without a second click.

The boarder authorization register, the substitute driver system, and the vehicle inspection log are the three safety rails the day runs on top of. Each one has its own chapter; all three converge on a single property: a driver app that only ever shows the routes and vehicles the driver is actually authorized to operate at this moment.

Eight chapters. One network. Read this one first.

Chapter 1

Routes & stops#

A route is a polyline plus an ordered list of stops. The polyline is real street geometry (PostGIS geography(LineString, 4326) per migration 00562); the parent live tracker reads it directly so the bus icon follows actual streets, not straight lines between stops.

Build the polyline three ways from /dashboard/transport/route-builder:

  1. Build from stops. Connects route_stops by stop_order as straight segments → writes a WKT LINESTRING. Quick, no external dependency.
  2. Paste custom coordinates. Admin pastes lat,lng pairs one per line; parser is forgiving.
  3. Snap to roads (OSRM). Calls the school's self-hosted OSRM server (deployed via yess/ops/geo) for real street-following geometry. Button only renders when NEXT_PUBLIC_OSRM_BASE_URL is set.

Each stop carries an arrival_radius_m integer (default 50). A BEFORE INSERT trigger, verify_transport_stop_event (00562:128), runs PostGIS ST_Distance against the stored stop geometry and sets NEW.verified = distance ≤ radius. Unverified events still insert — they surface on the ops dashboard as "ping rejected" so the school can investigate a misconfigured stop or a sloppy driver tap.

Chapter 2

Fuel logs#

Every refuel — litres, unit cost, odometer, vendor, driver — recorded once on /dashboard/transport/fuel, posted to the ledger once. The school never reconciles a fuel spreadsheet against the journal at month-end because the journal entry is written by the same INSERT that wrote the fuel log.

A BEFORE INSERT trigger, post_fuel_log_to_finance (00566:431), runs on every new row: it calls post_transport_fuel_expense(...) (00564), which auto-creates the expense_categories(code='TRANS-FUEL') row on first call, INSERTs an expenses row, posts a journal entry (DR 5400 Fuel / CR 1100 Cash), and returns the new expense_id which gets stamped back on the fuel-log row before the INSERT commits.

Chapter 3

Trip requests#

Field trips, school events, ad-hoc transport — every trip that isn't a regular pickup or drop-off. The request runs a five-state lifecycle on /dashboard/transport/requests, captures assignments and costs at the right step, posts the actual cost to the finance ledger, and mirrors the status onto the linked field trip in the same transaction.

The five-state lifecycle

Request raised

  1. pending — request raised
    • A row exists in transport_requests with status='pending'.
    • Departure time, headcount, pickup + drop-off addresses, route description are set.
    • No vehicle, no driver, no cost are bound yet.
  2. confirmed — vehicle + driver + supervisors + cost-estimate captured
    • Confirm dialog captures vehicle_id, driver_staff_id, optional assistant_staff_id, supervisor_staff_ids (multi-select), cost_estimate, notes.
    • Flips status='confirmed' and stamps confirmed_at + confirmed_by atomically.
    • sync_field_trip_on_transport_confirm trigger mirrors status onto field_trips.transport_status.
  3. in_progress — trip departed
    • A single Start button. Flips status to 'in_progress'.
    • Sync trigger mirrors the field-trip status the same way.
  4. completed — actuals posted to ledger
    • Complete dialog captures cost_actual and an optional vendor.
    • If cost_actual > 0 → post_transport_request_expense RPC creates TRANS-TRIPS category on first call, inserts the expenses row, posts a journal entry (DR 5410 / CR 1100), stamps cost_actual + expense_id back on the request.
    • Status flip to 'completed' comes after the post — if the ledger refuses, status stays at in_progress.
  5. rejected · cancelled — terminal off-paths
    • Reject (from pending) captures a required rejection_reason — visible to the requester.
    • Cancel (from confirmed) captures a reason appended to notes.
    • Both are terminal; field-trip mirror does NOT cancel the parent field trip.

Trip closed + ledger posted

Chapter 3 · continued

Field-trip materialisation#

When a field trip with "needs transport" is published, a row appears in the pending_field_trip_transport_requests view. Ops clicks Materialise → the materialize_field_trip_transport_request(p_field_trip_id) RPC INSERTs a transport_requests row populated from the field trip (route description, departure_at, pickup/dropoff addresses, estimated headcount from field_trip_consents with a fallback of 1) and flips the field-trip's transport_status='requested'. The view drops the row immediately. Re-running the RPC on the same field trip is rejected.

Chapter 4

Driver check-ins#

Per-student presence on every bus trip — and the boarder-safety trigger that protects every line. The driver opens the check-in screen on the mobile app (apps/mobile/src/app/(driver)/check-in.tsx) and marks every student present / absent / excused. Ops review on /dashboard/transport/driver-checkins.

The data shape is a header + line model. The header is one row in transport_driver_checkins for one driver + vehicle + route + trip type + date. The lines are rows in transport_driver_checkin_lines, one per student.

When the driver inserts a line with presence='present' for a student whose boarding_status is boarder or weekly_boarder, the BEFORE INSERT trigger enforce_boarder_safety_on_checkin (rewritten in 00633) runs three steps:

  1. Skip if not a boarder. Reads students.boarding_status; day students return immediately.
  2. Consult daily-absence declarations. Looks for an active row in transport_daily_absences covering (student, date, trip_type). If found, presence is rewritten to 'excused' with excused_reason='Pre-declared by parent (transport_daily_absences)' and the trigger returns. No alarm fires — this was the 00633 fix; before that release, parent-declared absences still raised false positives when the driver manually marked a student present.
  3. Check authorization, else alarm. Calls check_boarder_authorization(student_id, route_id, trip_type, trip_date). If it returns NULL, presence is rewritten to 'unauthorized_boarder' AND an INSERT into transport_boarder_safety_events fires the fanout trigger which writes notifications to the parent (multi-hop join through parent_student_links → parents → user_profile_id), the warden, and the school admin. The parent's phone buzzes before the driver advances to the next student.

The header row's totals (total_expected / total_present / total_absent / total_excused) are maintained by two triggers added in 00633: prepopulate_driver_checkin_total_expected stamps the expected count from student_transport_assignments on header INSERT (before 00633 the driver had to type it); rollup_driver_checkin_totals recomputes the present/absent/excused counts on every line change.

Chapter 5

Trip incidents#

Drivers report from apps/mobile/src/app/(driver)/log-incident.tsx: ten incident types (accident, breakdown, flat_tyre, fuel_out, route_change, traffic_jam, student_injury, medical_emergency, security_incident, other), four severities (low / medium / high / critical), description, optional photos, a parent_notify flag, and an injured_student_ids[] array.

Operations review on /dashboard/transport/incidents with a per-row detail dialog that shows the full context — trip, vehicle, driver, photos, injured-student names with parent phone numbers, and the resolution audit.

Parent fanout. AFTER INSERT, transport_trip_incident_notify_on_insert (00633:319) fires when parent_notify=true. If injured_student_ids is non-empty, it notifies the parents of those students; otherwise it notifies every parent riding the route via student_transport_assignments. The whole fanout is wrapped in EXCEPTION WHEN OTHERS so a comm-hub failure cannot poison the incident INSERT.

Infirmary fanout. A second trigger, transport_trip_incident_create_infirmary_visits (00633:408), runs when injuries_reported=true AND injured_student_ids is non-empty. For each injured student, INSERTs an infirmary_visits row with visit_kind='accident_emergency', severity mapped 1:1 from the incident, and a complaint/notes pair that carries the auto-trail back to the incident. The nurse on /dashboard/hostel/infirmary sees the inbound case before the bus arrives.

Chapter 6

Vehicle inspections#

The first safety rail — the vehicle walk-around. Default 18-item checklist (brakes, tyres, lights, mirrors, fuel, fluids, doors, emergency exits, first-aid, fire extinguisher, seatbelts, …) covers pre-trip, post-trip, periodic, and safety-recall types. Drivers run pre-trip on the mobile app; ops review on /dashboard/transport/inspections.

Three overall_status outcomes — pass, pass_with_notes, fail. When fail_blocks_trip=true AND resolved_at IS NULL, the mobile RPC driver_my_active_vehicles() excludes the vehicle until ops resolves. No trip can start.

Resolution goes through the transport_inspection_resolve(p_inspection_id, p_resolution_notes, p_workshop_reference) RPC (00575). The RPC stamps resolved_at, resolved_by, resolution_notes, workshop_reference atomically — once those are set, the fail_blocks_trip predicate stops matching in the driver's active-vehicles RPC and the vehicle reappears on the driver's home screen.

Chapter 7

Substitute drivers#

A driver calls in sick at 06:00. The ops chief opens /dashboard/transport/substitutions on her phone, taps Assign substitute, picks the vehicle + route + substitute + time window. The substitute's mobile app surfaces the route within seconds with a "Substitute" badge.

The driver's mobile home screen calls driver_my_active_vehicles() (00633:619). The RPC returns two unions: direct assignments from vehicle_assignments where driver_staff_id = current_staff_id() AND is_active = TRUE, and active substitutions from transport_driver_substitutions where substitute_driver_staff_id = current_staff_id() AND status = 'active' AND effective_from <= now() AND effective_until > now().

The substitution union is computed on every request — there is no realtime channel and no background sweep. An assignment INSERTed at 06:02 appears on the substitute's next pull (typically within seconds because the home screen uses React Query's auto-refetch on focus + a 30 s background poll). An expired window stops matching the instant effective_until passes.

Revoke early via driver_substitution_revoke(p_substitution_id, p_notes): stamps status='revoked', revoked_at, revoked_by_user_id, and the notes. The substitute's app loses the route on the next pull.

Chapter 8

Boarder safety#

The reason this cluster exists. A weekly boarder going home on a Friday she didn't have permission for; a regular boarder boarding a Wednesday bus he wasn't authorised on; either is a school-wide emergency. This chapter explains how YESS makes that emergency impossible to hide.

Two surfaces back two tables:

  • Authorizations. /dashboard/transport/boarder-authorizations. The register — transport_boarder_authorizations. Who has permission to be on what route on what date or which days of the week, with which pickup-person. Authorisation type is one_off, weekly_pattern (ISO weekday array), or standing (no end date).
  • Safety events. /dashboard/transport/boarder-safety. The alarm log — transport_boarder_safety_events. Events fired by the boarder-safety trigger when a boarder ends up on a bus the register doesn't cover.

The register's lifecycle has three steps, all stamped:

  • Grant. Ops creates with type (one_off / weekly_pattern / standing), effective window, trip types, optional pickup person + phone + ID doc, and co-sign flags. When the parent flag is true, INSERT auto-stamps authorized_by_parent_user_id + authorized_by_parent_at. Same for warden.
  • Edit. useUpdateBoarderAuthorization writes any field except status — the status path is reserved for the revoke RPC. Re-stamping the approval audit timestamps happens automatically when a flag flips to true.
  • Revoke. The page's Revoke icon opens a reason-required dialog that calls transport_boarder_authorization_revoke(p_id, p_reason) (00643). The RPC is idempotent — re-calling on an already-revoked row is a no-op. Stamps status='revoked', revoked_at, revoked_by, revoked_reason.

A BEFORE UPDATE trigger tba_auto_expire (00643) auto-flips status to 'expired' on any touched row whose effective_to < CURRENT_DATE. No midnight cron required.

Permissions

Who can do what#

Every Transport mutation is gated by one of four transport.* permission keys defined in packages/logic/src/constants/permissions.ts:

  • transport.view — read every surface. Default for school admins, ops managers, wardens, and (scoped) drivers + parents.
  • transport.create — create routes, vehicles, requests, check-ins, fuel logs, substitutions, inspections, boarder authorizations, incidents. Default for ops managers and (scoped) drivers.
  • transport.edit — confirm / start / complete / reject / cancel requests, revoke authorizations, resolve incidents and inspections, edit existing rows. Default for ops managers, school admins, and (scoped) wardens for the safety surfaces.
  • transport.delete — hard delete routes, vehicles, and trip logs. Soft-deletes use transport.edit via a deleted_at stamp instead. Default for school admins only.

Drivers and parents see the cluster through scoped role views: drivers see only the vehicles + routes their current assignment grants (including any active substitution); parents see only their child's stops and active subscriptions. Scoping is enforced at the RLS layer with public.get_school_id() and supplemental policies, not at the UI layer.

Data flow

What travels outward#

Transport is unusual in that nearly every meaningful event in this cluster writes outward to another cluster in the same transaction. There is no nightly batch and no "we'll notify the parents in the morning" delay.

  • Fuel → Finance. Every fuel-log INSERT fires a trigger that posts a journal entry (DR 5400 / CR 1100) and stamps the expense id back on the row.
  • Trip-request complete → Finance. post_transport_request_expense auto-creates TRANS-TRIPS, inserts the expense, posts the journal entry (DR 5410 / CR 1100), stamps cost_actual + expense_id. Status flip to "completed" comes after the post — if the ledger refuses, status stays at in_progress and the cashier sees an error rather than a half-closed trip.
  • Trip-request lifecycle → Academic (field trips). sync_field_trip_on_transport_confirm mirrors every status change (confirmed → in_progress → completed → cancelled) onto field_trips.transport_status.
  • Incident → Communication. transport_trip_incident_notify_on_insert walks parent_student_links → parents → user_profile_id and writes one notifications row per parent — of injured students if any, otherwise of every route subscriber.
  • Incident with injuries → Cluster 6 (Health). transport_trip_incident_create_infirmary_visits inserts an infirmary_visits row (visit_kind='accident_emergency') for each student in injured_student_ids[].
  • Boarder-safety event → Communication + Conduct. When the boarder-safety trigger fires on a driver check-in, the event is inserted AND a second trigger fans notifications to the parent, warden, and school admin within the same transaction the driver's tap was running in.

What makes this elite

  1. 01

    Same-transaction parent alarms alarm

    The boarder-safety trigger fans a notification to parent + warden + admin within the same transaction the driver's check-in line was inserted in. No queue. No background sweep. The parent's phone buzzes before the driver advances to the next student.

  2. 02

    Incidents create infirmary visits automatically automatically

    When an incident is flagged with injuries, the trigger inserts an infirmary_visits row (visit_kind='accident_emergency') for each injured student. The nurse sees the inbound case before the bus arrives at school.

  3. 03

    Trip-complete posts to the ledger atomically atomically

    Marking a trip request complete calls the post_transport_request_expense RPC: TRANS-TRIPS category, expense row, journal entry (DR 5410 / CR 1100), and the cost_actual + expense_id stamped back on the request — all in one transaction. The status flip waits for the post; the ledger never disagrees with the request.

  4. 04

    Real-time substitute filtering real-time

    driver_my_active_vehicles() re-checks effective_until > now() on every call. The 06:00 substitute sees the route within seconds; the 18:00 expiry stops matching the instant the window closes. No cron, no drift.

  5. 05

    Pre-trip inspections that actually block trips actually

    When a critical checklist item fails and stays unresolved, the vehicle is excluded from the driver's home screen. The mobile app refuses to start the trip — not because the UI politely asks, but because the RPC stops returning the vehicle.

  6. 06

    Routes follow real streets streets

    transport_routes.polyline is a PostGIS geography(LineString, 4326) populated by OSRM or hand. The parent live tracker reads the same polyline so the bus icon traces actual streets — not the lazy straight-line connector other SIS platforms ship.

  7. 07

    Idempotent RPCs everywhere idempotent

    Fuel-expense post, trip-expense post, authorization revoke — all idempotent by construction. A retry after a network blip never produces a duplicate journal entry or corrupts the audit trail.