Stripe 請款的時候,其實一次要跟商家收兩筆錢:一筆是平台的系統使用費,另一筆是信用卡本身的手續費。這兩筆錢的費率怎麼查、什麼時候確定、又是怎麼一起送給 Stripe 申報的,就是這篇要拆解的內容

兩種費用的差別

費用類型說明
系統使用費SalesOrder平台向商家收取的服務費,費率與商品分類、供應商有關
金流手續費PayProfileStripe 信用卡交易手續費,費率與「發卡國家+卡片品牌」的組合有關

費用取得的時間點(在 Pipeline 中的位置)

這兩種費率不是一開始就知道,而是隨著下單 Pipeline 一步步往下跑,才逐漸被鎖定、查出來、最後算成金額:

Pipeline 第 19 道
ArrangePayTypeExpressInfoProcessor — 設定卡片品牌與發卡國代碼(這兩個值就是後面手續費計算的依據)
Pipeline 第 26 道
GetOrderProcessingFeeProcessor — 分別載入「系統使用費」與「金流手續費」兩種費率
Pipeline 第 30 道
ThirdPartyPayApiProcessor — 呼叫費用計算方法算出最終要向 Stripe 申報的手續費金額,隨請款請求一併送出

Brand / 發卡國代碼的資料來源(依情境不同)

手續費要算得準,關鍵就在「卡片品牌」與「發卡國家」這兩個值有沒有抓對。金流手續費率正是依「發卡國家+卡片品牌」的組合去查,常見的卡片品牌如下:

Visa Mastercard JCB American Express 未知(全新卡查詢前)

但這兩個值在不同付款情境下,來源完全不一樣:

使用情境資料來源
記住信用卡(舊卡復用)PayTypeExpress 資料表讀取後設定
定期購自動成單從既有的定期購結帳資訊解析後設定
Google Pay / Apple Pay即時向 Stripe API 查詢取得
新信用卡(首次付款)付款完成後才能取得,因此計算手續費時暫時視為未知

前面提到 Pipeline 第 19 道會設定卡片品牌與發卡國代碼,但這一步不是「每次結帳都會設定」,而是有明確的觸發條件。這個 Processor 真正的角色是:把「使用者選擇的已記住付款工具」還原成付款所需的機敏資料,只有在使用者明確選擇快速結帳/記住的卡片時才會動作。

💡 一句話定位 它是 identityPayTypeExpress 記錄 → 機敏付款資訊的還原器,跟「這次是不是輸入全新卡號」完全無關;有沒有觸發,取決於前端這次結帳有沒有帶 identity 上來。
組別一:CheckoutDotCom / Stripe / KPay快速結帳 / 記住信用卡專用
  1. 前提:context.ThirdPartyPaymentInfo.ExtendInfo 裡必須帶有 identity(代表前端已指定「用第幾張記住的卡」)
  2. identity 查出對應的 PayTypeExpress 記錄,解密還原出機敏付款資訊(如 Stripe 的 customer_id / payment_method)寫回 context
  3. 只有 Stripe 這個分支才會額外多做一件事:把 Brand / IssueCountryCode 一併設進 context.CreditCardInfo,專門為了讓後面第 26 道能查到正確的金流手續費率
組別二:CreditCardOnce / CreditCardInstallment本土一次性 / 分期信用卡(限 Nine1Payment 金流)
  1. 邏輯類似,同樣需要 identity 才會觸發
  2. PayTypeExpress 撈出 CardToken / BankCode
  3. 處理的是「發卡行資訊可能過期需要更新」的問題,跟手續費計算無關,服務對象也不是 Stripe

GetOrderProcessingFeeProcessor 就是前面提到的 Pipeline 第 26~27 道,它唯一的工作是:把「系統使用費」與「金流手續費」這兩種費率查出來,放進 context.ProcessingFeeInfo,讓後面第 30 道 ThirdPartyPayApiProcessor 能直接拿來算 application_fee_amount。它會不會執行、抓到的是不是正確的值,是這篇最值得深挖的地方。

💡 一句話定位 它是「兩種費率的查詢器」,只認 PayProfileType 決定要不要查、以及查哪一種公式,完全不判斷這次結帳是不是快速結帳、記不記得卡、是不是全新卡

第一道關卡:只有 HK 環境才會執行 HK Only

1
2
3
4
5
// GetOrderProcessingFeeProcessor.Process()
if (SettingHelper.DefaultCountry != "HK")
{
return;
}

其他地區的商店,這個 Processor 一進來就直接跳過,context.ProcessingFeeInfo 全程維持空值。

