線稿索引所有 spec最後更新 2026-07-03
specs/payment-accounts-reconciliation.md

多收款帳戶分流與對帳

一句話:一筆收款實際進了哪個帳戶(台銀 / 永豐 / 中信 / 綠界 / 現金 / 豐收款…),且登山團與裝備住宿用不同帳戶組——把「收款帳戶」維度加進 order-money-model 的現金分類帳,並提供逐帳戶對帳視圖。

模組歸屬:屬進階會計模組(受 module_accounting gate);不開模組的租戶用核心掛帳台帳 + Excel 自行對帳。

1. 目標 / 問題

荒野現行內外帳表把收款拆到多個帳戶(RU台銀 / 永豐 / 中信 / 綠界(旅) / 現金 / 豐收款 / 登山社),且登山團裝備 / 住宿各用一套收款帳戶。但 order-money-modelpayment_transactions 目前沒有「這筆錢進哪個帳戶」的維度(其「不做」段明列「無 bank reconciliation」)。本檔刻意擴張該邊界:補上收款帳戶分流與逐帳戶對帳。

範圍:收款帳戶目錄(含幣別維度)+ 交易掛帳戶 + 逐帳戶對帳視圖 + 銀行對帳單匯入與半自動比對(§3.3,自訂範本)+ 換匯紀錄與各幣別餘額呈現(換匯 / 相對匯率機制本體在 currency-exchange,本檔負責其在對帳頁的目錄與呈現落點)。 不做:自動串銀行 API 拉帳(先人工 / 匯入對帳);正式總帳(未來會計模組);三大帳戶補款結構 / 通路→帳戶 routing / 退款執行對帳(子系統 B,獨立檔 account-settlement——帳戶能力欄、費用類型路由、跨帳戶補款 / 代墊 / 回填分類帳 account_transfers、帳戶間掛帳 account_obligations、月結互抵派生、刷退 vs 永豐匯出退款執行對帳)。

2. 資料模型

2.1 收款帳戶目錄 receiving_accounts(設計表,per-tenant)

欄位語意說明
code帳戶代號(bank_taiwan / sinopac / ctbc / ecpay / cash / fengshou…)。
display_name顯示名(永豐銀行…)。
kindbank / cash / gateway(綠界 / 豐收款等金流商沉澱帳)。
currency帳戶幣別(ISO 4217,default TWD)。既有帳戶皆 TWD外幣帳戶IDR / USD…)= 多幣別的「外幣池」,跨梯次共用,見 currency-exchange §2.2
is_active停用不刪(歷史交易仍引用)。
currency帳戶幣別(ISO 4217,default TWD;外幣帳戶=外幣池,見 currency-exchange §2.2)。
can_remit / can_chargeback / is_deprecated帳戶能力(可匯款 / 可刷退 / 廢帳戶),子系統 B 用,見 account-settlement §2.1

帳戶 product-agnostic(解綁 product_scope,2026-06-25):帳戶本質只是「錢放哪」=幣別 + 性質,不綁產品線。GS 鐵證——熊野「收款帳戶 for登山 / for裝備住宿」兩塊是同一組帳戶(永豐 / 中信 / 綠界 / 現金 / 豐收款 / 登山社都在兩塊重複出現),不是兩個互斥帳戶池;那兩塊是按費用類型的繳費 / 對帳檢查表。「團費→永豐、車費住宿→中信」是費用類型路由(子系統 B 的歸集規則),不是帳戶屬性。故移除 product_scope 欄與其在入帳時的硬篩。已完成:schema 無此欄、orders-new / lodging-book 皆不再硬篩、帳戶目錄已移至設定頁;product_scope 僅存在歷史 migration snapshot。

