🚕 Highway Taxi — WS Booking Flow Test Console

🚗 Driver App
disconnected
/ws/driver
Active Assignment
No booking assigned yet
Driver Actions
Driver Log
💬 Chat with Customer
No messages yet.
🧍 Customer App
disconnected
/ws/customer
Step 1 — Create Booking (REST)
Active Booking
No booking yet — create one above
Customer Log
💬 Chat with Driver
No messages yet.

📘 Booking Flow — Reference

Each block below is one network call, in the order it happens during a booking. For every call: URL, direction, payload, response, and what it does.

⚙ Concurrency

Both sides now allow multiple concurrent active bookings. A customer can place several bookings; a driver can be assigned to several at once. There is no DB unique constraint or picker filter preventing this — the system is per-booking, not per-user. Every WS event carries a booking_id and clients must route by that id, not by "the current booking."

⚙ Time windows (env-configurable)

1Request Booking (REST)

Customer creates a new booking. Server picks a driver, inserts the row, then pushes booking_assigned on the driver's socket.

URL

POST /api/mobile/customer/booking/request
Authorization: Bearer <firebase_id_token>
X-Device-ID:    <device_id>        // routing hint for FCM
X-Device-Model: <device_model>     // optional
Content-Type:  application/json

Payload

{
  "pickup_location_title":   "Home",
  "pickup_location_address": "12 King St, Sydney",
  "pickup_map_id":           "ChIJxxxxxxxxxxx",
  "pickup_latitude":         -33.8688,
  "pickup_longitude":        151.2093,
  "no_of_passengers":        2,
  "note":                    null,
  "scheduled_date":          null,
  "drop_offs": [
    {
      "drop_off_location_title":   "Airport",
      "drop_off_location_address": "Sydney Airport",
      "drop_off_map_id":           "ChIJxxxxxxxxxxx",
      "drop_off_latitude":         -33.9399,
      "drop_off_longitude":        151.1753
    }
  ]
}

Success response (201)

{
  "meta": { "code": 201, "status": "success", "timestamp": "..." },
  "code": "CREATED",
  "data": { "booking_id": "uuid", "booking_type": "instant", "driver_id": "uuid" },
  "user_message": { "message": "Booking request submitted successfully." }
}

2booking_assigned

Server tells the assigned driver about the new ride. Fired automatically after Step 1. Starts the driver-response timer (DRIVER_RESPONSE_TIME, default 30 s).

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Server → Driver

Response (server pushes)

{
  "type":             "booking_assigned",
  "booking_id":       "uuid",
  "booking_status":   "requested",
  "booking_type":     "instant",
  "date":             "2026-05-12T10:30:00.000Z",
  "no_of_passengers": 2,
  "note":             null,
  "customer_name":    "Alice",
  "customer_phone":   "211234567",
  "customer_phone_code": {
    "id":      "uuid",
    "name":    "New Zealand",
    "code":    "NZ",
    "ph_code": "+64",
    "flag":    "https://.../uploads/flags/nz.png"
  },
  "pickup_location":  { "title": "...", "address": "...", "map_id": "...", "latitude": -33.86, "longitude": 151.20 },
  "dropoff_location": { "title": "...", "address": "...", "map_id": "...", "latitude": -33.93, "longitude": 151.17 },
  "chat_history":     []
}

3submit_arrival_time

Driver accepts an instant booking by sending how many seconds until pickup. DB: arrival_time = N, booking_status = "confirmation". Triggers Step 4. Stops the driver-response timer; starts the customer-response timer (CUSTOMER_WAITING_TIME, default 2 min).

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Driver → Server

Payload

{
  "type":         "submit_arrival_time",
  "booking_id":   "uuid",
  "arrival_time": 300
}

Response

// Driver ack:
{
  "type":           "arrival_time_accepted",
  "booking_id":     "uuid",
  "booking_status": "confirmation",
  "arrival_time":   { "value": 5, "unit": "minutes", "label": "5 min", "seconds": 300 },
  "chat_history":   []
}

