← 回主控台

ChefsMate · Cross-App Payments

金流總體規劃 — POS + ArenaMate 整合

一份給「另一個 session」動工用的實作依據:拆帳分潤引擎、POS × Arena 金流整合、公司/稅務背景。核心原則 → 既有 infra → Gate/Orchestrator spec → 落地清單。

定位:實作規格 + 決策依據  ·  架構:金流核心放後端 edge functions,POS(Swift)/ Arena(Dart)當薄 client  ·  狀態:規劃稿,含既有 infra 盤點

0給 builder 的三句話

  1. 接在既有 infra 上,不要另起爐灶。POS 已有一套「#W13 現金流重構」:藍新 NewebPay 收款 + asset_transactions 複式記帳(清算戶/銀行戶)+ compute-settlement 關帳 + ezPay 發票。Arena 的拆帳/撥款要長在這套之上。
  2. 金流核心是後端。拆帳/撥款/退款/發票的決策邏輯寫在 supabase/functions/_shared(TS,單一真相源),POS 與 Arena 都是薄 client 呼叫同一組 API。兩端 client 語言不同(Swift / Dart)本來就無法共用程式碼,共用點在後端。
  3. 只有「你抽成的錢」准走平台帳戶。商家自己的營業額(POS 餐廳結帳、場館櫃檯收費)必須進商家自己的金流帳戶,否則會把全部營業額算進平台的「代理收付金額」,秒破 10 億門檻 → 被逼辦電子支付執照。細節見 §2。

1現況盤點:別重造的既有 infra

動工前先讀這些,能重用的重用。金流商後端主線是 藍新 NewebPay + ezPay 發票(不是綠界;綠界只在 Arena 櫃檯個人收款那條出現過,平台拆帳統一走藍新)。

Edge Function / 資產用途狀態
newebpay-launch藍新 MPG hosted 付款啟動(讀 intent → 組表單 POST 藍新)。前端 pay.html 已接在用
newebpay-notify藍新付款結果 server-to-server 回呼(TradeInfo/TradeSha 解碼)在用
newebpay-payout-webhook藍新 T+2~T+7 撥款回呼 → 拆 3 筆 asset_transactions(payout / fee / settlement 沖銷清算戶)在用 ⚠️ payload 待藍新 production 文件微調
newebpay-refund藍新退款在用
payment-execute / payment-status通用付款執行 / 查詢在用
checkout-reverse-payment結帳反向沖銷在用
compute-settlement關帳結算:聚合 Payment/Order/Invoice → DailyClose(來源 SettlementService.swift)在用
ezpay-invoice藍新 ezPay 電子發票開立在用
tappay-payment / tappay-invoiceTapPay 收款 / 發票(備援金流)在用
asset_transactions(DB)複式記帳骨幹:清算-藍新戶 / 銀行戶 / 現金戶 之間的分錄。撥款 webhook 已寫入在用
_shared/verify-caller.tsverifyRestaurantMember 等 caller 驗證(跨餐廳越權防護)在用
關鍵可重用資產:asset_transactions 已經是一套複式分錄帳本(清算戶 / 銀行戶),撥款 webhook 已在寫。這同時是 §8「自己做帳工具」的地基 —— Arena 的拆帳/撥款只要沿用同一組 asset 科目,帳本自動長出來,不用另做一套。

2⚠️ 分流鐵則(最高優先)

三種金流要嚴格分帳,決定你會不會被當成「電子支付機構」:

類型收款商是誰錢流向走平台帳戶?例子
① SaaS 訂閱你(公司)進你公司✅ 是(你的營收)餐廳付 POS 月費、場館付 Arena 月費
② 商家直收商家直接進商家自己帳戶絕對不可POS 餐廳結帳、場館櫃檯收會費
③ 平台代收拆帳你(代收)託管 → 拆帳 → 撥出✅ 是(受第三方支付規範)Arena 報名費、教練學費
為什麼 ② 一定要商家直收:若全台餐廳每筆結帳都經過你的藍新帳號,那些營業額全算成「你的代理收付金額」→ 一下破 10 億 → 被逼辦資本額 1 億起跳的電子支付執照。所以 POS 結帳、場館櫃檯收費的錢必須進商家自己的金流帳戶(商家各自申請藍新/綠界特約商店),你只賣軟體 + 收訂閱。

法規門檻速記

3統一架構:後端引擎 + 薄 client

        ┌──────────── 藍新 NewebPay 平台商帳號(你公司) ────────────┐
        │                                                            │
   ┌────┴──── supabase/functions/_shared/payout (單一真相源 TS) ─────┴────┐
   │  collectOrchestrator / splitPayoutOrchestrator / refundOrchestrator  │
   │  PayoutGates(computeSplit / canReleasePayout / canRefund)            │
   │  → 寫入 asset_transactions(沿用既有清算戶/銀行戶分錄)               │
   │  → ezpay-invoice 開平台抽成發票                                       │
   └───────▲──────────────────────────────────────▲─────────────────────┘
           │ 薄 client 呼叫                          │ 薄 client 呼叫
   ┌───────┴────────┐                       ┌───────┴─────────────────┐
   │  POS (Swift)   │                       │ ArenaMate (Dart)        │
   │  ①訂閱 ②餐廳直收 │                       │ ①訂閱 ②櫃檯直收 ③報名費/學費拆帳 │
   └────────────────┘                       └─────────────────────────┘

