# Add-on request communications — frontend integration guide

This document describes how to build a **chat-style thread** for a single add-on request: text messages, optional images/files, and how that maps to the current backend.

**Audience:** frontend / mobile developers.

---

## 1. Is the current API enough for “I send an image, then say ‘see like this’”?

**Yes**, as long as the client implements a **two-step flow** in one user action:

1. **Upload** the file(s) to the add-on request file endpoint → receive `file_storage_id` (and file metadata).
2. **Post** the communication with `message` (e.g. “see like this”) and `attachments: [<file_storage_id>, …]`.

From the user’s perspective this can still feel like **one message** (one bubble with image + caption): the UI waits for upload(s) to finish, then sends the text + IDs together.

The API does **not** accept raw multipart files on the communication endpoints; it only accepts **existing** `file_storage` IDs in `attachments`.

---

## 2. Authentication & base URL

- All routes below require **`auth:sanctum`** (e.g. `Authorization: Bearer {token}`).
- Paths are relative to your API root, typically: **`/api`**.

Example:

```http
GET /api/addon-requests/42/communications
Authorization: Bearer {access_token}
Accept: application/json
```

---

## 3. Endpoints overview

| Purpose | Method | Path |
|--------|--------|------|
| Load chat thread | `GET` | `/api/addon-requests/{request}/communications` |
| Send a general message (chat) | `POST` | `/api/addon-requests/{request}/communications` |
| Upload file (image/doc) for this request | `POST` | `/api/addon-requests/{request}/files` |
| List files linked to request | `GET` | `/api/addon-requests/{request}/files` |
| Delete uploaded file record | `DELETE` | `/api/addon-requests/files/{file}` |

`{request}` = **add-on request ID** (integer primary key).

Specialized flows (sales ↔ department ↔ customer) use additional `POST` routes under the same prefix; see section 7.

---

## 4. Chat thread: list messages

### Request

```http
GET /api/addon-requests/{request}/communications?include_internal=true
```

| Query param | Type | Default | Description |
|-------------|------|---------|----------------|
| `include_internal` | boolean | `true` | If `false`, internal-only messages are hidden (for customer-facing UIs). Staff UIs usually use `true`. |

### Response shape (success)

```json
{
  "success": true,
  "data": [
    {
      "id": 1,
      "message": "請看這張圖",
      "sender_type": "sales",
      "sender_label": "業務",
      "message_type": "note",
      "message_type_label": "備註",
      "user": { "id": 5, "name": "Display Name" },
      "quoted_price": null,
      "formatted_quoted_price": "",
      "customer_response": null,
      "customer_response_label": null,
      "is_internal": false,
      "requires_customer_response": false,
      "customer_response_deadline": null,
      "is_overdue": false,
      "can_customer_respond": false,
      "attachments": [
        {
          "id": 101,
          "name": "screenshot.png",
          "filename": "2026-03-19_12-00-00_abcd1234.png",
          "mime_type": "image/png",
          "extension": "png",
          "size": 123456,
          "human_size": "120.56 KB",
          "url": "https://your-app.example/storage/uploads/...",
          "full_path": "https://your-app.example/storage/uploads/...",
          "is_image": true
        }
      ],
      "created_at": "2026-03-19T08:00:00.000000Z",
      "formatted_date": "2026/03/19 16:00"
    }
  ],
  "meta": {
    "include_internal": true,
    "can_add_message": true,
    "sender_type_options": [],
    "message_type_options": []
  }
}
```

### Important for UI: `attachments` (read / timeline / single-message responses)

- **`GET` timeline** and **`POST` communication responses** return **`attachments` as an array of objects** (resolved from `file_storage`), so you can render images directly with `url` or `full_path`.
- Use **`is_image`** for thumbnails vs file icons; use **`mime_type`** / **`name`** as needed.
- If a stored id no longer exists in `file_storage`, the item is **`{ "id": 22, "missing": true }`**.
- **`attachments`** is **`null`** when none; **`[]`** when explicitly empty.

**Sending** a message still uses **`attachments` as an array of integer IDs** in the JSON body (see section 6).

Messages are ordered **oldest → newest** in `data` (ascending `created_at`).

---

## 5. Upload a file (image / document) for chat

Use this **before** (or in parallel with typing) sending the communication.

### Request

```http
POST /api/addon-requests/{request}/files
Content-Type: multipart/form-data
Authorization: Bearer {token}
```

| Field | Required | Rules |
|-------|----------|--------|
| `file` | Yes | File, max **10 MB** (`10240` KB). |
| `file_type` | Yes | One of: `quote`, `invoice`, `receipt`, `contract`, `other`. For informal chat images, use **`other`** unless product defines another type. |
| `description` | No | Max 255 characters. |

### PDF fails but images work?

Usually **not** a “PDF blocked” rule in this API — validation allows any `file` up to **10MB**. Typical causes:

1. **PHP limits** — `upload_max_filesize` or `post_max_size` in `php.ini` smaller than the PDF. The API now returns **422** with a clear message and `upload_error_code` (e.g. `UPLOAD_ERR_INI_SIZE` = 1).
2. **Web server limit** — e.g. Nginx `client_max_body_size` too small (returns **413** before Laravel).
3. **Request shape** — must be **`multipart/form-data`**, field name **`file`**, plus **`file_type`**.

### Response (201)

