# Admin — Order Conversation (Open / Get-or-Create) — Frontend Guide

Written for: frontend developers building the admin dashboard.

Admin can open the support chat for a specific order with **one call**. If the order already has a conversation, the API returns it with its messages. If not, the API creates an empty one (no messages) and returns it. The frontend never has to check first.

> **Scope:** admin side only. The client, company and technician APIs are unchanged.

---

## 1. What changed

| # | Change | Type |
|---|--------|------|
| 1 | New endpoint `POST /api/v1/admin/conversations/entity/order/{orderId}` | **New** |
| 2 | `entity` in the admin conversation resource now includes `uuid` | **Additive** (no field removed or renamed) |

Point 2 applies to every admin endpoint that returns a conversation: list, show, close, and the new endpoint.

---

## 2. Endpoint

```
POST /api/v1/admin/conversations/entity/{entityType}/{entityId}
```

| Part | Value | Notes |
|------|-------|-------|
| `entityType` | `order` | The only supported value for now. Anything else returns 404. |
| `entityId` | Order **numeric id** (e.g. `15`) | Use `order.id`, **not** `order.uuid`. Must be numeric, otherwise 404. |

- **Method:** `POST` (it can create a row, so it is not a `GET`).
- **Body:** none.
- **Headers:** `Authorization: Bearer <admin token>`, `Accept: application/json`, optional `Accept-Language: ar|en`.
- **Auth:** admin or super-admin only.
- **Permission:** none. Any admin or super-admin can call it (no role permission required).

### Which customer does the conversation belong to?

The **order's customer** (`orders.customer_id`). There is one conversation per order and customer. The technician assigned to the order is **not** part of this chat.

---

## 3. Responses

All responses use the standard envelope `{ status, message, data }`.

### 3.1 `201 Created` — no conversation existed, a new empty one was created

```json
{
  "status": "success",
  "message": "Data saved successfully",
  "data": {
    "id": 1,
    "customer": { "id": 2, "name": "Tess Grant V", "email": "okilback@example.net" },
    "subject": null,
    "entity": {
      "type": "order",
      "id": 1,
      "uuid": "61546feb-e472-48ba-9edb-40cefcaa8728"
    },
    "status": "Open",
    "created_at": "2026-10-05 13:56:13",
    "closed_at": null,
    "messages": []
  }
}
```

### 3.2 `200 OK` — conversation already existed, returned with its thread

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "id": 1,
    "customer": { "id": 2, "name": "Tess Grant V", "email": "okilback@example.net" },
    "subject": null,
    "entity": {
      "type": "order",
      "id": 1,
      "uuid": "61546feb-e472-48ba-9edb-40cefcaa8728"
    },
    "status": "Open",
    "created_at": "2026-10-05 13:56:13",
    "closed_at": null,
    "messages": [
      {
        "id": 1,
        "sender": { "id": 1, "name": "Pasquale Predovic I" },
        "sender_type": "staff",
        "body": "Hi",
        "sent_at": "2026-10-05 13:56:13"
      }
    ]
  }
}
```

### 3.3 Errors

| HTTP | When | Body |
|------|------|------|
| `401` | Missing or invalid token | standard auth error |
| `403` | The caller is not an admin or super-admin | standard forbidden error |
| `404` | Order does not exist, `entityType` is not `order`, or `entityId` is not numeric | `{ "status": "fail", "message": "Resource not found", "data": null }` |

There is no `422`: the endpoint takes no body.

---

## 4. Field reference

### Conversation (`data`)

| Field | Type | Notes |
|-------|------|-------|
| `id` | number | Conversation id. Use it for `show`, `reply` and `close`. |
| `customer` | `{ id, name, email }` | The order's customer. |
| `subject` | string \| null | Always `null` for conversations opened through this endpoint. |
| `entity` | object \| null | `null` for general support chats not linked to anything. |
| `entity.type` | string | `"order"`. |
| `entity.id` | number | The order's numeric id. |
| `entity.uuid` | string \| null | The order's uuid. **New.** `null` only if the linked record has none. |
| `status` | string | **Localized label** (`"Open"` / `"Closed"`, Arabic when `Accept-Language: ar`). Do not compare it with a hardcoded string. See [section 5](#5-things-to-know). |
| `created_at` | string | `YYYY-MM-DD HH:mm:ss` |
| `closed_at` | string \| null | Same format. |
| `messages` | array | Empty for a brand-new chat. Sort by `sent_at` on the client if you need a guaranteed order. |

### Message (`data.messages[]`)

| Field | Type | Notes |
|-------|------|-------|
| `id` | number | |
| `sender` | `{ id, name }` | |
| `sender_type` | string | `"staff"` (admin) or `"customer"`. Use this to decide which side of the chat to render. |
| `body` | string | |
| `sent_at` | string | `YYYY-MM-DD HH:mm:ss` |

---

## 5. Things to know

1. **A new chat is empty.** Creating it does **not** notify the customer. The customer only receives a notification once admin sends the first message with the reply endpoint below.
2. **Closed conversations are returned as they are.** If the order's conversation was closed earlier, the endpoint returns it with `status: "Closed"` and `closed_at` set. It does **not** reopen it and does **not** create a second one. If you want to block replies on closed chats in the UI, check `closed_at !== null`.
3. **Safe to call repeatedly.** Calling it twice never creates two conversations, even if two admins open the same order at the same moment. First call returns `201`, every later call returns `200`.
4. **`status` is a label, not a code.** To filter the list by status, send the raw code: `GET /conversations?filters[status]=open` or `closed`.
5. **Use `entity.uuid`** wherever the dashboard links to or identifies the order by uuid, for example to deep-link from the chat to the order page.

---

## 6. Suggested UI flow

On the admin order details page, add a **"Chat with customer"** button:

1. On click, call `POST /api/v1/admin/conversations/entity/order/{order.id}`.
2. Use `data.id` as the conversation id and render `data.messages` (empty list for a new chat).
3. When admin sends a message, call the existing reply endpoint (below). It returns the created message, so append it to the thread.
4. Optionally close the chat with the existing close endpoint.

```js
// Open (or create) the chat for an order
const res = await api.post(`/admin/conversations/entity/order/${order.id}`);
const conversation = res.data.data;      // { id, customer, entity, status, messages, ... }
const isNew = res.status === 201;        // optional: true when the chat was just created
```

### Existing endpoints used after opening the chat (unchanged)

| Action | Request | Notes |
|--------|---------|-------|
| Send message | `POST /api/v1/admin/conversations/{id}/reply` body `{ "body": "text" }` | `body` required, string, max 2000. Returns `201` with the message. |
| Reload chat | `GET /api/v1/admin/conversations/{id}` | Same shape as the response above, with `messages`. |
| Close chat | `POST /api/v1/admin/conversations/{id}/close` | Returns the conversation without `messages`. |
| List chats | `GET /api/v1/admin/conversations` | Each item now includes `entity.uuid`. Filter with `filters[entity_type]=order` and `filters[entity_id]=<order id>`. |

### List item shape (for reference)

```json
{
  "id": 1,
  "customer": { "id": 2, "name": "Tess Grant V", "email": "okilback@example.net" },
  "subject": null,
  "entity": { "type": "order", "id": 1, "uuid": "61546feb-e472-48ba-9edb-40cefcaa8728" },
  "status": "Open",
  "last_message": "Hi",
  "created_at": "2026-10-05 13:56:13",
  "closed_at": null
}
```

The list response lives under `data.conversations`, with pagination in `data.links` and `data.meta`, and counts in `data.stats`.
