---
feature: Customer orders and status tracking
slug: orders
surface: client
consumer: flutter
version: 3.8.0
status: ready
brd_version: "1.3"
brd_ids: [CUST-017, CUST-019, DRIV-015]
business_rules: [BR-12, BR-13, BR-14, BR-19, BR-21, BR-22, BR-28]
decisions: [D3, DEC-05, DEC-10, DEC-24, TD-19, TD-92, TD-94, TD-96, TD-100, TD-101, TD-104, TD-105, TD-106, TD-109, TD-110, TD-111, TD-113, TD-120, TD-144, TD-145, TD-147, TD-131, TD-148, TD-149]
schema_tables: [orders, finder_searches, order_items, order_item_options, order_status_history, payments, refunds, wallets, wallet_transactions, notifications]
screens: [customer--booking--05-*, customer--booking--05b-*, customer--booking--06-*, customer--orders--04-*, customer--order-tracking--01-*]
related_docs: [booking.md, cancellation.md, chat.md, ../provider/orders.md, ../provider/order-execution.md, ../admin/orders.md]
last_updated: 2026-11-09
---

# Customer orders and status tracking — Flutter Integration Guide (client)

> **Base URL:** `{HOST}/api/v1/client` · [Shared API conventions](../API_CONVENTIONS.md).

## Version history

| Version | Date | Type | Summary | App action required |
|---------|------|------|---------|---------------------|
| 3.8.0 | 2026-11-09 | MINOR | `transport.tracking` adds `freshness` (`fresh`/`stale`/`unavailable`), `calculated_at` and `source`, and `distance_km` / `eta_minutes` now carry values; a self-travel order shows the provider's position while `on_the_way` (R06–R08, TD-183). The location event carries the same fields. | Read the new keys; show "about N min" only when `eta_minutes` is not null and dim a `stale` pin. |

| 3.7.1 | 2026-11-09 | PATCH | Docs only: screens `customer--orders--04-details-info-price` and `customer--order-tracking--01-live-tracking` are mapped to this guide. Dates: §3 note that `scheduled_at` is returned in UTC (`+00:00`) while slots use `+03:00`; both are the same instant. | Convert with the device time zone; never compare strings. |
| 3.7.0 | 2026-11-09 | MINOR | Group orders (D2, TD-180): `is_group`, `people[]` and `services[].person_id` on the order; `is_group` on the list row. | Show the people breakdown when `is_group` is true; ignore the keys otherwise. |
| 3.6.2 | 2026-11-02 | PATCH | Docs only: new §3.4 documents the live tracking preview end to end (`transport` values, `tracking-subscription` request/response/errors, socket acknowledgments); removes stale notes (Rate hidden, delay alert "not built", provider cancellation "out of scope"); two Postman examples re-recorded with the always-present `transport.can_confirm_arrival`. | None. Check the `transport.stage` values in §3.4 against your Dart enum. |
| 3.6.1 | 2026-11-02 | PATCH | The Rate action is now live: `can_rate` drives the button and the rating endpoint is [ratings.md](ratings.md) (TD-162). No contract change. | Show the Rate button when `can_rate` is `true`; ignore the earlier "do not show Rate yet" notes. |
| 3.6.0 | 2026-11-01 | MINOR | `cancellation.cancelled_by` (`null` or `{value: customer\|provider\|admin\|system, label}`) says who cancelled; a provider cancellation refunds her wallet in full (PROV-016, TD-153). | Show "Cancelled by the provider" from `cancellation.cancelled_by`. |
| 3.5.0 | 2026-11-01 | MINOR | `POST /orders/{order}/confirm-arrival` lets her confirm the driver brought the provider; `transport.can_confirm_arrival` says when (TD-149). | Show "Confirm arrival" while `transport.can_confirm_arrival` is true. |
| 3.4.0 | 2026-11-01 | MINOR | Order list/details return `coupon` (`null` or `{code, discount_amount, funded_by}`); `discount_amount` can be non-zero (TD-131, TD-148). | Show the coupon line and the discount in the summary; no other change. |
| 3.3.0 | 2026-11-01 | MINOR | The preview room receives a stage event for every order transition into a tracked stage; repeats are possible; `tracking-subscription` can answer 503. | Treat `order.tracking.stage` as an idempotent projection; show a retry message on 503. |
| 3.2.0 | 2026-10-29 | MINOR | Details add `transport` and paid-order `can_chat`; a preview-only tracking grant is available. | Parse transport, use the tracking subscription lifecycle, and open chat only when allowed. |
| 3.1.0 | 2026-10-28 | MINOR | Driver travel synchronizes order departure and arrival; pickup generates a customer notification. | Handle trip_provider_picked_up and refetch the order. |
| 3.0.1 | 2026-10-27 | PATCH | Document payment gateway unavailability and simulator results. | None |
| 3.0.0 | 2026-10-27 | MAJOR | Package purchases and sessions add order kinds; purchase address is nullable and purchase cannot be rated. | Handle null purchase address fields; keep the Rate action hidden until the rating feature ships. |
| 2.5.0 | 2026-10-26 | Additive | Order details now return `source` and nullable `finder_search_id`; finder bookings can be traced to their original search. | When booking a Joshen result, send `source=joshen_finder` and the search UUID; otherwise keep `browse` or `search` and omit the id. |
| 2.4.0 | 2026-10-26 | Additive | New always-present `delay_alert` (`null` or `{alerted_at}`) in the details; live `order.updated` with `reason: delayed` and an inbox row when the provider's status update is overdue. | Add `delay_alert` to the model; show a "still waiting for the provider" notice while it is not `null`; keep using `can_cancel` for the Cancel button. |
| 2.3.0 | 2026-10-26 | Additive | `can_cancel` reflects active policy; details add stable `cancellation` fields. | Show Cancel from `can_cancel`; parse `cancellation`; refetch on `reason: cancelled`. |
| 2.2.0 | 2026-10-26 | Additive | `can_rate` on the order details; the provider can now really advance an order, so stages reach production. | Add `can_rate` to the model; **do not show the Rate action yet** (CUST-021 is a later feature). |
| 2.1.0 | 2026-10-26 | Additive | My Orders tabs, richer list tile, status `timeline`, `refund` follow-up, execution statuses and the `status_changed` live event. | Optionally pass `?tab=`; render `timeline` and `refund`; handle the new status values and `reason: status_changed`. |
| 2.0.0 | 2026-10-25 | **Breaking** | `booking_type` becomes `{value,label}`; realtime events replace polling. | Read `booking_type.value`; connect the socket (API conventions §9). |
| 1.0.0 | 2026-10-25 | Initial | Customer list and polling details for the Phase 2 cycle. | Poll owned order UUIDs and render capability flags. |

