---
doc_id: aipool-order-payment-api-integration-test-guide
title: AiPool 訂單建單支付統一 API 整合手冊
description: AiPool 訂單建立、訂單明細、支付發起、點數折抵、商品計價與失敗情境的 API 整合方法與測試驗證手冊。
slug: /developers/aipool-order-payment-api-integration-test-guide
product: AiPool
category: integration-guide
audience:
  - developer
  - qa
visibility: public
status: published
version: 1.0.0
owner: aile-platform
updated_at: 2026-06-24
tags:
  - AiPool
  - Order
  - Payment
  - API
  - 整合手冊
rendered_html: /rendered/developers/aipool-order-payment-api-integration-test-guide/
download: true
sidebar_position: 8
---

# 訂單建單支付統一 — API 整合手冊

Gateway 前綴：`/transaction`  
登入：使用者 session（`StpUtil` `userInfo`）  
成功：`code=1000`，`data` 見各情境

**標準驗證流程（成功情境）**

1. `POST /transaction/v1/user/order/create` → 記下 `data.orderId` 為 `{OID}`
2. `POST /transaction/v1/user/order/item` → 確認金額、點數與建單請求一致
3. `POST /transaction/v1/user/order/{OID}/pay` → 確認預扣點數、DDPay 建單、狀態流轉（建單時金額已定案，pay 仍須驗證租戶上下文、點數預扣、金流設定等執行期例外）

| 變數                | 說明                                                                                                                                |
| ------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `{ENT_TID}`         | 企業租戶 `businessType=0`                                                                                                           |
| `{MCH_TID}`         | 商家租戶 `businessType=1`                                                                                                           |
| `{ACCOUNT_SESSION}` | 付款人 session                                                                                                                      |
| `{G_ENT_COMB}`      | 企業商品：方案 **80 點 + 80 元**/件，`paymentOptionId=DEFAULT`                                                                      |
| `{G_ENT_PT}`        | 企業商品：純 40 點                                                                                                                  |
| `{G_ENT_CASH}`      | 企業商品：純 80 元                                                                                                                  |
| `{G_MCH}`           | 商家商品：`RATE_CONVERTED`，單價 40 元；點數由後端依 **1:1** 換算（1 元 = 1 點，見 `PointSystem.getRate`） |

**狀態碼對照**（斷言請用數字 code，勿與名稱混淆）

| 軸              | 名稱            | code   | 常見時機                                     |
| --------------- | --------------- | ------ | -------------------------------------------- |
| `orderStatus`   | CREATED         | **0**  | 建單成功                                     |
| `orderStatus`   | PAYMENT_PENDING | **20** | pay 後有 DDPay 待付                          |
| `orderStatus`   | DONE            | **30** | 純點 pay 完成，或 webhook 成功               |
| `pointsStatus`  | NONE            | **0**  | 無點數／純現金                               |
| `pointsStatus`  | RESERVED        | **10** | pay 後 Redis 預扣中                          |
| `pointsStatus`  | COMMITTED       | **30** | 扣點完成（`totalMoney=0` 的 pay 或 webhook） |
| `paymentStatus` | NONE            | **0**  | 未走金流                                     |
| `paymentStatus` | PENDING         | **10** | DDPay 待付                                   |

> 注意：`orderStatus=10` 為 `POINTS_UNKNOWN`（異常），**不是** CREATED。

原始服務文件參考：`新Order_建立到支付_流程與檔案說明.md` §名詞與狀態碼。

---

## API 參數說明

### `POST /transaction/v1/user/order/create`

建立訂單（可無商品）。金額由後端依租戶 `businessType`、商品定價、`pointsPlan` 計算後落庫。

