# 运输服务

对接物流公司自有运输服务：列出服务 → 读取服务配置 → 估价 → 创建订单 → 支付 → 查询 → 追踪 → 取消测试订单。

本专题仅覆盖下列接口，请按顺序调用。本流程使用**客户**账号。

请将 `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`）。
- [ ] 详情接口返回当前客户账号下的该订单。
- [ ] 生成运单号后，公开追踪接口可查询该票。
- [ ] 取消成功，**或**已确认该状态不能取消。
