# 倉儲與出庫

先將貨物入庫存放，再將在庫包裹出庫寄出：讀取設定 → 詢價 → 建立倉儲訂單 → 付款 → 查詢在庫包裹 → 出庫估價 → 建立出庫訂單 → 付款 → 追蹤 → 取消。

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

請將 `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"}'
```

```
Authorization: Bearer ACCESS_TOKEN
```

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

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

## 2. 倉儲設定

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

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

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

**驗證：** 已記錄倉庫 `id`；若包裝目錄非空，同時記錄包裝 `id`。

## 3. 倉儲詢價

**REST：** `POST /api/v1/customer/storage-orders/calculate-price`

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders/calculate-price \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "warehouse_id": 7,
    "start_date": "2026-09-10",
    "end_date": "2026-10-10",
    "items": [{
      "qty": 1,
      "length": 30,
      "width": 20,
      "height": 15,
      "dimension_unit": 2,
      "weight": 2,
      "weight_unit": 2
    }]
  }'
```

**驗證：** `success` 或 `result` 為 true，並且有價格。缺少 `warehouse_id` / 日期是 `400`。

## 4. 建立倉儲單

**REST：** `POST /api/v1/customer/storage-orders`

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/storage-orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-store-001" \
  -d '{
    "warehouse_id": 7,
    "start_date": "2026-09-10",
    "end_date": "2026-10-10",
    "items": [{
      "description": "Box A",
      "qty": 1,
      "length": 30,
      "width": 20,
      "height": 15,
      "dimension_unit": 2,
      "weight": 2,
      "weight_unit": 2,
      "value": 100
    }]
  }'
```

**驗證：** 回應包含 `data.id`。請儲存該倉儲訂單 id。

## 5. 付款倉儲

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

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

可先呼叫 `GET /api/v1/customer/storage-orders/{id}/payment-info`。

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

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

**驗證：** 倉儲單已付款 / 確認。`402` 表示須先為錢包儲值後再重試。

出庫介面僅在倉庫完成收貨後可用。測試時須等待收貨完成後再繼續。

## 6. 查詢在庫包裹

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

```bash
curl "https://YOUR_HOST/api/v1/customer/shipout-orders/available-items?warehouse_id=7" \
  -H "Authorization: Bearer ACCESS_TOKEN"
```

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

**驗證：** 已記錄至少一個 `storage_package_ids`（示例 `5001`）。空列表說明尚未收貨，請勿建立出庫訂單。`403` 表示該客戶未開通出庫。

## 7. 出庫估價並建立

**REST：** `GET /api/v1/customer/shipout-orders/services?warehouse_id=7` — [REST 手冊](/api/documentation#/paths/v1-customer-shipout-orders-services/get)

記錄 `service_code`。

**REST：** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/estimate` — [REST 手冊](/api/documentation#/paths/v1-customer-shipout-orders-services-serviceCode--estimate/post)

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

**REST：** `POST /api/v1/customer/shipout-orders/services/{serviceCode}/orders` — [REST 手冊](/api/documentation#/paths/v1-customer-shipout-orders-services-serviceCode--orders/post)

```bash
curl -X POST https://YOUR_HOST/api/v1/customer/shipout-orders/services/intl_express/orders \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: dev-shipout-001" \
  -d '{
    "warehouse_id": 7,
    "storage_package_ids": [5001],
    "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"
  }'
```

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

**驗證：** 回應包含出庫訂單 `id`。所選倉儲包裹已鎖定至該出庫請求。

## 8. 付款出庫、追蹤並訂閱事件

**REST：** `POST /api/v1/customer/shipout-orders/{id}/pay` — [REST 手冊](/api/documentation#/paths/v1-customer-shipout-orders-id--pay/post)

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

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

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

有運單號時：

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

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

**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;
}
```

**驗證：** 付款成功並記錄金額（或 `402` / `422` 原因清楚）。產生運單號後，公開追蹤介面可查詢該票。

## 9. 取消測試出庫訂單

**REST：** `POST /api/v1/customer/shipout-orders/{id}/cancel` — [REST 手冊](/api/documentation#/paths/v1-customer-shipout-orders-id--cancel/post)

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

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

取消成功後將釋放倉儲包裹鎖定。倉儲訂單在仍允許取消時，使用 `POST /api/v1/customer/storage-orders/{id}/cancel` 取消。

**驗證：** `422` 表示目前狀態不允許取消。出庫取消成功後，第 6 步將重新列出這些包裹。

## 驗收清單

- [ ] 倉儲設定回傳倉庫 `id`。
- [ ] 倉儲詢價回傳價格。
- [ ] 建立倉儲回傳 `data.id`。
- [ ] 倉儲付款成功，**或**已確認錢包需儲值。
- [ ] 在庫件列出已收貨包裹（`storage_package_ids`）。
- [ ] 出庫估價回傳價格或 `has_items_needing_quote`。
- [ ] 建立出庫回傳 `id` 並鎖定這些包裹。
- [ ] 出庫付款成功（或理解 `402` / `422`）。
- [ ] 產生運單號後，公開追蹤介面可查詢該票。
- [ ] 出庫取消釋放包裹，**或**該狀態不能取消。
