Busflow Docs

Internal documentation portal

Skip to content

Cross-Context Event Contracts: Operations (ServiceLeg Lifecycle) ​

Formal event contracts for ServiceLeg lifecycle events. For the state machine definition, see schema-operations.md Β§ServiceLeg State Machine. For the broader domain events catalog, see event-catalog.md.


Event Catalog ​

EventEmitterConsumerPurpose
ServiceLegStartedOperations (ServiceLeg β†’ ACTIVE)Communications (trip-in-progress notifications), Backoffice (dispatch board live status)Signals driver has started executing the leg
ServiceLegCompletedOperations (ServiceLeg β†’ COMPLETED)Commerce (triggers BookingNoShow evaluation window for final leg), Communications (trip-completed notifications), Backoffice (dispatch board)Signals leg execution finished
ServiceLegDelayedOperations (ServiceLeg β†’ DELAYED)Backoffice (dispatch board alert overlay)Signals ETA deviation exceeds delay_threshold or driver-reported delay. Does not trigger passenger broadcasts β€” passenger notifications flow exclusively through IncidentCreated (see Β§Incident Lifecycle Events). On transition, the system auto-creates a DELAY Incident if none exists (see note below).
ServiceLegDelayResolvedOperations (ServiceLeg DELAYED β†’ ACTIVE)Backoffice (dispatch board)Signals ETA has recovered below recovery_threshold and sustained for delay_dwell_minutes (hysteresis β€” see schema-operations.md). Auto-resolves any linked system-created DELAY Incident β†’ IncidentResolved fires β†’ Communications sends all-clear.
ServiceLegCancelledOperations (ServiceLeg β†’ CANCELLED)Commerce (seat reservation release), Communications (passenger notification at affected stops), Backoffice (dispatch board)Signals leg cancellation β€” always creates a linked Incident

Payload Schemas ​

ServiceLegStarted ​

FieldTypeDescription
event_idUUIDIdempotency key
tenant_idUUIDOwning operator
service_leg_idUUIDThe started leg
tour_departure_idUUIDParent departure
tour_offering_idUUIDParent offering (for Commerce cross-reference)
leg_typeStringPICKUP, TRANSIT, TRANSFER, DROPOFF, REPOSITIONING
driver_crew_member_idUUIDThe crew member who started the leg
actual_startTIMESTAMPTZActual start timestamp

ServiceLegCompleted ​

FieldTypeDescription
event_idUUIDIdempotency key
tenant_idUUIDOwning operator
service_leg_idUUIDThe completed leg
tour_departure_idUUIDParent departure
tour_offering_idUUIDParent offering
leg_typeStringLeg type
actual_startTIMESTAMPTZWhen the leg actually started
actual_endTIMESTAMPTZWhen the leg actually ended
is_final_legBooleanWhether this is the last leg in the departure (triggers NoShow evaluation)
boarding_countINTNumber of successful boardings (SUCCESS + MANUAL_OVERRIDE)

ServiceLegDelayed ​

FieldTypeDescription
event_idUUIDIdempotency key
tenant_idUUIDOwning operator
service_leg_idUUIDThe delayed leg
tour_departure_idUUIDParent departure
tour_offering_idUUIDParent offering
scheduled_endTIMESTAMPTZOriginal scheduled end
recalculated_etaTIMESTAMPTZNew estimated arrival
delay_minutesINTDeviation in minutes (recalculated_eta - scheduled_end)
delay_sourceStringAUTOMATIC (ETA recalculation) or DRIVER_REPORTED (1-Tap Incident)

ServiceLegDelayResolved ​

FieldTypeDescription
event_idUUIDIdempotency key
tenant_idUUIDOwning operator
service_leg_idUUIDThe recovered leg
tour_departure_idUUIDParent departure
recalculated_etaTIMESTAMPTZUpdated ETA (now within threshold)
resolved_atTIMESTAMPTZWhen the delay resolved

ServiceLegCancelled ​

FieldTypeDescription
event_idUUIDIdempotency key
tenant_idUUIDOwning operator
service_leg_idUUIDThe cancelled leg
tour_departure_idUUIDParent departure
tour_offering_idUUIDParent offering
leg_typeStringLeg type (determines cancellation resolution strategy)
cancelled_byStringDISPATCHER
incident_idUUIDThe linked Incident auto-created on cancellation
had_boarded_passengersBooleanWhether any BoardingEvents with SUCCESS/MANUAL_OVERRIDE exist (determines passenger impact)
cancelled_atTIMESTAMPTZCancellation timestamp

