company_pay 金流串接 PRD(橋外生人力銀行)
文件入口:company-pay.md
1. 文件資訊
- 單號:company_pay
- 類型:新功能(首次導入金流)
- 範圍:
apps/recruitment(橋外生雇主側)+ admin 後台 +packages/http-client+ 後端新建payment服務 - 最後更新:2026-06-22
- 狀態:Draft(前後端對齊中)
- 主要對象:前端、後端、PM、財務、法務
2. 商業背景與策略
橋外生人力銀行為 wport 平台底下的服務,為外國雇主 + 台灣雇主提供僑外籍學生人才搜尋與媒合服務。
本期定位:B2B 優先 + 先把金流跑通。付費方一律是企業(雇主),求職者不付費;B2C(求職者付費)明確延後到 Phase 2。本期不追求自助化、不追求漂亮的訂閱漏斗,只求以下兩條 B2B 路徑能穩定收到錢:
- 企業訂閱(雇主自助) — 雇主自己選方案、自己付款(單一方案、月扣為主)
- Admin 自定義收款(業務開單) — 業務談好的客製合約,由 admin 後台建立付款連結寄給客戶
商業模式優先序:B2B(雇主付費)為本期唯一範圍 → B2C(求職者付費,如履歷加值、報告下載)為 Phase 2 → 平台級多服務共用金流為更後期。
未來改版時會擴充訂閱漏斗(試用、升降級、折扣碼、多方案矩陣),本 PRD 預留抽象但不實作。
3. 變更範圍
3.1 In Scope(MVP / 本期金流通)
企業訂閱(自助):
- 單一方案訂閱購買(月扣 / 年扣,方案數量少、不做矩陣對比)
- 自動續扣 + 失敗重試(dunning)
- 訂閱管理頁(查看狀態、取消、更換卡片)
- 付款失敗通知與更新卡片流程
Admin 自定義收款(業務 / 後台):
- Admin 後台建立「自定義收款單」(金額、品名、付款人 email、發票資訊)
- 系統產生付款連結(hosted page)+ 寄 email 給客戶
- 客戶點連結 → 跳轉金流付款 → 完成
- Admin 後台查看收款狀態、補寄連結、作廢單據
- 一次性收款為主(不綁訂閱)
共用基礎:
- 雙軌金流(Stripe + 綠界)
- 雙發票(台灣電子發票 + 海外 invoice)
- PaymentProvider 抽象層
- Webhook 接收與對帳
3.2 Out of Scope(Phase 2 / 改版後)
- 免費試用期(綁卡或不綁卡)
- 方案升級 / 降級(proration)
- 折扣碼 / 優惠券
- 多幣別顯示(初期僅 TWD)
- 點數 / 儲值制
- 一次性購買(單筆職缺、單筆履歷下載)
- 求職者付費功能
- 退款自助操作(初期由客服處理)
- 統編開立紙本發票
- 行動裝置 RWD(依現有
apps/recruitment桌機優先策略) - Admin 自定義收款支援「分期」或「轉訂閱」
3.3 設計原則:抽象不實作
雖然 Phase 2 功能不做,但抽象層必須預留,避免未來重寫:
- 後端
PaymentProviderinterface 已涵蓋 trial / proration 的 API 形狀 - 資料庫
subscription表預留trial_end、pending_plan_change欄位(允許 NULL) - 前端
subscriptionstore 的 type 涵蓋trialing狀態(但 UI 不顯示) - 訂閱狀態機定義完整 5 個狀態(但本期只會出現 active / past_due / unpaid / canceled)
4. 非範圍與假設
- 假設:公司已具備台灣商業登記,可申請綠界商家與 Stripe Taiwan 帳號
- 假設:所有收款進台灣公司戶頭,Stripe 收外幣自動換 TWD(接受約 1% 匯損)
- 假設:初期不開海外公司,海外營收達 USD 50,000/月再評估
- 假設:橋外生求職者不付費,付費角色僅雇主
5. 用戶角色與場景
5.1 角色
| 角色 | 說明 | 付款方式 |
|---|---|---|
| 海外雇主 | 在台灣以外註冊的企業 | Stripe(信用卡、支援 3DS) |
| 台灣雇主 | 在台灣註冊的企業 | 綠界(信用卡、ATM、超商) |
| 橋外生求職者 | 外籍學生 | 不付費 |
| wport 業務 | 跟客戶談客製合約的內部人員 | 透過 admin 後台開單 |
| wport 客服 / 財務 | 後台處理退款、查詢、開發票補發 | 透過 admin 後台操作 |
5.2 核心場景
訂閱類(自助):
- 海外雇主首次訂閱:選方案 → Stripe Checkout → 立即扣款 → 收到英文 invoice email
- 台灣雇主首次訂閱:選方案 → 綠界結帳 → 立即扣款 → 收到電子發票
- 續扣失敗:第 1/3/5/7 天自動重試 → 同時寄 email 提醒更新卡片 → 第 7 天仍失敗則暫停服務
- 取消訂閱:本期到期後生效,不退費
Admin 自定義收款類: 5. 業務開單收款(一次性):業務談好 NT$200,000 客製方案 → admin 後台填單(金額、品名、客戶 email、發票資訊、選擇金流)→ 系統產生 hosted 付款連結 → email 寄給客戶 → 客戶點連結付款 → 完成後自動開發票 6. 補寄付款連結:客戶弄丟 email → admin 後台找到該筆單據 → 點「補寄」→ 系統重寄相同連結 7. 作廢收款單:客戶決定不買 / 談新條件 → admin 後台作廢未付款單據(已付款的不可作廢,須走退款流程) 8. 查詢與對帳:財務在 admin 後台查特定日期區間的收款明細,匯出 CSV
6. 現況對齊(Project Impact Snapshot)
6.1 Frontend(已存在)
apps/recruitment已有 Vite SPA + PrimeVue + Zod 架構packages/http-client提供 axios wrapper,已有 token 管理packages/auth-session提供登入狀態,需與訂閱狀態解耦(訂閱過期 ≠ 登入過期)packages/i18n已建立 zh-TW / en-US 對應,本次需新增payment.*命名空間
6.2 Frontend(需新增)
- 新路由:
/billing/plans、/billing/checkout/:planId、/billing/subscription、/billing/invoices、/billing/payment-method - 新 API 模組:
src/api/payment/index.ts - 新狀態 store:
src/stores/subscription.ts(Pinia,使用packages/store-core) - 新元件:
PlanCard、SubscriptionStatusBanner、InvoiceList、PaymentMethodCard、TrialCountdown - 新 guard:路由前置守衛,訂閱過期時導向方案頁但不登出
6.3 Backend(尚未開工,本 PRD 同步定義契約)
- 新建
payment-service(建議獨立微服務,因 webhook 流量與業務 API 解耦較安全) - 整合 Stripe SDK 與綠界 SDK
- 整合 ezPay(或綠界)電子發票
- Webhook 端點接收 Stripe / 綠界事件
- 訂閱狀態與發票記錄 DB schema
- Dunning 流程排程(cron 或 queue)
- 對帳作業(每日比對金流方與本地紀錄)
7. 收費模式設計
7.1 訂閱方案(範例,最終由 PM 定價)
| 方案 | 月費 (TWD) | 年費 (TWD) | 主要權限 |
|---|---|---|---|
| Basic | 3,000 | 30,000 | 月可發布 5 個職缺,搜尋 50 份履歷 |
| Pro | 8,000 | 80,000 | 月可發布 20 個職缺,搜尋 200 份履歷,AI 推薦 |
| Enterprise | 客製 | 客製 | 走 admin 自定義收款 |
設計原則:
- 年費為月費的 10 倍(等於送 2 個月,業界慣例)
- 本期不做免費試用(Phase 2 再加),訂閱即扣款
- 不做自助升降級(Phase 2 再加),客戶要換方案請聯絡客服 → 客服走 admin 自定義收款 / 退款
- Enterprise 一律走 admin 自定義收款
7.2 Admin 自定義收款
業務 / 客服在後台手動建立的收款單,欄位如下:
| 欄位 | 必填 | 說明 |
|---|---|---|
| 收款品名 | ✅ | 顯示在發票與付款頁面(如「橋外生人才搜尋 1 年方案」) |
| 金額 | ✅ | 整數 TWD(初期不支援外幣,海外客戶仍收 TWD 走 Stripe 換匯) |
| 客戶 email | ✅ | 寄付款連結用 |
| 客戶公司名 | ✅ | 開發票用 |
| 統編 | 條件 | 台灣公司必填,海外免填 |
| 客戶國別 | ✅ | 決定走 Stripe(非 TW)或綠界(TW) |
| 備註 | ❌ | 內部用,不顯示給客戶 |
| 到期日 | ✅ | 付款連結失效日(預設 7 天) |
| 關聯帳號 | ❌ | 若指定,付款成功後該帳號開通對應權限(否則僅收款不開通,由業務手動處理) |
8. 金流選型與分流邏輯
8.1 分流規則
使用者選方案後 →
判斷使用者帳戶屬性 company.country
├─ country === 'TW' → 走綠界(含台灣電子發票)
├─ country !== 'TW' → 走 Stripe(英文 invoice)
└─ 未填 country → 結帳前強制要求填寫
8.2 為何雙軌
| 需求 | Stripe | 綠界 |
|---|---|---|
| 海外信用卡支援 | ✅ 強 | ❌ 拒付率高 |
| 台灣電子發票 | ❌ 無法 | ✅ 內建 |
| 訂閱續扣(recurring) | ✅ 內建完整 | ✅ 定期定額 |
| Proration(升降級按比例) | ✅ 內建 | ❌ 需自行計算 |
| Smart Retries | ✅ 內建 | ❌ 需自行實作 |
| Webhook 穩定性 | ✅ 高 | ⚠️ 需重試機制 |
| ATM / 超商付款 | ❌ 無 | ✅ 有(但訂閱不適用) |
結論:海外用 Stripe、台灣用綠界,雙軌不可避免。
8.3 抽象層設計
後端必須抽象 PaymentProvider interface,前端 API 對外只暴露統一介面,不暴露 Stripe / 綠界差異:
interface PaymentProvider {
// 訂閱
createSubscription(input: CreateSubscriptionInput): Promise<SubscriptionResult>
cancelSubscription(subscriptionId: string, mode: 'immediate' | 'at_period_end'): Promise<void>
updatePaymentMethod(customerId: string): Promise<PaymentMethodUpdateUrl>
// 一次性收款(admin 自定義單)
createOneTimeCharge(input: CreateChargeInput): Promise<ChargeResult> // 回傳 hosted url
voidCharge(chargeId: string): Promise<void> // 作廢未付款單
// 共用
retrieveInvoice(invoiceId: string): Promise<Invoice>
// Phase 2 預留(不實作)
// changePlan(subscriptionId: string, newPlanId: string): Promise<ChangePlanResult>
// createTrialSubscription(...)
}
前端不該知道「這筆是 Stripe 還是綠界」。
9. 功能流程
9.1 首次訂閱(自助)
flowchart TD
A[雇主進入方案頁 /billing/plans] --> B{已登入?}
B -->|否| C[導向登入]
B -->|是| D[選擇方案]
D --> E{填寫公司國別}
E -->|TW| F[綠界結帳流程]
E -->|非 TW| G[Stripe Checkout]
F --> H[後端建立綠界訂單 + 跳轉]
G --> I[後端建立 Stripe Checkout Session + 跳轉]
H --> J[綠界 hosted page 付款]
I --> K[Stripe hosted page 付款]
J --> L[Webhook: payment_succeeded]
K --> L
L --> M[後端建立 subscription 記錄 status=active]
M --> N[開立發票 + 寄通知 email]
N --> O[前端導回 /billing/subscription?status=success]
9.2 Admin 自定義收款
flowchart TD
A[業務談妥客製方案] --> B[admin 後台 /admin/billing/charges/new]
B --> C[填寫: 品名/金額/email/國別/到期日]
C --> D{選擇金流}
D -->|TW 客戶| E[後端建立綠界一次性訂單]
D -->|海外客戶| F[後端建立 Stripe Payment Link]
E --> G[產生付款連結 + 寄 email 給客戶]
F --> G
G --> H[客戶收到 email 點連結]
H --> I[Stripe / 綠界 hosted page 付款]
I --> J[Webhook: payment_succeeded]
J --> K[後端更新收款單狀態 → paid]
K --> L[自動開發票]
L --> M{有關聯帳號?}
M -->|是| N[自動開通對應權限]
M -->|否| O[僅收款,由業務手動處理]
L --> P[email 通知客戶付款成功 + 發票]
狀態機(自定義收款單):
draft → pending → paid
↓
expired (到期未付)
↓
voided (admin 主動作廢)
9.3 [Phase 2] 試用、升降級
本期不實作,預留章節避免漏掉。詳細設計參見舊版 v0.1 草稿(已從本 PRD 移除)。
9.4 續扣失敗(Dunning)
flowchart TD
A[扣款失敗] --> B[第 0 天: 寄 email + 站內通知]
B --> C{Stripe Smart Retries 排程}
C --> D[第 3 天自動重試]
D -->|成功| Z[恢復 active]
D -->|失敗| E[第 5 天自動重試]
E -->|成功| Z
E -->|失敗| F[第 7 天最後重試]
F -->|成功| Z
F -->|失敗| G[status: active → past_due → unpaid]
G --> H[暫停服務但保留帳號 30 天]
H --> I[期間使用者可主動更新卡片觸發重試]
I --> J{30 天內更新?}
J -->|是| Z
J -->|否| K[status: unpaid → canceled]
10. 前端頁面清單
10.1 雇主側(apps/recruitment)
| 路由 | 頁面名稱 | 用途 |
|---|---|---|
/billing/plans | 方案選擇頁 | 方案卡片、月/年切換、行動按鈕 |
/billing/checkout/:planId | 結帳跳轉頁 | 確認資訊、呼叫後端取得 Stripe/綠界跳轉 URL |
/billing/checkout/success | 付款成功頁 | 顯示訂閱開始、導引到下一步 |
/billing/checkout/cancel | 付款取消頁 | 用戶取消 hosted page 時導回 |
/billing/subscription | 訂閱管理頁 | 目前方案、續期日、取消按鈕 |
/billing/payment-method | 付款方式頁 | 顯示卡片末四碼、更換卡片按鈕 |
/billing/invoices | 發票/收據列表 | 歷史扣款、下載發票 PDF |
/billing/pay/:token | 自定義收款付款頁 | 客戶從 email 點進來,顯示金額品名後跳金流 hosted page |
10.2 Admin 後台(路由依現有 admin 架構而定)
| 路由 | 頁面名稱 | 用途 |
|---|---|---|
/admin/billing/charges | 自定義收款單列表 | 篩選狀態、金額區間、客戶、日期 |
/admin/billing/charges/new | 建立收款單 | §7.2 的所有欄位 |
/admin/billing/charges/:id | 收款單詳情 | 狀態、付款記錄、補寄、作廢、退款(連結客服 SOP) |
/admin/billing/subscriptions | 所有訂閱列表 | 雇主訂閱狀態總覽,可手動取消、查看歷史 |
/admin/billing/invoices | 所有發票列表 | 全平台發票,可重發、查問題 |
/admin/billing/reconciliation | 對帳頁 | 顯示對帳差異、可手動標記處理 |
Admin 後台位置:本 PRD 不指定 admin 後台的程式碼位置,請後端 / 全端團隊評估是放在現有後台、獨立 admin app、或 retool 等低程式碼工具。前端工作量可大可小,取決於這個決定。
10.3 共用元件(雇主側)
<PlanCard :plan />— 方案卡片<SubscriptionStatusBanner />— 全站頂部訂閱狀態提醒(付款失敗、未付款)<InvoiceListItem :invoice />— 發票列表項<PaymentMethodCard :method />— 卡片資訊顯示<CancelSubscriptionDialog />— 取消訂閱確認對話框<CustomChargePayPanel :token />— 自定義收款付款頁主面板(顯示金額、品名、客戶資訊確認)
10.4 路由守衛
// router/guards/subscription.ts
beforeEach((to, from) => {
const sub = useSubscriptionStore()
if (to.meta.requiresActiveSubscription) {
if (sub.status === 'unpaid' || sub.status === 'canceled') {
return { path: '/billing/plans', query: { reason: 'subscription_required' } }
}
if (sub.status === 'past_due') {
return { path: '/billing/payment-method', query: { reason: 'update_required' } }
}
}
})
注意:
- 訂閱狀態與登入狀態解耦。訂閱過期不可登出使用者,否則無法引導回方案頁。
/billing/pay/:token路徑不需要登入(客戶可能不是平台用戶),純粹用 token 驗證。
11. API 契約(前後端介面)
所有 API 對前端統一介面,內部 provider 差異由後端封裝。
11.1 取得方案列表
GET /api/v1/billing/plans
Response:
{
"plans": [
{
"id": "basic-monthly",
"name": "Basic",
"interval": "month",
"price": { "amount": 3000, "currency": "TWD" },
"features": ["5 job posts/month", "50 resume search"],
"trial_days": 14
},
...
]
}
11.2 建立 Checkout Session
POST /api/v1/billing/checkout
Body:
{
"plan_id": "basic-monthly",
"success_url": "https://wport.me/recruitment/billing/checkout/success",
"cancel_url": "https://wport.me/recruitment/billing/checkout/cancel"
}
Response:
{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_xxx",
"provider": "stripe", // 或 "ecpay"
"session_id": "cs_xxx"
}
前端拿到 checkout_url 後 window.location.href = checkout_url 跳轉。不可開新分頁(iOS Safari 會擋)。
11.3 取得目前訂閱
GET /api/v1/billing/subscription
Response:
{
"id": "sub_xxx",
"status": "trialing" | "active" | "past_due" | "unpaid" | "canceled",
"plan": { "id": "basic-monthly", "name": "Basic", ... },
"current_period_start": "2026-06-22T00:00:00Z",
"current_period_end": "2026-07-22T00:00:00Z",
"trial_end": "2026-07-06T00:00:00Z" | null,
"cancel_at_period_end": false,
"pending_plan_change": null | { "plan_id": "pro-monthly", "effective_at": "..." },
"payment_method": {
"type": "card",
"brand": "visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2027
}
}
11.4 [Phase 2] 變更方案
本期不實作,預留 API path:POST /api/v1/billing/subscription/change-plan
11.5 取消訂閱
POST /api/v1/billing/subscription/cancel
Body:
{
"mode": "at_period_end" | "immediate" // 預設 at_period_end
}
Response:
{
"status": "canceled" | "active", // immediate=canceled, at_period_end=active 但 cancel_at_period_end=true
"ends_at": "2026-07-22T00:00:00Z"
}
11.6 更新付款方式
POST /api/v1/billing/payment-method/update-session
Response:
{
"update_url": "https://billing.stripe.com/p/session/xxx",
"provider": "stripe"
}
前端跳轉到 update_url,使用者更新完後跳回 /billing/payment-method?status=updated。
11.7 發票列表
GET /api/v1/billing/invoices?page=1&limit=20
Response:
{
"invoices": [
{
"id": "inv_xxx",
"number": "INV-2026-0001",
"amount": 3000,
"currency": "TWD",
"status": "paid",
"issued_at": "2026-06-22T00:00:00Z",
"pdf_url": "https://...",
"invoice_type": "ecpay_tax_invoice" | "stripe_receipt",
"tax_invoice_number": "AB12345678" // 僅綠界電子發票
}
],
"total": 12
}
11.8 自定義收款 API(雇主側 + admin 後台)
客戶付款頁(公開,token 驗證):
GET /api/v1/billing/charges/by-token/:token
Response:
{
"id": "ch_xxx",
"status": "pending" | "paid" | "expired" | "voided",
"description": "橋外生人才搜尋 1 年方案",
"amount": 200000,
"currency": "TWD",
"customer_email": "ceo@example.com",
"customer_company": "Example Inc.",
"expires_at": "2026-06-29T23:59:59Z"
}
POST /api/v1/billing/charges/by-token/:token/checkout
Response:
{
"checkout_url": "https://checkout.stripe.com/c/pay/cs_xxx",
"provider": "stripe" | "ecpay"
}
Admin 後台:
POST /api/v1/admin/billing/charges
Body:
{
"description": "橋外生人才搜尋 1 年方案",
"amount": 200000,
"currency": "TWD",
"customer_email": "ceo@example.com",
"customer_company": "Example Inc.",
"customer_tax_id": "12345678", // 統編,可選
"customer_country": "TW",
"internal_note": "業務 Alice,2026 Q3 案",
"expires_at": "2026-06-29T23:59:59Z",
"linked_account_id": "user_xxx" // 可選
}
Response:
{
"id": "ch_xxx",
"pay_url": "https://pay.wport.me/abc123token",
"status": "pending",
...
}
GET /api/v1/admin/billing/charges?status=&from=&to=&q=
POST /api/v1/admin/billing/charges/:id/resend-email
POST /api/v1/admin/billing/charges/:id/void
11.9 錯誤碼
| Code | HTTP | 說明 | 前端處理 |
|---|---|---|---|
subscription_already_exists | 409 | 已有 active 訂閱 | 導向訂閱管理頁 |
plan_not_found | 404 | 方案不存在 | 導回方案頁 |
payment_method_required | 402 | 需先設定付款方式 | 跳付款方式頁 |
provider_error | 502 | Stripe/綠界回應錯誤 | 顯示通用錯誤,請使用者稍後再試 |
country_required | 400 | 未填公司國別 | 跳出國別選擇對話框 |
charge_expired | 410 | 自定義收款單已過期 | 顯示「連結已過期,請聯絡業務重新開立」 |
charge_already_paid | 409 | 自定義收款單已付款 | 顯示「此單據已完成付款」 |
charge_voided | 410 | 自定義收款單已作廢 | 顯示「此單據已作廢」 |
12. 訂閱狀態機
┌──────────┐
建立 → │ active │ ←──┐
└────┬─────┘ │ 重試成功 / 更新卡片
│ 扣款失敗 │
▼ │
┌──────────┐ │
│ past_due │─────┘
└────┬─────┘
│ 7 天重試全部失敗
▼
┌──────────┐
│ unpaid │
└────┬─────┘
│ 30 天未補繳 / 主動取消
▼
┌──────────┐
│ canceled │ (終態)
└──────────┘
(預留: trialing 狀態,Phase 2 使用)
前端對應行為:
trialing→ 預留,本期不會出現active→ 正常使用past_due→ 顯示紅色 banner「付款失敗,請更新卡片」,功能仍可用(grace period)unpaid→ 唯讀模式,所有付費功能擋下,僅可進入/billing/*canceled→ 同unpaid,但可重新訂閱
13. [Phase 2] 試用期邏輯
本期不實作。Phase 2 規劃時需決定:
- 綁卡 vs 不綁卡試用(建議綁卡,業界轉換率與詐騙風險平衡)
- 試用天數(建議 14 天)
- 是否限制同一公司只能試用一次(後端
has_used_trialflag)
14. [Phase 2] 升降級 Proration
本期不實作,客戶需要換方案請走客服 → admin 自定義收款 / 退款流程。Phase 2 規劃時參考:
- Stripe
subscription.update內建proration_behavior: 'create_prorations' - 綠界需後端自行計算剩餘天數差額
- UI 需在對話框顯示「將立即扣款 NT$X」或「將於 YYYY-MM-DD 起以新方案扣款」
15. 發票處理
15.1 綠界電子發票(台灣雇主)
- 後端在
payment_succeededwebhook 中呼叫綠界發票 API - 開立兩聯式(個人)或三聯式(公司,需統編)
- 雇主在訂閱前必須填寫:公司名、統編、發票寄送 email
- 發票 PDF 由綠界產生,後端儲存 URL
15.2 Stripe Invoice(海外雇主)
- Stripe 自動產生 PDF invoice,email 給使用者
- 內容包含公司名、地址、金額、稅務說明
- 後端只需儲存 invoice ID 與 PDF URL,不需自行渲染
15.3 統一前端呈現
/billing/invoices 頁面對前端只看到統一格式,不暴露 provider 差異:
type Invoice = {
id: string
number: string
amount: number
currency: string
status: 'paid' | 'open' | 'void'
issued_at: string
pdf_url: string
invoice_type: 'ecpay_tax_invoice' | 'stripe_receipt'
tax_invoice_number?: string // 僅綠界
}
16. Webhook 與資料同步(後端責任,前端無感)
16.1 Stripe Webhook 必須處理的事件
| 事件 | 動作 |
|---|---|
checkout.session.completed | 訂閱:建立 subscription;一次性:更新 charge 為 paid |
customer.subscription.created | 設定 status |
customer.subscription.updated | 同步 status |
customer.subscription.deleted | status → canceled |
invoice.payment_succeeded | 開立發票、寄通知 |
invoice.payment_failed | 進入 dunning |
payment_intent.succeeded | 一次性收款付款成功(admin 自定義單) |
customer.subscription.trial_will_end | [Phase 2] 寄試用即將結束 email |
16.2 綠界 Webhook
綠界 webhook 穩定性較差,後端必須:
- 接收後立即回
1|OK(綠界規範) - 用 message queue 非同步處理
- 同一筆訂單冪等處理(避免重複)
- 每日跑對帳排程比對綠界後台與本地 DB
16.3 對帳機制
- 每日 02:00 拉前一天 Stripe / 綠界交易,比對本地 DB
- 不一致時寄 alert 給工程與財務
- 對帳結果存
payment_reconciliationtable
17. 安全與合規(必讀)
17.1 PCI DSS
- 前端絕對不能收集卡號
- 卡號輸入一律走 Stripe Checkout(hosted page)或綠界 hosted page
- 若未來需要嵌入式體驗,必須使用 Stripe Elements / 綠界 SDK(卡號送到第三方 iframe,不經過本站伺服器)
- 違反 PCI DSS 罰款最高每月 USD 100,000,且銀行可終止商戶帳號
17.2 機密管理
| 機密 | 存放位置 | 前端可見? |
|---|---|---|
| Stripe Secret Key | 後端環境變數 | ❌ |
| Stripe Publishable Key | 後端配置 / 前端 build-time 環境變數 | ✅ |
| 綠界 HashKey / HashIV | 後端環境變數 | ❌ |
| Webhook Signing Secret | 後端環境變數 | ❌ |
17.3 Webhook 簽章驗證
所有 webhook 必須驗證簽章,否則攻擊者可偽造 webhook 觸發業務邏輯。
17.4 退款政策
- 退款一律由客服後台處理,前端不提供使用者自助退款
- 退款規則由 PM 與法務確認後寫入 ToS
18. 監控與告警
18.1 關鍵指標
| 指標 | 告警閾值 |
|---|---|
| Checkout 成功率 | < 80% 連續 1 小時 |
| Webhook 處理成功率 | < 99% 連續 1 小時 |
| 續扣失敗率 | > 5% 單日 |
| Stripe / 綠界 API 5xx 率 | > 1% 連續 15 分鐘 |
| 對帳差異筆數 | > 0 即告警 |
18.2 Logs
- 所有金流相關請求與 webhook 必須記錄完整 payload(注意脫敏卡號)
- 保留至少 1 年供稽核
19. 里程碑與時程(3 個月,目標「金流通」)
Sprint 1(Week 1-2):規劃與帳號準備
- 前後端對齊本 PRD
- 申請 Stripe Taiwan 商戶帳號
- 申請綠界商戶帳號
- 申請電子發票(ezPay 或綠界)
- DB schema 設計(含 Phase 2 預留欄位)
- PaymentProvider interface 定義
- Admin 後台位置決策(既有後台 / 新 app / 低程式碼)
Sprint 2-3(Week 3-6):後端核心
- PaymentProvider 抽象層(訂閱 + 一次性收款)
- Stripe 整合(checkout、subscription、payment link、webhook)
- 綠界整合(定期定額、一次性訂單、webhook)
- 電子發票整合
- 訂閱狀態機與 DB
- 自定義收款單 CRUD + 寄信
- 前後端 API 串接
Sprint 4(Week 7-8):雇主側前端
- 路由與 store 建置
- 方案頁、結帳頁、訂閱管理頁
- 發票列表、付款方式管理
- 自定義收款付款頁(
/billing/pay/:token) - 路由守衛、狀態 banner
- i18n(zh-TW、en-US)
Sprint 5(Week 9-10):Admin 後台
- 自定義收款單列表、建立、詳情頁
- 訂閱列表、發票列表
- 對帳頁
- 補寄、作廢、查詢功能
Sprint 6(Week 11-12):Dunning、對帳、上線準備
- Dunning 流程(後端)
- 對帳排程(後端)
- 監控告警
- 客服 SOP 文件(退款、補單、查詢)
- 法務審 ToS / 退款政策
- E2E 測試(Cypress)
- Beta 內測
20. 風險與緩解
| 風險 | 影響 | 緩解 |
|---|---|---|
| 綠界 webhook 不穩 | 訂閱狀態不同步 | 對帳排程 + 主動 polling |
| Stripe Taiwan 申請被拒 | 海外金流卡關 | 提早申請(4 週緩衝),備案改 PayPal |
| 法律 ToS 未準備好 | 上線延期 | Sprint 1 同步啟動法務作業 |
| PCI DSS 不熟導致誤收卡號 | 巨額罰款 | Code review 強制檢查卡號相關欄位 |
| 海外雇主拒付 (chargeback) | 收入損失 + 罰款 | 啟用 Stripe Radar 風控 |
| 匯損超出預期 | 毛利下降 | 監控匯率,必要時啟動雙幣戶頭 |
21. Open Questions(待確認)
- PM:實際方案定價與權限對應(本 PRD 為範例)
- PM:是否需要年費自動續訂提前通知(GDPR / 加州法規要求)
- 法務:ToS 與退款政策草稿
- 財務:發票抬頭、稅務處理(是否要報營業稅)
- 老闆:台灣公司商業登記是否已完成
- 後端:是否獨立
payment-service微服務 vs 併入既有 monolith - 設計師:完整 Figma 流程(方案頁、結帳頁、訂閱管理頁)
- 客服:退款 SOP 與後台需求
22. 參考資料
- Stripe Subscriptions: https://stripe.com/docs/billing/subscriptions/overview
- Stripe Smart Retries: https://stripe.com/docs/billing/revenue-recovery/smart-retries
- 綠界定期定額: https://www.ecpay.com.tw/Service/credit_card_subscription
- ezPay 電子發票: https://www.ezpay.com.tw/about/about_invoice
- PCI DSS Quick Reference: https://www.pcisecuritystandards.org/
前端團隊 TL;DR:
雇主側做 §10.1 的 8 個頁面、串 §11 的 API、掛上 §12 的訂閱狀態機(簡化版,無 trialing)、路由守衛照 §10.4 寫。Admin 後台做 §10.2 的 6 個頁面(或評估用低程式碼工具)。所有卡號相關 UI 絕對交給 Stripe / 綠界 hosted page,自己不要收。試用 / 升降級本期不做,但 store 與 type 預留欄位。預估前端工作量約 4 個 sprint(雇主側 2 + admin 2)。
後端團隊 TL;DR:
PaymentProvider 抽象(§8.3)涵蓋訂閱 + 一次性收款、Webhook(§16)、訂閱狀態機(§12)、Dunning(§9.4)、對帳(§16.3)、電子發票整合(§15)、自定義收款單系統(§7.2 + §9.2 + §11.8)。預估後端工作量約 6 個 sprint,建議獨立 payment-service 微服務。
本期 MVP 不做但要預留的事:
- 試用期(DB 預留
trial_end欄位、PaymentProvider 預留 method、subscription type 預留trialing狀態) - 升降級(DB 預留
pending_plan_change、PaymentProvider 預留changePlanmethod) - 折扣碼、多幣別、點數、自助退款
版本歷史:
- v0.2(2026-06-22):縮減 MVP 範圍,移除試用 / 升降級到 Phase 2,新增 admin 自定義收款
- v0.1(2026-06-22):初版,含完整訂閱漏斗(試用、升降級、proration)