| 欄位               | 必填       | 說明                                                                                                                |
| ------------------ | ---------- | ------------------------------------------------------------------------------------------------------------------- |
| `tenantId`         | 是         | 訂單所屬租戶 ID                                                                                                     |
| `title`            | 否         | 訂單標題                                                                                                            |
| `description`      | 否         | 訂單描述                                                                                                            |
| `moneyAmount`      | 非商品必填 | **最後應付現金**（折抵後剩餘，= 訂單 `totalMoney`）。須與 `grossMoneyAmount` 成對；商品單可省略由 server 計算 |
| `grossMoneyAmount` | 非商品必填 | **折抵前現金總額**（企業＝現金軸 cashLeg；商家＝合併池 gross）。商家須 **> 0**；企業純點或僅券可省略 |
| `pointsPlan`       | 視情境     | 點數支付規劃，見下表                                                                                                |
| `items`            | 商品單必填 | 商品明細，見下表                                                                                                    |
| `carrierId`        | 否         | 手機條碼載具，格式 `/` + 7 碼                                                                                       |
| `carrierType`      | 否         | 保留欄位，server 固定 `3J0002`                                                                                      |
| `npoban`           | 否         | 捐贈碼（3–7 位愛心碼或 8 位社福統編）                                                                               |
| `buyerIdentifier`  | 否         | 買方統編（8 位數字）                                                                                                |
| `buyerName`        | 否         | 買方名稱                                                                                                            |

**`pointsPlan` 子欄位**

| 欄位             | 說明                                                                                                                           |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `tenantPoints`   | 企業點（AiPool）。企業商品 `PAYMENT_OPTIONS` 時須等於 Σ(單件點數 × qty)；商家合併池折抵 gross，可 ≤ gross（1:1 時折抵上限 = 商品現金總額） |
| `districtPoints` | 商圈點。企業商品訂單不可 > 0（`90015`）。商家 `RATE_CONVERTED` 合併池折抵 gross，本手冊 D 章以 `tenantPoints`／UUPON 為主 |
| `uuponPoints`    | UUPON（AiCoin），折抵現金軸，1 TWD = 4 UUPON；不可與 `tenantPoints` 加總混算                                                   |
| `couponIds`      | 優惠券 ID 陣列                                                                                                                 |
| `earnLimitInfos` | 賺屬性條件（有屬性賺時必填）                                                                                                   |

**`items[]` 子欄位（商品訂單）**

| 欄位              | 必填   | 說明                         |
| ----------------- | ------ | ---------------------------- |
| `goodsId`         | 是     | 商品 ID                      |
| `qty`             | 否     | 數量，預設 1，最小 1         |
| `paymentOptionId` | 視商品 | 多方案時必填；單一方案可省略 |

**計價規則摘要**

| 租戶類型                     | 點數軸（tenantPoints leg）        | 現金軸（cashLeg／gross） | `totalMoney`（DDPay 應付）                                |
| ---------------------------- | --------------------------------- | ------------------------ | --------------------------------------------------------- |
| 企業 `businessType=0` 非商品 | 與現金軸**獨立**（不折抵 gross）  | `grossMoneyAmount`       | `max(0, cashLeg − uupon/4)`，**須** = `moneyAmount`       |
| 企業 `businessType=0` 商品   | 商品點數 leg 加總（雙軸獨立）     | 商品現金 leg 加總        | `max(0, cashLeg − uupon/4)`                               |
| 商家 `businessType=1`        | 合併池：`tenantPoints` 折抵 gross | `grossMoneyAmount`（>0） | `max(0, gross − tenantPoints − uupon/4)`，**須** = `moneyAmount` |

**非商品金額雙欄位**（企業 A 章、商家 B 章；`tenantPoints` 企業不折現金軸）

| 欄位               | 語意                  | 關係式（須前端帶入並自洽）                                      | 企業範例（A2）         | 商家範例（B1）                     |
| ------------------ | --------------------- | --------------------------------------------------------------- | ---------------------- | ---------------------------------- |
| `grossMoneyAmount` | 折抵前現金總額        | 商家非商品須 **> 0**；企業含現金軸時須 **> 0**                  | `100`（現金軸）        | `100`（gross）                     |
| `moneyAmount`      | 最後應付現金（DDPay） | 企業：`gross − uupon/4`；商家：`gross − tenantPoints − uupon/4` | `50`（100 − UUPON 50） | `60`（100 − 30 tenant − 10 UUPON） |