4拆帳分潤 Spec(payout 模組)

照 4 層架構(Route/View → Orchestrator → Gate + Primitive + Repository)。Gate 為純函數,一律要單元測試。

4.1 狀態機

collected ──► held(escrow) ──► releasable ──► paid
                  │
                  ├──► refunding ──► refunded   (撥款前可退)
                  └──► reversed                 (爭議/退單反轉拆帳)

4.2 Gate(純函數,必測)

// lib/data/payout/payout_gates.dart  &  supabase/functions/_shared/payout/gates.ts(鏡像)

class SplitBreakdown {          // 金額一律用整數「元」,避免浮點
  final int gross;              // 消費者付的總額
  final int paymentCost;        // 金流手續費(給金流商)
  final int platformFee;        // 平台抽成(給你)
  final int payeeNet;           // 撥給場館/教練的淨額
  // 恆等式:gross == paymentCost + platformFee + payeeNet
}

enum SplitRuleType { registrationFee, coachTuition }

class SplitPolicy {             // 唯一真相源,別散落 View
  static const registrationPlatformPct = 0.05;   // 報名費平台抽 5%
  static const coachPlatformPct        = 0.20;   // 教練學費平台抽 20%
  static const coachHoldbackPct        = 0.15;   // 學費保留 15% 準備金到課程週期結束
  static const paymentCostPct          = 0.028;  // 信用卡,實際以藍新合約為準
}

SplitBreakdown computeSplit(int gross, SplitRuleType type);   // 冪等;餘數固定歸金流成本

bool canReleasePayout(SettlementEntry e, DateTime now);
//  報名費: 賽事已結束 && now >= eventEndedAt + settleDelay(T+N)
//  學費  : 該堂已「銷課完成」 || (月結日已到 && 已扣準備金)

int reserveHoldback(int payeeNet, double pct) => (payeeNet * pct).floor();

class RefundDecision { final bool allowed; final int refundable; final bool needsReversal; }
RefundDecision canRefund(SettlementEntry e, DateTime now);
//  held → 可全退;paid → 只能反轉平台抽成 or 走人工
必測案例:拆帳恆等式(加總 == gross、無漏元)、報名費 T+N 邊界、學費銷課前不可撥、準備金保留、退款在 held vs paid 兩態的不同決策。

4.3 Primitive(單一副作用)

insertSettlementEntry(...)                    // 建一筆代收帳
callGatewayCollect(orderId)                   // 藍新代收 → collected
callGatewaySplitPayout(entryId, breakdown)    // 下拆帳撥款指令
writeAssetTransactions(entryId, breakdown)    // 沿用既有複式分錄(清算戶↔應付戶↔銀行戶)
markPayoutPaid(entryId, txnRef)
issuePlatformFeeInvoice(entryId)              // 平台抽成 → ezpay-invoice
callGatewayRefund(entryId, amount)
recordHoldback(entryId, amount)               // 準備金掛帳

4.4 Orchestrator

// 選手繳報名費 → 代收 → held
collectRegistrationFeeOrchestrator(matchId, playerId, amount)
  = insertSettlementEntry → callGatewayCollect → status=held

// 賽事結束 → 撥款場館 + 平台抽成入帳 + 開發票
releaseVenuePayoutOrchestrator(matchId, now)
  = for each entry: if canReleasePayout(e, now)
      → computeSplit → callGatewaySplitPayout → writeAssetTransactions
      → issuePlatformFeeInvoice → markPayoutPaid
    → Result(paidCount, heldCount)

// 教練學費撥款(月結 / 銷課) → 扣抽成 + 保留準備金
coachPayoutOrchestrator(coachId, period, now)
  = for each tuition entry: if canReleasePayout
      → computeSplit → holdback = reserveHoldback(payeeNet, coachHoldbackPct)
      → callGatewaySplitPayout(payeeNet - holdback) → recordHoldback → invoice → paid

// 取消/退費
refundOrchestrator(entryId, now)
  = d = canRefund(e, now); if !d.allowed → Err
    → callGatewayRefund(d.refundable) → (d.needsReversal ? reverse split) → refunded

5抽成參數 + 教練學費 escrow

項目金流成本平台抽成撥款時機準備金對標
報名費(區域聯賽/館間賽)~2.8%5%賽事結束 T+7Accupass 5%+NT$10
教練學費~2.8%20%月結 / 銷課後保留 15% 到課程結束課程平台 10~30%
教練學費的押款風險:課還沒上完就撥款給教練,學員中途退費你會賠。解法 = escrow 概念:「銷課」後才釋放對應金額,並保留 15% 準備金到課程週期結束才放。canReleasePayout 對學費要卡「該堂銷課完成」。

抽成比例集中在 SplitPolicy,並做成後台可調(不同賽別/教練可 override 但預設走 policy)。