2.2 payment_transactions 加帳戶維度

  • payment_transactions.receiving_account_id:這筆現金(in / out)落在哪個帳戶(FK receiving_accountsnullableonDelete restrict)。

    收款(direction='in')應掛帳戶;退款 / 出款(direction='out')標從哪個帳戶出。現有不變式(H4 等)不變,只多一個維度。 append-only 狹窄豁免payment_transactions 仍 append-only(金額 / 方向 / 型別 / intent / refund… 不可改),但歸戶是「事後補的非金流標註」,故 protect_payment_transactions trigger 改為只允許 UPDATE receiving_account_id 一欄(逐欄 IS DISTINCT FROM 比對 OLD/NEW,其餘任一欄變動即 RAISE EXCEPTION)。DELETE 仍全擋。set null FK 行為刻意不用(會觸發 trigger UPDATE 路徑且語意是「刪帳戶就抹歸戶」,違反停用不刪原則)—— 帳戶停用即可,被引用者硬刪由 restrict 擋下。

2.3 對帳單匯入批次 statement_imports(設計表,per-tenant)

欄位語意說明
receiving_account_id哪個帳戶的對帳單(FK receiving_accounts,restrict)。kind ∈ bank/cashcurrency='TWD':gateway 彙撥(綠界撥款明細 + 手續費 net 語意)另案;外幣帳戶金額表達不了整數 TWD,v1 不開(外幣池交易量少,走 currency-exchange WAC 機制人工核)。
period_start / period_end對帳單期間。冪等以「帳戶 + 期間」為單位:同帳戶期間重疊直接擋(銀行對帳單天然月結形狀;row-level fingerprint 刻意不做硬 unique——同日同額同摘要多筆是合法流水)。重傳先刪整批。
filename / imported_by / created_at匯入痕跡(audit 另記)。
  • 刪批:批內存在任何人工痕跡(manual / ignored / excluded 行)即不可刪;純 auto / unmatched 批可刪(auto 配對隨批釋放)。

2.4 對帳單行 statement_lines(設計表,per-tenant)

欄位語意說明
import_idFK statement_imports(cascade)。
line_no / txn_date / direction / amount_twd / memo匯入事實(direction in/outamount_twd 正整數 TWD、memo = 範本「摘要」+「備註」合併,非銀行對帳單摘要原文)。txn_date 一律須落在批次宣告期間內(commit fail-closed 擋期間外行)——匯入後不可變;可變的只有配對欄與狀態。
statusunmatched(未配對)→ auto(自動配對)/ manual(人工配對)/ ignored(略過,原因必填:手續費 / 利息等 ERP 不記的行);解除配對 → excluded不再被自動配,只可人工重配)。
matched_source_type / matched_source_id多型配對目標:payment_transaction / payable / account_transfer不用兩個 nullable FK——status-aware CHECK 較乾淨、未來加 source 免 migration)。
ignored_reasonignored 時必填。
  • status-aware CHECKstatus ∈ (auto, manual) ⟺ 配對兩欄皆非空;其餘狀態皆空。
  • 同一 ERP 交易只能有一個 active 配對;account_transfer 例外為每側(from/to)各一:transfer 是雙邊事實——補款 A→B 會同時出現在 A 的對帳單(支出行)與 B 的對帳單(存入行),兩側各自勾銷同一筆 transfer 是正確語意。故 partial unique 拆兩條:非 transfer 來源 (matched_source_type, matched_source_id) WHERE active AND type <> 'account_transfer';transfer 來源加 direction 維度 (matched_source_type, matched_source_id, direction) WHERE active AND type = 'account_transfer'(同側重配仍擋)。跨批重配(5/31 付款 6/2 入帳、五月批已對、六月批再對)由此硬性擋下。

3. 邏輯 / 存取層

3.1 收款落帳

收款建現金列時決定 receiving_account_id

  • gateway(綠界 / 豐收款):依 provider 對應固定 gateway 帳戶(webhook 收款經 getReceivingAccountByCode(db, 'ecpay') 解析,查無回 null → 掛未歸戶桶,不 throw)。
  • 手動入帳(ATM / 臨櫃 / 現金):由經手人選帳戶(receivingAccountId),下拉列出所有啟用的手動帳戶(gateway 自動掛帳除外),不依產品線篩選(帳戶 product-agnostic,§2.1)。
  • 落帳與既有現金列同一交易寫入,不另開流程。

