支付系統有一條不成文的鐵律:技術再先進,最終商家在意的只有錢有沒有進來、以及進來的金額對不對;對帳系統(Reconciliation System)就是回答這兩個問題的機制,它的工作不是處理付款,而是在每筆付款完成後,持續驗證「系統認為發生的事」和「銀行認為發生的事」是否一致;任何一筆不一致,都是潛在的帳務問題,可能是系統 bug、可能是閘道錯誤、也可能是真實的資金損失。

本文是對帳系統設計的上篇,從三本帳的概念模型出發,說明以結算日為基準的關鍵決策、日終對帳的五個步驟、軋差的計算邏輯,以及差異單的分類與處理方式,下篇則進入資料庫 Schema 設計與 .NET 實作骨架。


三本帳:對帳的概念模型

對帳的本質是核對三個來源的數字,確保它們指向同一個現實。

系統帳(Internal Ledger)

系統帳是自己的資料庫,以訂單視角記錄每一筆交易,核心資料來自 orders 表和 refunds 表,記錄的是「系統認為發生了什麼」:哪些訂單已結算、結算金額是多少、退款了多少。

閘道報表(Gateway Settlement Report)

閘道報表由支付閘道或電子支付平台提供,通常是 CSV 格式或 API 回應,以交易視角記錄「閘道認為發生了什麼」:哪些交易已完成、手續費扣了多少、淨額是多少;不同閘道的報表格式差異很大,Stripe 有 API 可以即時拉取,有些則是 SFTP 上的 CSV 檔案,需要個別處理。

銀行帳(Bank Statement)

銀行帳是公司銀行帳戶的對帳單,以資金視角記錄「銀行認為發生了什麼」:哪一天入帳了多少、哪一天被扣了多少,銀行帳通常透過銀行 API 或每日 SFTP 傳送取得。

為什麼三本帳的數字不一樣?

一筆 NT$1,000 的刷卡,三本帳的數字各自不同:系統帳記錄 NT$1,000(訂單金額)、閘道報表顯示 NT$985(扣除 MDR 1.5% 後的淨額)、銀行帳入帳 NT$985(和閘道報表一致,但時間晚 T+1);這三個數字都是正確的,只是視角不同,對帳系統的工作是理解這些差異是否在預期內,像是 MDR 費率是否符合合約、時間差是否合理,還是有一筆錢真的消失了。


以結算日為基準的關鍵決策

對帳系統最重要的設計決策之一,是選擇以 結算日(Settlement Date) 為基準,而不是訂單建立日,原因在於閘道報表的組織方式:閘道不是按照「消費者刷卡的時間」整理報表,而是按照「資金批次結算的日期」去整理;一筆週五下午的訂單,可能要到週一才進入閘道的結算批次(跨週末無結算),如果對帳系統用訂單建立日去比對閘道報表,週五的訂單永遠在週五找不到配對,就會產生大量的假差異單。

這個決策影響兩個地方:

  • orders 表需要有 settled_date 欄位,在收到結算 Webhook 時寫入,而不是使用 created_at
  • gateway_transactionssettlement_date 來自閘道報表本身,不是匯入時間。

日終對帳的完整流程

對帳通常排程在市場收盤後、次日開盤前的深夜窗口執行,依序走完以下五個步驟。

步驟一:資料收集

從三個來源拉取當日資料:

  • 掃描系統帳中 settled_date 等於目標結算日的所有訂單和退款。
  • 呼叫閘道 API 下載結算報表,或解析閘道透過 SFTP 推送的 CSV 檔案,將資料匯入 gateway_transactions 表。
  • 透過銀行 API 或銀行對帳單取得當日的入帳和出帳記錄,匯入 bank_statements 表。

步驟二:資料標準化

三個來源的資料格式各不相同,需要轉換成統一的比對格式,其中的映射關係是交易 ID:系統用 order_id,閘道用 gateway_txn_id,銀行用 reference_no,三者需要能夠串聯。

gateway_transactions 表設計時,order_id 這個外鍵就是串聯系統帳和閘道報表的橋樑;閘道報表裡通常會帶上商家傳入的 merchant_reference,也就是系統的 order_id,匯入時就能直接建立關聯。

步驟三:交易比對(Matching)

order_id 為 key,將系統帳的每一筆訂單和閘道報表的對應交易配對,配對成功且金額一致的標記為已核銷,無法配對或金額不符的則建立差異單;比對邏輯允許少量的金額容差(通常是 NT$0.01),以處理浮點數計算造成的尾數差異,超過容差的差異才產生 critical 差異單。

步驟四:軋差驗算

對所有已配對的交易加總,計算當日應結算的淨額,與銀行實際入帳金額比對,計算公式如下:

預期撥款淨額 = Σ(已結算訂單金額) − Σ(已結算退款金額) − Σ(閘道手續費)

若預期淨額和銀行入帳金額的差距超過閾值(建議和財務部門共同定義,通常是 NT$1 以上),產生 netting_mismatch 差異單。

步驟五:產生報表與通知

輸出完整的對帳結果,更新對帳執行記錄的狀態為 completed,記錄配對成功數和差異單數,並對 severity = 'critical' 的差異單發送即時通知(Email、Slack),讓財務人員在次日開始工作前就能看到需要處理的問題。


軋差(Netting)的設計細節

軋差是把當日所有應收應付合併計算,一次移動淨額,而不是為每一筆交易各自撥款。

閘道的軋差方式

閘道通常在日終將當日所有交易加總後,一次撥付淨額至商家銀行帳戶。以具體數字說明:

當日收款 10 筆,共 NT$50,000
當日退款  2 筆,共  NT$3,000
閘道手續費(MDR 1.5%):NT$705

實際撥款淨額 = NT$50,000 − NT$3,000 − NT$705 = NT$46,295

這就是為什麼商家的銀行帳不是一筆筆入帳,而是每天只有一到幾筆大額入帳。對帳系統需要能把這一筆大額入帳,對應回當日的幾十甚至幾百筆訂單。

跨日交易的處理

信用卡授權和結算之間有 T+1 到 T+2 的時間差,退款的結算也可能在幾天後才進閘道報表,對帳系統會以每筆交易實際進入閘道結算批次的 settlement_date 進行處理,不強行對齊到訂單建立日;這意味著同一張訂單的付款和退款可能落在不同的對帳批次,這是正常現象,不應該產生差異單,因此對帳系統要能區分「跨日的時間差」和「真實的金額不符」。

多幣別的處理

若系統支援多幣別,軋差計算必須以幣別為單位分開進行,不能混合加總;每個幣別的淨額獨立驗算,再對應到各自的銀行帳戶入帳。


差異單的分類與處理流程

並非所有差異都需要相同等級的關注,應該依嚴重程度分三級處理,才能讓財務資源集中在真正重要的問題上。

Critical:金額不符(Amount Mismatch)

系統帳和閘道報表都有這筆交易但金額不一致,可能原因包含 Capture 金額和授權金額不同但系統未正確更新、退款金額計算錯誤、閘道手續費費率異常(可能是費率合約有更新但系統未同步);這時應該立即通知財務人員,凍結該筆訂單的後續退款操作,人工核查後在差異單的 resolution_note 欄位記錄說明,並手動調帳。

Critical:Chargeback 扣回未入帳

銀行帳少了一筆錢(被 Chargeback 扣回),但系統帳沒有對應的 DISPUTED 訂單記錄,這代表 Chargeback Webhook 沒有被正確接收或處理;這時應該立即追查 Webhook 接收日誌,確認是否有漏收或處理失敗的事件,補建 DISPUTED 記錄,通知財務備妥舉證資料。

Warning:單邊出現(One-sided)

只在系統帳出現、閘道報表沒有,或只在閘道報表出現、系統帳沒有,常見原因是 Webhook 通知延遲、退款尚在處理中、或閘道報表有時間差(T+1 才出現在報表中);這時應該設定等待窗口(建議 3 個工作天),在窗口內若對應記錄出現則自動核銷,超過窗口仍未配對才升級為人工處理,這樣的作法能避免大量因時間差造成的假警報。

Info:時間差(Timing Difference)

交易存在、金額正確,但結算日期跨日,例如週五的訂單在週一才進閘道報表(跨週末無結算批次);這時系統應該自動標記為「時間差,待下期核銷」,不需要人工介入,在下一個工作日的對帳執行時,這筆記錄會自動找到配對。


對帳系統的概念模型中,每一個細節都有它存在的理由:

  • 三本帳各司其職:系統帳是訂單視角,閘道報表是交易視角,銀行帳是資金視角,三者數字不同是正常現象,對帳的目的是理解差異是否在預期內。
  • 以結算日為基準:閘道報表以結算批次為單位,對帳系統的基準日期必須與此對齊才能準確配對,避免假差異單。
  • 日終對帳是五步驟的流水線:資料收集、標準化、比對、軋差驗算、通知,每一步都有明確的輸入和輸出,也都有可能出錯的地方需要設計保護機制。
  • 差異單要分級critical 立即通知、warning 等待窗口後升級、info 系統自動核銷,讓財務資源集中在真正需要人工處理的問題上。

理解這些概念之後,下一步是把它們落地:資料表怎麼設計、對帳作業的冪等性如何保障、比對邏輯的程式碼怎麼寫,這些內容將在下篇展開。


本文為個人學習筆記,持續更新中。