訂單建單支付統一 — API 整合手冊
Gateway 前綴:/transaction
登入:使用者 session(StpUtil userInfo)
成功:code=1000,data 見各情境
標準驗證流程(成功情境)
POST /transaction/v1/user/order/create→ 記下data.orderId為{OID}POST /transaction/v1/user/order/item→ 確認金額、點數與建單請求一致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
{
"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
{
"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
{
"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
{
"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
{ "tenantId": "{ENT_TID}", "reserveTimeoutSeconds": 600 }
/pay 驗證:paymentUrl=null,orderStatus=30(DONE),pointsStatus=30(COMMITTED),paymentStatus=0(NONE)
A4 純現金
POST /transaction/v1/user/order/create
{
"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
{
"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
{
"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
{
"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
{
"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
{
"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
{
"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
{
"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 金額 = 140orderStatus=20(PAYMENT_PENDING),pointsStatus=10(RESERVED),paymentStatus=10(PENDING)
C2 純租戶點商品
POST /transaction/v1/user/order/create
{
"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
{
"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
{
"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
{
"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
{
"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 一致 → 成功。