Delivery Contracts ​

EventMechanismTriggerIdempotency
ServiceLegStartedHasura Event Trigger (async, post-commit)operations.service_legs.status β†’ ACTIVEevent_id
ServiceLegCompletedHasura Event Trigger (async, post-commit)operations.service_legs.status β†’ COMPLETEDevent_id
ServiceLegDelayedHasura Event Trigger (async, post-commit)operations.service_legs.status β†’ DELAYEDevent_id
ServiceLegDelayResolvedHasura Event Trigger (async, post-commit)operations.service_legs.status DELAYED β†’ ACTIVEevent_id
ServiceLegCancelledHasura Event Trigger (async, post-commit)operations.service_legs.status β†’ CANCELLEDevent_id

All consumers implement at-least-once processing with event_id-based deduplication. See domain-driven-design.md Β§4 for the standard delivery pattern.

NOTE

ServiceLegDelayResolved trigger (hysteresis): This event is not fired by a simple Hasura Event Trigger on status change. Instead, the ETA recalculation service maintains a debounce timer. When the ETA drops below recovery_threshold (default: 5 min, asymmetric from the 15 min delay_threshold), the service starts a delay_dwell_minutes countdown (default: 3 min). Only if the ETA remains below the recovery threshold for the full dwell period does the service update status β†’ ACTIVE, which then fires the Hasura Event Trigger β†’ NestJS consumer emits ServiceLegDelayResolved. If the ETA rises above recovery_threshold during the dwell period, the countdown resets. This prevents notification flapping when the bus oscillates around the threshold boundary. See schema-operations.md Β§Delay hysteresis for the full parameter table.

IMPORTANT

Auto-incident creation on ServiceLegDelayed: The NestJS webhook handler for the ServiceLegDelayed Event Trigger checks whether a DELAY Incident already exists for the same service_leg_id (dedup: (service_leg_id, type='DELAY', occurred_at Β± 5min)). If no driver-reported Incident exists, the handler auto-creates a system Incident: type=DELAY, severity=CRITICAL, reporter_crew_id=NULL, description='Automatic delay detection', occurred_at=now(). This INSERT fires the IncidentCreated Event Trigger, which enters the unified broadcast chain. If a driver-reported DELAY Incident already exists (driver reported via 1-Tap before the telemetry threshold triggered), the handler skips creation β€” the driver's Incident already fired IncidentCreated. This ensures every delay has exactly one Incident record and one broadcast evaluation, regardless of detection source.

Auto-resolution on ServiceLegDelayResolved: The NestJS webhook handler for the ServiceLegDelayResolved Event Trigger auto-resolves the linked DELAY Incident: status β†’ RESOLVED, resolution_notes='ETA recovered below threshold', resolved_at=now(). This fires IncidentResolved β†’ Communications sends the all-clear to passengers who received the initial delay broadcast. Driver-reported DELAY Incidents with severity=CRITICAL that were explicitly managed by a dispatcher (status β‰  OPEN) are not auto-resolved β€” the dispatcher retains ownership.


Incident Lifecycle Events ​

For the Incident state machine definition, see schema-operations.md Β§Incident State Machine. For the full broadcast chain protocol, see incident-broadcast-protocol.md.

Event Catalog ​

EventEmitterConsumerPurpose
IncidentCreatedOperations (incidents INSERT)Communications (broadcast for CRITICAL, all types), Backoffice (dispatch board alert via Hasura subscription)Signals operational disruption (driver-reported or system-created for telemetry delays)
IncidentResolvedOperations (incidents.status β†’ RESOLVED)Communications (all-clear for passengers who received broadcast)Signals incident resolution

NOTE

Unified broadcast path: IncidentCreated is the sole trigger for all passenger broadcasts β€” DELAY, BREAKDOWN, and PASSENGER_ISSUE. The ServiceLegDelayed event is a pure state-change event (dispatch board + auto-incident creation only; see Β§ServiceLegDelayed note above). For telemetry-detected delays, the ServiceLegDelayed handler auto-creates a DELAY Incident, which fires IncidentCreated β†’ Communications. For driver-reported delays, the driver's Incident fires IncidentCreated directly. This eliminates the previous duplicate broadcast path and conditional consumer routing.

IncidentCreated ​

