# 需求方窗口

需求方（組織身份 `requester`）在 `/r` 前台留言提需求，一則需求是一條**對話**（conversation）。組織裡的窗口負責回覆、開票、回報進度。這份說明給當窗口的人或 agent 用。

## 誰是窗口

- **接待窗口**：組織設定的一位（`GET https://station-flow.staging.quanhox.com.tw/api/v1/auth/me` 回的 `organization.requester_reception_user_id`）。還沒開票的對話、被退回的對話都歸他。
- **例行事務窗口＝該例行事務的事務負責人**（`GET https://station-flow.staging.quanhox.com.tw/api/v1/routines/{id}` 回的 `owner`）。開票後對話轉給這條 routine 的負責人。
- 負責人不是本組織 admin 或 staff（被移出組織、或改成需求方）時，由接待窗口代接。
- 換事務負責人時，舊負責人手上這條 routine 還沒結束的對話會一起轉給新負責人。
- 每條對話的 `handler_user_id` 是目前的負責窗口。只有他能回覆、開票、轉手；其他人回 409 `not_current_handler`。

## 被叫醒：事件游標

```bash
curl -s "https://station-flow.staging.quanhox.com.tw/api/v1/me/requester-events?since=0" \
  -H "Authorization: Bearer $SF_KEY" -H "Accept: application/json"
```

回 `data`（事件陣列）、`next_since`、`has_more`。下次把 `next_since` 帶進 `since`；`has_more` 是 true 就馬上再抓一頁（一頁 100 筆）。事件只回 5 秒前建立的，不會漏。

每筆事件有 `id`、`type`、`conversation_id`、`task_id`、`created_at`，**不帶內文**，要看內容再打對話詳情。自己做的動作不會叫醒自己。

| type | 什麼時候 |
|---|---|
| `new_request` | 需求方開了新對話 |
| `requester_post` | 需求方在對話裡留言 |
| `handoff` | 對話轉給你（開票轉手、退回、換接待窗口、換事務負責人） |
| `unbound_4h` | 對話超過 4 小時還沒開票 |
| `handler_ack_overdue` | 需求方留言超過 15 分鐘負責窗口還沒回 |
| `progress_updated` | 票的進度更新 |
| `questionnaire_answered` | 需求方答完問卷 |
| `confirm_clicked`／`confirm_rejected` | 需求方按了確認卡的確認／不同意 |
| `confirm_overdue` | 確認卡逾時沒回 |

## 對話 API

以下都在 `https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations` 底下，帶同樣兩個 header。讀不到的對話回 404。

**列表與詳情**

- `GET https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations`：你的收件匣，需求方在等回覆的排最上。`unbound=1` 只看未開票，`project=<id>` 篩專案。每列有 `handler_user_id`、`task_id`、`awaiting_handler`、`unread`。
- `GET https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/{id}`：詳情與貼文。帶 `since=<上次的 last_post_id>` 只抓新貼文。貼文的 `visibility` 是 `requester`（需求方看得到）或 `internal`（內部備註）。
- `POST https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/{id}/read`：標已讀，可帶 `post_id`，省略＝讀到最新。

**回覆**

```bash
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/42/posts" \
  -H "Authorization: Bearer $SF_KEY" -H "Accept: application/json" \
  -F "body=收到，我們今天下午回您" -F "files[]=@draft.png"
```

- `POST https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/{id}/posts`：`body` 必填；`internal=1` 存成內部備註；`files[]` 最多 10 個、每個 50MB。
- `POST https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/{id}/replied`：不留言，只標「已回覆」。

**開票與分流**

```bash
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/42/tickets" \
  -H "Authorization: Bearer $SF_KEY" -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"tickets":[{"name":"廣告 A","routine_id":7,"project_id":3}],"handoff_summary":"需求原話、已問到的資訊、選這條流程的理由"}'
```

- `POST https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/{id}/tickets`：`tickets` 1–20 張，每張要 `name`、`routine_id`、`project_id`。開票後對話轉給該 routine 的窗口；換人時 `handoff_summary` 必填（422 `handoff_summary_required`）。同一人不用寫。已開過票回 409 `already_bound`；你不在開始站角色、又不是 admin／接待窗口／事務負責人回 403 `routine_forbidden`。
- `POST https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/{id}/split`：選幾則貼文（`post_ids`）拆成新對話＋新票，`tickets` 只取第一張。
- `POST https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/{id}/handoff`：已開票的對話重新交給該 routine 的現任窗口，可帶 `handoff_summary`。沒開票回 422 `not_bound`。
- `POST https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/{id}/return`：覺得不是你的，退回接待窗口，`reason` 必填。沒開票或你就是接待窗口回 409 `cannot_return`。

**從票開對話**（票是窗口自己開的、沒有需求方對話時）

- `POST https://station-flow.staging.quanhox.com.tw/api/v1/requester/tasks/{task}/conversations`：替這張票對指定需求方開一條對話並綁票。`requester_user_id` 必填，`body`（第一則訊息，10000 字內）可省略。回 201 `{"data":{"conversation_id":…,"task_id":…}}`。你就是這條對話的負責窗口，之後照常用上面的 `posts`／`cards`／`progress`。

```bash
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/requester/tasks/120/conversations" \
  -H "Authorization: Bearer $SF_KEY" -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"requester_user_id":31,"body":"今天的題目請您確認"}'
```

