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 §名詞與狀態碼。
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。
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
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)
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)
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)
POST /transaction/v1/user/order/create
{
"tenantId": "{ENT_TID}",
"title": "A5",
"moneyAmount": 0,
"pointsPlan": {
"tenantPoints": 0,
"districtPoints": 0,
"uuponPoints": 0,
"couponIds": []
}
}回傳:code=90041(不進 pay)
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。
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。
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
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)
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
{G_ENT_COMB} 80 點 + 80 元/件,qty=2:
totalMoney = 140POST /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=80paymentUrl 非空,DDPay 金額 = 140orderStatus=20(PAYMENT_PENDING),pointsStatus=10(RESERVED),paymentStatus=10(PENDING)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 點
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
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
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(點數已正確,僅測現金不符)
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
前置:企業商品 pricingType=RATE_CONVERTED(非 {G_ENT_COMB} 固定方案)。
tenantPoints 可 ≤ 後端換算上限(ceil(cashAmount × 匯率) × qty)totalMoney = max(0, gross − tenantPoints);UUPON 仍只折現金軸建單時 tenantPoints 超過上限 → 90212;少於上限且 moneyAmount 與 totalMoney 一致 → 成功。
POST /transaction/v1/user/order/create
{
"tenantId": "{ENT_TID}",
"title": "C8",
"items": [{ "goodsId": "{G_ENT_PT}", "qty": 1 }],
"pointsPlan": {
"tenantPoints": 40,
"districtPoints": 1,
"uuponPoints": 0,
"couponIds": []
}
}回傳:code=90015(此訂單不可使用商圈點)
商家商品定價類型為 RATE_CONVERTED:gross 由 paymentOptions[].cashAmount × qty 加總;可折抵點數由後端 固定 1:1 換算(items[].point = cashAmount,不依租戶 transferToMoney)。商品單只需帶 moneyAmount(= 折抵後 totalMoney),不需 grossMoneyAmount。
POST /transaction/v1/user/order/create
{
"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
POST /transaction/v1/user/order/create
{
"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。
前置:A3 或 C2 已 pay 至 orderStatus=30
POST /transaction/v1/user/order/{OID}/pay(tenantId 正確)
回傳:code=1000,orderStatus=30,不重複扣點
前置:A4 已 pay 一次,orderStatus=20,已有 paymentUrl
再次 POST /transaction/v1/user/order/{OID}/pay
回傳:相同 paymentUrl,不重複建 DDPay
前置:C1 建單;商品 tenantId ≠ 訂單 {ENT_TID} 時
POST /transaction/v1/user/order/{OID}/pay 後查 Mongo pointReservation 或點數異動:
tenantId = 商品所屬租戶(OrderPointTenantResolver.resolve(order),有 tenantPoints 時取 goods.tenantId)tenantId(order.getTenantId())前置:任意建單成功
POST /transaction/v1/user/order/{OID}/pay
{ "tenantId": "WRONG_TENANT_ID" }回傳:code=90200(查無訂單)
前置:A4 建單;訂單租戶未設定 DDPAY_shopId
POST /transaction/v1/user/order/{OID}/pay(tenantId 正確)
回傳:code=90204
自治商圈:HQ session 代 MEMBER 付時,檢查的是訂單租戶設定,非 HQ 自身設定
前置:訂單已 FAILED 或 TIMEOUT
POST /transaction/v1/user/order/{OID}/pay
回傳:code=90202
POST /transaction/v1/user/order/create
{
"tenantId": "{ENT_TID}",
"title": "F1",
"grossMoneyAmount": 100,
"moneyAmount": 100,
"carrierId": "ABC123"
}回傳:code=90044
POST /transaction/v1/user/order/create
{
"tenantId": "{ENT_TID}",
"title": "F2",
"moneyAmount": 0,
"pointsPlan": { "couponIds": ["{COUPON_ID}"] }
}回傳:code=1000(有 coupon 允許建單)
POST /transaction/v1/user/order/item
{
"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) |