# 運輸服務

對接物流公司自有運輸服務：列出服務 → 讀取服務設定 → 估價 → 建立訂單 → 付款 → 查詢 → 追蹤 → 取消測試訂單。

本專題僅涵蓋下列介面，請按順序呼叫。本流程使用**客戶**帳號。

請將 `YOUR_HOST` 與 `ACCESS_TOKEN` 替換為實際環境中的值。

## 1. 登入（客戶）

```bash
curl -X POST https://YOUR_HOST/api/v1/user/customer/login \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"your_password"}'
```

將回傳的 `access_token` 置於請求頭：

```
Authorization: Bearer ACCESS_TOKEN
```

GraphQL 請求提交至 `POST /api/graphql`，並使用相同的 Authorization 請求頭。

**驗證：** 登入成功後回傳 `access_token`。後續請求若未攜帶該權杖，將回傳 `401`。

## 2. 列出服務

**REST：** `GET /api/v1/customer/shipping-orders/services` — [REST 手冊](/api/documentation#/paths/v1-customer-shipping-orders-services/get)

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/services \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**GraphQL：** `customerShippingOrderServices` ([GraphQL 手冊](/api/graphql/documentation#/customer/customerShippingOrderServices))

每一行有 `service_code`、`name`、`offer_pickup`、`allow_warehouse_delivery`。空列表表示尚未向該客戶分配服務。

**驗證：** 已記錄一個 `service_code`（下面示例用 `intl_express`）。

## 3. 載入該服務設定

**REST：** `GET /api/v1/customer/shipping-orders/services/{serviceCode}/config` — [REST 手冊](/api/documentation#/paths/v1-customer-shipping-orders-services-serviceCode--config/get)

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/config \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**GraphQL：** `customerShippingOrderServiceConfig` ([GraphQL 手冊](/api/graphql/documentation#/customer/customerShippingOrderServiceConfig))

**驗證：** 若服務允許入倉，至少有一個倉庫 `id`。`403` 表示該客戶無權使用此服務。

## 4. 估價

**REST：** `POST /api/v1/customer/shipping-orders/services/{serviceCode}/estimate-price`

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/estimate-price \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "delivery_postcode": "M5V2H1",
    "delivery_country": "CA",
    "packages": [{
      "weight": 2,
      "length": 30,
      "width": 20,
      "height": 15,
      "weight_unit": 2,
      "dimension_unit": 2
    }]
  }'
```

`origin_type`：`warehouse`（入倉）或 `pickup`（上門取件）。須與第 3 步中該服務允許的始發方式一致。

**驗證：** `result` 為 true，並且有價格（或人工報價標記）。若目的地或包裹規格被拒絕，請勿建立訂單。

## 5. 下單

**REST：** `POST /api/v1/customer/shipping-orders/services/{serviceCode}/orders`

請求須包含 `Idempotency-Key`。

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/services/intl_express/orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-ship-001" \
  -d '{
    "origin_type": "warehouse",
    "warehouse_id": 7,
    "reference": "DEV-SHIP-001",
    "delivery_name": "Jane Recipient",
    "delivery_telephone": "5555555555",
    "delivery_email": "jane@example.com",
    "delivery_address_1": "123 King St W",
    "delivery_city": "Toronto",
    "delivery_province": "ON",
    "delivery_country": "CA",
    "delivery_postcode": "M5V2H1",
    "package": [{
      "weight": 2,
      "length": 30,
      "width": 20,
      "height": 15,
      "weight_unit": 2,
      "dimension_unit": 2,
      "quantity": 1
    }]
  }'
```

**驗證：** 回應包含訂單 `id`。請儲存 `id`，以及回傳的 `tracking_number` / `reference_number`（如有）。

## 6. 付款

**REST：** `POST /api/v1/customer/shipping-orders/{id}/pay`

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/9001/pay \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

可先呼叫 `GET /api/v1/customer/shipping-orders/{id}/payment-info`。`402` 表示錢包餘額不足，請完成儲值後重試。

**驗證：** 訂單應處於不可再次付款狀態，或 `remaining_balance` 為 `0`。

## 7. 查詢訂單並追蹤

**REST：** `GET /api/v1/customer/shipping-orders/{id}` — [REST 手冊](/api/documentation#/paths/v1-customer-shipping-orders-id/get)

```bash
curl https://YOUR_HOST/api/v1/customer/shipping-orders/9001 \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

**GraphQL：** `customerShippingOrderShow` ([GraphQL 手冊](/api/graphql/documentation#/customer/customerShippingOrderShow))

訂單上有 `tracking_number` 時，公開追蹤（無需權杖）：

```bash
curl https://YOUR_HOST/api/v1/tracking/SR123456789012
```

[REST 手冊](/api/documentation#/operations/getPublicTracking) · [GraphQL 手冊](/api/graphql/documentation#/tracking/trackingPublic)

**驗證：** 詳情介面回傳目前客戶帳號下的該訂單。產生運單號後，公開追蹤介面可查詢該票。

## 8. 設定事件通知

| 設定項 | 事件 | 觸發時機 |
|---|---|---|
| `order_create_webhook_url` | `order.created` | 儲存 `id` 和運單號 |
| `tracking_event_webhook_url` | `tracking.event` | 軌跡時間線 |
| `order_status_change_webhook_url` | `order.status_change` | 業務系統中的訂單狀態 |

**REST：** `PUT /api/v1/webhook-settings` — [REST 手冊](/api/documentation#/paths/v1-webhook-settings/put)

**GraphQL：** `webhookSettingsUpdate` ([GraphQL 手冊](/api/graphql/documentation#/webhooks/webhookSettingsUpdate))。

用原始報文校驗 **v2**：`HMAC_SHA256(timestamp + "." + raw_body, secret)` 對照 `X-Webhook-Signature-V2`。用 `X-Webhook-Event-Id` 去重。**3 秒內回傳 2xx**。

```php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE_V2'] ?? '';
$expected = hash_hmac('sha256', $ts . '.' . $raw, $sharedSecret);
if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}
```

**驗證：** 一次測試下單應產生包含該 `id` 的 `order.created` 事件。

## 9. 取消測試訂單

**REST：** `POST /api/v1/customer/shipping-orders/{id}/cancel`

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipping-orders/9001/cancel \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

**驗證：** 重複取消應保持冪等，或由介面回傳已取消狀態。`422` 表示目前狀態不允許取消。

## 驗收清單

請使用測試參考號，例如 `DEV-SHIP-001`：

- [ ] 服務列表非空；已記錄一個 `service_code`。
- [ ] 設定回傳該服務的倉庫 / 包裝。
- [ ] 估價回傳價格（或明確的待報價標記）。
- [ ] 下單回傳 `id`；同一 `Idempotency-Key` 不會重複建立訂單。
- [ ] 付款成功，**或**已確認錢包需儲值（`402`）。
- [ ] 詳情介面回傳目前客戶帳號下的該訂單。
- [ ] 產生運單號後，公開追蹤介面可查詢該票。
- [ ] 取消成功，**或**已確認該狀態不能取消。
