ChefsMate · Cross-App Payments
一份給「另一個 session」動工用的實作依據:拆帳分潤引擎、POS × Arena 金流整合、公司/稅務背景。核心原則 → 既有 infra → Gate/Orchestrator spec → 落地清單。
asset_transactions 複式記帳(清算戶/銀行戶)+ compute-settlement 關帳 + ezPay 發票。Arena 的拆帳/撥款要長在這套之上。supabase/functions/_shared(TS,單一真相源),POS 與 Arena 都是薄 client 呼叫同一組 API。兩端 client 語言不同(Swift / Dart)本來就無法共用程式碼,共用點在後端。動工前先讀這些,能重用的重用。金流商後端主線是 藍新 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-invoice | TapPay 收款 / 發票(備援金流) | 在用 |
asset_transactions(DB) | 複式記帳骨幹:清算-藍新戶 / 銀行戶 / 現金戶 之間的分錄。撥款 webhook 已寫入 | 在用 |
_shared/verify-caller.ts | verifyRestaurantMember 等 caller 驗證(跨餐廳越權防護) | 在用 |
asset_transactions 已經是一套複式分錄帳本(清算戶 / 銀行戶),撥款 webhook 已在寫。這同時是 §8「自己做帳工具」的地基 —— Arena 的拆帳/撥款只要沿用同一組 asset 科目,帳本自動長出來,不用另做一套。三種金流要嚴格分帳,決定你會不會被當成「電子支付機構」:
| 類型 | 收款商是誰 | 錢流向 | 走平台帳戶? | 例子 |
|---|---|---|---|---|
| ① SaaS 訂閱 | 你(公司) | 進你公司 | ✅ 是(你的營收) | 餐廳付 POS 月費、場館付 Arena 月費 |
| ② 商家直收 | 商家 | 直接進商家自己帳戶 | ❌ 絕對不可 | POS 餐廳結帳、場館櫃檯收會費 |
| ③ 平台代收拆帳 | 你(代收) | 託管 → 拆帳 → 撥出 | ✅ 是(受第三方支付規範) | Arena 報名費、教練學費 |
┌──────────── 藍新 NewebPay 平台商帳號(你公司) ────────────┐
│ │
┌────┴──── supabase/functions/_shared/payout (單一真相源 TS) ─────┴────┐
│ collectOrchestrator / splitPayoutOrchestrator / refundOrchestrator │
│ PayoutGates(computeSplit / canReleasePayout / canRefund) │
│ → 寫入 asset_transactions(沿用既有清算戶/銀行戶分錄) │
│ → ezpay-invoice 開平台抽成發票 │
└───────▲──────────────────────────────────────▲─────────────────────┘
│ 薄 client 呼叫 │ 薄 client 呼叫
┌───────┴────────┐ ┌───────┴─────────────────┐
│ POS (Swift) │ │ ArenaMate (Dart) │
│ ①訂閱 ②餐廳直收 │ │ ①訂閱 ②櫃檯直收 ③報名費/學費拆帳 │
└────────────────┘ └─────────────────────────┘
asset_transactions 分錄 + newebpay-payout-webhook 撥款回呼;Arena 拆帳只是多幾種 asset 科目(平台抽成戶 / 場館應付戶 / 教練應付戶 / 準備金戶)。payout 模組)照 4 層架構(Route/View → Orchestrator → Gate + Primitive + Repository)。Gate 為純函數,一律要單元測試。
collected ──► held(escrow) ──► releasable ──► paid
│
├──► refunding ──► refunded (撥款前可退)
└──► reversed (爭議/退單反轉拆帳)
// 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 走人工
insertSettlementEntry(...) // 建一筆代收帳
callGatewayCollect(orderId) // 藍新代收 → collected
callGatewaySplitPayout(entryId, breakdown) // 下拆帳撥款指令
writeAssetTransactions(entryId, breakdown) // 沿用既有複式分錄(清算戶↔應付戶↔銀行戶)
markPayoutPaid(entryId, txnRef)
issuePlatformFeeInvoice(entryId) // 平台抽成 → ezpay-invoice
callGatewayRefund(entryId, amount)
recordHoldback(entryId, amount) // 準備金掛帳
// 選手繳報名費 → 代收 → 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
| 項目 | 金流成本 | 平台抽成 | 撥款時機 | 準備金 | 對標 |
|---|---|---|---|---|---|
| 報名費(區域聯賽/館間賽) | ~2.8% | 5% | 賽事結束 T+7 | — | Accupass 5%+NT$10 |
| 教練學費 | ~2.8% | 20% | 月結 / 銷課後 | 保留 15% 到課程結束 | 課程平台 10~30% |
canReleasePayout 對學費要卡「該堂銷課完成」。抽成比例集中在 SplitPolicy,並做成後台可調(不同賽別/教練可 override 但預設走 policy)。
撥給場館的 payeeNet 進場館的收支帳本(這層不碰金流 API,純 CRUD + Gate):
VenueLedger(matchId)
收入: 平台撥入的報名費淨額
支出分類(場館自己登記,可核銷):
├─ 獎金 prize ├─ 獎品 goods
├─ 裁判費 referee ├─ 工作人員 staff
└─ 直播 livestream └─ 其他 misc
結餘 = 撥入 − 支出 (可匯出報表 / 對帳)
Gate: canEditLedger(role, matchStatus) // 僅場館幹部可改;賽事結算後鎖定
主體選一人有限公司(不是行號):代收代付 + 抽成 + 未來擴張/募資都指向法人;行號無限責任、不能發股份、金流商談不到拆帳方案。
可同時掛 第三方支付 / 資訊軟體服務 / 運動場館・活動企劃 / 餐館業。餐廳只是掛項目不用額外執照;真的開店才要食品業登記等。公司一律要開發票。
| 項目 | 月成本 | 備註 |
|---|---|---|
| 記帳士 | NT$2,000~3,000 | 每 2 月營業稅 + 年度營所稅(零申報也要報) |
| 負責人健保 | NT$1,300~2,700 | 公司=你的健保投保單位;已由配偶/他職投保可省 |
| 登記地址 | NT$1,000~2,000 | 沒自有地址租商業登記地址 |
| 合計 | 約 NT$4,000~7,000/月 | 台灣無年度存在稅;一次性另有設立規費+資本簽證 |
自己做帳法律上可行,務實走「混合制」:記帳 + 每 2 月營業稅 401 自己做,每年 5 月營所稅結算外包記帳士(單次 8k~20k,最難最容易罰的一關買專業背書)。第一年建議記帳士帶一輪把科目表立好。
| 關卡 | 頻率 | DIY 難度 |
|---|---|---|
| 複式簿記(記帳) | 每月 | 🟡 中(《商業會計法》要複式,非流水帳) |
| 營業稅 401 申報 | 每 2 月 | 🟢 低(財政部網路申報) |
| 營所稅結算申報 | 每年 5 月 | 🔴 高(損益表+資產負債表+稅務調整) |
asset_transactions 已是複式分錄帳本 → 只要在其上加「會計視圖」:
金流事件(payout 引擎 / 既有 asset_transactions)
└─► 依固定規則 mapping 到會計科目
├─ 月損益表 / 資產負債表(草稿)
├─ 每 2 月「營業稅 401 試算表」(銷項/進項/應納)
├─ 代收款 vs 已撥款 vs 準備金 對帳表
├─ 平台抽成營收 & 已開發票勾稽
└─ 一鍵匯出 CSV/Excel 給記帳士 / 貼進財政部系統
supabase/functions/_shared/payout(Gates + Orchestrators),寫入既有 asset_transactions。新增 asset 科目:平台抽成戶 / 場館應付戶 / 教練應付戶 / 準備金戶。test/payout/ + TS 端鏡像,拆帳恆等式與 escrow 邊界全綠。collectRegistrationFeeOrchestrator / releaseVenuePayoutOrchestrator;教練 coachPayoutOrchestrator(卡銷課)。canEditLedger Gate + 匯出。SplitPolicy 可調 + 賽別/教練 override。