// Customer gets `arrival_time_offered` (Step 4).

// On failure:
{ "type": "error", "code": "BOOKING_NOT_ASSIGNABLE" | "INVALID_PAYLOAD", "message": "..." }

4arrival_time_offered

Server forwards the driver's arrival time to the customer with a friendly display shape. Carries the driver's contact info.

URL

WS  /ws/customer?token=<firebase_id_token>

Direction

Server → Customer

Response (server pushes)

{
  "type":             "arrival_time_offered",
  "booking_id":       "uuid",
  "booking_status":   "confirmation",
  "arrival_time":     { "value": 5, "unit": "minutes", "label": "5 min", "seconds": 300 },
  "no_of_passengers": 2,
  "driver_name":      "Bob",
  "driver_phone":     "219876543",
  "driver_phone_code": { "id": "...", "name": "...", "code": "...", "ph_code": "+64", "flag": "..." },
  "vehicle_number":   "ABC-123",
  "driver_code":      "DRV-0042",
  "pickup_location":  { ... },
  "dropoff_location": { ... },
  "chat_history":     []
}

5accept

Customer confirms the offer. DB: booking_status = "on_the_way". Cancels the customer-response timer. Triggers Step 6.

URL

WS  /ws/customer?token=<firebase_id_token>

Direction

Customer → Server

Payload

{ "type": "accept", "booking_id": "uuid" }

Response

// Customer ack (socket stays open for the ride):
{
  "type":           "accept_accepted",
  "booking_id":     "uuid",
  "booking_status": "on_the_way",
  "driver_name":    "Bob",
  "driver_phone":   "...",
  "driver_phone_code": { ... },
  "vehicle_number": "ABC-123",
  "driver_code":    "DRV-0042",
  "chat_history":   []
}

// Driver gets `customer_accepted` (Step 6).

// On failure:
{ "type": "error", "code": "BOOKING_NOT_ACCEPTABLE", "message": "..." }

6customer_accepted

Server tells the driver the ride is on. Includes the customer's contact info. Carries an auto_promoted: true flag when this fires because the offer timer expired (instead of a manual customer accept).

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Server → Driver

Response (server pushes)

{
  "type":              "customer_accepted",
  "booking_id":        "uuid",
  "booking_status":    "on_the_way",
  "no_of_passengers":  2,
  "note":              null,
  "customer_name":     "Alice",
  "customer_phone":    "...",
  "customer_phone_code": { ... },
  "pickup_location":   { ... },
  "dropoff_location":  { ... },
  "chat_history":      [],
  "auto_promoted":     false      // true if the customer-waiting timer expired
}

7mark_as_completed

Driver ends the ride. DB: booking_status = "completed". Triggers Step 8. Chat for this booking is cleared from memory after the event ships.

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Driver → Server

Payload

{ "type": "mark_as_completed", "booking_id": "uuid", "status": "completed" }

Response

// Driver gets ack (socket stays open for the next assignment):
{
  "type":         "complete_accepted",
  "booking_id":   "uuid",
  "status":       "completed",
  "chat_history": [ ... ]    // final transcript snapshot
}

// Customer gets `ride_completed` (Step 8), socket closes 200ms later.

// On failure:
{ "type": "error", "code": "BOOKING_NOT_COMPLETABLE", "message": "..." }

8ride_completed

Server tells the customer the ride is finished. Customer socket auto-closes ~200 ms later.

URL

WS  /ws/customer?token=<firebase_id_token>

Direction

Server → Customer

Response (server pushes)

{
  "type":         "ride_completed",
  "booking_id":   "uuid",
  "status":       "completed",
  "chat_history": [ ... ]
}

↪ Branch events (not part of the happy path)

decline_booking

Driver refuses a requested booking. Each booking gets up to MAX_ATTEMPTS (= 3) driver attempts total (initial + 2 retries), with cumulative exclusion — no driver is asked twice for the same booking. An attempt is consumed by an explicit decline, no response within DRIVER_RESPONSE_TIME (default 30 s), or no eligible driver available.