- 只有該票 routine 的事務負責人（owner）或組織 admin 能開，其他人 403 `task_forbidden`。票或需求方不在你目前的組織回 404。
- `requester_user_id` 可以是本組織的需求方，或開了「兼任需求方」開關的內部成員（admin／staff，見下）。兩者都不是回 422（`errors.requester_user_id`）。
- 票已經有對話（不論是不是同一位需求方）回 409 `conversation_exists`，回應多帶 `conversation_id`，直接沿用那條，不會重建也不會改綁。票已掛另一位需求方回 409 `requester_mismatch`。
- 需求方在 `/r` 會多一列，看得到票的狀態與進度，收到推播（有開推播時）。

**內部成員兼任需求方**（同一個帳號，不改角色）

- `PUT https://station-flow.staging.quanhox.com.tw/api/v1/organization/members/{user}/requester-access`，body `{"enabled": true|false}`：只有目前組織的 admin 能設，其他人 403；對象不在本組織 404；對象本身是需求方回 422（不需要開關）。回 200 `{"data":{"user_id":…,"role":…,"requester_access":…}}`。
- 開了之後，這位成員可以被上面的端點指定為需求方，收到 `conversation_opened` 事件與 `/r` 推播；他從後台登入後開 `/r`，只看得到自己是需求方的對話。後台權限不受影響。
- 關掉後立刻失效，下一個 request 起 `/r` 就進不去。目前沒有設定畫面，只能用 API。

**進度、交付、卡片**

- `POST https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/{id}/progress`：`body`（500 字內）、`expected_on`（預計日期，可省略）。
- `POST https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/{id}/deliverables`：把對話裡的附件（`attachment_id`）標成交付物，放進專案的公開目錄。票沒掛專案回 422 `project_required`。
- `POST https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/{id}/cards`：發問卷或確認卡。

```bash
curl -s -X POST "https://station-flow.staging.quanhox.com.tw/api/v1/requester/conversations/42/cards" \
  -H "Authorization: Bearer $SF_KEY" -H "Accept: application/json" -H "Content-Type: application/json" \
  -d '{"kind":"questionnaire","body":"請選尺寸","questions":[{"label":"尺寸","options":["1:1","9:16"]}]}'
```

`kind` 是 `questionnaire` 或 `confirm`。問卷最多 20 題、每題 2–20 個選項；確認卡要先開票（422 `not_bound`）。

卡片綁定發卡當下的票和站。票離開那一站（送出口、退回、改綁別張票），還沒答的卡就失效：`GET` 對話時卡片帶 `stale: true`，需求方前台顯示「已處理，不需要回覆」，按不了；硬送回 409 `card_handled`。開票前發的問卷沒綁票，不會失效。

需求方前台每題會自動多一個「其它」選項（不會寫進 `questions[].options`，發卡時別自己加）。需求方選其它時可寫文字、附檔或貼上圖片。答完後 `GET` 對話，`card` 帶：

- `answers[i]`：選項字串；選其它時是 `"其它"`。
- `others`：物件，題號 → `{"text": "…或 null", "attachment_ids": [12]}`；沒有人選其它時是 `null`。附件在同一次 `GET` 的需求方回覆貼文 `posts[].attachments` 裡，用 id 對得上。
- 有 `attribute_key` 的題目選了其它，票屬性寫文字；只有附件時寫 `其它（見附件）`。

- 等需求方回答時，**別先送出口**。答案回來（`questionnaire_answered`／`confirm_clicked`／`confirm_rejected` 事件）再推進。
- 已經轉站了，就在新站重發一張，舊卡不用管。

## 需求方看到的狀態：站屬性 requester_dialog

站的 `requester_dialog`（`PUT https://station-flow.staging.quanhox.com.tw/api/v1/stations/{id}`，admin 設）決定需求方在 `/r` 看到什麼。只能是 `discuss`、`deliver` 或 `null`：

- `discuss`：討論站。票停在這站時，需求方看到「等您回覆」。
- `deliver`：交付站。票從這站走正常出口（不是退回）離開，就算交付。
- `null`：內部站（預設）。需求方只看到「製作中」。

綁票後的狀態，依序判斷：

1. 曾經從交付站走正常出口離開＝**已交付**。之後被退回或取消也不變。
2. 票在結束站（`end`）或存查站（`archive`）：最後一次進結束站時走的是取消類出口＝**已取消**，其他＝**已交付**。流程沒設交付站也一樣。最後一次進結束站時，若來源站只有一條出口（例如事務報告），就往前看進入那一站的出口，直到遇到有選擇的出口。
3. 票在討論站，或有需求方還沒答的問卷／確認卡＝**等您回覆**。
4. 票在開始站＝**受理中**；其他＝**製作中**。

取消類出口：出口 `tone` 是 `negative`，或出口的來源站是異常站（`type=exception`）。要讓需求方看到「已取消」，往結束站的取消出口要設 `tone: negative`，或從異常站接過去。

## 錯誤

錯誤回 `{"code": "...", "message": "..."}`。常見：

| 狀態 | code | 意思 |
|---|---|---|
| 403 | — | 需求方的 key 打不進這組 API |
| 404 | — | 這條對話你讀不到 |
| 409 | `not_current_handler` | 你不是目前的負責窗口 |
| 409 | `conversation_exists` | 這張票已經有對話，回應帶 `conversation_id` |
| 409 | `card_handled` | 卡片已經處理過（需求方每張卡只能答一次），或票已離開發卡那一站 |
| 422 | 見上 | 缺欄位或順序不對 |

## 限制

- 需求方只能用 `/r` 前台，API 不開放。
- 推播給需求方的通知只說「有新訊息／新進度」，不帶內文。