> 非商品含現金軸時，`grossMoneyAmount` 與 `moneyAmount` **必須成對帶入**；後端驗證關係式與落庫值一致，供前端／APP 稽核客戶所見應付、實付。企業**純租戶點**（A3）或**僅券**（F2）可不帶雙欄位。

建單成功回傳 `data.orderId`、`data.orderNo`；訂單狀態 `orderStatus=0`（CREATED）、`pointsStatus=0`（NONE）、`paymentStatus=0`（NONE）。

---

### `POST /transaction/v1/user/order/{id}/pay`

對已建單訂單發起支付：預扣點數（若有）+ 建立 DDPay（若 `totalMoney>0`）。**金額以訂單落庫值為準**，body 不帶金額。

| 欄位                    | 必填 | 說明                                                                  |
| ----------------------- | ---- | --------------------------------------------------------------------- |
| `tenantId`              | 是   | 須與訂單 `tenantId` 一致，供 `loadMyOrder` 查單；未帶或錯誤則 `90200` |
| `reserveTimeoutSeconds` | 否   | 點數預扣 TTL，60–86400 秒；不帶用系統預設                             |
| `resultURL`             | 否   | DDPay 付款成功導向（Web 金流）                                        |
| `cancelURL`             | 否   | DDPay 付款取消導向                                                    |

**pay 行為摘要**

| 條件                                 | 行為                                                                                                                                         |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `totalMoney=0` 且有點數／UUPON／券   | 預扣 → commit → `orderStatus=30`、`pointsStatus=30`（COMMITTED）                                                                             |
| `totalMoney>0`                       | 預扣點數／UUPON（若有）→ 檢查訂單租戶 DDPay 設定 → 建 DDPay → `orderStatus=20`、`pointsStatus=10`（RESERVED）、`paymentStatus=10`（PENDING） |
| `totalMoney>0` 且無點數              | 不預扣 → 建 DDPay → `orderStatus=20`、`pointsStatus=0`（NONE）、`paymentStatus=10`                                                           |
| 商品單含 `tenantPoints`              | 預扣租戶為**商品所屬租戶**（`OrderPointTenantResolver.resolve`），非 necessarily 訂單 `tenantId`                                             |
| 已完成（DONE）                       | 冪等回傳                                                                                                                                     |
| 付款中（PENDING）且已有 `paymentUrl` | 冪等回傳原連結                                                                                                                               |

成功回傳 `data.paymentUrl`（純點為 null）、`data.orderStatus`、`data.pointsStatus`、`data.paymentStatus`。

---

## 企業 — 非商品

### A1 雙軸（tenantPoints + 現金）

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "A1",
    "grossMoneyAmount": 80,
    "moneyAmount": 80,
    "pointsPlan": {
        "tenantPoints": 40,
        "districtPoints": 0,
        "uuponPoints": 0,
        "couponIds": []
    }
}
```

> 企業雙軸：`tenantPoints` 不折現金，故 `grossMoneyAmount` 與 `moneyAmount` 相同（皆 80）。

`/item` 驗證：`totalMoney=80`，`pointsPlan.tenantPoints=40`，`orderStatus=0`（CREATED）

`POST /transaction/v1/user/order/{OID}/pay`

```json
{
    "tenantId": "{ENT_TID}",
    "reserveTimeoutSeconds": 600,
    "resultURL": "https://example.com/ok",
    "cancelURL": "https://example.com/cancel"
}
```

`/pay` 驗證：`pointsStatus=10`（RESERVED）；`paymentUrl` 非空；`orderStatus=20`（PAYMENT_PENDING）；`paymentStatus=10`（PENDING）；DDPay 金額 = 80

---

### A2 現金 + UUPON 折抵

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "A2",
    "grossMoneyAmount": 100,
    "moneyAmount": 50,
    "pointsPlan": {
        "tenantPoints": 0,
        "districtPoints": 0,
        "uuponPoints": 200,
        "couponIds": []
    }
}
```