6場館「報名費管理系統」

撥給場館的 payeeNet 進場館的收支帳本(這層不碰金流 API,純 CRUD + Gate):

VenueLedger(matchId)
  收入: 平台撥入的報名費淨額
  支出分類(場館自己登記,可核銷):
    ├─ 獎金 prize      ├─ 獎品 goods
    ├─ 裁判費 referee  ├─ 工作人員 staff
    └─ 直播 livestream └─ 其他 misc
  結餘 = 撥入 − 支出   (可匯出報表 / 對帳)

Gate: canEditLedger(role, matchStatus)  // 僅場館幹部可改;賽事結算後鎖定

7有限公司開立(背景 / 非工程)

主體選一人有限公司(不是行號):代收代付 + 抽成 + 未來擴張/募資都指向法人;行號無限責任、不能發股份、金流商談不到拆帳方案。

設立步驟(政府「公司一站式線上申請」)

  1. 公司名稱預查(1~2 工作天,備 2~3 名)。
  2. 開銀行籌備戶 + 存資本額(一人公司無下限,建議 10 萬以上;要貸款/補助抓 50~100 萬)。
  3. 會計師資本額查核簽證 —— 不分金額,有限公司一律要(約 NT$3,000~8,000)。
  4. 設立登記(同時給統編 + 稅籍)。
  5. 刻公司大小章 → 辦發票字軌 / 電子發票 → 開正式帳戶 → 勞健保投保單位 → 申請工商憑證(報稅要用)。
  6. 另辦:數位發展部「第三方支付洗錢防制能量登錄」。

營業項目

可同時掛 第三方支付 / 資訊軟體服務 / 運動場館・活動企劃 / 餐館業。餐廳只是掛項目不用額外執照;真的開店才要食品業登記等。公司一律要開發票。

沒營運也跑不掉的月成本(約)

項目月成本備註
記帳士NT$2,000~3,000每 2 月營業稅 + 年度營所稅(零申報也要報)
負責人健保NT$1,300~2,700公司=你的健保投保單位;已由配偶/他職投保可省
登記地址NT$1,000~2,000沒自有地址租商業登記地址
合計約 NT$4,000~7,000/月台灣無年度存在稅;一次性另有設立規費+資本簽證
省錢策略:確定要串金流/上架前 1~2 個月才設立公司,別提早半年空養。

8自己做帳 + 記帳工具

自己做帳法律上可行,務實走「混合制」:記帳 + 每 2 月營業稅 401 自己做,每年 5 月營所稅結算外包記帳士(單次 8k~20k,最難最容易罰的一關買專業背書)。第一年建議記帳士帶一輪把科目表立好。

關卡頻率DIY 難度
複式簿記(記帳)每月🟡 中(《商業會計法》要複式,非流水帳)
營業稅 401 申報每 2 月🟢 低(財政部網路申報)
營所稅結算申報每年 5 月🔴 高(損益表+資產負債表+稅務調整)

記帳工具(可做,且與金流引擎同源)

asset_transactions 已是複式分錄帳本 → 只要在其上加「會計視圖」:

金流事件(payout 引擎 / 既有 asset_transactions)
  └─► 依固定規則 mapping 到會計科目
       ├─ 月損益表 / 資產負債表(草稿)
       ├─ 每 2 月「營業稅 401 試算表」(銷項/進項/應納)
       ├─ 代收款 vs 已撥款 vs 準備金 對帳表
       ├─ 平台抽成營收 & 已開發票勾稽
       └─ 一鍵匯出 CSV/Excel 給記帳士 / 貼進財政部系統
工具做得到 / 做不到:能把數字全準備好;不能幫你「按送出」向政府申報(401/營所稅要用財政部軟體 + 工商憑證 IC 卡)。科目 mapping 與營所稅調整需記帳士/會計師覆核一次(非會計師產出)。建議形態:長在老闆主控台的「帳本/對帳」分頁。

9落地順序 + builder checklist

  1. P1 共用 payout 引擎:supabase/functions/_shared/payout(Gates + Orchestrators),寫入既有 asset_transactions。新增 asset 科目:平台抽成戶 / 場館應付戶 / 教練應付戶 / 準備金戶。
  2. P1 Gate 單元測試:Dart 端 test/payout/ + TS 端鏡像,拆帳恆等式與 escrow 邊界全綠。
  3. P2 Arena client 接單:報名費 collectRegistrationFeeOrchestrator / releaseVenuePayoutOrchestrator;教練 coachPayoutOrchestrator(卡銷課)。
  4. P2 場館 VenueLedger:CRUD + canEditLedger Gate + 匯出。
  5. P3 會計視圖 / 記帳工具:asset_transactions → 損益表 + 401 試算 + 對帳 + 匯出。
  6. P3 後台抽成設定:SplitPolicy 可調 + 賽別/教練 override。
Definition of Done:一筆報名費從代收 → held → 賽後撥款 → 平台抽成入帳 → 開發票 → asset_transactions 對帳歸零,能在測試台完整跑通;教練學費能卡在銷課前不可撥、退款在 held/paid 兩態行為正確。

10待老闆拍板