系統地圖 老闆報告 Spec · 取消退款邏輯 audit
SPEC · 2026-05-17 · 老闆選 ⭐ Option C · ✅ SHIPPED Phase 1

取消訂位退款邏輯 audit + 藍新整合 spec

老闆 dogfood 問:「點同意退款會真的退款嗎?藍新測試環境會收到 callback 嗎?」
Audit 結論:不會 —目前 POS 同意退款只是「假退款」,完全沒呼叫藍新
老闆選 Option C → 已 ship orchestrator dispatch(等老闆 sandbox 測試卡 dogfood)

✅ Phase 1 SHIPPED(commit 待 push)— Option C orchestrator dispatch 完成: 下一步:老闆刷一筆 sandbox 測試卡 → 取消 → 同意退款 → 看藍新後台是否真退款

📍 老闆 dogfood 問題

1. 餐廳端點客人申請取消如果需要退款時,一樣會退款嗎?
2. 客人現在在測試環境的藍新金流刷卡後,我這邊操作退款,如果測試環境有退款成功是不是應該要收到藍新金流的通知?

🔴 Audit 結論:目前 POS 同意退款只是「假退款」

沒實際呼叫藍新 refund API
`setCancellationResolution` 直接設 DB deposit_status='refunded' + refund_status='succeeded' 假裝退款,但完全沒呼叫 newebpay-refund edge function(該 function 已實作但從未被 cancellation flow 呼叫過)。

現狀流程

POS 老闆按「同意取消並退款」
  ↓
[POST /api/reservation/<id>/cancellation/approve]
  body: { refund_kind: 'refund', refund_amount, refund_account_digits, ... }
  ↓
[approveCancellation orchestrator]
  ↓
[reservationRepo.setCancellationResolution]
  CASE refund:
    deposit_status = 'refunded'        ← 假設成功
    refund_status = 'succeeded'         ← 假設成功
    refunded_at = now                   ← 假設成功
    refund_amount, refund_account_last_four, refund_note 寫入
  ↓
[fire-and-forget triggerCancellationEmail]
  → send-cancellation-email edge function (寄信通知客人)
  ↓
return success

❌ 完全沒呼叫 newebpay-refund edge function

後果

  1. 客人信用卡 沒收到退款(藍新測試環境 / production 都一樣)
  2. DB 顯示「已退款」(deposit_status=refunded, refund_status=succeeded)→ 老闆以為退了
  3. 藍新沒收 callback(因為沒 invoke API)
  4. 老闆對帳會發現「DB 寫退款 vs 藍新沒記錄」對不起來

連帶 Bug 4(老闆原話)— 已部分修

退款待處理完成之後,應該要從「退款待處理」的標籤底下移到已取消的分類

實際 server 設 deposit_status='refunded' → POS badge filter 確實看 .paid 才算,理論上 .refunded 不算。但 SwiftData sync lag 老闆當下看到沒更新

短期已修(commit 3dd61a5f):POS filter 加 refundedAt == nil 排除已退款。Long-term 需 sync handler 改進

💡 Spec 提案 — 3 個 Option

Option A · 走藍新自動退款

客人原本用藍新刷卡付訂金 → 老闆同意退款 → server 自動呼叫藍新 CreditReturn API。

POS 老闆按「同意取消並退款」
  ↓
[approveCancellation orchestrator]
  ↓
[setCancellationResolution]
  refund_status = 'processing'   ← 改成 pending(等藍新 callback)
  deposit_status 暫不改          ← 等 callback 才設 refunded
  ↓
[invoke supabase functions newebpay-refund]
  body: { action: 'deposit-refund', payment_id, amount, reservation_id }
  ↓
[藍新呼叫 CreditReturn API]
  ├─ 成功 → 藍新 callback → /api/newebpay/deposit/notify
  │         → set deposit_status='refunded' + refund_status='succeeded'
  │         → 寄 confirmation email
  └─ 失敗 → callback 帶 error → set refund_status='failed' + refund_failed_reason
            → cron retry-failed-refunds 重試 / 升級 manual_required

POS 在這段時間 badge 顯示「⏳ 處理中」直到 callback。

優點:全自動,客人立刻收到藍新退款。
缺點:只能用於藍新付的訂位(手動匯款的不適用)。

Option B · 老闆手動退款

客人原本用銀行轉帳付訂金 → 老闆同意退款 → 老闆自己 ATM 匯回客人 → POS 標記完成。

POS 老闆按「同意取消並退款」
  ↓
[setCancellationResolution]
  deposit_status = 'refunded'(立刻設)
  refund_status = 'manual_completed'
  refund_account_last_four 記錄(老闆已 ATM 匯款的帳號)
  ↓
[寄 email 給客人]
  「店家已退款 NT$X 至 ATM 帳號 ****X,1-3 天到帳」

優點:手動匯款的訂位可用。
缺點:老闆要手動操作 ATM,不靠藍新。

❓ 給老闆的問題

Q1. 選哪個 Option?
推薦 Option C — 兩條 path 自動分流,涵蓋所有訂位類型。
Q2. 測試環境驗證機制
• 藍新 sandbox 退款是否真會 callback?(會,但 status 是 code=SUCCESS test mode)
• 測試 callback URL 設定:https://chefsmate.app/api/newebpay/deposit/notify(production)+ ngrok / preview env?
Q3. 失敗 retry 策略
目前 retry-failed-refunds cron 5 次後升級 manual_required,要保留嗎?
Q4. 老闆 UX:藍新退款處理中(等 callback)時 POS 顯示什麼?
建議:「⏳ 退款處理中,稍後自動完成」+ 不允許重複按確認。

⚠️ 風險

🚀 建議 ship 順序

  1. 先寫單元測試:approveCancellation refund path 應走藍新
  2. dev 環境 wire:approveCancellation 條件式呼叫 newebpay-refund
  3. 老闆 sandbox dogfood:刷測試卡 → 取消 → 同意退款 → 看藍新後台是否 refund
  4. production 推(老闆 OK 才 ship)
預估 2-3 天 implementation + 1 天 dogfood

📦 關聯既有 spec

檔案狀態
supabase/functions/newebpay-refund/index.tsedge function 已實作
supabase/functions/retry-failed-refunds/index.tscron retry 已實作
supabase/functions/send-refund-confirmation/index.ts退款確認 email 已實作
缺的只是「approveCancellation 呼叫 newebpay-refund」這個 wire本 spec 要做的事

2026-05-17 dogfood spec · commit 3dd61a5f 含 Bug 1+4 mitigation · Bug 2/3 等老闆 confirm Option 才 ship