If attempts remain AND a replacement is found → new driver gets booking_assigned, declining driver gets { reassigned: true } (customer sees nothing). If attempts are exhausted OR no eligible driver is left → booking is cancelled with ride_cancelled_by = "admin", customer gets driver_cancelled with reason_code: "NO_DRIVER_AVAILABLE", declining driver gets { reassigned: false }.

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Driver → Server

Payload

{
  "type":                      "decline_booking",
  "booking_id":                "uuid",

  // exactly one of:
  "cancellation_template_id":  "uuid",            // pre-saved template (admin or driver's own)
  "cancellation_description":  "free-text reason" // up to 512 chars
}

Response

{ "type": "decline_accepted", "booking_id": "uuid", "reassigned": true | false }

offer_expired (driver-side)

Driver was assigned a booking but didn't act within DRIVER_RESPONSE_TIME. The server consumes their attempt and forwards the booking to the next driver in the chain. The expired driver gets this courtesy notice so their UI clears the offer card.

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Server → Driver

Response (server pushes)

{
  "type":       "offer_expired",
  "booking_id": "uuid",
  "reason":     "Response window elapsed."
}

driver_cancelled

Server tells the customer the booking has been cancelled by the driver side. Customer socket auto-closes ~200 ms later. reason_code distinguishes the cause:

URL

WS  /ws/customer?token=<firebase_id_token>

Direction

Server → Customer

Response (server pushes)

{
  "type":         "driver_cancelled",
  "booking_id":   "uuid",
  "status":       "cancelled",
  "reason":       "No driver available after multiple attempts.",
  "reason_code":  "NO_DRIVER_AVAILABLE" | "DRIVER_CANCELLED" | "SCHEDULED_DRIVER_NO_SHOW",
  "chat_history": [ ... ]   // final transcript when chat existed
}

booking_cancelled (driver-side)

Server tells the driver one of their bookings was cancelled by the system (3-attempt exhaustion, scheduled no-show, etc.). The driver's UI should drop the matching offer/ride from view. Driver socket stays open — they keep any other concurrent bookings.

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Server → Driver

Response (server pushes)

{
  "type":         "booking_cancelled",
  "booking_id":   "uuid",
  "status":       "cancelled",
  "reason":       "...",
  "reason_code":  "NO_DRIVER_AVAILABLE" | "SCHEDULED_DRIVER_NO_SHOW"
}

booking_cancelled_by_customer (driver-side)

Server tells the driver that the customer cancelled the ride via the HTTP cancel endpoint (e.g. customer ended the trip from the app after acceptance). This is distinct from the WS reject branch, which only fires from requested / confirmation.

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Server → Driver

Response (server pushes)

{
  "type":         "booking_cancelled_by_customer",
  "booking_id":   "uuid",
  "status":       "cancelled",
  "chat_history": [ ... ]
}

reject (customer-side WS cancel)

Customer cancels their own booking via WS. Allowed from requested or confirmation. Scheduled bookings: blocked within 1 hr of pickup. DB: booking_status = "cancelled", ride_cancelled_by = "customer". Customer socket auto-closes ~200 ms after the ack.

URL

WS  /ws/customer?token=<firebase_id_token>

Direction

Customer → Server

Payload

{ "type": "reject", "booking_id": "uuid" }

Response

// Customer ack, socket terminates ~200 ms later:
{
  "type":         "reject_accepted",
  "booking_id":   "uuid",
  "status":       "cancelled",
  "driver_name":  "...",
  "driver_phone": "...",
  "driver_phone_code": { ... },
  "vehicle_number": "...",
  "driver_code":  "...",
  "chat_history": [ ... ]
}

// Driver gets:
{
  "type":         "customer_rejected",
  "booking_id":   "uuid",
  "status":       "cancelled",
  "note":         null,
  "customer_name":  "Alice",
  "customer_phone": "...",
  "customer_phone_code": { ... },
  "chat_history": [ ... ]
}