## Changelog

### 3.7.1 — 2026-11-09

- **Fixed (docs):** two prototype screens had no registry row. The order date-time offset (`+00:00` here, `+03:00` on slots) is documented as intentional: same instant, parse it as an ISO 8601 date-time.
- **Breaking:** No. **App action required:** None; parse with a date-time parser, never compare strings.


### 3.7.0 — 2026-11-09

- **Added:** `is_group` on list rows and the order; `people[]` (`id`, `label`, `is_customer`, `sort_order`, `services_count`, `duration_min`, `services_amount`) and `services[].person_id`. A single-person order has one `people` row (the customer).
- **Breaking:** No. **App action required:** Render the per-person breakdown for group orders.

### 3.6.2 — 2026-11-02

- **Added (docs):** §3.4 *Live tracking preview*: every `transport` key and value (`mode`, the seven `stage` values, `tracking.location` with `updated_at`), the `POST /orders/{order}/tracking-subscription` endpoint with its real response and errors, and the Socket.IO `tracking:subscribe` acknowledgment codes (`invalid_grant`, `expired_grant`, `forbidden`, `too_many_rooms`).
- **Fixed (docs):** removed notes that went stale — the Rate action is live (3.6.1), the delay alert is built (2.4.0), provider cancellation is built (3.6.0); §7 lists every `order.updated` reason (`cancelled`, `package_purchased` were missing).
- **Fixed (Postman):** `client.orders.show` › "200 Accepted order awaiting customer payment" and "200 Payment deadline expired on read" were captured before 3.5.0 and lacked `transport.can_confirm_arrival`; they are re-recorded. The API always sent the key.
- **Breaking:** No. **App action required:** None.

### 3.6.0 — 2026-11-01

- **Added:** `cancellation.cancelled_by` on order list/details (`null` while the order is live).
- **Changed:** A cancelled order can now be cancelled by the provider; `refund.returned_to_wallet` is `true` once her wallet is credited in full.
- **Breaking:** No. **App action required:** Use `cancellation.cancelled_by` to word the cancelled screen.
- **BRD / decisions:** PROV-016, CUST-017; TD-153.

### 3.5.0 — 2026-11-01

- **Added:** `POST /orders/{order}/confirm-arrival` (`client.orders.confirm-arrival`, DRIV-015, TD-149). While the Joshen driver has reported arrival, she may confirm the provider has arrived; this completes the driver's trip. The driver or the provider may confirm first — then her call is a 200 no-op. The order status stays `arrived` until the provider starts the service.
- **Added:** `transport.can_confirm_arrival` (bool) in `GET /orders/{order}` and in the `order.tracking.stage` socket payload; `true` only while the trip is arrived and unconfirmed.
- **Breaking:** No. **App action required:** show "Confirm arrival" while `transport.can_confirm_arrival` is true and hide it on the next stage event.
- **BRD / decisions:** DRIV-015, DEC-08 → TD-149.

### 3.4.0 — 2026-11-01

- **Added:** always-present `coupon` on the order resource: `null` when no coupon was applied, else `{code, discount_amount, funded_by:{value,label}}` (the same object as the [booking quote](booking.md)). The order keeps it after the use is released by a rejected / expired / cancelled order.
- **Changed:** `discount_amount` is no longer always `0.00`; `total_amount = services + delivery − discount`.
- **Breaking:** No. **App action required:** render the coupon line when `coupon` is not `null`.
- **Endpoints affected:** `GET /orders`, `GET /orders/{order}`, `POST /orders`, `POST /orders/{order}/pay`, `POST /orders/{order}/cancel`.
- **BRD / decisions:** CUST-011/014, TD-131, TD-148.