`/item` 驗證：`totalMoney=50`，`pointsPlan.uuponPoints=200`

`POST /transaction/v1/user/order/{OID}/pay`（body 同 A1，`tenantId={ENT_TID}`）

`/pay` 驗證：`paymentUrl` 非空，`orderStatus=20`；預扣 `uuponPoints=200`（`tenantPoints=0`）；`pointsStatus=10`（RESERVED）；`paymentStatus=10`（PENDING）

---

### A3 純租戶點

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "A3",
    "moneyAmount": 0,
    "pointsPlan": {
        "tenantPoints": 40,
        "districtPoints": 0,
        "uuponPoints": 0,
        "couponIds": []
    }
}
```

`/item` 驗證：`totalMoney=0`，`pointsPlan.tenantPoints=40`

`POST /transaction/v1/user/order/{OID}/pay`

```json
{ "tenantId": "{ENT_TID}", "reserveTimeoutSeconds": 600 }
```

`/pay` 驗證：`paymentUrl=null`，`orderStatus=30`（DONE），`pointsStatus=30`（COMMITTED），`paymentStatus=0`（NONE）

---

### A4 純現金

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "A4",
    "grossMoneyAmount": 100,
    "moneyAmount": 100,
    "pointsPlan": {
        "tenantPoints": 0,
        "districtPoints": 0,
        "uuponPoints": 0,
        "couponIds": []
    }
}
```

`/item` 驗證：`totalMoney=100`

`POST /transaction/v1/user/order/{OID}/pay`（body 同 A1）

`/pay` 驗證：`paymentUrl` 非空，`orderStatus=20`，`pointsStatus=0`（NONE），`paymentStatus=10`（PENDING）

---

### A5 失敗 — 金額與點數皆 0

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "A5",
    "moneyAmount": 0,
    "pointsPlan": {
        "tenantPoints": 0,
        "districtPoints": 0,
        "uuponPoints": 0,
        "couponIds": []
    }
}
```

回傳：`code=90041`（不進 pay）

---

### A6 失敗 — UUPON 超過現金軸

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "A6",
    "grossMoneyAmount": 50,
    "moneyAmount": 0,
    "pointsPlan": {
        "tenantPoints": 0,
        "districtPoints": 0,
        "uuponPoints": 204,
        "couponIds": []
    }
}
```

回傳：`code=90207`（`uuponPoints=204` 折抵 51 元 > cashLeg 50）。若 UUPON 非 4 的倍數則 `90208`。

---

### A7 失敗 — 雙欄位關係不符（gross=0、money>0）

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "A7",
    "grossMoneyAmount": 0,
    "moneyAmount": 10,
    "pointsPlan": {
        "tenantPoints": 0,
        "districtPoints": 0,
        "uuponPoints": 0,
        "couponIds": []
    }
}
```

回傳：`code=90203`（`moneyAmount` 須等於 `grossMoneyAmount` 折抵後金額；`gross=0` 時不可聲稱應付 10 元）

變體：僅帶 `moneyAmount`、省略 `grossMoneyAmount`（非純點）→ 同樣 `90203`。

---

## 商家 — 非商品

### B1 合併池（tenantPoints + UUPON + 現金）

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{MCH_TID}",
    "title": "B1",
    "grossMoneyAmount": 100,
    "moneyAmount": 60,
    "pointsPlan": {
        "tenantPoints": 30,
        "districtPoints": 0,
        "uuponPoints": 40,
        "couponIds": []
    }
}
```

`/item` 驗證：`totalMoney=60`（gross 100 − tenant 30 − UUPON 40/4=10）

`POST /transaction/v1/user/order/{OID}/pay`（`tenantId={MCH_TID}`，含 `resultURL` / `cancelURL`）

`/pay` 驗證：預扣 `tenantPoints=30`、`uuponPoints=40`；`pointsStatus=10`（RESERVED）；`paymentUrl` 非空；DDPay 金額 = 60；`orderStatus=20`

---

