# Uniorder：所有貨件統一一個介面

Uniorder 是一組統一的介面，整合方透過它對帳號的每一票貨件進行詢價、下單、列印、追蹤和取消，無論該貨件以何種方式履約。一次詢價回傳商戶自有配送，並可依請求回傳帳號的每一項承運商面單服務，每項均帶有 `rate_id`。下單時回傳所選的 `rate_id` 即可；請求中的其他任何內容都不用於選擇服務。

## 1. 可以建構的應用

- **一次列出全部運送選項的結帳頁面。** 客戶填寫地址，結帳頁面呼叫一個介面，頁面即可將本地配送與 UPS、Canada Post 及帳號使用的其他承運商並列顯示，每項附帶價格。
- **只需一套程式路徑的訂單管理或 ERP 連接器。** 來自各通路的訂單均透過相同的下單、查詢、面單、追蹤和取消呼叫處理。連接器無需為本地配送和承運商面單分別撰寫邏輯。
- **夜間批次處理。** 一個排隊任務最多可詢價或建立 500 票貨件，並依任務 id 讀取結果。
- **客服介面。** 客服人員查詢訂單、重新列印面單、檢視追蹤時間軸並取消訂單，所有訂單均使用相同的四個呼叫。

## 2. Uniorder 帶來的變化

| 不使用 Uniorder | 使用 Uniorder |
|---|---|
| 本地配送訂單使用一套 API，承運商面單使用另一套 API，各自有不同的欄位和回應 | 所有服務使用同一種請求結構（`from_*`、`to_*`、`packages`）和同一種回應結構 |
| 由整合方決定呼叫哪個承運商的 API | 詢價列出所有服務；由所選報價的 `rate_id` 決定 |
| 每種服務各有單獨的面單、追蹤和取消介面 | `GET /label`、`GET /tracking` 和 `POST /cancel` 適用於所有訂單 |
| 批次下單僅支援本地配送 | 所有服務均支援批次詢價和批次下單，可同步或排隊執行 |

Uniorder 不取代現有介面；現有介面繼續可用且保持不變。新整合建議以 Uniorder 為入口。

## 3. 介面一覽（依整合呼叫順序）

| 步驟 | 用途 | REST | GraphQL |
|---|---|---|---|
| 1 | 取得存取權杖 | `POST /api/v1/user/login` | `userLogin` |
| 2 | 對所有服務詢價 | `POST /api/v1/uniorder/rate` | `uniorderRate` |
| 3 | 依所選報價建立訂單 | `POST /api/v1/uniorder` | `uniorderCreate` |
| 4 | 列印面單 | `GET /api/v1/uniorder/{orderId}/label` | `uniorderLabel` |
| 5 | 查詢訂單 | `GET /api/v1/uniorder/{orderId}` | `uniorder` |
| 6 | 追蹤訂單 | `GET /api/v1/uniorder/{orderId}/tracking` | `uniorderTracking` |
| 7 | 取消訂單 | `POST /api/v1/uniorder/{orderId}/cancel` | `uniorderCancel` |
| — | 購買下單時未能購買的面單 | `POST /api/v1/uniorder/{orderId}/label` | `uniorderPurchaseLabel` |
| — | 一次詢價或建立最多 20 行 | `POST /api/v1/uniorder/rate/batch`, `POST /api/v1/uniorder/batch` | `uniorderRateBatch`, `uniorderCreateBatch` |
| — | 排隊處理最多 500 行 | `POST /api/v1/uniorder/rate/batch-async`, `POST /api/v1/uniorder/batch-async`, `GET /api/v1/uniorder/jobs/{jobId}` | `uniorderRateBatchAsync`, `uniorderCreateBatchAsync`, `uniorderJob` |

[REST 手冊](/api/documentation#/paths/v1-uniorder-rate/post) · [GraphQL 手冊](/api/graphql/documentation#/orders/uniorderRate)

逐步的請求、回應和檢查項見專題 **一次詢價與下單**。

## 4. 範例：提供全部選項的結帳頁面

蒙特婁的一家花店在線上銷售。結帳時，它對包裹詢價一次，並包含面單承運商：

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder/rate \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "from_name": "Fleurs du Plateau", "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis", "from_city": "Montreal", "from_province": "QC", "from_country": "CA", "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient", "to_telephone": "5145550199",
    "to_address": "6841 Rue Saint-Denis", "to_city": "Montreal", "to_province": "QC", "to_country": "CA", "to_postcode": "H2S2S3",
    "quote_labels": true,
    "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }]
  }'