### 3.3.0 — 2026-11-01

- **Added:** `order.tracking.stage` is now published for every order change into `confirmed`, `preparing`, `on_the_way`, `arrived`, `in_progress`, `completed` or `cancelled`, including self-travel departure, cancellation and completion (previously only trip changes published it). One change can emit the same projection twice (a driver trip that also moves the order), so apply the newest and ignore repeats. `pending_provider`, `awaiting_payment`, `rejected` and `expired` never publish.
- **Added:** `POST /orders/{order}/tracking-subscription` answers 503 `INFRASTRUCTURE_FAILURE` when the server's realtime secret is not configured. Postman: `client.orders.tracking-subscription` › "503 Realtime tracking not configured". The grant lifetime is configurable (default 120 s) and expiry never removes a joined room (TD-147).
- **App action required:** none mandatory; make the stage handler idempotent and show a retry message on 503. Open: stale-location threshold, ETA and distance, live-map content (SCR-16).

### 3.2.0 — 2026-10-29

- `GET /orders/{order}` now includes a stable `transport` object with nullable mode/stage and a tracking block. Driver identity, ETA and distance are not exposed. The existing order-details Postman examples were recaptured with this field.
- `can_chat` now follows the shared paid-confirmed gate (BR-15, TD-144). See [chat](chat.md).
- `POST /orders/{order}/tracking-subscription` grants a preview-only room; see [API conventions §9.2](../API_CONVENTIONS.md). App action required: listen, subscribe, await acknowledgment, fetch details, and unsubscribe on preview close/background.

### 3.1.0 — 2026-10-28

- **Changed:** Driver travel synchronizes order departure and arrival; pickup generates a customer notification.
- **App action required:** Handle trip_provider_picked_up and refetch the order.
- **BRD / decisions:** Cycle F, TD-132–140; DEC-08 remains open.

### 3.0.1 — 2026-10-27

- **Fixed:** Document payment gateway unavailability and simulator results.
- **Breaking:** No. **App action required:** None.


### 3.0.0 — 2026-10-27

- **Added / changed:** Package purchases and sessions add order kinds; purchase address is nullable and purchase cannot be rated.
- **Fixed / removed:** None.
- **Breaking:** Yes.
- **Endpoints affected:** Existing order or package reads and actions described below.
- **App action required:** Handle null purchase address fields; keep the Rate action hidden until the rating feature ships.
- **BRD / decisions:** D1, TD-123–128.


### 2.5.0 — 2026-10-26
- **Added:** `source: {value,label}` and nullable `finder_search_id` on order responses. `joshen_finder` requires the finder UUID; `browse` and `search` cannot include one (CUST-009, TD-120). A single search may be linked to multiple orders.
- **Changed:** None to the existing order journey.
- **Fixed:** N/A.
- **Removed / Deprecated:** None.
- **Breaking:** No.
- **Endpoints affected:** `POST /orders`, order details and shared order-resource responses.
- **App action required:** Keep the search UUID from finder results and send it with `source=joshen_finder`; continue to send no finder id for browse/search bookings.
- **BRD / decisions:** CUST-009, CUST-012, TD-120.

---

### 2.4.0 — 2026-10-26
- **Added:** `delay_alert` (never omitted) on `GET /orders/{order}` and the create / pay / cancel responses that share the resource: `null`, or `{"alerted_at": "2026-10-25T07:16:00+00:00"}` while the customer has been alerted **for the stage the order is in now** (BR-21, TD-111). It clears when the provider moves the order to the next stage; each stage alerts at most once. Postman: `client.orders.show` › "200 Order with a delay alert", "200 Order without a delay alert".
- **Added (event):** `order.updated` with `reason: delayed` ([API conventions §9.1](../API_CONVENTIONS.md)) plus an inbox row (`event_key: order_delayed`, title "Order update delayed" / «تأخر تحديث الطلب»). `payload.status` is the unchanged status.
- **Changed:** nothing in existing fields. The alert never changes `status` or `provider`, never reassigns, and never touches cancellation: `can_cancel` still comes from the cancellation gate, so it stays `false` once the provider has left (`on_the_way` onward) even when `delay_alert` is set (Postman: "422 Cancel after execution began and a delay alert"). The customer may keep waiting for the same provider or cancel only when `can_cancel` is `true`.
- **Fixed / Removed:** None. **Breaking:** No.
- **Endpoints affected:** `GET /orders/{order}` (and the responses sharing the resource). The list tile is unchanged.
- **App action required:** add `delay_alert` to the Dart model (nullable object); show a waiting notice while it is non-null; refetch on `reason: delayed`. No FCM push yet. Timing values (15-minute period, 60-minute scheduled lead) are working assumptions awaiting client confirmation (TD-110).
- **BRD / decisions:** CUST-019, PROV-019 (delay bullet), BR-21/22, TD-109…111, TD-113.

### 2.3.0 — 2026-10-26
- **Added:** `cancellation {reason,at,fee,rule_id}` is always present in details. `can_cancel` now comes from the same gate as `POST /orders/{order}/cancel`; `refund` shows the result for paid customer cancellations. See [cancellation](cancellation.md) 1.0.0.
- **Changed:** `order.updated` may carry `reason: cancelled`; the cancelled tab contains customer-cancelled orders.
- **Fixed / Removed:** None. **Breaking:** No.
- **Endpoints affected:** `GET /orders/{order}` and other responses using the details resource; the new endpoint is in the linked guide.
- **App action required:** use `can_cancel`, parse `cancellation`, refetch on the event. `can_chat` stays false.
- **BRD / decisions:** CUST-020, BR-19/20/21/22, DEC-10/24, TD-105/106.

### 2.2.0 — 2026-10-26
- **Added:** `can_rate` (boolean, never omitted) on `GET /orders/{order}`: `true` when the order is `completed` and she has not yet rated the provider (no `customer_to_provider` review, BR-28, TD-104); otherwise `false`. The provider's separate `provider_to_customer` review does not affect it.
- **Changed:** nothing in existing fields. Orders now leave `confirmed` in production: the provider app calls the stage endpoints ([provider/order-execution](../provider/order-execution.md), PROV-019/020), so `preparing`, `on_the_way`, `arrived`, `in_progress` and `completed` arrive with the `order.updated` (`status_changed`) event and inbox row already described in 2.1.0. A scheduled order can only start inside its window (TD-102); one with a Joshen driver departs from the driver trip.
- **Fixed / Removed:** None. **Breaking:** No.
- **Endpoints affected:** `GET /orders/{order}` (also the create and pay responses, which share the resource).
- **App action required:** add `can_rate` to the Dart model. **Do not show a Rate action yet**, even when it is `true`: the rating endpoint (CUST-021) is a separate Phase 4 feature, and DEC-19's customer submission limits are still open (only the schema's one-review-per-direction rule applies). The list tile is unchanged.
- **BRD / decisions:** CUST-017, CUST-019 (customer result), BR-19/22/28, TD-101/104.

### 2.1.0 — 2026-10-26
- **Added:** `GET /orders?tab=current|upcoming|completed|cancelled` (optional; omit for every order, TD-100). List tile gains `service_name` and `address {text}`. Details gain `timeline[]` (every status the order passed through with its time — no actor, so the customer sees one status, BR-22) and `refund` (`null`, or amount, `{value,label}` status and destination, `returned_to_wallet`, `refunded_at`).
- **Added (status values):** `preparing`, `on_the_way`, `arrived`, `in_progress`, `completed`, `cancelled` may now appear in `status`, each with its label. Execution stages raise `order.updated` with `reason: status_changed` and an inbox row `order_status_changed`.
- **Changed / Fixed / Removed:** None. **Breaking:** No.
- **App action required:** Add tabs to My Orders; draw the progress from `timeline`; show the refund line from `refund`; treat unknown `status.value` defensively (show `label`); on `order.updated` refetch the order.
- **BRD / decisions:** CUST-017, BR-22, TD-100. Stages are applied by `AdvanceOrderAction`; the provider/driver endpoints that call it (PROV-019/020, DRIV) are separate features, so in production no order leaves `confirmed` until they ship. `can_cancel` and `can_chat` stay `false`.

### 2.0.0 — 2026-10-25
- **Changed (breaking):** `booking_type` was a plain string, now `{"value","label"}` in the list and the details. `can_pay` is always a boolean.
- **Added:** realtime `order.updated` events for the customer ([API conventions §9](../API_CONVENTIONS.md)); the list uses a dedicated lean resource (same fields as before).
- **Migration (old → new):** `order.booking_type == 'instant'` → `order.booking_type.value == 'instant'`.
- **App action required:** change the model, connect the socket, refetch the order on each event and reconnect; keep a slow poll only while disconnected.
- **BRD / decisions:** CUST-017, D3, TD-96.

### 1.0.0 — 2026-10-25
- **Added:** `GET /orders`, `GET /orders/{order}`; **Changed/Fixed/Removed:** None; **Breaking:** No.
- **App action required:** Use `{value,label}` status objects; follow `can_pay`, `can_cancel`, `can_chat` and rejection action flags.
- **BRD / decisions:** CUST-017 status view only, D3, TD-16/92/94.

## 1. Business summary

Internally, purchases use `package_purchase` and sessions use `package_session`; the customer order resource does not expose `order_kind`. The purchase address object has null fields and `can_rate` is false. Sessions have an address and use the ordinary completed-session rating flag. See [my packages](my-packages.md).

A customer sees only her orders. The list is lean; details expose the snapshotted service, options, address and exact money strings. Each read applies overdue response/payment deadlines before returning, so a waiting screen never shows an expired request as still active.

## 2. User flow and screens

| Step | Screen | Endpoint | App behavior |
|------|--------|----------|--------------|
| 1 | Orders list | `GET /orders` | Newest first; show status and total. |
| 2 | Waiting | `GET /orders/{order}` | Poll while `pending_provider` or `awaiting_payment`; stop when confirmed/rejected/expired. |
| 3 | Rejected 05b | Same | Show `rejection_reason`; show another-time/provider choices from flags. |
| 4 | Confirmed 06 | Same | Show snapshot and confirmed state. |
| 5 | My orders tabs | `GET /orders?tab=` | Current / Upcoming / Completed / Cancelled; each tile shows service, provider, address, type, status, payment status and total. |
| 6 | Track an order | `GET /orders/{order}` + `order.updated` | Draw the progress from `timeline`; refetch on every event. |
| 7 | Follow a refund | Same | Show `refund` (amount, status, `returned_to_wallet`) on rejected, expired or cancelled orders. |

## 3. Endpoints

### 3.1 Customer orders

`GET /api/v1/client/orders` · customer bearer token · optional query `tab` (`current`, `upcoming`, `completed`, `cancelled`). `data[]` has order UUID/code, `{value,label}` status, payment status and booking type, provider UUID/name, `service_name` (first ordered service, snapshot), `address {text}`, scheduled timestamp, total money string, `can_pay`, and created timestamp. **Real responses:** Postman: `client.orders.index` › "200 Customer order list", "200 Current orders tab". Empty `data` is a valid state. Unknown tab: 422, Postman: `client.orders.index` › "422 Unknown orders tab". Guest: 401.

| Tab | Holds (TD-100) |
|-----|----------------|
| `current` | `pending_provider`, `awaiting_payment`, `preparing`, `on_the_way`, `arrived`, `in_progress`, and a confirmed **instant** order |
| `upcoming` | Confirmed **scheduled** orders whose execution has not begun |
| `completed` | `completed` |
| `cancelled` | `cancelled`, `rejected`, `expired` |

### 3.2 Customer order details

`GET /api/v1/client/orders/{order}` · customer bearer token · order UUID. `data` always includes `status`, `payment_status`, `booking_type`, `source: {value,label}`, nullable `finder_search_id`, provider, address, selected services/options, amounts, `scheduled_at`, `eta_minutes`, deadlines, `can_pay`, `can_cancel`, `can_chat`, `can_rate`, `timeline`, `refund`, `cancellation`, `transport`, and timestamps. `can_chat` follows BR-15/TD-144. `transport` is a current order projection with mode, stage and tracking availability, never driver identity. Dates are ISO 8601 with offsets; money is a two-decimal string. **Real responses:** Postman: `client.orders.show` › "200 Completed order with execution timeline", "200 Rejected order with refund returned to wallet". A foreign or integer order ID returns 404.

`timeline[]` is ordered oldest first: `{status:{value,label}, at}` for every status the order has held, written by the database for each change. It never says who made the change, so provider and driver updates look like one order status (BR-22). `refund` is `null` until a refund exists, then `{id, amount, status, destination, returned_to_wallet, refunded_at}`; `returned_to_wallet` is true once a wallet refund is completed. **Real responses:** Postman: `client.orders.show` › "200 Rejected order with refund returned to wallet", "200 Completed order with execution timeline".

```json
"timeline": [
  {"status": {"value": "awaiting_payment", "label": "Awaiting payment"}, "at": "2026-10-26T06:56:52+00:00"},
  {"status": {"value": "pending_provider", "label": "Waiting for provider"}, "at": "2026-10-26T06:56:52+00:00"},
  {"status": {"value": "rejected", "label": "Rejected"}, "at": "2026-10-26T06:56:52+00:00"}
],
"refund": {
  "id": "01a176ff-6aea-7210-8700-db16463ecec7", "amount": "150.00",
  "status": {"value": "completed", "label": "Refunded"},
  "destination": {"value": "wallet", "label": "Wallet"},
  "returned_to_wallet": true, "refunded_at": "2026-10-25T07:00:00+00:00"
}
```

`delay_alert` is `null`, or the notice to show while the provider's status update is overdue (BR-21). It reflects the stage the order is in now and clears when the order moves on:

```json
"delay_alert": {"alerted_at": "2026-10-25T07:16:00+00:00"}
```

It never replaces `status` and never changes what the customer may do: keep Cancel tied to `can_cancel`, which stays `false` once execution has begun (BR-19/21). Postman: `client.orders.show` › "200 Order with a delay alert".

### 3.3 Confirm the driver's arrival

`POST /api/v1/client/orders/{order}/confirm-arrival` · customer bearer token · order UUID · no body · `throttle:order-arrival` (per user, `ORDER_ARRIVAL_PER_MINUTE`, default 10). Returns the order details (same shape as §3.2). Postman: `client.orders.confirm-arrival` › "200 Customer confirms the driver arrival" (abridged):

```json
{
  "status": "success",
  "message": "Arrival confirmed. The trip is complete.",
  "data": {
    "id": "01a197a9-6ac6-7106-8959-c2a48f17fa30",
    "code": "JSH-iurn03095419",
    "status": {
      "value": "arrived",
      "label": "Arrived"
    },
    "transport": {
      "mode": {
        "value": "joshen_driver",
        "label": "Joshen driver"
      },
      "stage": {
        "value": "arrived",
        "label": "Arrived"
      },
      "tracking": {
        "available": false,
        "location": null,
        "freshness": "unavailable",
        "distance_km": null,
        "eta_minutes": null,
        "calculated_at": null,
        "source": null
      },
      "can_confirm_arrival": false
    }
  }
}
```

| HTTP | `error_code` | When | Postman |
|------|--------------|------|---------|
| 200 | — | Already confirmed by the driver or provider: order returned unchanged | "200 Customer confirmation after another party is a no-op" |
| 422 | `INVALID_STATE_TRANSITION` | Self-travel order, or the driver has not reported arrival | "422 Order has no driver trip to confirm" |
| 404 | `ENTITY_NOT_FOUND` | Another customer's, unknown or integer order id | "404 Foreign order is hidden" |
| 429 | `RATE_LIMITED` | More than `ORDER_ARRIVAL_PER_MINUTE` (default 10) calls a minute; honor `Retry-After` | "429 Too many arrival confirmations" |

### 3.4 Live tracking preview

The order details carry a `transport` block (always present), and while the order is under way the customer can open a live preview that receives stage and driver-location events over Socket.IO ([API conventions §9.2](../API_CONVENTIONS.md)). The preview is a projection of the order: the HTTP details stay the source of truth.

**`transport` keys**

