一句話:一筆收款實際進了哪個帳戶(台銀 / 永豐 / 中信 / 綠界 / 現金 / 豐收款…),且登山團與裝備住宿用不同帳戶組——把「收款帳戶」維度加進 order-money-model 的現金分類帳,並提供逐帳戶對帳視圖。
模組歸屬:屬進階會計模組(受
module_accountinggate);不開模組的租戶用核心掛帳台帳 + Excel 自行對帳。
荒野現行內外帳表把收款拆到多個帳戶(RU台銀 / 永豐 / 中信 / 綠界(旅) / 現金 / 豐收款 / 登山社),且登山團與裝備 / 住宿各用一套收款帳戶。但 order-money-model 的 payment_transactions 目前沒有「這筆錢進哪個帳戶」的維度(其「不做」段明列「無 bank reconciliation」)。本檔刻意擴張該邊界:補上收款帳戶分流與逐帳戶對帳。
範圍:收款帳戶目錄(含幣別維度)+ 交易掛帳戶 + 逐帳戶對帳視圖 + 銀行對帳單匯入與半自動比對(§3.3,自訂範本)+ 換匯紀錄與各幣別餘額呈現(換匯 / 相對匯率機制本體在 currency-exchange,本檔負責其在對帳頁的目錄與呈現落點)。
不做:自動串銀行 API 拉帳(先人工 / 匯入對帳);正式總帳(未來會計模組);三大帳戶補款結構 / 通路→帳戶 routing / 退款執行對帳(子系統 B,獨立檔 account-settlement——帳戶能力欄、費用類型路由、跨帳戶補款 / 代墊 / 回填分類帳 account_transfers、帳戶間掛帳 account_obligations、月結互抵派生、刷退 vs 永豐匯出退款執行對帳)。
receiving_accounts(設計表,per-tenant)| 欄位語意 | 說明 |
|---|---|
code | 帳戶代號(bank_taiwan / sinopac / ctbc / ecpay / cash / fengshou…)。 |
display_name | 顯示名(永豐銀行…)。 |
kind | bank / 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。
payment_transactions 加帳戶維度payment_transactions.receiving_account_id:這筆現金(in / out)落在哪個帳戶(FK receiving_accounts,nullable、onDelete restrict)。
收款(
direction='in')應掛帳戶;退款 / 出款(direction='out')標從哪個帳戶出。現有不變式(H4 等)不變,只多一個維度。 append-only 狹窄豁免:payment_transactions仍 append-only(金額 / 方向 / 型別 / intent / refund… 不可改),但歸戶是「事後補的非金流標註」,故protect_payment_transactionstrigger 改為只允許 UPDATEreceiving_account_id一欄(逐欄IS DISTINCT FROM比對 OLD/NEW,其餘任一欄變動即RAISE EXCEPTION)。DELETE 仍全擋。set nullFK 行為刻意不用(會觸發 trigger UPDATE 路徑且語意是「刪帳戶就抹歸戶」,違反停用不刪原則)—— 帳戶停用即可,被引用者硬刪由restrict擋下。
statement_imports(設計表,per-tenant)| 欄位語意 | 說明 |
|---|---|
receiving_account_id | 哪個帳戶的對帳單(FK receiving_accounts,restrict)。限 kind ∈ bank/cash 且 currency='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 配對隨批釋放)。statement_lines(設計表,per-tenant)| 欄位語意 | 說明 |
|---|---|
import_id | FK statement_imports(cascade)。 |
line_no / txn_date / direction / amount_twd / memo | 匯入事實(direction in/out、amount_twd 正整數 TWD、memo = 範本「摘要」+「備註」合併,非銀行對帳單摘要原文)。txn_date 一律須落在批次宣告期間內(commit fail-closed 擋期間外行)——匯入後不可變;可變的只有配對欄與狀態。 |
status | unmatched(未配對)→ auto(自動配對)/ manual(人工配對)/ ignored(略過,原因必填:手續費 / 利息等 ERP 不記的行);解除配對 → excluded(不再被自動配,只可人工重配)。 |
matched_source_type / matched_source_id | 多型配對目標:payment_transaction / payable / account_transfer(不用兩個 nullable FK——status-aware CHECK 較乾淨、未來加 source 免 migration)。 |
ignored_reason | ignored 時必填。 |
status ∈ (auto, manual) ⟺ 配對兩欄皆非空;其餘狀態皆空。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 入帳、五月批已對、六月批再對)由此硬性擋下。收款建現金列時決定 receiving_account_id:
getReceivingAccountByCode(db, 'ecpay') 解析,查無回 null → 掛未歸戶桶,不 throw)。receivingAccountId),下拉列出所有啟用的手動帳戶(gateway 自動掛帳除外),不依產品線篩選(帳戶 product-agnostic,§2.1)。已接(2026-06-23):所有收款 / 出款點都已傳
receivingAccountId到recordCashTransaction/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 / 入帳)。
packages/core/src/orders/reconciliation.ts(db: ScopedDb):逐帳戶、依期間(Asia/Taipei,比照既有 repo 時區慣例)合併兩來源 —— payment_transactions(收款 in / 退款·gateway out,依 occurred_at)+ payables paid 供應商出款(net_amount_twd,依 paid_at,分源欄 payableOutTwd);netTwd = in − out − payableOut。給會計與銀行 / 金流商實際入帳核對。
payment_transactions 內(order_id NOT NULL),故出款歸戶存 payables.receiving_account_id,對帳時依 receiving_account_id 與交易源 union(IS NOT DISTINCT FROM 讓未歸戶桶也對得上)。packages/core/src/orders/statement-import.ts(設計檔名,db: ScopedDb):把銀行對帳單逐行對上 ERP 現金流水,取代 GS 人工檢查表。
.xlsx 範本下載(同欄位 .csv 也收)——日期 YYYY-MM-DD、存入金額 / 支出金額(二擇一,正整數 TWD)、摘要、備註。營運自行從網銀轉範本;系統不解析任何銀行原生格式(多租戶開放、免格式脆弱——比照 bulk-trip-import 固定範本先例)。payment_transactions(收款 in / 退款 out)payables paid(out,net_amount_twd)account_transfers(雙邊:行的帳戶 = from_account → 配支出行、= to_account → 配存入行;金額取 TWD 側)。排除 kind='refund_payout'——markRefundPaid 同 tx 雙寫(現金列 + 移轉列)是同一筆銀行匯出的兩個表徵,一律以現金列為準,否則雙配。排除被沖正者與沖正列(沖正是更正記錄,非銀行流水)。落地口徑:出帳側(from_account)配對金額 = amount_twd + fee_twd(含轉帳本身手續費的實際離帳額),入帳側(to_account)配對金額 = amount_twd(對方收到的淨額,不含這筆手續費)。status='unmatched' 行):同帳戶 + 同金額 + 同方向 + 日期差 ±2 工作天(重用 workdays.ts + public_holidays)+ 雙方候選皆全域唯一(候選池是該帳戶全體未配對者,非本批)——一對多 / 多對一一律留人工。ignored + 註記;拆分 / 彙配 v1 不做),日期不限。excluded;略過 → ignored(原因必填)。逐行動作(import / auto / manual / unmatch / ignore / 刪批)皆記 audit。code 被現金列引用即鎖不可改)是租戶配置,移到 設定 · 收款帳戶 tab(與 ECPay / 匯率等整合設定同層)。/admin/reconciliation 專注對帳(期間切換 + 各帳戶 in/out/淨額 + 未歸戶桶),右上「管理收款帳戶」link 到設定。
bank_taiwan / sinopac / ctbc / ecpay… 只是荒野的範例資料,非系統內建枚舉。新增帳戶 = receiving_account.manage。fx_exchange.read/manage)。牌告匯率表是獨立頁 /admin/exchange-rates(currency-exchange §4.1)。receiving_account.read / receiving_account.manage —— admin / accountant 可維護目錄(設定頁)與對帳;op 持 receiving_account.read(可選帳戶入帳,不維護目錄);sales 無。對帳單匯入 / 配對走獨立 resource bank_statement(read / manage;admin / accountant = read+manage,op / sales 無)——對帳是會計作業,不掛帳戶目錄權限(語意錯位);所有寫 action 受 module_accounting gate fail-closed。refunds 退款出帳可標 receiving_account_id(markRefundPaid 選填);payables 供應商出款 payables.receiving_account_id(markPaymentRequestPaid 標記已付時選出款帳戶)→ 對帳出款側 payableOutTwd 分源呈現(2026-06-24)。| 版本 | 日期 | 變更 | Commit |
|---|---|---|---|
| 2 | 2026-06-23 | First-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。待接:收款流程入帳時實際帶帳戶。 | — |
| 3 | 2026-06-23 | 回寫 code-is-truth:§4 目錄維護在 /admin/reconciliation(非 settings)、與對帳同頁;§3.1 收款帶帳戶已接 5 點(gateway webhook / 手動 K 單 / lodging 直訂 / admin-repo / test notify,gateway 走 getReceivingAccountByCode('ecpay') 查無回 null)。 | — |
| 4 | 2026-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,估價 = 成交價)。 | — |
| 5 | 2026-06-24 | payables 供應商出款入對帳(§3.2 / §5):payables 加 receiving_account_id(nullable FK,migration 0020)+ markPaymentRequestPaid 標記已付選出款帳戶 + 請款詳情 MarkPaidDialog 帳戶 picker;getReceivingAccountReconciliation 兩源 union(payment_transactions ∪ payables paid),新分源欄 payableOutTwd、net = in − out − payableOut;對帳頁加「供應商出款」欄。銀行 CSV 匯入仍 future。 | — |
| 6 | 2026-06-24 | 帳戶加幣別維度 + 換匯落點(§1 / §2.1 / §4):receiving_accounts.currency(default TWD,外幣帳戶=多幣別外幣池跨梯次共用);對帳頁擴充「各幣別總覽卡」(外幣餘額 / WAC / 台幣基礎,= 各幣別 Dashboard 不另開頁)+「換匯紀錄」列表/新增/沖正。換匯 / 相對匯率 / 牌告匯率機制本體在新 currency-exchange,本檔負責對帳頁的目錄與呈現。§1「不做」明列三大帳戶補款 / 通路 routing / 退款執行對帳為另案。未實作。 | — |
| 7 | 2026-06-25 | 帳戶解綁產品線 + 目錄移至設定(§2.1 / §3.1 / §4 / §5):移除 receiving_accounts.product_scope(GS 鐵證:for登山 / for裝備住宿 是同一組帳戶的費用類型檢查表,非互斥池;歸集屬子系統 B)——手動入帳改列全部啟用帳戶、不依產品線硬篩。目錄維護移至設定 · 收款帳戶 tab(覆寫 v3「目錄在對帳頁」),對帳頁右上 link 過去、專注對帳。wireframe 已對齊。 | — |
| 8 | 2026-07-01 | Code-is-truth 收斂:product_scope 移除已完成(schema 無此欄、orders-new / lodging-book 無硬篩、目錄已在設定頁),刪除 §2.1 與 v7 的「待移除」stale 尾註。 | — |
| 9 | 2026-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 同步)。 | — |
| 10 | 2026-07-03 | §2.3 / §2.4 / §3.3 全落地(migrations 0038/0039、e2e 46);§2.4 account_transfers 每側各一裁決;§3.3 出帳側含 fee 口徑補記。 | — |
| 11 | 2026-07-03 | 終審 fix wave 收斂:§2.4 補「txn_date 必落在批次期間內」(期間界限屬去重防線,期間外行 commit 拒收)+ memo 語意 realign(範本摘要+備註合併,非銀行原文);§3.3 人工配對補「所有來源一律同方向」。 | — |
| 1 | 2026-06-20 | 初版(草案):收款帳戶目錄 + payment_transactions.receiving_account_id 維度 + 逐帳戶對帳視圖;刻意擴張 order-money-model 的「無 reconciliation」邊界。未實作。 | — |