### B2 合併池 — 純點付清

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{MCH_TID}",
    "title": "B2",
    "grossMoneyAmount": 100,
    "moneyAmount": 0,
    "pointsPlan": {
        "tenantPoints": 100,
        "districtPoints": 0,
        "uuponPoints": 0,
        "couponIds": []
    }
}
```

`/item` 驗證：`totalMoney=0`

`POST /transaction/v1/user/order/{OID}/pay`（`tenantId={MCH_TID}`）

`/pay` 驗證：`paymentUrl=null`，`orderStatus=30`（DONE），`pointsStatus=30`（COMMITTED），`paymentStatus=0`（NONE）

---

### B3 失敗 — 折抵超過 gross

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{MCH_TID}",
    "title": "B3",
    "grossMoneyAmount": 50,
    "moneyAmount": 0,
    "pointsPlan": {
        "tenantPoints": 60,
        "districtPoints": 0,
        "uuponPoints": 0,
        "couponIds": []
    }
}
```

回傳：`code=90207`

---

## 企業 — 商品（PAYMENT_OPTIONS）

### C1 雙軸（租戶點 + 現金 + UUPON）

`{G_ENT_COMB}` 80 點 + 80 元/件，qty=2：

- 點數軸（tenantPoints leg）：80 × 2 = **160**
- 現金軸（cashLeg）：80 × 2 = **160**
- UUPON 80 → 折 20 元 → `totalMoney` = **140**

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "C1",
    "moneyAmount": 140,
    "items": [
        { "goodsId": "{G_ENT_COMB}", "qty": 2, "paymentOptionId": "DEFAULT" }
    ],
    "pointsPlan": {
        "tenantPoints": 160,
        "districtPoints": 0,
        "uuponPoints": 80,
        "couponIds": []
    }
}
```

`/item` 驗證：`totalMoney=140`，`pointsPlan.tenantPoints=160`，`items[0].point=80`，`items[0].points=160`，`items[0].moneyAmount=160`

`POST /transaction/v1/user/order/{OID}/pay`（body 同 A1，`tenantId={ENT_TID}`）

`/pay` 驗證：

- 預扣 `tenantPoints=160`（扣點租戶 = 商品所屬租戶，見 E3）
- 預扣 `uuponPoints=80`
- `paymentUrl` 非空，DDPay 金額 = 140
- `orderStatus=20`（PAYMENT_PENDING），`pointsStatus=10`（RESERVED），`paymentStatus=10`（PENDING）

---

### C2 純租戶點商品

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "C2",
    "moneyAmount": 0,
    "items": [{ "goodsId": "{G_ENT_PT}", "qty": 1 }],
    "pointsPlan": {
        "tenantPoints": 40,
        "districtPoints": 0,
        "uuponPoints": 0,
        "couponIds": []
    }
}
```

`/item` 驗證：`totalMoney=0`，`pointsPlan.tenantPoints=40`

`POST /transaction/v1/user/order/{OID}/pay`（`tenantId={ENT_TID}`）

`/pay` 驗證：`paymentUrl=null`，`orderStatus=30`（DONE），`pointsStatus=30`（COMMITTED），預扣並 commit 40 點

---

### C3 純現金商品 + UUPON

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "C3",
    "moneyAmount": 60,
    "items": [{ "goodsId": "{G_ENT_CASH}", "qty": 1 }],
    "pointsPlan": {
        "tenantPoints": 0,
        "districtPoints": 0,
        "uuponPoints": 80,
        "couponIds": []
    }
}
```

`/item` 驗證：`totalMoney=60`，`pointsPlan.uuponPoints=80`

`POST /transaction/v1/user/order/{OID}/pay`（body 同 A1）

`/pay` 驗證：預扣 UUPON 80；`pointsStatus=10`（RESERVED）；`paymentUrl` 非空，金額 = 60；`orderStatus=20`

---

### C4 失敗 — tenantPoints 與商品不符

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "C4",
    "items": [{ "goodsId": "{G_ENT_PT}", "qty": 1 }],
    "pointsPlan": {
        "tenantPoints": 39,
        "districtPoints": 0,
        "uuponPoints": 0,
        "couponIds": []
    }
}
```

回傳：`code=90212`

---

### C5 失敗 — moneyAmount 與後端不符

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "C5",
    "moneyAmount": 139,
    "items": [
        { "goodsId": "{G_ENT_COMB}", "qty": 2, "paymentOptionId": "DEFAULT" }
    ],
    "pointsPlan": {
        "tenantPoints": 160,
        "districtPoints": 0,
        "uuponPoints": 80,
        "couponIds": []
    }
}
```

回傳：`code=90203`（點數已正確，僅測現金不符）

---

### C6 失敗 — 庫存不足

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "C6",
    "items": [{ "goodsId": "{G_ENT_CASH}", "qty": 99999 }],
    "pointsPlan": {
        "tenantPoints": 0,
        "districtPoints": 0,
        "uuponPoints": 0,
        "couponIds": []
    }
}
```

回傳：`code=90039`

---

### C7 企業 RATE_CONVERTED — 可少付 tenantPoints（選測）

前置：企業商品 `pricingType=RATE_CONVERTED`（非 `{G_ENT_COMB}` 固定方案）。

- gross = Σ(cashAmount × qty)
- 前端 `tenantPoints` 可 **≤** 後端換算上限（`ceil(cashAmount × 匯率) × qty`）
- `totalMoney = max(0, gross − tenantPoints)`；UUPON 仍只折現金軸

建單時 `tenantPoints` **超過**上限 → `90212`；**少於**上限且 `moneyAmount` 與 `totalMoney` 一致 → 成功。

---

### C8 失敗 — 企業商品不可帶商圈點

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "C8",
    "items": [{ "goodsId": "{G_ENT_PT}", "qty": 1 }],
    "pointsPlan": {
        "tenantPoints": 40,
        "districtPoints": 1,
        "uuponPoints": 0,
        "couponIds": []
    }
}
```

回傳：`code=90015`（此訂單不可使用商圈點）

---

## 商家 — 商品（RATE_CONVERTED）

商家商品定價類型為 `RATE_CONVERTED`：gross 由 `paymentOptions[].cashAmount × qty` 加總；可折抵點數由後端 **固定 1:1** 換算（`items[].point = cashAmount`，不依租戶 `transferToMoney`）。商品單只需帶 `moneyAmount`（= 折抵後 `totalMoney`），不需 `grossMoneyAmount`。

### D1 合併池付清（僅點）

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{MCH_TID}",
    "title": "D1",
    "moneyAmount": 0,
    "items": [{ "goodsId": "{G_MCH}", "qty": 2, "paymentOptionId": "DEFAULT" }],
    "pointsPlan": {
        "tenantPoints": 80,
        "districtPoints": 0,
        "uuponPoints": 0,
        "couponIds": []
    }
}
```

`/item` 驗證：`totalMoney=0`（gross 80 = 40×2；`tenantPoints=80` 合併池 1:1 折抵完畢）；`items[0].point=40`，`items[0].points=80`

`POST /transaction/v1/user/order/{OID}/pay`（`tenantId={MCH_TID}`）

`/pay` 驗證：`orderStatus=30`（DONE），`pointsStatus=30`（COMMITTED），預扣 80 點後 commit；`paymentUrl=null`

---

### D2 合併池 + 剩餘現金

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{MCH_TID}",
    "title": "D2",
    "moneyAmount": 5,
    "items": [{ "goodsId": "{G_MCH}", "qty": 2, "paymentOptionId": "DEFAULT" }],
    "pointsPlan": {
        "tenantPoints": 0,
        "districtPoints": 0,
        "uuponPoints": 300,
        "couponIds": []
    }
}
```

`/item` 驗證：`totalMoney=5`（gross 80 − UUPON 300/4=75）；`moneyAmount` 須與折抵後剩餘一致

`POST /transaction/v1/user/order/{OID}/pay`（`tenantId={MCH_TID}`，含 URL）

`/pay` 驗證：預扣 UUPON 300；`pointsStatus=10`（RESERVED）；`paymentUrl` 非空，金額 = 5；`orderStatus=20`

---

## 建單後支付 — 專項與失敗

以下可獨立執行，或作為各情境 pay 步驟的補充說明。`{OID}` = 對應建單回傳之 `orderId`。

### E1 冪等 — 已完成訂單重複 pay

前置：A3 或 C2 已 pay 至 `orderStatus=30`

`POST /transaction/v1/user/order/{OID}/pay`（`tenantId` 正確）

回傳：`code=1000`，`orderStatus=30`，不重複扣點

---

### E2 冪等 — 付款中重複 pay

前置：A4 已 pay 一次，`orderStatus=20`，已有 `paymentUrl`

再次 `POST /transaction/v1/user/order/{OID}/pay`

回傳：相同 `paymentUrl`，不重複建 DDPay

---

### E3 商品單扣點租戶

前置：C1 建單；商品 `tenantId` ≠ 訂單 `{ENT_TID}` 時

`POST /transaction/v1/user/order/{OID}/pay` 後查 Mongo `pointReservation` 或點數異動：

- 預扣 `tenantId` = **商品所屬租戶**（`OrderPointTenantResolver.resolve(order)`，有 `tenantPoints` 時取 `goods.tenantId`）
- DDPay 設定檢查仍用**訂單** `tenantId`（`order.getTenantId()`）

---

### E4 失敗 — pay 租戶錯誤

前置：任意建單成功

`POST /transaction/v1/user/order/{OID}/pay`

```json
{ "tenantId": "WRONG_TENANT_ID" }
```

回傳：`code=90200`（查無訂單）

---

### E5 失敗 — DDPay 設定缺失

前置：A4 建單；訂單租戶未設定 `DDPAY_shopId`

`POST /transaction/v1/user/order/{OID}/pay`（`tenantId` 正確）

回傳：`code=90204`

> 自治商圈：HQ session 代 MEMBER 付時，檢查的是**訂單租戶**設定，非 HQ 自身設定

---

### E6 失敗 — 訂單狀態不可 pay

前置：訂單已 `FAILED` 或 `TIMEOUT`

`POST /transaction/v1/user/order/{OID}/pay`

回傳：`code=90202`

---

## 發票 / 其他校驗

### F1 載具格式錯誤

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "F1",
    "grossMoneyAmount": 100,
    "moneyAmount": 100,
    "carrierId": "ABC123"
}
```

回傳：`code=90044`

---

### F2 僅 coupon、無金額無點

`POST /transaction/v1/user/order/create`

```json
{
    "tenantId": "{ENT_TID}",
    "title": "F2",
    "moneyAmount": 0,
    "pointsPlan": { "couponIds": ["{COUPON_ID}"] }
}
```

回傳：`code=1000`（有 coupon 允許建單）

---

## 訂單明細查詢（共用驗證）

`POST /transaction/v1/user/order/item`

```json
{
    "tenantId": "{ENT_TID}",
    "orderId": "{OID}"
}
```

成功回傳欄位對照：

| 欄位                  | 說明                                                                             |
| --------------------- | -------------------------------------------------------------------------------- |
| `totalMoney`          | DDPay 應付現金                                                                   |
| `pointsPlan`          | 與建單請求一致（null 補 0 後）                                                   |
| `orderStatus`         | 建單後 `0`（CREATED）；有 DDPay 時 pay 後 `20`；純點 pay 完成 `30`               |
| `pointsStatus`        | 建單後 `0`（NONE）；pay 預扣後 `10`（RESERVED）；純點 pay 完成 `30`（COMMITTED） |
| `paymentStatus`       | 建單後 `0`（NONE）；DDPay 待付 `10`（PENDING）                                   |
| `orderNo`             | 顯示用編號                                                                       |
| `items[].point`       | 單件點數（商品單；商家 `RATE_CONVERTED` 快照 = `cashAmount`，1:1）             |
| `items[].points`      | 合計點數（單件 × qty）                                                           |
| `items[].moneyAmount` | 合計現金 leg（單件現金 × qty）                                                   |
