Skip to content

feat(#45): 通知 slot 底座 + 遷移危險修復 + 退回必發回條 (P0-5) - #50

Merged
poterpan merged 6 commits into
mainfrom
ux/45-member-feedback
Aug 29, 2026
Merged

poterpan merged 6 commits into
mainfrom
ux/45-member-feedback

Conversation

@poterpan

Copy link
Copy Markdown
Owner

UX 健檢批次 C(#45)的第一批:通知 slot 底座、遷移危險修復,以及 P0-5 的主軸「退回必發回條」。

本 PR 刻意不涵蓋 #45 的全部 16 項任務——這批本身完整且獨立,而且含有一個會破壞正式環境資料的修復,值得先落地讓 main 穩定(#44 / #46 的 rebase 基準也才穩)。

🚨 最重要的一項:修掉會靜默清空正式資料的遷移

本分支原有 0006_notification_event.sql,與已上線的 #48 0006_overdue_daily.sql 撞號。更嚴重的是它的搬移語句把 event 硬寫成空字串:

SELECT ..., subscription_id, '', ...   -- ❌ 會清空 #48 的逐日 day-key

分支切出去時這是對的(那時 event 欄還不存在),但 #48 已上線,正式環境的 notification_logs.event 現在存著逐日催繳的 day-key。照原樣套用會:

  • 把所有 day-key 清成 '' → [Feature] 催繳邏輯改善 #48 的「每日催繳」退化成「每期一則」
  • 完全無聲:不報錯,測試也抓不到(測試環境從 0001 重跑,重建的 SELECT 永遠碰不到測試列)

修法:改名 0007_notification_event.sql、event 改為原值傳遞、不重新宣告已存在的欄位,本次重建只把 type CHECK 擴充加入 'nudge'。

驗證方式(不是靠測試,因為測試根本測不到):用 sqlite3 實際重放 0001 → 0007,種一筆帶 day-key 的 overdue 列:

版本 結果
修正版 0007 event = 2026-08-29 ✅ 保留
反證(換回 '') event = '' ❌ 確實被清空

這個限制已寫成註解留在 test/schema.test.ts,避免後人誤以為有測試保護。

rebase:兩套語意都保留

分支基底是 #48 之前的 main,core/notify.ts 與 core/billing.ts 有實質衝突(非單純文字衝突)。已手工合併,兩邊都保留:

P0-5:退回必發回條

成員送出繳費被退回後,過去完全收不到任何通知——這是 UX 健檢列為 P0 的死路。

  • 新增 core/receipt.ts 的 announcePaymentReceipt:以 (帳單, event) 為去重鍵,退回與確認是兩個 slot
  • 一位成員同期多筆合併成一則訊息,不是 N 則(一鍵全部核准會驗 N 筆)
  • 沒有頻道或 bot token 時不佔用 slot(同 cron 規則),之後設定好才不會撲空
  • 送失敗把 slot 還回去,避免這張帳單的回條被永久靜音
  • 成員重新送出時釋放該期回條 slot——下次審核是新事實,要能再講一次
  • 接上 POST /admin/payments/:id/reject:退回已提交,Discord 失敗不轉成 500,誠實回報 notified 筆數

測試

382 passed / 53 files(main 基準 355)。pnpm -r typecheck 綠,admin + web build 皆綠。

新增涵蓋:回條去重、跨 event 分離、多筆合併成一則、重送後可再發、送失敗還 slot、無投遞管道不佔 slot、路由層退回會記錄、Discord 失敗不 500。

尚未涵蓋(#45 後續 PR)

Task 4–16:確認回條開關、P0-6 錯誤中文化、C1–C9(金額收斂、三入口能力差異、/我的帳單、綁錯名字、個別催繳、未綁定成員可見度等)。

審查註記

Codex 跨引擎審查因額度用盡尚未執行,額度恢復後補送(owner 已知悉)。

同一張帳單的退回與確認需要兩個 dedup slot,個別催繳需要自己的 type。
SQLite 無法 ALTER CHECK/UNIQUE,故以 0006 重建表。收回本期開繳時一併清掉
該期的 receipt/nudge slot。
該列不是送信紀錄,而是「本期已開繳」的定義;單獨刪除會讓待繳帳單留在
成員已無法繳費的期別裡(半套收回)。以 ReleasableKey 於編譯期擋掉,
並以 @ts-expect-error 測試釘住(tsconfig 涵蓋 test/,放寬即建置失敗)。
投遞走帳單頻道 @:adapter 只有 channel API,且既有的開繳/催繳/入職提醒
都在同一個頻道,成員的目光已經在那裡。未綁定者退回粗體姓名、不 ping。

回傳 boolean 而非計畫寫的 void,跟齊 #43 的 Notifier 契約:只有 Discord 真的
回 2xx 才算送達,呼叫端才有辦法在失敗時把 receipt 名額釋放掉。
rebase 到含 #48 的 main 之後:

- 遷移改名 0007_notification_event.sql(原 0006 與已上線的 0006_overdue_daily 撞號)
- **關鍵**:INSERT ... SELECT 的 event 改為原值傳遞(原本硬寫 '')。0006 已在正式環境
  寫入逐日催繳 day-key,硬寫 '' 會靜默清空它們,使 #48 退化成每期一則且無任何錯誤。
  已用 sqlite3 重放 0001→0007 驗證:day-key 保留;替換成 '' 作反證:確實被清空。
- 本次重建只擴充 type CHECK 加入 'nudge',不重新宣告已存在的 event 欄
- schema 測試補上 event 參與去重鍵、nudge type 與 (entity,event) 去重
- overdue-daily 測試的 Notifier 假件補 sendPaymentReceipt(介面已加寬)
成員送出繳費後被退回,過去完全收不到任何通知——這是 UX 健檢的 P0-5。

- 新增 core/receipt.ts 的 announcePaymentReceipt:以 (帳單, event) 為去重鍵,
  退回與確認是兩個 slot;一位成員同期多筆合併成「一則」訊息而非 N 則
- 沒有頻道或 bot token 時不佔用 slot(同 cron 規則),之後設定好才不會撲空
- 送失敗把 slot 還回去,避免這張帳單的回條被永久靜音
- 接上 POST /admin/payments/:id/reject:退回已提交,Discord 失敗不轉成 500,
  誠實回報 notified 筆數
- 成員重新送出時(settleUserPeriod)釋放該期回條 slot,下次審核是新事實要再講

測試 373 → 382
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant