---
feature: Order management
slug: orders
surface: admin
consumer: react
version: 2.5.0
status: ready
brd_version: "1.3"
brd_ids: [ADMIN-013, CUST-019, CUST-018, PROV-021]
business_rules: [BR-21, BR-22, BR-29]
decisions: [D3, DEC-05, DEC-14, TD-12, TD-16, TD-19, TD-27, TD-109, TD-110, TD-111, TD-112, TD-113, TD-131, TD-148, TD-150, TD-165]
schema_tables: [coupons, orders, order_items, order_item_options, order_people, order_status_history, payments, invoices, refunds, notifications, trips]
related_docs: [../client/orders.md, ../client/cancellation.md, ../provider/orders.md, ../provider/order-execution.md, settings.md]
last_updated: 2026-11-10
---

# Order management — React Dashboard Integration Guide

> **Base URL:** `{HOST}/api/v1/admin` · Shared rules (headers, envelope, errors,
> pagination, axios setup, TS types): [API_CONVENTIONS.md](../API_CONVENTIONS.md)

## Version history

Newest first. Never edit an old row.

| Version | Date | Type | Summary | Dashboard action required |
|---------|------|------|---------|---------------------------|
| 2.5.0 | 2026-11-10 | MINOR | Customer references in admin list, details and conversation rows now also carry `customer.user_id`, the **user** uuid that opens the customer at `GET /admin/users/{id}`; `customer.id` is unchanged (the customer-profile uuid, still what `filters[customer_id]` takes). Fixes the order → customer link that returned 404 (QA-03). | Link to `/users/{customer.user_id}` instead of `/users/{customer.id}`; ignore the field if absent. |
| 2.4.1 | 2026-11-08 | PATCH | ADMIN-018 and ADMIN-022 are built (linked); Cycle F row now describes the trip summary and the null top-level `driver` / `dispatch`. | None. |
| 2.4.0 | 2026-11-03 | MINOR | Add invoice summary to order details (TD-165). | Show invoice summary card in order drawer; handle null. |
| 2.3.0 | 2026-11-01 | MINOR | Read-only, audited chat monitor: `GET /conversations` and `GET /conversations/{conversation}`; order `has_chat` and `chat_status` now reflect the conversation (TD-150). | Add the chat monitor page and link it from the order drawer via `chat_status.conversation_id`; gate by `orders.view`. |
| 2.2.0 | 2026-11-01 | MINOR | Order details add a nullable `coupon` block (`id`, `code`, `discount_amount`, `funded_by`). | Show the coupon next to the amounts; handle `null`. |
| 2.1.1 | 2026-10-28 | PATCH | Document a recorded order detail with its latest trip. | None. |
| 2.1.0 | 2026-10-28 | MINOR | Add latest non-cancelled trip summary to order details. | Handle the nullable trip block. |
| 2.0.0 | 2026-10-27 | MAJOR | Package identity and allocated session value are exposed; purchase location fields can be null. | Handle nullable purchase location fields and package item fields. |
| 1.0.0 | 2026-10-26 | Initial | Read-only order list and details: filters, sorts, full status history with actors, payments, notification history, refunds, delay flag. | Build the Orders page and the order drawer; gate by `orders.view`. |

## Changelog

### 2.5.0 — 2026-11-10

- **Added:** Customer references in admin list, details and conversation rows now also carry `customer.user_id`, the **user** uuid that opens the customer at `GET /admin/users/{id}`; `customer.id` is unchanged (the customer-profile uuid, still what `filters[customer_id]` takes). Fixes the order → customer link that returned 404 (QA-03).
- **Note:** the JSON examples in this guide were captured before `customer.user_id` existed and do not show it; the current captured responses are in the Postman collection.
- **Breaking:** No. **App action required:** Link to `/users/{customer.user_id}` instead of `/users/{customer.id}`; ignore the field if absent.

### 2.4.1 — 2026-11-08

- **Changed:** ADMIN-018 and ADMIN-022 are built (linked); Cycle F row now describes the trip summary and the null top-level `driver` / `dispatch`.
- **Breaking:** No. **App action required:** None.

### 2.4.0 — 2026-11-03

- **Added:** `invoice` summary object on `GET /orders/{order}`: `null` when unpaid, or `{id, invoice_no, subtotal, delivery_fee, discount, vat_amount, total, seller, payment_method, order_code, items, plan_name, qr_payload, issued_at}`. Postman: `admin.orders.show` › "200 Admin order details with invoice summary".
- **Breaking:** No. **Dashboard action required:** show invoice summary card in the order drawer when `invoice` is present; handle `null`.
- **BRD / decisions:** ADMIN-013, TD-165.

### 2.3.0 — 2026-11-01

- **Added:** `GET /conversations` (`admin.conversations.index`) lists every order conversation, newest message first, with metadata only — never message text. Filters `status` (`open`, `closed`), `provider_id`, `customer_id`; `search` on the order code. Postman: `admin.conversations.index` › "200 Chat monitor list".
- **Added:** `GET /conversations/{conversation}` (`admin.conversations.show`) returns the conversation and 50 messages per page (`before` cursor). Read-only: it never marks anything read. Each call writes an audit row `orders.conversation.viewed` naming the conversation, order and message ids (never the text) and answers `Cache-Control: no-store, private`. Postman: `admin.conversations.show` › "200 Read an order conversation".
- **Changed:** `has_chat` (list and details) is `true` once the order has a conversation; `chat_status` is `null` before the first message, otherwise `{conversation_id, status {value,label}, messages_count, last_message_at}`.
- **Breaking:** No (`has_chat` keeps its boolean type; `chat_status` was always `null`). **Dashboard action required:** add the chat monitor and open it from `chat_status.conversation_id`.
- **BRD / decisions:** ADMIN-013, AC-ADMIN-013, BR-15, DEC-14 (partly resolved by TD-150; retention still open).

### 2.2.0 — 2026-11-01

- **Added:** `coupon` on `GET /orders/{order}`: `null` or `{id (coupon uuid), code, discount_amount, funded_by:{value,label}}`. It stays on a rejected / expired / cancelled order even though the counted use was released ([coupons](coupons.md)).
- **Breaking:** No. **Dashboard action required:** show the coupon (link to the coupon by `id`) next to `amounts.discount_amount`; handle `null`.
- **BRD / decisions:** ADMIN-015/016, TD-131, TD-148.

### 2.1.1 — 2026-10-28

- **Changed:** Document a recorded order detail with its latest trip.
- **App action required:** None.
- **BRD / decisions:** Cycle F, TD-132–140; DEC-08 remains open.

### 2.1.0 — 2026-10-28

- **Added:** Nullable `trip` with id, code, status, driver and search expiry for the latest non-cancelled trip.
- **App action required:** Handle the nullable trip block.
- **BRD / decisions:** ADMIN-013, TD-134–139.

### 2.0.0 — 2026-10-27

- **Added / changed:** Package identity and allocated session value are exposed; purchase location fields can be null.
- **Fixed / removed:** None.
- **Breaking:** Yes.
- **Endpoints affected:** Existing order or package reads and actions described below.
- **App action required:** Handle nullable purchase location fields and package item fields.
- **BRD / decisions:** D1, TD-123–128.


### 1.0.0 — 2026-10-26
- **Added:** `GET /orders`, `GET /orders/{order}`; permission section `orders` (route group `orders` mapped in `config/rbac.php`); `is_delayed` / `delay_alerted_at` (BR-21, TD-111).
- **Changed / Fixed / Removed / Deprecated:** —
- **Breaking:** No.
- **Endpoints affected:** the two endpoints above (new).
- **Dashboard action required:** add the "Orders" sidebar entry; the screen is **read-only** — there is no button that changes an order (the BRD only monitors, TD-112).
- **BRD / decisions:** ADMIN-013, CUST-019, BR-21/22/29, TD-109…113.

---

## 1. Business summary

Purchase `service_location` remains an object whose address fields and city can be null. Item `package` carries package UUID, customer-package UUID and name; `allocated_session_value` reports the session allocation and the final completed session includes the item remainder. See [my packages](../client/my-packages.md).

Admins monitor every order — instant and scheduled, service and package — from one table and
open any order to see how it was confirmed, paid, executed, cancelled and refunded. The
screen **changes nothing**: no cancel, refund, reassign or status button exists (cancellation
and refund management is ADMIN-018, Sprint 3).

- **Instant** orders are paid first, then the provider accepts in a short window (D3); **scheduled**
  orders are accepted first and paid afterwards. `confirmation` in the details shows the
  deadlines and answers of both flows.
- The **status history** lists every change with **who** made it (customer, provider, driver, admin or
  system). Customers see the same changes without the actor (BR-22); only admins see it.
- **Delayed** (`is_delayed`): the customer was alerted that the provider has not updated the
  status in time (BR-21). The alert is informational — it never changes the status, cancellation
  or provider. See §9.
- **Travel method** is `null` until chosen, then *Independent arrival* or *Joshen driver*. The driver
  stages are represented by the nullable `trip` summary. The older `driver` and `dispatch` keys remain `null`.
- **Chat** (TD-150): admins with `orders.view` can read any order chat, read-only; every history read is audited. `has_chat` / `chat_status` link the order to its conversation.

## 2. Pages and permissions

| Page / component | Purpose | Endpoint(s) | RBAC permission (route name) | Section · action |
|------------------|---------|-------------|------------------------------|------------------|
| OrdersListPage | Table of all orders with filters | `GET /orders` | `admin.orders.index` | orders · view |
| OrderDetailsDrawer | Everything about one order | `GET /orders/{order}` | `admin.orders.show` | orders · view |
| ChatMonitorPage | All order conversations (metadata only) | `GET /conversations` | `admin.conversations.index` | orders · view |
| ConversationDrawer | One conversation's messages (audited) | `GET /conversations/{conversation}` | `admin.conversations.show` | orders · view |

Sidebar entry: *Orders*. Hide it when the admin lacks `admin.orders.index`. A supervisor without
the `orders` section gets `403` on both routes (BR-29).

## 3. Lists

### 3.1 Orders — `GET /api/v1/admin/orders` — list key `orders`

| Column | Field | Sortable (`sort[...]`) | Filter (`filters[...]`) | Notes |
|--------|-------|------------------------|-------------------------|-------|
| Code | `code` | no | `search` | Partial match on the code (`JSH-48`) |
| Customer | `customer.full_name`, `customer.id` (profile uuid; the filter value), `customer.user_id` (user uuid; opens `/users/{id}`) | no | `filters[customer_id]` (uuid) | |
| Provider | `provider.display_name`, `provider.id` | no | `filters[provider_id]` (uuid) | |
| Order type | `order_kind` `{value,label}` | no | `filters[order_kind]=service\|package_purchase\|package_session` | |
| Booking type | `booking_type` `{value,label}` | no | `filters[booking_type]=instant\|scheduled` | |
| Service time | `scheduled_at` | `sort[scheduled_at]` | `filters[scheduled_from]`, `filters[scheduled_to]` (`YYYY-MM-DD`, compare **`scheduled_at`**) | `null` for instant orders; they never match a scheduled range |
| Created | `created_at` | `sort[created_at]` (default, newest first) | `filters[created_from]`, `filters[created_to]` (`YYYY-MM-DD`, compare **`created_at`**) | |
| Status | `status` `{value,label}` | no | `filters[status]` | All 11 order statuses |
| Payment | `payment_status` `{value,label}` | no | `filters[payment_status]=unpaid\|pending\|paid\|failed\|refunded\|partially_refunded` | |
| Total | `total_amount` | `sort[total_amount]` | — | SAR, 2-decimal string |
| Address | `address` | no | `filters[city_id]` (uuid of the city, `governorates`) | Address snapshot text |
| Travel method | `travel_method` `{value,label}` or `null` | no | — | `self` = Independent arrival, `joshen_driver` |
| Chat | `has_chat` | no | — | `true` once the order has a conversation (TD-150) |
| Delayed | `is_delayed` | no | `filters[delayed]=1\|0` | `1` = alerted for the stage the order is in now |

- `*_to` is rejected (`422`) when earlier than its `*_from`. Dates are inclusive.
- Unknown `filters` or `sort` keys, a bad enum value, or an integer id in a uuid filter answer `422`.
- `per_page` 1–100 (default 15); `paginate=false` returns a plain array **capped at 500 rows**.

Postman: `admin.orders.index` › "200 Order List", "200 Order List Unpaginated", "422 Order Date Range Backwards", "422 Order Integer Provider Id", "422 Order Unknown Sort". Example row:

```json
{
  "id": "01a177ea-1667-70c3-8371-87f79b320034",
  "code": "JSH-hgsj36761582",
  "customer": { "id": "01a177ea-166a-7070-8054-a549c6f786fe", "full_name": "Mathilde Little" },
  "provider": { "id": "01a177ea-166d-7201-97d7-bcec3892cff2", "display_name": "Miss Alessia Hermiston IV" },
  "order_kind": { "value": "package_purchase", "label": "Package purchase" },
  "booking_type": { "value": "instant", "label": "Instant" },
  "scheduled_at": null,
  "created_at": "2026-10-25T07:00:00+00:00",
  "status": { "value": "confirmed", "label": "Confirmed" },
  "payment_status": { "value": "paid", "label": "Paid" },
  "total_amount": "0.00",
  "address": "13355 Alysson Mills\nFavianbury, VT 38768-9902",
  "travel_method": null,
  "has_chat": false,
  "is_delayed": false
}
```

## 4. Details view

### 4.1 Order — `GET /api/v1/admin/orders/{order}` (`{order}` = uuid)

Postman: `admin.orders.show` › "200 Order Details", "200 Order Details Cancelled Refunded", "404 Order Not Found".
The list fields plus:

| Key | Content |
|-----|---------|
| `customer` | `id` (customer-profile uuid), `user_id` (user uuid, opens `GET /users/{id}`), `full_name`, `phone_code`, `phone` |
| `provider` | `id`, `display_name`, `provider_type {value,label}`, `phone_code`, `phone` |
| `is_group`, `people[]` | Group booking people (D2): `id`, `label`, `is_customer` |
| `services[]` | Snapshot lines: `id`, `person_id`, `name`, `duration_min`, `unit_price`, `options_price`, `quantity`, `line_total`, `options[] {id,name,price}` |
| `amounts` | `services_amount`, `package_amount`, `delivery_fee`, `discount_amount`, `vat_amount`, `total_amount` |
| `coupon` | `null`, or `id` (coupon uuid), `code`, `discount_amount`, `funded_by {value,label}` — Postman: `admin.orders.show` › "200 Order Details" shows `null`; a coupon order is covered by `CouponBookingTest` |
| `service_location` | `text`, `lat`, `lng`, `city {id,name}`, `district {id,name}` or `null` |
| `estimated_duration_min`, `eta_minutes`, `notes_to_provider` | Booking details |
| `trip` | Latest non-cancelled trip: `{id, code, status {value,label}, driver {id,name} or null, search_expires_at}`; null when none exists |
| `driver`, `dispatch` | Currently null |
| `payments[]` | `id`, `method`, `status`, `amount`, `wallet_amount`, `gateway_amount`, `gateway`, `paid_at`, `created_at` |
| `invoice` | `null` when unpaid, else `{id, invoice_no, subtotal, delivery_fee, discount, vat_amount, total, seller, payment_method, order_code, items, plan_name, qr_payload, issued_at}` (TD-165) |
| `confirmation` | `provider_response_due_at`, `provider_responded_at`, `rejection_reason`, `payment_due_at`, `confirmed_at` — shows the instant vs scheduled difference |
| `execution` | `departed_at`, `arrived_at`, `started_at`, `completed_at` |
| `delay_alerted_at` | When the customer was last alerted (ISO 8601), or `null` |
| `status_history[]` | Oldest first: `id`, `from {value,label}\|null`, `to {value,label}`, `actor {type {value,label}, id, name}`, `note`, `at`. `actor.id` / `name` are `null` for the system |
| `notifications[]` | Events sent for this order: `id`, `event {value,label}`, `recipient_app {value,label}`, `sent_at`, `read_at` |
| `chat_status` | `null` before the first message, else `{conversation_id, status {value: open\|closed, label}, messages_count, last_message_at}` (TD-150) |
| `cancellation` | `cancelled_by {value,label}\|null`, `reason`, `at`, `fee`, `rule_id` |
| `refunds[]` | `id`, `amount`, `status`, `destination`, `reason`, `processed_at`, `created_at` |
| `updated_at` | ISO 8601 |

**Personal data (TD-112).** Customer and provider show only name and phone, exactly what the
customers and providers screens already show. National id, commercial register, VAT number,
IBAN, authorized person, documents and payout account are **never** returned. Notification
history carries no title, body or phone number. Payments omit the gateway reference and the failure text.

Example (abridged):

```json
{
  "id": "01a177ea-2237-709e-9f21-68219932ae56",
  "code": "JSH-jedz20900149",
  "customer": { "id": "01a177ea-2229-7163-96f4-3b331b8a69c0", "full_name": "Precious Rosenbaum", "phone_code": "966", "phone": "594306645" },
  "booking_type": { "value": "scheduled", "label": "Scheduled" },
  "status": { "value": "confirmed", "label": "Confirmed" },
  "travel_method": { "value": "self", "label": "Independent arrival" },
  "is_delayed": false,
  "driver": null,
  "payments": [{ "method": { "value": "card", "label": "Bank card" }, "status": { "value": "paid", "label": "Paid" }, "amount": "150.00", "gateway": "sim" }],
  "invoice": {
    "id": "01a1a5ba-3de0-737f-b678-ff968b993543",
    "invoice_no": "INV-100015",
    "subtotal": "100.00",
    "delivery_fee": "10.00",
    "discount": "0.00",
    "vat_amount": "16.50",
    "total": "126.50",
    "seller": { "name": "Joshen / جوشن", "vat_number": "300000000000003" },
    "payment_method": { "value": "card", "label": "Bank card" },
    "order_code": "JSH-bxjz76689725",
    "items": [{ "name": "Nail Care", "quantity": 1, "unit_price": "100.00", "options_price": "0.00", "line_total": "100.00" }],
    "plan_name": null,
    "qr_payload": "ARFKb3NoZW4gLyDYrNmI2LTZhgIPMzAwMDAwMDAwMDAwMDAzAxkyMDI2LTExLTA0VDA4OjQzOjI4KzAwOjAwBAYxMjYuNTAFBTE2LjUw",
    "issued_at": "2026-11-04T08:43:28+00:00"
  },
  "status_history": [{
    "from": null, "to": { "value": "confirmed", "label": "Confirmed" },
    "actor": { "type": { "value": "system", "label": "System" }, "id": null, "name": null },
    "note": null, "at": "2026-10-26T11:13:15+00:00"
  }],
  "notifications": [{ "event": { "value": "order_confirmed", "label": "Order confirmed" }, "recipient_app": { "value": "customer", "label": "Customer app" }, "sent_at": "2026-10-25T07:00:00+00:00", "read_at": null }],
  "chat_status": null,
  "cancellation": { "cancelled_by": null, "reason": null, "at": null, "fee": "0.00", "rule_id": null },
  "refunds": []
}
```

Recorded cycle F example (Postman: `admin.orders.show` › "200 Order details with trip"):

```json
{
    "status": "success",
    "message": "Data retrieved successfully",
    "data": {
        "id": "01a18164-2694-72ae-ba0a-f4be351f340e",
        "code": "JSH-mvdv21716560",
        "customer": {
            "id": "01a18164-26a4-730f-aa0e-dea868ac30cc",
            "full_name": "Prof. Wilburn Brakus II",
            "phone_code": "966",
            "phone": "577540818"
        },
        "provider": {
            "id": "01a18164-26a8-7028-9250-b0b4da2b7dbb",
            "display_name": "Mrs. Jazmyn Tremblay DVM",
            "provider_type": {
                "value": "individual",
                "label": "Individual"
            },
            "phone_code": "966",
            "phone": "537138676"
        },
        "order_kind": {
            "value": "service",
            "label": "Service"
        },
        "booking_type": {
            "value": "instant",
            "label": "Instant"
        },
        "scheduled_at": null,
        "created_at": "2026-10-25T07:00:00+00:00",
        "status": {
            "value": "confirmed",
            "label": "Confirmed"
        },
        "payment_status": {
            "value": "paid",
            "label": "Paid"
        },
        "total_amount": "0.00",
        "address": "1608 Kutch Throughway\nWest Zackstad, MO 56328",
        "travel_method": null,
        "has_chat": false,
        "is_delayed": false,
        "is_group": false,
        "people": [],
        "services": [],
        "amounts": {
            "services_amount": "0.00",
            "package_amount": "0.00",
            "delivery_fee": "0.00",
            "discount_amount": "0.00",
            "vat_amount": "0.00",
            "total_amount": "0.00"
        },
        "service_location": {
            "text": "1608 Kutch Throughway\nWest Zackstad, MO 56328",
            "lat": null,
            "lng": null,
            "city": {
                "id": "01a18164-2695-73e6-b488-7ed9573e5f53",
                "name": "Sample-crge49860063"
            },
            "district": null
        },
        "estimated_duration_min": null,
        "eta_minutes": null,
        "notes_to_provider": null,
        "driver": null,
        "dispatch": null,
        "trip": {
            "id": "01a18164-271b-71ad-9e7e-c2b4d2a9bc25",
            "code": "TRP-jnqn67890214",
            "status": {
                "value": "accepted",
                "label": "Driver accepted"
            },
            "driver": {
                "id": "01a18164-2705-7127-a0ed-e4583292498a",
                "name": "Donnell Greenfelder"
            },
            "search_expires_at": "2026-10-25T07:05:00+00:00"
        },
        "payments": [],
        "confirmation": {
            "provider_response_due_at": null,
            "provider_responded_at": null,
            "rejection_reason": null,
            "payment_due_at": null,
            "confirmed_at": "2026-10-25T07:00:00+00:00"
        },
        "execution": {
            "departed_at": null,
            "arrived_at": null,
            "started_at": null,
            "completed_at": null
        },
        "delay_alerted_at": null,
        "status_history": [
            {
                "id": "6ba8c697-d2a0-11f1-81d8-14b31f1c60bc",
                "from": null,
                "to": {
                    "value": "confirmed",
                    "label": "Confirmed"
                },
                "actor": {
                    "type": {
                        "value": "system",
                        "label": "System"
                    },
                    "id": null,
                    "name": null
                },
                "note": null,
                "at": "2026-10-28T07:23:06+00:00"
            }
        ],
        "notifications": [],
        "chat_status": null,
        "cancellation": {
            "cancelled_by": null,
            "reason": null,
            "at": null,
            "fee": "0.00",
            "rule_id": null
        },
        "refunds": [],
        "updated_at": "2026-10-25T07:00:00+00:00"
    }
}
```

## 5. Forms (create / edit)

N/A — read-only. `POST`, `PUT`, `PATCH` and `DELETE` on `/orders` answer `405`.

## 6. Actions (non-CRUD)

No order is changed here — the BRD only monitors orders (TD-112). Cancellation and refund management is ADMIN-018 (Sprint 3).

### Chat monitor (TD-150)

Read-only. Admins never post, edit, delete or mark messages read, so the customer's and provider's unread counts are unaffected. Messages are kept; no retention purge exists (DEC-14 still open).

#### `GET /api/v1/admin/conversations` — `admin.conversations.index` (orders · view)

| Param | Rules | Meaning |
|-------|-------|---------|
| `page`, `per_page` | integer ≥ 1; 1–100 (default 15) | Pagination |
| `search` | nullable string ≤ 50 | Partial order code |
| `filters[status]` | `open` \| `closed` | Stored conversation status: `closed` once the order completed or was cancelled (BR-15) |
| `filters[provider_id]` | uuid of `providers` | Chats of one provider |
| `filters[customer_id]` | uuid of `customers` | Chats of one customer |

Rows carry no message text, so listing is not audited. Postman: `admin.conversations.index` › "200 Chat monitor list".

```json
{
    "status": "success",
    "message": "Data retrieved successfully",
    "data": {
        "conversations": [
            {
                "id": "01a19790-31d3-71ba-8d58-193f9f9b392d",
                "order": {
                    "id": "01a19790-31cd-71d8-905b-20c3d6b184d8",
                    "code": "JSH-nrww02563085",
                    "status": {
                        "value": "completed",
                        "label": "Completed"
                    }
                },
                "customer": {
                    "id": "01a19790-31c6-73c2-8f0b-258a8604706b",
                    "full_name": "Sarina Waters"
                },
                "provider": {
                    "id": "01a19790-31c9-7143-b325-28c834ca29f8",
                    "display_name": "Trever Macejkovic"
                },
                "status": {
                    "value": "closed",
                    "label": "Closed"
                },
                "messages_count": 2,
                "last_message_at": "2026-11-01T14:37:51+00:00",
                "opened_at": "2026-11-01T13:42:51+00:00",
                "closed_at": "2026-11-01T14:42:51+00:00"
            },
            {
                "id": "01a19790-31c1-70b9-9665-9e4bb77f42ff",
                "order": {
                    "id": "01a19790-31b8-70f0-8724-fa6ac5bae967",
                    "code": "JSH-tvqs67327276",
                    "status": {
                        "value": "confirmed",
                        "label": "Confirmed"
                    }
                },
                "customer": {
                    "id": "01a19790-31af-737c-aebf-5f3f9acdb869",
                    "full_name": "Vincent Ernser"
                },
                "provider": {
                    "id": "01a19790-31b4-72b8-bf63-f5a18b22152d",
                    "display_name": "Madisen Heidenreich"
                },
                "status": {
                    "value": "open",
                    "label": "Open"
                },
                "messages_count": 2,
                "last_message_at": "2026-11-01T14:37:51+00:00",
                "opened_at": "2026-11-01T13:42:51+00:00",
                "closed_at": null
            }
        ],
        "links": {
            "first": "http://localhost/api/v1/admin/conversations?page=1",
            "last": "http://localhost/api/v1/admin/conversations?page=1",
            "prev": null,
            "next": null
        },
        "meta": {
            "current_page": 1,
            "from": 1,
            "last_page": 1,
            "links": [
                {
                    "url": null,
                    "label": "&laquo; Previous",
                    "page": null,
                    "active": false
                },
                {
                    "url": "http://localhost/api/v1/admin/conversations?page=1",
                    "label": "1",
                    "page": 1,
                    "active": true
                },
                {
                    "url": null,
                    "label": "Next &raquo;",
                    "page": null,
                    "active": false
                }
            ],
            "path": "http://localhost/api/v1/admin/conversations",
            "per_page": 15,
            "to": 2,
            "total": 2
        }
    }
}
```

Errors: 403 without `orders.view` (Postman: "403 Chat monitor access forbidden"); 422 for an unknown filter value (Postman: "422 Unknown conversation status filter").

#### `GET /api/v1/admin/conversations/{conversation}` — `admin.conversations.show` (orders · view)

Optional `before` (uuid of a message already shown) returns the 50 older messages; `has_more` says whether to offer "load older". Messages are oldest → newest within the page; `read_at` is when the recipient read it. Every call is audited (`orders.conversation.viewed`) and is `Cache-Control: no-store, private` — do not cache it in the dashboard. Postman: `admin.conversations.show` › "200 Read an order conversation".

```json
{
    "status": "success",
    "message": "Data retrieved successfully",
    "data": {
        "conversation": {
            "id": "01a197a9-8fcb-7374-b23f-8eb0bfb5f4f6",
            "order": {
                "id": "01a197a9-8fbf-721f-a233-3a5ad5bb80a9",
                "code": "JSH-hlga03497559",
                "status": {
                    "value": "confirmed",
                    "label": "Confirmed"
                }
            },
            "customer": {
                "id": "01a197a9-8fb4-73e4-809b-faf412c6ed33",
                "full_name": "Emmie Weimann"
            },
            "provider": {
                "id": "01a197a9-8fb9-72fb-a3c3-2547c12ea307",
                "display_name": "Dr. Arden Corwin PhD"
            },
            "status": {
                "value": "open",
                "label": "Open"
            },
            "messages_count": 2,
            "last_message_at": "2026-11-01T15:05:34+00:00",
            "opened_at": "2026-11-01T14:10:34+00:00",
            "closed_at": null
        },
        "messages": [
            {
                "id": "01a197a9-8fcd-722a-a8cb-e6b370c0315e",
                "sender_role": {
                    "value": "customer",
                    "label": "Customer"
                },
                "type": {
                    "value": "text",
                    "label": "Text"
                },
                "body": "Please bring the hair dryer.",
                "read_at": "2026-11-01T15:10:34+00:00",
                "created_at": "2026-11-01T15:00:34+00:00"
            },
            {
                "id": "01a197a9-8fcf-7053-8238-a42a4ea258f5",
                "sender_role": {
                    "value": "provider",
                    "label": "Provider"
                },
                "type": {
                    "value": "text",
                    "label": "Text"
                },
                "body": "Sure, on my way.",
                "read_at": null,
                "created_at": "2026-11-01T15:05:34+00:00"
            }
        ],
        "has_more": false
    }
}
```

| HTTP | error_code | Meaning |
|------|------------|---------|
| 403 | `PERMISSION_DENIED` | No `orders.view` |
| 404 | `ENTITY_NOT_FOUND` | Unknown conversation or integer id (Postman: "404 Unknown conversation"), or a `before` message outside it (Postman: "404 Unknown history cursor") |
| 422 | — (`data.errors.before`) | `before` is not a uuid (Postman: "422 History cursor must be a UUID") |

## 7. Statuses and enums

| Enum | Values | Notes |
|------|--------|-------|
| `status` | `pending_provider`, `awaiting_payment`, `confirmed`, `preparing`, `on_the_way`, `arrived`, `in_progress`, `completed`, `rejected`, `expired`, `cancelled` | Labels follow `Accept-Language` |
| `payment_status` | `unpaid`, `pending`, `paid`, `failed`, `refunded`, `partially_refunded` | |
| `booking_type` | `instant`, `scheduled` | |
| `order_kind` | `service`, `package_purchase`, `package_session` | |
| `travel_method` | `self` (Independent arrival), `joshen_driver` | `null` until chosen |
| `actor.type` | `customer`, `provider`, `driver`, `admin`, `system` | |
| `refunds[].status` | `pending`, `completed`, `failed` | |
| `refunds[].reason` | `customer_cancel`, `provider_cancel`, `provider_rejected`, `provider_timeout`, `admin`, `package_refund`, `other` | |
| `notifications[].event.value` | `order_request_received`, `order_accepted_awaiting_payment`, `order_rejected`, `order_expired`, `order_confirmed`, `order_status_changed`, `order_cancelled`, `order_delayed` | Unknown keys come back as `label = value` |

## Data model

Reads `orders` (+ `delay_alerted_at`), `order_items`, `order_item_options`, `order_people`,
`order_status_history`, `payments`, `refunds`, `notifications` (by `order_id`) and the customer /
provider profiles. `trips` is read in cycle F. No table is written.

## 8. TypeScript types

```ts
type EnumValue<T extends string = string> = { value: T; label: string };

interface OrderRow {
  id: string; code: string;
  customer: { id: string; user_id: string; full_name: string };
  provider: { id: string; display_name: string };
  order_kind: EnumValue<'service' | 'package_purchase' | 'package_session'>;
  booking_type: EnumValue<'instant' | 'scheduled'>;
  scheduled_at: string | null; created_at: string;
  status: EnumValue; payment_status: EnumValue;
  total_amount: string; address: string;
  travel_method: EnumValue<'self' | 'joshen_driver'> | null;
  has_chat: boolean; is_delayed: boolean;
}

interface OrderDetails extends OrderRow {
  customer: { id: string; user_id: string; full_name: string; phone_code: string | null; phone: string | null };
  provider: { id: string; display_name: string; provider_type: EnumValue; phone_code: string | null; phone: string | null };
  is_group: boolean;
  people: { id: string; label: string; is_customer: boolean }[];
  services: { id: string; person_id: string | null; name: string; duration_min: number | null; unit_price: string; options_price: string; quantity: number; line_total: string;
              options: { id: string; name: string; price: string }[] }[];
  amounts: Record<'services_amount' | 'package_amount' | 'delivery_fee' | 'discount_amount' | 'vat_amount' | 'total_amount', string>;
  service_location: { text: string; lat: string | null; lng: string | null; city: { id: string; name: string } | null; district: { id: string; name: string } | null };
  estimated_duration_min: number | null; eta_minutes: number | null; notes_to_provider: string | null;
  trip: { id: string; code: string; status: EnumValue; driver: { id: string; name: string } | null; search_expires_at: string | null } | null;
  driver: null; dispatch: null;
  payments: { id: string; method: EnumValue; status: EnumValue; amount: string; wallet_amount: string; gateway_amount: string; gateway: string | null; paid_at: string | null; created_at: string }[];
  confirmation: { provider_response_due_at: string | null; provider_responded_at: string | null; rejection_reason: string | null; payment_due_at: string | null; confirmed_at: string | null };
  execution: { departed_at: string | null; arrived_at: string | null; started_at: string | null; completed_at: string | null };
  delay_alerted_at: string | null;
  status_history: { id: string; from: EnumValue | null; to: EnumValue; actor: { type: EnumValue; id: string | null; name: string | null }; note: string | null; at: string }[];
  notifications: { id: string; event: EnumValue; recipient_app: EnumValue<'customer' | 'provider' | 'driver'>; sent_at: string; read_at: string | null }[];
  chat_status: { conversation_id: string; status: { value: 'open' | 'closed'; label: string }; messages_count: number; last_message_at: string | null } | null;
  cancellation: { cancelled_by: EnumValue | null; reason: string | null; at: string | null; fee: string; rule_id: string | null };
  refunds: { id: string; amount: string; status: EnumValue; destination: EnumValue; reason: EnumValue | null; processed_at: string | null; created_at: string }[];
  updated_at: string;
}
```

## 9. Business rules enforced by the backend

| Rule | What the backend does | What the admin sees |
|------|-----------------------|---------------------|
| BR-29 | Route name is the permission; supervisors need the `orders` section | `403` otherwise |
| BR-22 | One customer-facing status; history keeps the actor | `status_history[].actor` (customers never get it) |
| BR-21 | A delayed provider update may notify the customer; it never allows cancellation after execution and never reassigns | `is_delayed`, `delay_alerted_at`; status, `provider` and cancellation are untouched |
| TD-131/148 | Coupon discount comes off the services amount only; the order keeps the coupon as history after a release | `coupon` block, `amounts.discount_amount` |
| TD-150 | Chat monitor | Read-only, audited; `has_chat` / `chat_status` reflect the conversation |

**Delay clock (TD-109…111).** `orders:alert-delays` runs every minute and judges each
confirmed or executing visit order. A missing update becomes overdue at:

| Stage | Deadline |
|-------|----------|
| `confirmed`, `preparing` — instant | `confirmed_at` + period |
| `confirmed`, `preparing` — scheduled | later of (stage entry, `scheduled_at` − lead) + period |
| `on_the_way` | expected arrival + period, never before `departed_at` + period (expected arrival = `confirmed_at` + `eta_minutes` for instant, `scheduled_at` for scheduled) |
| `arrived` | `arrived_at` + period |
| `in_progress` | `started_at` + `estimated_duration_min` + period (not watched when the duration is null) |

Period = `order_status_delay_minutes` (default 15), lead = `scheduled_delay_clock_lead_minutes`
(default 60) — both editable in [Settings](settings.md) and **awaiting client confirmation**.
An order is alerted **once per stage**: `is_delayed` is `true` while `delay_alerted_at` is at or
after the moment the current stage began, so it clears when the provider moves the order on. Not
watched: orders waiting for a provider or payment, finished orders, `package_purchase` orders
(no visit) and `joshen_driver` orders before provider-managed arrival and service stages.

## 10. Notifications triggered

This feature only reads notifications. The delay job writes one for the customer:

| Event | Who is notified | Channel |
|-------|-----------------|---------|
| `order_delayed` — provider update overdue | Customer | in-app inbox + live `order.updated` with `reason: delayed` ([API_CONVENTIONS §9.1](../API_CONVENTIONS.md)) |

No FCM push yet. The provider is not alerted (out of scope).

## 11. Integration checklist

- [ ] Types match the real responses above
- [ ] Orders menu and drawer gated by `admin.orders.index` / `admin.orders.show`
- [ ] Filters, sorts, search and pagination wired to the documented params (dates are `YYYY-MM-DD`)
- [ ] Booleans in filters sent as `1` / `0`
- [ ] Status history shows the actor; the page has no order-changing button
- [ ] `driver` and `dispatch` rendered as "not available yet" when `null`; `chat_status: null` means no messages yet
- [ ] Chat monitor gated by `admin.conversations.index` / `.show`; history never cached
- [ ] RTL (`ar`) and `en` verified

## 12. Acceptance scenarios

| AC ID | Scenario | Expected |
|-------|----------|----------|
| AC-ADMIN-013 | Open instant and scheduled orders | Both appear; `confirmation` shows the accept window (instant) or payment window (scheduled) |
| AC-ADMIN-013 | Inspect status history | Every change with its actor; the customer API shows none |
| AC-ADMIN-013 | Inspect travel method, payment, notifications, chat, refund | All present; chat is `null`/`false` |
| AC-ADMIN-013 | Supervisor access | `403` without the `orders` section, `200` with it |
| AC-CUST-019 | Delay alert | Overdue update alerts the customer once per stage; no status change, no reassignment, no cancellation after execution |

Tests: `tests/Feature/Api/V1/Admin/AdminOrderManagementTest.php`, `tests/Feature/Api/V1/Client/OrderDelayAlertTest.php`, `tests/Feature/Api/V1/Admin/BookingSettingsTest.php`.

## 13. Open decisions and known limitations

| ID | Impact on this feature | Current behavior |
|----|------------------------|------------------|
| DEC-05 | Delay period and clock start | 15 and 60 minutes are working assumptions awaiting client confirmation (TD-110) |
| DEC-14 | Chat retention | Admin monitoring is read-only and audited (TD-150); no retention period, nothing is deleted |
| Cycle F | Driver dispatch stages | `trip` shows the latest non-cancelled trip and carries the assigned driver (`trip.driver {id, name}`) and its status. The top-level `driver` and `dispatch` keys are still `null` (kept for shape stability): read the trip block. Live trip monitoring is [transportation-monitoring](transportation-monitoring.md) |
| ADMIN-018 | Cancellation / refund management | Built: see [cancellations-refunds](cancellations-refunds.md) (2.0.0); the order also shows cancellation and refund status |
| ADMIN-022 | Reports | Built: see [reports](reports.md) (1.0.0) |

## 14. Requirement status (verified 2026-10-26)

`true` = built and covered by a passing test. `false` = not built; the note says why.

| Source | Requirement | Done | Note |
|--------|-------------|------|------|
| ADMIN-013 | List: customer, provider, order type, booking type, date/time, status, payment status, total, address, travel method, chat | true | Chat is always `false` (TD-16) |
| ADMIN-013 | Details: customer, provider, services, options, location, travel method, payment, status, history, notifications, chat status, refund status | true | |
| ADMIN-013 | Driver information | true | Available in `trip.driver` after acceptance |
| ADMIN-013 | Driver-request / acceptance stages | false | Cycle F (`trips`) |
| ADMIN-013 | Filters, search, sorts | true | §3.1 |
| ADMIN-013 | Admin actions that change an order | false | Out of scope: the BRD only monitors (TD-112) |
| CUST-019 / BR-21 | Delay alert without cancellation or reassignment | true | `OrderDelayAlertTest` |
| BR-29 | Supervisors limited to their sections | true | |
| Tests | Feature tests, Postman examples for every route, `postman:generate --check` | true | |