兩條各自獨立的查詢邏輯

GetSalesOrderProcessingFee查「系統使用費」→ context.ProcessingFeeInfo.SalesOrder
  1. CreditCardOnce_Stripe / GooglePay / ApplePay 才會查,其餘付款方式回傳 null
  2. 邏輯很單純:拿購物車第一個非贈品的商品分類 SourceCategoryId,查該分類 + 供應商對應的系統使用費率
  3. 這條路徑跟卡片品牌/發卡國完全無關,不會受全新卡或記住卡影響
GetPayProfileProcessingFee查「金流手續費」→ context.ProcessingFeeInfo.PayProfile
  1. CreditCardOnce_Stripe:直接拿當下context.CreditCardInfo.Brand / IssueCountryCode 去查——這兩個值是不是正確
  2. GooglePay / ApplePay:呼叫 GetBrandAndCountry(),即時打 Stripe API(RetrievePaymentMethod)用 payment_method 查出真實卡片品牌與發卡國
  3. 其餘付款方式回傳 null
GET /v1/payment_methods/{id}

三種 PayProfileType 分支對照

CreditCardOnce_Stripe 資料待確認 兩種費率都查。金流手續費用「context 當下的 Brand / Country」,不會主動去問 Stripe 要真實資料
GooglePay / ApplePay 資料最可靠 兩種費率都查。金流手續費即時呼叫 Stripe API 取得真實 Brand / Country,資料最可靠
其他付款方式 不計費 兩個方法都直接回傳 nullProcessingFeeInfo 維持空值,後面也不會計入 application_fee_amount

前兩個 tab 一直圍繞著一個假設:全新卡(沒有 identity)在後端 Pipeline 裡「查不到」正確的卡片品牌/發卡國。實際追下去才發現,這個假設只對了一半——後端的確沒有主動去查,但前端在送出訂單之前,就已經先把真實資料準備好了

送單前的隱藏一步:CardValidate

前端 SubmitOrder()
判斷條件:!PayProcessData.HasCreditCard(全新卡) PayProfileType === CreditCardOnce_Stripe,才會進入下一步;記住的卡(HasCreditCard = true)不會走這條路
前端 ValidateService.CardValidate()
把使用者剛輸入的卡號、到期日、CVC 包成 CardOptions,POST 到後端 /CreditCard/Validate
後端 CreditCardController.Validate → StripeService.CreditCardValidation
直接拿卡片資料呼叫真正的 Stripe 官方 API,換回這張卡真實的 brandcountry
POST /v1/payment_methods
前端拿到回應
驗證成功後寫回 PayProcessData.CreditCardInfo.Brand / IssueCountryCode,這兩個值現在是真實卡片資訊,不再是預設值
前端 AfterCheck() → Send()
把整包 PayProcessData(也就是後端的 PayProcessContextEntity)送給 CompleteForNewCartV2,Pipeline 開始跑第 1 道時,context.CreditCardInfo 就已經帶著真實資料進來了

拿到兩種費率之後,接下來就是把它們套進公式,算出一個要跟 Stripe 申報的 application_fee_amount。這筆金額由三個部分加總而成:

① 商品系統使用費 所有商品(實付金額扣除購物金攤提後)× 系統使用費率,加總
② 運費系統使用費 所有運費(實付金額扣除購物金攤提後)× 系統使用費率,加總
③ 金流手續費(僅 Custom 帳戶類型才會加上) 總支付金額 × 金流手續費率 + 金流固定手續費
最終金額 application_fee_amount ① + ② + ③,四捨五入到小數第 2 位;若計算結果大於 0 但小於 0.01,強制設為 0.01
💡 PMW 收到金額後的用途 PMW 會把算好的手續費金額(換算成最小貨幣單位、取整數)放進 Stripe 的請款請求裡的 application_fee_amount 欄位。Stripe 扣款成功後,這筆金額會直接從交易款項中撥給 91APP 的主帳號,作為平台向商家收取的服務費。