```json
{
  "success": true,
  "message": "檔案已成功上傳",
  "data": {
    "id": 7,
    "addon_request_id": 42,
    "file_storage_id": 101,
    "file_type": "other",
    "uploaded_by_user_id": 5,
    "description": null,
    "file_storage": {
      "id": 101,
      "name": "screenshot.png",
      "path": "uploads/addon-requests/42/2026-03-19_12-00-00_abcd1234.png",
      "full_path": "https://your-app.example/storage/uploads/...",
      "mime_type": "image/png",
      "extension": "png",
      "size": 123456,
      "is_public": true
    },
    "uploaded_by": { "id": 5, "name": "...", "display_name": "..." }
  }
}
```

**Use for the next step:** `data.file_storage_id` (e.g. `101`).

Images are stored on the **public** disk; `full_path` (and/or constructing URL from `path`) is what you use for previews.

---

## 6. Send a chat message (text + optional attachments)

### Request

```http
POST /api/addon-requests/{request}/communications
Content-Type: application/json
Authorization: Bearer {token}
```

### Body (typical “chat” message)

```json
{
  "message": "你看是不是像這樣？",
  "message_type": "note",
  "attachments": [101]
}
```

| Field | Required | Description |
|-------|----------|-------------|
| `message` | Yes | Max **2000** characters. |
| `message_type` | No | Default behavior depends on backend; for casual chat use **`note`**. |
| `sender_type` | No | Usually omitted; server infers from the logged-in user vs request roles. |
| `attachments` | No | Array of integers; each must exist in `file_storage` (upload first). |
| `metadata` | No | Arbitrary JSON object if needed. |
| `is_internal` | No | Boolean; `true` = staff-only, not shown when `include_internal=false`. |
| `quoted_price`, `price_currency`, `requires_customer_response`, `customer_response_deadline` | No | For quote / workflow messages. |

### Response (201)

Returns the same **shaped object as the timeline** (including **enriched `attachments`** with `url` / `is_image`, etc.). Use it for optimistic UI updates.

---

## 7. Other communication endpoints (workflow, not casual chat)

Same request class as `store` → also support **`attachments`** where applicable.

| Action | POST path |
|--------|-----------|
| Sales inquiry to department | `/api/addon-requests/{request}/communications/sales-inquiry` |
| Department quote to sales | `/api/addon-requests/{request}/communications/department-quote` |
| Sales forward quote to customer | `/api/addon-requests/{request}/communications/sales-forward` |
| Sales confirm with department | `/api/addon-requests/{request}/communications/sales-confirm` |
| Department final confirmation | `/api/addon-requests/{request}/communications/department-final` |

Extra validation applies on some routes (e.g. `quoted_price` required for quotes). Read server validation errors for details.

---

## 8. Customer response (accept / reject / negotiate)

```http
PATCH /api/addon-requests/communications/{communication}/customer-response
Content-Type: application/json
```

```json
{
  "customer_response": "accepted",
  "response_message": "我們接受此報價"
}
```

`customer_response`: `accepted` | `rejected` | `negotiating`.

**Note:** This flow does **not** currently accept **`attachments`**. If customers must send images with their reply, that requires a **backend change** (extend `CustomerResponseRequest` + `AddonRequestCommunicationService::customerResponse`).

---

## 9. Recommended frontend flow (chat bubble with image + text)

1. User selects image(s) and types text.
2. On **Send**:
   - Show uploading state.
   - `POST .../{request}/files` for each file (or parallel with a concurrency limit).
   - Collect all `file_storage_id` values.
   - `POST .../{request}/communications` with `message` + `attachments: [ids...]`.
3. On success, append the returned message to the thread (or refetch `GET` communications). The response already includes **enriched `attachments`** for immediate preview.
4. After a **GET** refresh, each message’s **`attachments`** still includes **`url`** / **`full_path`**, so no extra lookup is required for display.

### Error handling

- If upload succeeds but communication fails, you have an **orphan** `AddonRequestFile` + `file_storage` row; product may allow retry with the same IDs or re-upload. Optionally call `DELETE /api/addon-requests/files/{file}` using the **addon request file** `id` from the upload response (`data.id`), not the `file_storage_id`.

---

## 10. Meta flags for the UI

From `GET .../communications`:

- **`meta.can_add_message`**: disable input when `false` (e.g. request completed/cancelled).
- **`meta.sender_type_options` / `meta.message_type_options`**: labels for filters or badges.

---

## 11. Pagination / real-time

- The list endpoint currently returns the **full** timeline for that request (no `page` parameter in controller). For very long threads, consider asking backend for **cursor/limit** pagination if needed.
- **Real-time** (WebSocket / SSE) is not described here; clients can **poll** `GET` communications on an interval if required.

---

## 12. Summary

| Question | Answer |
|----------|--------|
| Enough for image + “see like this” text? | **Yes**: upload → then post message with `attachments`. |
| One HTTP request only? | **No** (today): two calls minimum; UX can still feel like one send. |
| What do I send in `attachments`? | **Array of `file_storage` integer IDs** (after upload). |
| What does GET / POST return in `attachments`? | **Array of file objects** (`url`, `full_path`, `name`, `mime_type`, `is_image`, …) or `null` / `[]`. |

---

*Document version: 2026-03-19 (attachments enriched on read). Matches routes in `routes/api.php`, `AddonRequestCommunicationController`, `AddonRequestCommunicationService`, `CreateCommunicationRequest`, and `AddonRequestController::uploadFile`.*