```

回應列出一條 `self_delivery` 報價，以及每項承運商服務各一條 `label_service` 報價。結帳頁面將其作為選項顯示；客戶選擇當日本地配送。使用該報價的 `rate_id` 以及相同的地址和包裹建立訂單：

```bash
curl -X POST https://YOUR_HOST/api/v1/uniorder \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-10045" \
  -d '{
    "rate_id": "eyJpdiI6Ik1rT2Z...",
    "ref": "WEB-10045",
    "from_name": "Fleurs du Plateau", "from_telephone": "5145550100",
    "from_address": "4500 Rue Saint-Denis", "from_city": "Montreal", "from_province": "QC", "from_country": "CA", "from_postcode": "H2J2L3",
    "to_name": "Jane Recipient", "to_telephone": "5145550199",
    "to_address": "6841 Rue Saint-Denis", "to_city": "Montreal", "to_province": "QC", "to_country": "CA", "to_postcode": "H2S2S3",
    "packages": [{ "weight": 1.2, "weight_unit": 2, "length": 40, "width": 25, "height": 25, "dimension_unit": 2 }]
  }'
```

回應回傳訂單 `id` 及其運單號。店鋪透過 `GET /api/v1/uniorder/{orderId}/label` 列印面單，並在客戶的訂單頁面上顯示 `GET /api/v1/uniorder/{orderId}/tracking` 回傳的時間軸。

## 5. 範例：每晚出貨的 ERP

ERP 在 22:00 匯出當天的訂單。它將地址傳送至 `POST /api/v1/uniorder/rate/batch-async`，輪詢該任務直到 `status` 為 `done`，依自身規則為每一行選擇報價，再將所選的行傳送至 `POST /api/v1/uniorder/batch-async`。每筆結果都帶有該行的 `reference`，ERP 據此將每筆結果與自己的訂單行對應。`rate_id` 的有效期為 30 分鐘，因此下單任務應在詢價任務完成後盡快提交。

## 6. 範例：客服介面

處理客戶來電時，客服介面呼叫 `GET /api/v1/uniorder/{orderId}` 取得狀態和地址，呼叫 `GET /api/v1/uniorder/{orderId}/tracking` 取得時間軸和簽收憑證，客戶取消時呼叫 `POST /api/v1/uniorder/{orderId}/cancel`。配送訂單和面單訂單使用相同的呼叫；透過 `type` 欄位區分兩者。

```bash
curl https://YOUR_HOST/api/v1/uniorder/123456/tracking \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

透過 GraphQL 查詢同一訂單（[GraphQL 手冊](/api/graphql/documentation#/orders/uniorder)）：

```graphql
query {
  uniorder(order_id: 123456)
}
```

## 7. 設計時須遵循的規則

- **由 `rate_id` 決定服務。** 其有效期為 30 分鐘，且僅對發起詢價的帳號有效。
- **訂單在建立時定價。** `shipping_price` 為實際收取的價格；`quoted_price` 為詢價時的價格。兩者可能不同。
- **面單訂單不會遺失。** 下單時若無法購買面單，訂單仍會保留，回應為 `LABEL_PURCHASE_FAILED` 並附帶訂單 `id`；之後透過 `POST /api/v1/uniorder/{orderId}/label` 購買面單。
- **重試是安全的。** 每次下單呼叫都應傳送 `Idempotency-Key` 請求標頭；對已取消的訂單再次取消時回傳 `already_cancelled` `true`。
- **狀態統一。** 配送訂單的狀態為 `pending`、`in_transit`、`out_for_delivery`、`delivered`、`exception` 或 `cancelled`；面單訂單的狀態為 `label_pending`、`label_purchased` 或 `cancelled`，包裹所在位置由其承運商的追蹤資訊反映。

## 8. 上線檢查清單

- [ ] 帳號已開通 API 權限，權杖保存在伺服器端，而非瀏覽器中。
- [ ] 詢價時提供完整的寄件人和收件人地址。
- [ ] 在詢價後 30 分鐘內建立訂單，並攜帶 `Idempotency-Key`。
- [ ] 遇到 `LABEL_PURCHASE_FAILED` 時，稍後購買面單，而不是重新建立訂單。
- [ ] 透過 `GET /api/v1/uniorder/{orderId}/tracking` 讀取追蹤資訊，或透過 Webhook 接收。
- [ ] 取消遇到 `ORDER_STATUS_NOT_CANCELLABLE` 和 `ORDER_CANCEL_REFUSED` 時，保持訂單現狀不變。