// On failure:
{ "type": "error", "code": "REJECT_WINDOW_CLOSED" | "BOOKING_NOT_REJECTABLE", "message": "..." }

offer_auto_accepted (⏱ customer-timeout auto-accept)

If the customer doesn't accept or reject within CUSTOMER_WAITING_TIME (default 2 min) after Step 4, the server auto-accepts on their behalf — same effect as a manual accept. DB: booking_status = "on_the_way". Customer socket stays open for the ride.

URL

WS  /ws/customer?token=<firebase_id_token>
WS  /ws/driver?token=<firebase_id_token>

Direction

Server → Customer  AND  Server → Driver

Response (server pushes)

// Customer side (socket stays open):
{
  "type":           "offer_auto_accepted",
  "booking_id":     "uuid",
  "booking_status": "on_the_way",
  "chat_history":   []
}

// Driver side — same shape as a manual customer_accepted, with auto_promoted flag set:
{
  "type":              "customer_accepted",
  "booking_id":        "uuid",
  "booking_status":    "on_the_way",
  "no_of_passengers":  2,
  "customer_name":     "Alice",
  "customer_phone":    "...",
  "customer_phone_code": { ... },
  "pickup_location":   { ... },
  "dropoff_location":  { ... },
  "chat_history":      [],
  "auto_promoted":     true
}

accept_booking (scheduled only)

Driver accepts a scheduled booking. No arrival time involved. Triggers booking_confirmed on the customer side.

URL

WS  /ws/driver?token=<firebase_id_token>

Direction

Driver → Server

Payload

{ "type": "accept_booking", "booking_id": "uuid" }

Response

{
  "type":         "accept_accepted",
  "booking_id":   "uuid",
  "chat_history": []
}

booking_confirmed (scheduled only)

Server tells the customer their scheduled booking has been confirmed by a driver. The ride flips to on_the_way automatically at pickup time via a cron — that transition fires customer_accepted on the driver and offer_auto_accepted on the customer, just like the instant flow's auto-accept path.

URL

WS  /ws/customer?token=<firebase_id_token>

Direction

Server → Customer

Response (server pushes)

{
  "type":             "booking_confirmed",
  "booking_id":       "uuid",
  "booking_status":   "confirmation",
  "scheduled_date":   "2026-05-13T08:00:00.000Z",
  "no_of_passengers": 2,
  "driver_name":      "Bob",
  "driver_phone":     "...",
  "driver_phone_code": { ... },
  "vehicle_number":   "...",
  "driver_code":      "...",
  "pickup_location":  { ... },
  "dropoff_location": { ... },
  "chat_history":     []
}

💬 Chat (per-booking)

One-to-one chat between the assigned customer and driver, on the same WS sockets. Persisted in the DB table booking_chat_messages — survives server restarts and reconnects. Allowed only while the booking is on_the_way; before that (assignment, offer, confirmation) chat is gated and the server returns BOOKING_NOT_CHATTABLE.

The server hydrates an in-memory cache from the DB on demand. Every booking-state event (assignment, accept, offer, completion, cancellation, etc.) carries a full chat_history snapshot — clients don't need to call resync just to rehydrate chat. Live chat_message events instead carry a trimmed trailing window (~last 100 entries) for bandwidth.

Per-message text length is capped at 1000 characters. When a booking reaches a terminal state (completed, cancelled, driver_cancelled, customer reject), the final event carries a transcript snapshot, then the in-memory cache for that booking is cleared. The DB rows stay for audit.

chat_message

Send a text message to the other party. Allowed only while the booking is on_the_way and the sender is the booking's customer or driver.

URL

WS  /ws/driver  OR  /ws/customer

Direction

Client → Server  (then Server fans out to both sides)

Payload

{
  "type":       "chat_message",
  "booking_id": "uuid",
  "text":       "Hi, I'm at the front entrance."
}

Response (server pushes — same payload to both sides)

{
  "type":         "chat_message",
  "booking_id":   "uuid",
  "from":         "driver" | "customer",
  "text":         "Hi, I'm at the front entrance.",
  "sent_at":      "2026-05-12T10:32:15.000Z",
  "chat_history": [ ... ]   // trailing ~100 messages
}