| Key | Type | Meaning |
|-----|------|---------|
| `mode` | `{value,label}` / null | `self` (provider travels herself) or `joshen_driver`; null until the provider chooses |
| `stage` | `{value,label}` / null | Driver trip stage, null for self travel or before a trip exists (values below) |
| `tracking.available` | bool | `true` while the driver trip is live (`accepted` → `to_customer`), or while the provider travels herself and the order is `on_the_way` (R08) |
| `tracking.location` | `{lat, lng, updated_at}` / null | The driver's (or, for self travel, the provider's) last position; null until one exists |
| `tracking.freshness` | `fresh` / `stale` / `unavailable` | Server-owned age of that position (R07): `stale` after `tracking_stale_after_seconds` (default 60), `unavailable` when there is no position. Dim a stale pin and show its time; never hide the last known place while tracking is available |
| `tracking.distance_km`, `tracking.eta_minutes` | number / null | Remaining road estimate (R06): straight-line distance × a detour factor, over the city's average speed (set on its delivery fee rule). Before pickup it covers driver → provider → you; after pickup, driver → you. `null` when unknown, never 0 |
| `tracking.calculated_at`, `tracking.source` | ISO date / null, `region_speed` / null | When the position behind the estimate was recorded, and how it was calculated. Treat the number as an estimate ("about N min") |
| `can_confirm_arrival` | bool | Show "Confirm arrival" ([§3.3](#33-confirm-the-drivers-arrival)) |

| `stage.value` | Show |
|---------------|------|
| `searching_driver` | Searching for a driver |
| `driver_assigned` | A driver accepted |
| `heading_to_provider` | Driver on the way to the provider |
| `at_provider` | Driver at the provider |
| `picked_up` | Provider in the car |
| `heading_to_you` | On the way to you |
| `arrived` | Arrived (also after the trip completes) |

The driver's identity is never shown to the customer. A self-travel order has `mode.value: self`, `stage: null` and no live position (open, TD-147).

**`POST /api/v1/client/orders/{order}/tracking-subscription`** · customer bearer token · order UUID · no body · `throttle:order-tracking` (per user). Allowed while the order is `confirmed`, `preparing`, `on_the_way`, `arrived` or `in_progress`. Returns a short-lived signed `grant` for the socket. Postman: `client.orders.tracking-subscription` › "200 Tracking subscription granted".

```json
{
    "status": "success",
    "message": "",
    "data": {
        "grant": "eyJhdWQiOiJvcmRlci10cmFja2luZyIsInN1YiI6MzYsImFwcCI6ImN1c3RvbWVyIiwib3JkZXIiOiIwMWExOTYxOS03OTlkLTczOTEtOGE3Zi0yODZmOGQ1NTQ1NDMiLCJleHAiOjE3OTI5MTE3MjB9.g673saV1878P5uF1UjV-9ibNUxX-G7Fgtou_smjqzvI",
        "expires_at": "2026-10-25T07:02:00+00:00"
    }
}
```

| HTTP | `error_code` | When | Postman |
|------|--------------|------|---------|
| 401 | `UNAUTHENTICATED` | Missing or expired token | "401 Missing or expired token" |
| 403 | `PERMISSION_DENIED` | Token issued by another app | "403 Token issued by another app" |
| 404 | `ENTITY_NOT_FOUND` | Another customer's, unknown or integer order id | "404 Order not found" |
| 422 | `ORDER_TRACKING_UNAVAILABLE` | Order not under way (waiting, completed, cancelled…) | "422 Order tracking unavailable" |
| 429 | `RATE_LIMITED` | Too many grants; honor `Retry-After` | "429 Tracking subscription rate limited" |
| 503 | `INFRASTRUCTURE_FAILURE` | Server realtime secret not configured; show "live tracking unavailable" and keep the details screen | "503 Realtime tracking not configured" |

The grant is opaque: send it as is, never decode it. It can be used to join within `expires_at` (default 120 s); a joined room is not dropped when it expires.

**Socket flow.** Register `order.tracking.stage` and `order.tracking.location` listeners, get a grant, emit `tracking:subscribe` `{grant}` and wait for the acknowledgment, then fetch `GET /orders/{order}`. On success the ack is `{ok: true, order_id}`; on failure it is `{ok: false, error}`:

| Ack `error` | Meaning | App action |
|-------------|---------|------------|
| `invalid_grant` | Malformed or tampered grant (or relay misconfigured) | Request a new grant once; if it fails again, show the details without live tracking |
| `expired_grant` | Grant older than `expires_at` | Request a new grant and subscribe again |
| `forbidden` | Grant belongs to another user or app socket | Reconnect with the current customer token, then retry |
| `too_many_rooms` | This socket already joined three tracking rooms | Unsubscribe the previews that are closed |

Leave with `tracking:unsubscribe` `{order_id}` when the preview closes or the app goes to the background. On reconnect, start again from the grant. `order.tracking.stage` carries `{order_id, order_status, transport, occurred_at}` (the same `transport` block as above); `order.tracking.location` carries `{order_id, lat, lng, recorded_at, freshness, distance_km, eta_minutes, calculated_at, source}` (the same estimate fields as `transport.tracking`). The same projection can arrive twice, so apply the newest and ignore repeats; ignore a location older than the one displayed.

### Payment error: `POST /orders/{order}/pay`

| HTTP | `error_code` | When | App action |
|------|--------------|------|------------|
| 422 | `PAYMENT_GATEWAY_UNAVAILABLE` | Payment driver is unavailable | Show a retry-later message. |

A 200 response can represent either outcome: inspect `data.payment_status.value` (`paid` or `failed`). See [booking](booking.md) for payment details.

## 4. Statuses and enums

| `status.value` | Meaning | App action |
|----------------|---------|------------|
| `pending_provider` | Provider response pending. | Poll; show `respond_before`. |
| `awaiting_payment` | Customer payment needed. | Offer pay only if `can_pay`. |
| `confirmed` | Payment/decision complete. | Show confirmation. |
| `preparing` | Provider is getting ready. | Show progress. |
| `on_the_way` | Execution has started (BR-19): provider or driver is travelling. | Show progress; cancellation is locked here (BR-19). |
| `arrived` | She has arrived. | Show progress. |
| `in_progress` | Service is being performed. | Show progress. |
| `completed` | Service finished. | Move to the Completed tab. |
| `cancelled` | Cancelled (cancellation feature, CUST-020). | Show `refund`. |
| `rejected` | Provider declined. | Show reason and alternate selection flags. |
| `expired` | Provider or payment deadline passed. | Stop waiting; no payment. |

`payment_status.value` is one of the schema values `unpaid`, `pending`, `paid`, `failed`, `refunded`, `partially_refunded`; the latter two can be shown after automatic refunds. `can_cancel` follows the active cancellation rule and pre-execution status ([cancellation](cancellation.md)); `can_chat` follows the paid-confirmed chat gate ([chat](chat.md)). `can_rate` is true only for a completed non-purchase order the customer has not rated yet; it drives the Rate button ([ratings](ratings.md)). Postman: `client.orders.show` › "200 Completed order can be rated".

## Data model

| Table | Read / write | Notes |
|-------|--------------|-------|
| `orders`, `order_items`, `order_item_options` | read | Immutable checkout snapshots. |
| `order_status_history`, `payments`, `refunds` | read indirectly | State/paid/refund status. |
| `notifications` | write by lifecycle | Shared schema inbox event, carrying public `order_id`. |

## 5. Dart models

`OrderSummary.id` is a UUID string; `status` and `payment_status` each have `value` and localized `label`. Parse money with a decimal library, never binary float. Keep nullable `respond_before`, `payment_due_at`, `scheduled_at` and `rejection_reason` in the model.

## 6. Business rules enforced by the backend

| Rule | Backend behavior | App response |
|------|------------------|--------------|
| BR-12/D3 | Paid instant rejection/timeout refunds to wallet and returns terminal status. | Show `refunded` from the order, not a new payment action. |
| BR-13/14 | Scheduled payment after acceptance confirms; failed attempt leaves state waiting. | Keep retry visible only while `can_pay`. |
| TD-131/148 | The order keeps `coupon_id` and `discount_amount` as history; the counted use is released on rejected / expired / cancelled. | Keep showing the coupon on a cancelled order; do not offer to re-apply it automatically. |
| DEC-05/TD-92 | Read enforces configured deadlines lazily. | Poll details; do not trust a client-only countdown as state. |

## 7. Notifications and real-time events

`order_accepted_awaiting_payment`, `order_rejected`, `order_expired`, `order_confirmed`, `order_status_changed` (execution stages), `order_delayed` (provider update overdue, BR-21) carry public `data.order_id`; open this details route on tap. The shared inbox row is persisted.

**Realtime (TD-96, [API conventions §9](../API_CONVENTIONS.md)).** Connect the socket with the customer access token and listen for `order.updated`. `payload.reason` is `accepted` (scheduled request accepted — show "Pay now" until `payment_due_at`), `rejected`, `expired`, `confirmed`, `status_changed`, `cancelled`, `delayed` or `package_purchased`. Refetch `GET /orders/{order}` for the authoritative state. Delivery is best effort, so refetch on reconnect and poll while disconnected. Preview-only transport events use the separate subscription ([§3.4](#34-live-tracking-preview), API conventions §9.2). FCM delivery depends on configuration and registered devices (TD-141–143).

## 8. Localization and UI notes

Show `label` from the API, preserving RTL. Convert absolute timestamps to Saudi local display. Use server flags for final actions; never render the prototype's accept/reject simulation buttons.

## 9. Integration checklist

- [ ] Render list empty/loading/error, owned details, and 404 fallback.
- [ ] Connect the socket; refetch the order on `order.updated` and on reconnect; poll only while disconnected, and stop on a terminal state.
- [ ] Add the four My Orders tabs (`?tab=`) and the richer tile.
- [ ] Render `timeline` and `refund`; show the label for any status value you do not know.
- [ ] Honor `can_pay`, `can_cancel`, `can_chat`, `can_rate`, `transport.can_confirm_arrival` and rejection choice flags.
- [ ] Live preview: listeners → grant → `tracking:subscribe` ack → details fetch; unsubscribe on close/background; handle every ack error and the 422 / 503 of the grant (§3.4).
- [ ] Test Arabic/English labels and offset dates.

## 10. Acceptance scenarios

| AC ID | Scenario | Expected |
|-------|----------|----------|
| AC-CUST-017 | View owned order state | Details show status, amounts and final decision. |
| AC-CUST-017 | Another customer or integer ID | 404 without leaking order data. |
| AC-CUST-017 | Read overdue order | Expired state returned, not stale waiting state. |
| AC-CUST-014 | Order created with a coupon | `coupon` is `{code, discount_amount, funded_by}` and the total is the discounted one; Postman: `client.orders.store` › "201 Order created with a coupon". |
| AC-CUST-017 | Tabs | Each tab holds exactly its statuses; another customer's orders never appear; unknown tab is 422. |
| AC-CUST-017 | Tile | Service, provider, address, type, status, payment status, total. |
| AC-CUST-017 | Rejected paid instant order | `timeline` ends `rejected`; `refund.returned_to_wallet` is true. |
| AC-PROV-020 | Completed order, rating flag | `can_rate` is `false` until the provider completes, `true` after, and `false` again once a `customer_to_provider` review exists; a cancelled or unfinished order is never rateable. |
| AC-CUST-019 | Delay alert | An overdue provider update sends one `order.updated` (`delayed`) and one inbox row per stage and fills `delay_alert`; `status`, provider and `can_cancel` are unchanged; nothing is reassigned. Settings: 15 / 60 minutes, awaiting client confirmation (TD-110). Tests: `OrderDelayAlertTest`. |
| AC-PROV-019 | Execution stages | Customer gets one `order.updated` (`status_changed`) per stage, a matching timeline, and no actor detail; a repeat is a no-op; a skipped or early stage is refused. |

## 11. Open decisions and known limitations

TD-92 values are working assumptions: overlap statuses pending_provider/awaiting_payment/confirmed, 15-minute slots, 60-minute lead, 45-minute instant ETA, 30-minute scheduled response and 15-minute payment. Values proposed by the project owner on 2026-10-25; DEC-05 stays Partly resolved until the client confirms the numbers. Fixed-amount provider commission is not supported at checkout until the earnings feature adds a snapshot column (TD-93). The simulated gateway is a launch blocker (TD-94).

Group booking (D2) is created by [booking](booking.md) §3.5 and read here. Out of scope: VAT on orders, wallet/payment/invoice screens. Rating is in [ratings](ratings.md), cancellation (customer and provider) in [cancellation](cancellation.md), chat in [chat](chat.md); driver trip execution and location are in the delivery guides. FCM delivery depends on configuration and registered devices (TD-141–143).

**Live tracking (TD-147, TD-183):** the estimate and freshness above are working assumptions (fallback speed 30 km/h, detour factor 1.3, stale after 60 s) that the owner may change in settings. The map shows the moving pin plus the provider and your address as markers; the driver's identity stays hidden. Still open: DEC-07 (driver cancel / no-show after acceptance) and the final live-map design (SCR-16).

**TD-100 (working assumption):** tab groupings and stage order are interpreted from BRD CUST-017 / the lifecycle in the business-logic rules; `preparing` is optional (`confirmed` may go straight to `on_the_way`). Invoice view and "Book again" (SCR-15) belong to later features.