付款成功後,系統會把這次交易用到的費率資訊(付款流程類型、帳戶類型、卡片品牌、發卡國家、各項費率與固定費用)整批序列化,存進這筆訂單的第三方付款紀錄裡,供後續 ERP 轉單時讀取使用。實際負責這件事的是 StripePayChannelService.GetTradesOrderThirdPartyPaymentInfo(),它會把當次交易用到的費率組成一段 JSON,寫進 TradesOrderThirdPartyPayment.Info 欄位:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"PaymentFlow": "DirectCharge|DestinationCharge",
"AccountType": "Standard|Custom|Express",
"SubAccount": "acct_xxx",
"CardBrand": "Visa|MasterCard|...",
"CardIssueCountry": "HK|TW|...",
"FeeRate": 0.015,
"FixedFee": 0.00,
"SourceId": "123",
"SourceType": "...",
"SCMSalesFeeRate": 0.05,
"PaymentServiceProvider": "PaymentMiddleware"
}
欄位意義
PaymentFlow這筆交易走的是 DirectCharge 還是 DestinationCharge
AccountType子帳戶類型:Standard / Custom / Express,決定金流手續費要不要平台自己算
SubAccount實際收款的 Stripe 子帳戶 ID
CardBrand / CardIssueCountry這次刷卡用的卡片品牌與發卡國,也就是查金流手續費率的組合鍵
FeeRate / FixedFeePayProfile 查出來的金流手續費率與固定費
SourceId / SourceType這個費率的來源,SourceId 對應 PayProfileProcessingFee 表的主鍵
SCMSalesFeeRateSalesOrder 查出來的系統使用費率
PaymentServiceProvider固定為 PaymentMiddleware,標示這筆交易走哪一套金流串接

ERP 轉單時的欄位映射

ERP 轉單 Job 會讀出 TradesOrderThirdPartyPayment.Info 這段 JSON,把它還原成一般會計看得懂的訂單欄位:

Info 欄位ERP 目標欄位說明
FeeRateSalesOrderSlave_SCMCreditCardFeeRate /SCMCreditCardFeeRate2信用卡手續費率
SCMSalesFeeRateSalesOrderSlave_SCMSalesFeeRate系統使用費率
FixedFeeSalesOrderGroup_PaymentFixedFee付款固定費用
CardBrand / CardIssueCountrySalesOrder 相關欄位卡別與國家資訊

把前面幾個 tab 拆解的內容串成一條完整的線,從 Pipeline 第 19 道設定卡片資訊開始,一路走到 ERP 生出費用單為止:

費用資料流向全圖
[Step #19] ArrangePayTypeExpressInfoProcessor
    設定 context.CreditCardInfo.Brand / IssueCountryCode
         │
         ▼
[Step #26] GetOrderProcessingFeeProcessor(HK Only)
    ┌──────────────────────────┐   ┌───────────────────────────────┐
    │ ProcessingFeeService     │   │ ProcessingFeeService           │
    │ .GetSalesFeeInfo()       │   │ .GetCreditCardPayProfileFeeInfo│
    │                          │   │                                │
    │ DB: SupplierContract     │   │ DB: csp_GetPayProfileProcessing│
    │     SalesFeeRate 表      │   │     Fee SP(WebStoreDB)       │
    └──────────────────────────┘   └───────────────────────────────┘
         │                                      │
         ↓ context.ProcessingFeeInfo.SalesOrder  ↓ context.ProcessingFeeInfo.PayProfile
         │                                      │
         └─────────────────┬────────────────────┘
                           ▼
[Step #30] ThirdPartyPayApiProcessor
    → PaymentMiddlewareService.PaymentRequest()
        → StripePayChannelService.GetPayExtendInfo()
            → GetStripeApplicationFee()
                ① 商品費 = Σ(商品金額 - 購物金) × SalesOrder.Rate
                ② 運費費 = Σ(運費 - 購物金) × SalesOrder.Rate
                ③ Custom Only: + TotalPayment × PayProfile.Rate + PayProfile.FixedFee
                ─────────────────────────────────────────────
                → ExtendInfo["application_fee_amount"] = ①+②+③
        → HTTP POST PMW /api/v1/Pay/CreditCardOnce_Stripe/{tgCode}
            (帶 application_fee_amount)
                 │
                 ▼
            Stripe API POST /v1/payment_intents
                 (application_fee_amount * 100 → 整數,送給 Stripe)
                 │
                 ▼
            Stripe 扣款成功
                 │
                 ▼
[付款成功後] StripePayChannelService.GetTradesOrderThirdPartyPaymentInfo()
    → 序列化 FeeRate/SCMSalesFeeRate/FixedFee/CardBrand/CardIssueCountry
    → 存入 TradesOrderThirdPartyPayment.Info(JSON)
                 │
                 ▼
[TransferOrderProcessor → AfterOrderProcessor → ERP Job]
    csp_TradesOrderTransToSalesOrderWithFlow_Mall
    → 讀取 ThirdPartyPayment_Info
    → 更新 SalesOrderSlave_SCMCreditCardFeeRate / SCMSalesFeeRate / PaymentFixedFee
    → 生成 ExpenseOrder(費用單)