// On failure:
{ "type": "error", "code": "BOOKING_NOT_CHATTABLE" | "INVALID_PAYLOAD", "message": "..." }

📞 Contact info in booking payloads

Every booking-detail payload carries the other party's contact info so each side can call the other if needed.

Driver-facing payloads (booking_assigned, customer_accepted, customer_rejected, snapshot)

{
  ...
  "customer_name":  "Alice",
  "customer_phone": "211234567",
  "customer_phone_code": {
    "id":      "uuid",
    "name":    "New Zealand",
    "code":    "NZ",
    "ph_code": "+64",
    "flag":    "https://.../uploads/flags/nz.png"
  }
}

Customer-facing payloads (arrival_time_offered, accept_accepted, booking_confirmed, snapshot)

{
  ...
  "driver_name":  "Bob",
  "driver_phone": "219876543",
  "driver_phone_code": { "id": "...", "name": "...", "code": "...", "ph_code": "+64", "flag": "..." },
  "vehicle_number": "ABC-123",
  "driver_code":    "DRV-0042"
}

Note: phone numbers are stored as local digits only (no country prefix). Combine ph_code + phone when dialling. The country's phone_limit (expected digit count) is exposed on the profile-fetch endpoints but not on these embedded snapshots.

🔌 Utility messages

connected (greeting)

Server's first message after a successful WS handshake. Lets the client confirm auth + know its own ID. The client may also pass ?device_id=<id> in the connect URL; the server bumps that device's last_seen_at so subsequent FCM pushes for the user route to this device.

URL

WS  /ws/driver?token=<firebase_id_token>[&device_id=<id>]
WS  /ws/customer?token=<firebase_id_token>[&device_id=<id>]

Direction

Server → Client

Response (server pushes)

// Driver:
{ "type": "connected", "driver_id": "uuid" }

// Customer:
{ "type": "connected", "customer_id": "uuid" }

resync (after reconnect)

After a network drop the client reconnects and asks the server to rebuild its state from DB. Useful when the client missed a push while offline. Both sides accept resync with or without booking_id.

URL

WS  /ws/driver  OR  /ws/customer

Direction

Client → Server

Driver payload

// No booking_id — server returns the driver's OLDEST in-progress booking
// (status in requested / confirmation / on_the_way, ordered by created_at ASC).
// Concurrent bookings: only one snapshot is returned today — other active
// bookings are visible only via their original `booking_assigned` events.
{ "type": "resync" }

Customer payload

// With booking_id — server returns that exact booking (must belong to caller):
{ "type": "resync", "booking_id": "uuid" }

// Without booking_id — server returns the customer's MOST RECENTLY
// created in-progress booking (ordered by created_at DESC). When the
// customer has no active booking, server returns
// { type: "snapshot", booking_id: null, booking_status: null }.
{ "type": "resync" }

Driver snapshot response

{
  "type": "snapshot",
  "assignment": {
    "booking_id":       "uuid",
    "booking_status":   "requested" | "confirmation" | "on_the_way",
    "booking_type":     "instant" | "scheduled",
    "date":             "ISO string",
    "no_of_passengers": 2,
    "arrival_time":     { "value": 5, "unit": "minutes", "label": "5 min", "seconds": 300 } | null,
    "note":             null,
    "customer_name":    "Alice" | null,
    "customer_phone":   "..." | null,
    "customer_phone_code": { ... } | null,
    "pickup_location":  { ... },
    "dropoff_location": { ... },
    "chat_history":     [ ... ]
  }
}
// `assignment: null` means no active booking.

Customer snapshot response

{
  "type":             "snapshot",
  "booking_id":       "uuid",
  "booking_status":   "...",
  "no_of_passengers": 2,
  "arrival_time":     { "value": 5, "unit": "minutes", "label": "5 min", "seconds": 300 } | null,
  "driver_name":      "Bob" | null,
  "driver_phone":     "..." | null,
  "driver_phone_code": { ... } | null,
  "vehicle_number":   "ABC-123" | null,
  "driver_code":      "DRV-0042" | null,
  "pickup_location":  { ... },
  "dropoff_location": { ... },
  "chat_history":     [ ... ]
}