已接(2026-06-23):所有收款 / 出款點都已傳 receivingAccountIdrecordCashTransaction / payment_transactions.receiving_account_id:gateway webhook(payment-notify.ts,走 getReceivingAccountByCode(db,'ecpay'),test provider notify 共用此路徑)、手動 K 單(orders/new)、lodging 直訂(lodging/book)、admin-repo 手動入帳(markOrderManuallyPaid,opts 帶 receivingAccountId)、客戶退款出帳(refunds/repo.ts markRefundPaid,選填出帳帳戶)。getReceivingAccountByCode 查無回 null(掛未歸戶桶不 throw,絕不卡住 webhook / 入帳)。

3.2 對帳視圖

packages/core/src/orders/reconciliation.tsdb: ScopedDb):逐帳戶、依期間(Asia/Taipei,比照既有 repo 時區慣例)合併兩來源 —— payment_transactions(收款 in / 退款·gateway out,依 occurred_at)+ payables paid 供應商出款net_amount_twd,依 paid_at,分源欄 payableOutTwd);netTwd = in − out − payableOut。給會計與銀行 / 金流商實際入帳核對。

  • 純讀派生,沿用 money helper 口徑,不另算金額。
  • payables 不在 order-scoped payment_transactions 內(order_id NOT NULL),故出款歸戶存 payables.receiving_account_id,對帳時依 receiving_account_id 與交易源 union(IS NOT DISTINCT FROM 讓未歸戶桶也對得上)。
  • 差異標記由人工確認(系統不假設銀行真值)。

3.3 對帳單匯入與比對引擎

packages/core/src/orders/statement-import.ts(設計檔名,db: ScopedDb):把銀行對帳單逐行對上 ERP 現金流水,取代 GS 人工檢查表。

  • 範本(自訂格式,銀行無關):固定欄位 .xlsx 範本下載(同欄位 .csv 也收)——日期 YYYY-MM-DD、存入金額 / 支出金額(二擇一,正整數 TWD)、摘要、備註。營運自行從網銀轉範本;系統不解析任何銀行原生格式(多租戶開放、免格式脆弱——比照 bulk-trip-import 固定範本先例)。
  • 兩階段匯入:parse → dry-run 預覽(期間重疊擋 / 逐行解析錯誤 / 自動配對預覽)→ 確認 commit(同 tx:插批 + 行 + 自動配對)。
  • ERP 側候選三源(帳戶內、未被 active 配對者):
    1. payment_transactions(收款 in / 退款 out
    2. payables paid(outnet_amount_twd
    3. account_transfers雙邊:行的帳戶 = from_account → 配支出行、= to_account → 配存入行;金額取 TWD 側)。排除 kind='refund_payout'——markRefundPaid 同 tx 雙寫(現金列 + 移轉列)是同一筆銀行匯出的兩個表徵,一律以現金列為準,否則雙配。排除被沖正者與沖正列(沖正是更正記錄,非銀行流水)。落地口徑:出帳側(from_account)配對金額 = amount_twd + fee_twd(含轉帳本身手續費的實際離帳額),入帳側(to_account)配對金額 = amount_twd(對方收到的淨額,不含這筆手續費)。
  • 自動配對(commit 時跑;可對批重跑,只碰 status='unmatched' 行):同帳戶 + 同金額 + 同方向 + 日期差 ±2 工作天(重用 workdays.ts + public_holidays)+ 雙方候選皆全域唯一(候選池是該帳戶全體未配對者,非本批)——一對多 / 多對一一律留人工。
  • 人工配對:仍要求同帳戶同金額(金額不符走 ignored + 註記;拆分 / 彙配 v1 不做),日期不限。
  • 解除配對excluded略過ignored(原因必填)。逐行動作(import / auto / manual / unmatch / ignore / 刪批)皆記 audit。

4. UI 與權限

  • 目錄維護移至設定(2026-06-25,覆寫 2026-06-23「目錄在對帳頁、非 settings」):收款帳戶目錄維護(新增 / 改名 / 改性質 / 設幣別 / 停用;code 被現金列引用即鎖不可改)是租戶配置,移到 設定 · 收款帳戶 tab(與 ECPay / 匯率等整合設定同層)。/admin/reconciliation 專注對帳(期間切換 + 各帳戶 in/out/淨額 + 未歸戶桶),右上「管理收款帳戶」link 到設定。
    • 租戶自管、不寫死(多租戶):帳戶目錄是 per-tenant 表,每租戶自由增減自己的銀行 / 金流商 / 現金 / 外幣池,數量與組合不限;bank_taiwan / sinopac / ctbc / ecpay… 只是荒野的範例資料,非系統內建枚舉。新增帳戶 = receiving_account.manage
  • 各幣別總覽 + 換匯(多幣別,currency-exchange §4.1):對帳頁擴充「各幣別總覽」(每外幣帳戶卡:外幣餘額 / 相對匯率 WAC / 台幣成本基礎)=即各幣別狀態 Dashboard,不另開頁;外加「換匯紀錄」列表 + 新增 / 沖正(fx_exchange.read/manage)。牌告匯率表是獨立頁 /admin/exchange-ratescurrency-exchange §4.1)。
  • 手動入帳 UI:收款表單多「收款帳戶」選擇,列出所有啟用手動帳戶(不依產品線)
  • 對帳單匯入區(§3.3):對帳頁帳戶卡展開「對帳單」區,不另開頁——匯入 dialog(期間 + 檔案 + 範本下載 → 預覽 → 確認)、配對進度、行列表(filter:未配對 / 已配對 / 已略過)、人工配對 / 略過 / 解除配對 dialog。gateway 帳戶不顯示此區;外幣帳戶註記暫不支援。
  • 權限receiving_account.read / receiving_account.manage —— admin / accountant 可維護目錄(設定頁)與對帳;opreceiving_account.read(可選帳戶入帳,不維護目錄);sales 無。對帳單匯入 / 配對走獨立 resource bank_statement(read / manage;admin / accountant = read+manage,op / sales 無)——對帳是會計作業,不掛帳戶目錄權限(語意錯位);所有寫 action 受 module_accounting gate fail-closed。

