一句話:成本側(地接 / 嚮導報帳 / 內外帳)支援以外幣登記,系統用匯率自動換成台幣——內帳用真實取得成本(外幣帳戶相對匯率 WAC 或當日匯率),外帳用台銀牌告匯率;外幣以「帶幣別的收款帳戶」承載(延用 payment-accounts-reconciliation),換匯是帳戶間轉換。只進成本側,營收側鐵守 TWD。
畫面(UI = wireframe):
多帳戶對帳(含換匯 / 各幣別餘額)·匯率設定 tab· 成本登記 / 內外帳的外幣欄見 internal-external-ledger模組歸屬:匯率 / 換匯 / 外幣帳戶 WAC 精算屬進階會計模組(受
module_accountinggate);成本以外幣登記的金額留核心掛帳(accounting-module §4)。
荒野海外團的真實成本大量發生在外幣(日本地接日幣、印尼盾、未來美金),現行 GS 兩種模式並存:
現行系統金額一律整數 TWD(order-money-model §2「拒非 TWD」),成本表 departure_costs.amount_twd 只能填台幣,逼線控 / 財務在 GS 心算換匯、結果不可稽核(內帳 254,511 vs 外帳 168,942 連經手人都說不清)。本檔讓成本原生記外幣 + 匯率,台幣由系統派生。
設計原則:不寫死「日本型 / 印尼型」。成本列上是通用的「內帳匯率來源」(帳戶 WAC / 當日牌告 / 手填),使用者自由選;荒野的日本、印尼只是兩種選法,其他租戶可任意組合。
範圍:匯率牌告表(平台抓台銀 + 租戶覆蓋)+ 帳戶幣別維度 + 換匯紀錄 + 外幣帳戶相對匯率(WAC)+ 各幣別對帳呈現。成本列的外幣欄與雙率換算在 internal-external-ledger。
不做:營收側多幣別(orders / 收款 / payment_schedules / refunds 一律 TWD,客人付台幣);正式總帳 / 合併報表(未來會計模組);自動串銀行 API 拉換匯實際成交(先人工登記換匯);三大帳戶補款 / 通路 routing / 退款執行對帳(子系統 B,獨立檔 account-settlement;換匯表已併入其統一 account_transfers)。
exchange_rates(牌告匯率表,每租戶 DB · 平台共寫 + 租戶覆蓋)逐日逐幣別的台銀牌告,一列存台銀 4 報價(現金 / 即期 × 買入 / 賣出),對齊台銀牌告頁與 GS「出團帳務範本 · 填入匯率」工作表(掛牌日期 × 幣別 × 本行買入 / 賣出 × 2)。每個報價 canonical 存「1 外幣單位 = ? 台幣」(numeric)。這是全系統唯一允許非整數的金額相關欄——是匯率不是金額;換算後的台幣一律 round 成整數。
| 欄位 | 型別 | 說明 |
|---|---|---|
id | text PK(xr_<ulid>) | 主鍵。 |
rate_date | date NOT NULL | 掛牌日期(Asia/Taipei)。 |
currency | text NOT NULL | ISO 4217(JPY / IDR / USD…)。 |
cash_buy | numeric nullable | 現金買入(1 外幣 = ? 台幣)。台銀對部分幣別(如 IDR)無現金匯率 → NULL。 |
cash_sell | numeric nullable | 現金賣出。 |
spot_buy | numeric nullable | 即期買入。 |
spot_sell | numeric nullable | 即期賣出。成本回補 / 外帳報稅預設取此(§3.1)。 |
source | text NOT NULL | bot(台灣銀行牌告,平台每日寫入)/ manual(租戶手動覆蓋 / 自訂)。泛化:來源是欄值、不寫死台銀。 |
created_at / updated_at | timestamptz | — |
約束:unique (rate_date, currency, source);各報價 > 0(NULL 允許,表該幣別無此報價)。
4 報價的取捨:現金 = 帶現鈔買賣(價差大);即期 = 帳戶電匯買賣(價差小)。成本回補與報稅是「把已花的外幣折回台幣」,性質接近賣出,預設取即期賣出(
spot_sell);要改現金 / 買入由 §3.1 解析參數調。顯示可換算成倒數(如 IDR 即期賣出 0.00208 → 480 RP/TWD)。
平台共寫機制:匯率解析跑在 tenant 請求(
db: ScopedDb),故exchange_rates住每租戶 DB(讀本地零跨庫)。「共用」靠平台層每日排程(control plane cron)掃所有 active 租戶 DB、把台銀當日牌告 upsert 成source='bot'列(比照 ECPay webhook 的 cross-DB 列舉模式,見 roadmap Phase 2)。租戶覆蓋寫source='manual'同表。不寫死台銀:來源是source欄的值,未來租戶可換不同銀行牌告(呼應多租戶開放原則)。
receiving_accounts.currency(加欄)payment-accounts-reconciliation §2.1 的收款帳戶目錄加幣別維度——外幣池 = 帶外幣幣別的帳戶,跨梯次共用。
| 欄位 | 型別 | 說明 |
|---|---|---|
currency | text NOT NULL default 'TWD' | 帳戶幣別(ISO 4217)。既有帳戶皆 TWD;新增外幣帳戶如 印尼盾(印尼帳戶)=IDR、美金(永豐)=USD。 |
currency ≠ 'TWD')才有**相對匯率(WAC)**與外幣餘額(§3.2)。account_transfers kind='fx_conversion'一筆換匯 = 台幣帳戶出錢、外幣帳戶進外幣,在多帳戶對帳頁登記。換匯不是獨立表:併入子系統 B 的統一帳戶間移轉分類帳 account_transfers(account-settlement §2.4)——換匯 = kind='fx_conversion'、外幣期初 = kind='opening'、沖正 = kind='reversal'。該表 SSOT 在 account-settlement;本檔只描述其在多幣別 / WAC 語意下的用法。
換匯相關欄位對應(account-settlement §2.4 表):from_account_id(出台幣帳戶,opening 為 NULL)、amount_twd(換出台幣 / 期初台幣成本基礎)、to_account_id(進外幣帳戶)、currency(= to_account.currency)、foreign_amount(外幣實拿淨額)、gross_rate(牌價 provenance)、fee_foreign(換匯外幣手續費)、occurred_at(換匯日期)。
約束:kind='fx_conversion' ⟹ to_account.currency = currency ≠ 'TWD'、foreign_amount NOT NULL;kind='opening' ⟹ from_account_id IS NULL。
Append-only:account_transfers 是 WAC 與外幣餘額的事實來源,不可 UPDATE / DELETE(DB-level trigger,比照 payment_transactions);改錯開 kind='reversal'(反向對沖)。
resolveExchangeRate(db, currency, date, quote='spot_sell')回該幣別該日指定報價的 canonical「1 外幣 = ? 台幣」:
source='manual'(租戶覆蓋),無則 source='bot'(台銀)。quote 對應欄(預設 spot_sell 即期賣出 = 成本回補 / 外帳報稅率;可指定 cash_sell / spot_buy / cash_buy)。該報價為 NULL(台銀無此幣別現金匯率)→ fallback spot_sell。TWD = round(foreign_amount × rate)(一律 round 成整數;金額不浮點)。
外幣帳戶的「持有外幣 / 已付台幣」加權平均,每次換匯重算、花外幣不變(已用 GS 爪哇 520.64 驗證):
held_foreign = Σ account_transfers.foreign_amount (kind ∈ {opening, fx_conversion, reversal},to=該帳戶)
twd_basis = Σ account_transfers.amount_twd (同上)
balance_foreign = held_foreign − Σ 外幣成本支出(該帳戶, foreign) -- 外幣現額
relative_rate (外幣/台幣顯示) = held_foreign / twd_basis -- 如 520.64 RP/TWD
wac_twd_per_unit = twd_basis / held_foreign -- canonical(內帳換算用)
round(foreign × wac_twd_per_unit),外幣餘額與剩餘台幣基礎同比例下降,WAC 恆定(庫存加權平均的標準性質)。wac_twd_per_unit 快照進該列(internal-external-ledger §2 的 internal_rate);之後再換匯改變 WAC 不回頭改既有成本列(庫存「發出時點均價」語意)。computeForeignAccountPeriodMovement(db, account, period) 派生——期初 = occurred_at < 本月月初的累計 WAC 基礎、期末 = < 次月月初。因 account_transfers append-only 且累計,本期期初恆等於上期期末(相同 cutoff 對同一 immutable 表求和),故不再需要人工開 kind='opening' 列、也不寫入任何合成 opening 列(比照 settlement 跨期結轉「只入顯示不寫入」,防雙重計入)。租戶若仍手動開 opening 列,與此派生天然共存不雙計(派生只是對含該 opening 列在內的同批移轉求和的視圖)。WAC 基礎純由帳戶間移轉決定(不含成本支出扣減;花外幣不改 WAC)。packages/core helper,比照對帳 / money helper 不 inline 算式);外幣帳戶餘額與 GS 手填現額對不上時,由人工核對(系統不假設真值,沿用對帳頁慣例)。成本列(internal-external-ledger §2.1 的 departure_costs / expense_proofs)登記外幣時,內帳匯率來源三選一(internal_rate_source):
internal_rate_source | 內帳 internal_rate 解析 | 對應 GS 用法 |
|---|---|---|
fx_account | 取 fx_account_id 外幣帳戶當下 WAC(§3.2)快照。 | 印尼(從預換池支出) |
published | 取 resolveExchangeRate(currency, 成本日) 台銀牌告快照。 | 日本(pay-as-you-go,預設) |
manual | 使用者手填實際成交率(刷卡 / 現鈔換匯與牌告有落差時)。 | 任意 |
external_rate 一律 resolveExchangeRate(currency, 成本日)(台銀牌告)快照,與內帳來源無關。amount_twd / invoice_amount_twd 見 internal-external-ledger §3。currency='TWD')不走本機制,amount_twd 直接填、rate 欄留空(零行為改變)。/admin/exchange-rates):逐月逐日逐幣別的台銀牌告表(掛牌日期 × 4 報價:現金買入 / 賣出、即期買入 / 賣出),幣別 + 月份切換;bot 唯讀、manual 覆蓋 / 新增(橘底標示、解析優先)。版型對照 GS「出團帳務範本 · 填入匯率」。獨立一頁、非 settings tab(資料量大、需月曆式逐日瀏覽)。/admin/reconciliation)擴充:
currency 欄、新增外幣帳戶)移至 設定 · 收款帳戶(租戶配置,與 ECPay 等整合設定同層;對帳頁右上「管理收款帳戶」link 過去)。見 payment-accounts-reconciliation §4。| action | 用途 | op | admin | accountant | guide |
|---|---|---|---|---|---|
fx_exchange.read | 看換匯紀錄 / 外幣帳戶餘額 / WAC(對帳頁) | ✅ | ✅ | ✅ | ❌ |
fx_exchange.manage | 登記 / 沖正換匯 | ❌ | ✅ | ✅ | ❌ |
role.manage(admin),不另開 resource;台銀 bot 列由平台 cron 寫、租戶不可改(只能加 manual 覆蓋)。manage 鎖 admin / accountant;op 可讀(對帳頁與成本登記需看 WAC)。op 是否可登記換匯見 §5。account_transfers kind='fx_conversion',權限收斂為單一 account_transfer.read/manage(account-settlement §4.3)——同一張表只一套權限,上表 fx_exchange.* 即 account_transfer.* 的舊名,不另立資源。departure_cost.create(internal-external-ledger §4.2),不在本 resource。quote 已預留可調。翰昇 / 小御 × 現金 / 銀行;第一版以單一外幣帳戶涵蓋,子持有人 / 現金銀行細分留 follow-up(可開多個外幣帳戶或加子維度)。op 是否可登記換匯:第一版鎖 admin / accountant;線控若需現場登記換匯再放寬。computeForeignAccountPeriodMovement 純派生:本期期初 = 上期期末,由 append-only account_transfers 累計和天然結轉,不寫合成列)。第一期(系統導入前既有餘額)仍以 kind='opening'(account_transfers)人工填期初外幣 + 對應台幣基礎(GS 爪哇起始 56,462,731 RP @ 522.74)當種子;其後各期期初自動派生、與手動 opening 列共存不雙計。fx_account / published,範本層不支援 manual),套用當下解析雙率快照進 departure_costs(同本檔 §3.2 庫存均價不漂移語意)。| 版本 | 日期 | 變更 | Commit |
|---|---|---|---|
| 6 | 2026-07-04 | G2-3 外幣帳戶期初跨期自動結轉(§3.2 / §5):新增 computeForeignAccountWacAsOf + computeForeignAccountPeriodMovement 純派生期初 / 期末 WAC 基礎(occurred_at cutoff),本期期初 = 上期期末由 append-only account_transfers 累計和天然結轉,不寫合成 opening 列(防雙計,比照 settlement 只入顯示)。手動 opening 列與自動派生共存不雙計;第一期仍以人工 opening 列種子。 | — |
| 5 | 2026-07-03 | §5 末條更新:cost-template 外幣範本行落地(範本行帶幣別 + 外幣單價 + 內帳來源 fx_account/published,套用當下解析雙率快照,重算沿用快照不漂移;範本層不支援 manual)。匯率解析 / WAC / round 機制不變。 | — |
| 4 | 2026-07-02 | F2 全鏈落地 + 台銀牌告每日 cron:exchange_rates(台銀四報價 + 租戶 manual 覆蓋)+ 換匯 account_transfers fx kind(append-only + 沖正)+ 外幣帳戶 WAC + departure_costs/expense_proofs 外幣雙率派生 + 對帳頁各幣別總覽 + 匯率獨立頁 /admin/exchange-rates + fx_exchange(=account_transfer)權限全部落地(e2e 35)。平台每日抓台銀 cron 落地:parseBotRatesCsv 純函式 + syncBotRates per-tenant upsert + /api/jobs/sync-bot-rates route(X-Job-Token 閘、parse fail → 502 零租戶半套)+ Cloud Scheduler 30 10 * * * Asia/Taipei;parser 經 live 台銀 CSV 樣本核實(cash/spot = col 2/3/12/13)+ 欄位漂移 label 守衛(NO_ROWS fail-closed,絕不靜默寫錯值)+ 讀取端 carry-forward(假日/未更新沿用最近牌告)。 | — |
| 1 | 2026-06-24 | 初版:成本側多幣別 + 匯率管理。exchange_rates(每租戶 DB·平台每日寫台銀 bot + 租戶 manual 覆蓋,canonical twd_per_unit)+ receiving_accounts.currency(外幣池=帶幣別帳戶,跨梯次共用)+ fx_exchanges(換匯紀錄,append-only + 沖正)+ 外幣帳戶相對匯率 WAC(持有外幣/已付台幣,花外幣不變、登記快照不漂移)。成本內帳匯率來源通用三選一(fx_account WAC / published 台銀 / manual 手填,不寫死團型),外帳一律台銀牌告。各幣別 Dashboard = 多帳戶對帳頁擴充。新 resource fx_exchange(read op/admin/accountant、manage admin/accountant)。營收側鐵守 TWD。配合 internal-external-ledger 成本外幣欄與雙率換算。未實作(spec/wireframe 先行)。 | — |
| 3 | 2026-06-25 | 換匯表併入子系統 B 的統一 account_transfers(新檔 account-settlement):§2.3 原獨立 fx_exchanges 表移除,改為 account_transfers kind ∈ {fx_conversion, opening, reversal}(換匯 / 期初 / 沖正),SSOT 移至 account-settlement §2.4;§3.2 WAC 推導欄名同步(amount_twd / foreign_amount,kind 篩選)。§1「不做」的三大帳戶補款 / routing / 退款執行對帳改指向已落 spec 的 account-settlement。語意 / WAC 機制不變。未實作。 | — |
| 2 | 2026-06-25 | §2.1 牌告改台銀 4 報價(cash_buy/cash_sell/spot_buy/spot_sell,對齊 GS「填入匯率」掛牌日期×幣別×本行買入賣出×2;移除 twd_per_unit/quote_kind 單值);成本回補/外帳報稅預設取 spot_sell(§3.1 resolveExchangeRate 加 quote 參數)。§4.1 匯率改獨立 nav 頁 /admin/exchange-rates(月曆式逐日、幣別/月份切換,非 settings tab)。收款帳戶目錄維護移至設定(payment-accounts-reconciliation §4)。日幣無池(pay-as-you-go、按支出當日匯率回補,published)——外幣池只有事先大額換匯的幣別(印尼盾)。wireframe 已對齊。未實作。 | — |