force_disconnect

Server kicks the socket — typically when admin blocks/deletes the user or kills the session deliberately. Socket terminates immediately after this event.

URL

WS  /ws/driver  OR  /ws/customer

Direction

Server → Client

Response (server pushes)

{ "type": "force_disconnect", "reason": "Account blocked." }

ping / pong

App-level keepalive. Server also sends WS-level ping frames every 30 s and terminates sockets that miss a pong.

URL

WS  /ws/driver  OR  /ws/customer

Direction

Client → Server

Payload

{ "type": "ping" }

Response

{ "type": "pong" }

error

Any failed action returns one of these. Some errors also terminate the socket (auth failures, session replacement, account blocked).

Direction

Server → Client

Response (server pushes)

{ "type": "error", "code": "<CODE>", "message": "human readable" }

Common codes

UNAUTHORIZED            — no token
INVALID_TOKEN           — bad/expired Firebase token
SESSION_REPLACED        — same user connected from elsewhere
ACCOUNT_BLOCKED         — userStatus = "blocked"
ACCOUNT_DELETED         — userStatus = "deleted"
DRIVER_NOT_FOUND        — token email isn't tied to a driver
CUSTOMER_NOT_FOUND      — token email isn't tied to a customer
ONGOING_RIDE_ELSEWHERE  — driver has an on_the_way ride; blocks new login here
INVALID_JSON            — message wasn't valid JSON
INVALID_PAYLOAD         — required field missing/wrong type
BOOKING_NOT_FOUND       — resync booking_id not yours or doesn't exist
BOOKING_NOT_ASSIGNABLE  — submit_arrival_time on wrong booking/state
BOOKING_NOT_ACCEPTABLE  — accept on wrong booking/state
BOOKING_NOT_REJECTABLE  — reject too late or wrong booking
BOOKING_NOT_DECLINABLE  — decline_booking on wrong booking/state
BOOKING_NOT_COMPLETABLE — mark_as_completed on wrong booking/state
BOOKING_NOT_CHATTABLE   — chat sent before the booking is on_the_way, or for a booking that isn't yours / has ended
REJECT_WINDOW_CLOSED    — scheduled reject within 1 hr of pickup
INVALID_TEMPLATE        — bad cancellation_template_id
UNKNOWN_MESSAGE_TYPE    — unrecognised `type`
INTERNAL_ERROR          — unexpected server-side error

📊 Status machine

requested  ──submit_arrival_time / accept_booking──▶  confirmation
                                                          │
                                                          ├──customer accept──▶  on_the_way  ──mark_as_completed──▶  completed
                                                          │
                                                          ├──customer reject──▶  cancelled  (by "customer")
                                                          │
                                                          └──⏱ CUSTOMER_WAITING_TIME elapses──▶  on_the_way
                                                                                                  (offer_auto_accepted to customer,
                                                                                                   customer_accepted{auto_promoted:true} to driver)

requested  ──[MAX_ATTEMPTS exhausted: each driver either declined or timed out at DRIVER_RESPONSE_TIME]──▶  cancelled
                                                          (by "admin", reason_code="NO_DRIVER_AVAILABLE")
requested  ──customer reject────────────────────────────────────────────────────────────────────────▶  cancelled
                                                          (by "customer")
on_the_way ──HTTP cancel by active driver──▶  cancelled  (by "driver", reason_code="DRIVER_CANCELLED")
on_the_way ──HTTP cancel by customer──▶      cancelled  (by "customer"; driver gets booking_cancelled_by_customer)
scheduled  ──promotion cron, no driver in confirmation──▶  cancelled
                                                          (by "admin", reason_code="SCHEDULED_DRIVER_NO_SHOW")

🔁 Lifecycle summary