5. 開放議題

  • 帳戶決定規則:帳戶已解綁產品線(§2.1),手動入帳列全部啟用帳戶。是否允許一張訂單分次收款落不同帳戶(party 拆收)待定。「團費→永豐 / 車費住宿→中信」的自動歸集是子系統 B(費用類型路由 + 跨帳戶補款),另案。
  • 銀行對帳資料來源 已定(v9):自訂範本匯入 + 三源半自動比對(§2.3 / §2.4 / §3.3)。剩 gateway 彙撥對帳(綠界撥款明細,多筆訂單 net 一筆入帳 + 手續費語意)另案。
  • 出款 / 退款帳戶 已落地:客戶 refunds 退款出帳可標 receiving_account_idmarkRefundPaid 選填);payables 供應商出款 payables.receiving_account_idmarkPaymentRequestPaid 標記已付時選出款帳戶)→ 對帳出款側 payableOutTwd 分源呈現(2026-06-24)。
  • 與內外帳:帳戶分流是「現金真相」維度,與 internal-external-ledger 的內外帳是兩個正交軸(一個是錢進哪、一個是成本可不可申報),不混。

Changelog

版本日期變更Commit
22026-06-23First-pass 落地:receiving_accounts 目錄(additive schema,code uniq / kind / product_scope)+ payment_transactions.receiving_account_id(nullable,append-only trigger 狹窄豁免只准改此欄)+ packages/core/src/orders/reconciliation.ts(目錄 CRUD / setTransactionReceivingAccount 歸戶 / getReceivingAccountReconciliation 逐帳戶 in·out·net + 未歸戶桶 + Asia/Taipei 期間窗)+ /admin/reconciliation 頁 + receiving_account.read/manage 權限。recordCashTransaction 加選擇性 receivingAccountId。待接:收款流程入帳時實際帶帳戶。
32026-06-23回寫 code-is-truth:§4 目錄維護在 /admin/reconciliation(非 settings)、與對帳同頁;§3.1 收款帶帳戶已接 5 點(gateway webhook / 手動 K 單 / lodging 直訂 / admin-repo / test notify,gateway 走 getReceivingAccountByCode('ecpay') 查無回 null)。
42026-06-23收款帶帳戶實際 wiring 落地:新增 getReceivingAccountByCode(db,code)(查無回 null 不 throw);payment-notify gateway 入帳帶 'ecpay' 帳戶(test notify 共用);markOrderManuallyPaid opts 加 receivingAccountId(→ markPaidAction 第三參,orders/[id] 標記已付款 UI 加收款帳戶 Select,tour scope);orders/new K 單現金 + lodging/book 現金帶表單選的 receivingAccountId(依產品線 tour/lodging 篩,現場現金時才顯示 Select);markRefundPaid 加選填 receivingAccountId(出帳帳戶)。順手修真 bug:orders/new 手動 K 單估價 SQL 補 select promo_* 並把 promoActive/promoPriceTwd/promoAgencyPriceTwd 帶進 resolveDeparturePrice(直客/同業雙通路皆 promo-aware,估價 = 成交價)。
52026-06-24payables 供應商出款入對帳(§3.2 / §5):payablesreceiving_account_id(nullable FK,migration 0020)+ markPaymentRequestPaid 標記已付選出款帳戶 + 請款詳情 MarkPaidDialog 帳戶 picker;getReceivingAccountReconciliation 兩源 union(payment_transactions ∪ payables paid),新分源欄 payableOutTwdnet = in − out − payableOut;對帳頁加「供應商出款」欄。銀行 CSV 匯入仍 future。
62026-06-24帳戶加幣別維度 + 換匯落點(§1 / §2.1 / §4):receiving_accounts.currency(default TWD,外幣帳戶=多幣別外幣池跨梯次共用);對帳頁擴充「各幣別總覽卡」(外幣餘額 / WAC / 台幣基礎,= 各幣別 Dashboard 不另開頁)+「換匯紀錄」列表/新增/沖正。換匯 / 相對匯率 / 牌告匯率機制本體在新 currency-exchange,本檔負責對帳頁的目錄與呈現。§1「不做」明列三大帳戶補款 / 通路 routing / 退款執行對帳為另案。未實作。
72026-06-25帳戶解綁產品線 + 目錄移至設定(§2.1 / §3.1 / §4 / §5):移除 receiving_accounts.product_scope(GS 鐵證:for登山 / for裝備住宿 是同一組帳戶的費用類型檢查表,非互斥池;歸集屬子系統 B)——手動入帳改列全部啟用帳戶、不依產品線硬篩。目錄維護移至設定 · 收款帳戶 tab(覆寫 v3「目錄在對帳頁」),對帳頁右上 link 過去、專注對帳。wireframe 已對齊。
82026-07-01Code-is-truth 收斂:product_scope 移除已完成(schema 無此欄、orders-new / lodging-book 無硬篩、目錄已在設定頁),刪除 §2.1 與 v7 的「待移除」stale 尾註。
92026-07-03銀行對帳單匯入設計定案(§2.3 / §2.4 / §3.3 / §4 / §5,經 self + codex 雙輪對抗覆核):自訂固定範本(不解析銀行原生格式)+ statement_imports(帳戶+期間冪等,重疊擋)+ statement_lines(多型 matched_source_type/id + status-aware CHECK + active 配對 partial unique 防跨批重配)+ 比對引擎(三源account_transfers 雙邊、排除 refund_payout 雙寫與沖正;同額同向 ±2 工作天雙方全域唯一才自配;unmatch→excluded 不再自配、重跑只碰 unmatched)+ 新 resource bank_statement。v1 限 TWD 銀行/現金帳戶,gateway 彙撥另案。未實作(wireframe 同步)。
102026-07-03§2.3 / §2.4 / §3.3 全落地(migrations 0038/0039、e2e 46);§2.4 account_transfers 每側各一裁決;§3.3 出帳側含 fee 口徑補記。
112026-07-03終審 fix wave 收斂:§2.4 補「txn_date 必落在批次期間內」(期間界限屬去重防線,期間外行 commit 拒收)+ memo 語意 realign(範本摘要+備註合併,非銀行原文);§3.3 人工配對補「所有來源一律同方向」。
12026-06-20初版(草案):收款帳戶目錄 + payment_transactions.receiving_account_id 維度 + 逐帳戶對帳視圖;刻意擴張 order-money-model 的「無 reconciliation」邊界。未實作。