FieldTypeDescription
event_idUUIDIdempotency key
tenant_idUUIDOwning operator
incident_idUUIDThe created incident
service_leg_idUUIDImpacted leg
tour_offering_idUUIDResolved from service_legs.tour_offering_id β€” enables passenger query without cross-schema join
tour_departure_idUUIDResolved from service_legs.tour_departure_id β€” enables boarding point lookup
boarding_point_idUUIDNullable. Resolved from service_legs.boarding_point_id for PICKUP legs. Enables downstream passenger targeting ([v0.2] β€” requires boarding_order, see note).

WARNING

Schema change: backoffice.boarding_points has been replaced by boarding_point_library (single operator-level library with optional door pickup per stop). The boarding_order column does not exist at V0.1 β€” it is deferred to [v0.2] as a dispatch-side concern. The downstream passenger targeting logic (comparing boarding_order to filter passengers at stops after the incident) must be redesigned when boarding order lands. See boarding-points.md. | severity | String | LOW, MEDIUM, CRITICAL | | type | String | DELAY, BREAKDOWN, PASSENGER_ISSUE | | description | String | Driver's free-text description | | geo_coordinates | JSONB | GPS pin { lat, lng } | | reporter_crew_id | UUID | Soft FK to backoffice.crew_members. The driver who reported. | | recalculated_eta | TIMESTAMPTZ | Nullable. ETA for the next downstream boarding stop (populated if type=DELAY and ETA service has run). Null on initial creation if ETA recalculation is async. | | occurred_at | TIMESTAMPTZ | When the incident happened |

IncidentResolved ​

FieldTypeDescription
event_idUUIDIdempotency key
tenant_idUUIDOwning operator
incident_idUUIDThe resolved incident
service_leg_idUUIDImpacted leg
tour_offering_idUUIDFor passenger re-query
tour_departure_idUUIDFor boarding point lookup
severityStringOriginal severity (determines whether the system sends an all-clear message)
typeStringIncident type
resolution_notesStringDispatcher's resolution summary
resolved_atTIMESTAMPTZWhen resolved

Delivery Contracts ​

EventMechanismTriggerIdempotency
IncidentCreatedHasura Event Trigger (async, post-commit) β†’ NestJS webhookoperations.incidents INSERTevent_id
IncidentResolvedHasura Event Trigger (async, post-commit) β†’ NestJS webhookoperations.incidents.status β†’ RESOLVEDevent_id

All consumers implement at-least-once processing with event_id-based deduplication.

Enrichment pattern: The NestJS webhook handler for the Hasura Event Trigger on incidents INSERT must enrich the raw row payload by joining service_legs (for tour_offering_id, tour_departure_id, boarding_point_id) and optionally the latest route_waypoints.eta (for recalculated_eta). This follows the same pattern used by ServiceLegDelayed which includes recalculated_eta not present on the service_legs row itself.


API Endpoints ​

Consumer ETA Tracking ​

Endpoint: GET /api/track/:tracking_tokenAuth: None (public). Token is a signed, time-limited JWT containing { service_leg_id, tenant_id, exp }. Rate limit: 1 req/5s per token.

Response:

FieldTypeSource
vehicle_position{ lat, lng }Latest telemetry_points row for the leg's vehicle
speed_kmhDECIMALLatest telemetry_points.speed
next_stop_nameSTRINGNext route_waypoints.label where waypoint_type = BOARDING_STOP and sequence_order > current
next_stop_etaTIMESTAMProute_waypoints.eta for the next boarding stop
leg_statusSTRINGservice_legs.status (ACTIVE, DELAYED, COMPLETED)
updated_atTIMESTAMPtelemetry_points.recorded_at

Delivery: The passenger's Apple/Google Wallet pass and the WhatsApp tracking link both embed this token. The tracking page polls every 10s. Phase 2: upgrade to Server-Sent Events (SSE) for push updates.


IssueReport Lifecycle Events ​

For the IssueReport table definition and Hasura Actions, see schema-operations.md Β§issue_reports. For the cross-context maintenance feedback loop, see ADR-008.

Event Catalog ​

EventEmitterConsumerPurpose
VehicleMaintenanceRequiredOperations (issue_reports INSERT where maintenance_urgency = IMMEDIATE)Backoffice (creates VehicleInspection)Triggers cross-context maintenance feedback loop
IssueReportCreatedOperations (issue_reports INSERT)Backoffice (dispatch board feed)Signals new field observation for dispatcher awareness
VehicleInspectionScheduledBackoffice (vehicle_inspections INSERT)Operations (transitions IssueReport β†’ IN_PROGRESS)Signals maintenance inspection scheduled for the originating IssueReport
VehicleInspectionCompletedBackoffice (vehicle_inspections.status β†’ COMPLETED)Operations (transitions IssueReport β†’ RESOLVED)Signals maintenance inspection completed

VehicleMaintenanceRequired ​

FieldTypeDescription
event_idUUIDIdempotency key
tenant_idUUIDOwning operator
issue_report_idUUIDOriginating IssueReport
vehicle_idUUIDTarget vehicle
categoryStringMECHANICAL, CLEANLINESS, DAMAGE
maintenance_urgencyStringIMMEDIATE
descriptionStringIssue description
has_attachmentsBooleanWhether the driver attached photos/videos

Delivery: Hasura Event Trigger on issue_reports INSERT where maintenance_urgency = IMMEDIATE. NestJS consumer in Backoffice creates a VehicleInspection row and links the originating issue_report_id via the vehicle_inspection_issue_reports join table (for the feedback loop). If an open VehicleInspection already exists for the same vehicle_id, the consumer links the new IssueReport to the existing inspection instead of creating a new one (dedup β€” see schema-operations.md Β§Duplicate IssueReport Dedup).

IssueReportCreated ​

FieldTypeDescription
event_idUUIDIdempotency key
tenant_idUUIDOwning operator
issue_report_idUUIDCreated IssueReport
vehicle_idUUIDTarget vehicle
reporter_crew_idUUIDReporting driver
categoryStringMECHANICAL, CLEANLINESS, DAMAGE
maintenance_urgencyStringNONE, IMMEDIATE, SCHEDULED
descriptionStringTruncated to 200 chars for lightweight payload

Delivery: Hasura Event Trigger on issue_reports INSERT (async, post-commit). Consumer: Backoffice dispatch board feed.

VehicleInspectionScheduled ​

FieldTypeDescription
event_idUUIDIdempotency key
tenant_idUUIDOwning operator
vehicle_inspection_idUUIDCreated inspection
vehicle_idUUIDTarget vehicle
issue_report_idUUIDOriginating IssueReport (from vehicle_inspection_issue_reports join table)
scheduled_dateDATEThe date the system scheduled the inspection

Delivery: Fan-out β€” one event per linked IssueReport. On backoffice.vehicle_inspections INSERT, the Backoffice NestJS handler iterates vehicle_inspection_issue_reports rows and emits one VehicleInspectionScheduled event per row. Each event carries a single issue_report_id. The Operations consumer processes each independently: UPDATE issue_reports SET status = 'IN_PROGRESS' WHERE id = issue_report_id AND status = 'OPEN'. Guard: if status β‰  OPEN, log warning and skip (see schema-operations.md Β§Backward Transition Guards).

VehicleInspectionCompleted ​

FieldTypeDescription
event_idUUIDIdempotency key
tenant_idUUIDOwning operator
vehicle_inspection_idUUIDCompleted inspection
vehicle_idUUIDTarget vehicle
issue_report_idUUIDOriginating IssueReport
completed_dateTIMESTAMPTZWhen completed
outcomeStringPASSED (no further action), DEFECT_CONFIRMED (repair needed), DEFECT_NOT_FOUND

Delivery: Fan-out β€” one event per linked IssueReport. On backoffice.vehicle_inspections UPDATE where status β†’ COMPLETED, the Backoffice NestJS handler iterates vehicle_inspection_issue_reports rows and emits one VehicleInspectionCompleted event per row. The Operations consumer processes each independently: UPDATE issue_reports SET status = 'RESOLVED' WHERE id = issue_report_id AND status = 'IN_PROGRESS'. Guard: if status β‰  IN_PROGRESS, log warning and skip.

Delivery Contracts ​

EventMechanismTriggerIdempotency
VehicleMaintenanceRequiredHasura Event Trigger (async, post-commit) β†’ NestJS webhookoperations.issue_reports INSERT where maintenance_urgency = IMMEDIATEevent_id
IssueReportCreatedHasura Event Trigger (async, post-commit) β†’ NestJS webhookoperations.issue_reports INSERTevent_id
VehicleInspectionScheduledFan-out: Hasura Event Trigger β†’ NestJS emitter (one event per linked IssueReport)backoffice.vehicle_inspections INSERTevent_id
VehicleInspectionCompletedFan-out: Hasura Event Trigger β†’ NestJS emitter (one event per linked IssueReport)backoffice.vehicle_inspections.status β†’ COMPLETEDevent_id

All consumers implement at-least-once processing with event_id-based deduplication.

Internal documentation β€” Busflow