Skip to content

[UX-C] 成員回饋迴路 #45

Closed

Description

@poterpan

UX 健檢批次 C(P0×2 + 9 項)。唯一新增對外訊息的批次,所有新通知必須走 claimNotification dedup。

  • P0-5 審核結果回不到成員:新增 receipt 通知(至少 reject 帶原因 @ 當事人;verify 可選)
  • P0-6 成員繳費頁英文技術錯誤中文化(Discord 路徑的中文字串已存在)
  • C1 手動/CSV 入職零通知(nudge 勾選框因 diff 為空不出現;且該 nudge 未走 claim) / C2 個別催繳(sendPaymentNudge 已支援 people[]) / C3 成員「我的帳單」查詢 / C4 綁錯名字死路 / C5 成員 web 頁不可達+失效頁死路 / C6 兩套「你要繳多少」算法收斂(等 A 的改價語意定案) / C7 R2 未設定時 /繳費 截圖靜默丟棄 / C8 三繳費入口能力差異提前告知 / C9 未綁定成員後台可見(listUnboundUsers 已存在)

Activity

  1. poterpan commented on Aug 29, 2026

    @poterpan
    OwnerAuthor

    ⚠️ 遷移檔名衝突警告(2026-08-29)

    本 issue 分支的 0006_notification_event.sql 與已部署的 #48 0006_overdue_daily.sql 撞號。

    合併前必須:

    1. 本線遷移改名為 0007_notification_event.sql。
    2. 與 0006 的既有 schema 對齊:event 欄已經存在,重建時要保留它、並把 type 的 CHECK 從現有值擴充為 nudge / receipt(不可假設表還是 0005 的舊結構)。
    3. 不可直接推原 0006 檔名。
  2. poterpan commented on Aug 29, 2026

    @poterpan
    OwnerAuthor

    🚨 高風險:本線遷移會靜默清空 #48 的正式資料

    承上一則撞號警告,實際比對後發現不只是「檔名撞號」,內容也會造成資料流失。

    packages/worker/migrations/0006_notification_event.sql 的搬移語句目前是:

    SELECT id, workspace_id, type, period, plan_id, user_id, subscription_id, '',
           external_channel_type, external_message_id, sent_at
    FROM notification_logs;

    第 4 個位置把 event 硬寫成空字串 ''。在本線切出去的當下這是正確的(那時 event 欄還不存在)。但 #48(PR #49)已經上線,正式環境的 notification_logs.event 現在存著逐日催繳的 day-key(如 2026-08-29)。

    若照現況套用,會把所有 day-key 清成 '':

    • [Feature] 催繳邏輯改善 #48 的「每期每日一則」去重鍵全毀,退化成「每期一則」
    • UNIQUE 撞列風險
    • 完全無聲——不會報錯,測試也不會抓到(測試環境是從 0001 重跑)

    改法(改名 0007_notification_event.sql 後)

    1. 保留欄位值:'' → event,不可覆寫。
    2. 不要重新宣告 event TEXT NOT NULL DEFAULT '',它已由 0006 建立;本次只需把 type 的 CHECK 從 ('billing_opened','overdue','receipt') 擴充為加上 'nudge'。
    3. 檔頭註解說明:接在 0006_overdue_daily 之後,且 event 現由三種語意共用——overdue = day-key、receipt = reject/verify、billing_opened/nudge = ''。

    另外,rebase 會有實質衝突(非單純文字衝突)

    本線的 core/notify.ts 與 core/scheduled.ts 停在舊 main。#48 在同樣位置加了 claimNotificationSlot / releaseSlot / dayKey 逐日邏輯。rebase 時必須兩套語意都保留(#48 的逐日 + 本線的 receipt/nudge),不可二選一覆蓋。

  3. poterpan commented on Aug 29, 2026

    @poterpan
    OwnerAuthor

    ✅ 遷移危險已修復 + rebase 完成(2026-08-29)

    分支 ux/45-member-feedback 已 rebase 到含 #48 的 main(f246c71),前述資料流失風險已解除。

    處理內容:

    1. 遷移改名 0006_notification_event.sql → 0007_notification_event.sql
    2. 修掉資料流失:INSERT ... SELECT 的 event 改為原值傳遞(原本硬寫 '')
    3. 不重新宣告 已存在的 event 欄,本次重建只把 type CHECK 擴充加入 'nudge'
    4. 實質衝突已合併:core/notify.ts 與 core/billing.ts 兩邊語意都保留——[Feature] 催繳邏輯改善 #48 的
      claimNotificationSlot / releaseSlot / dayKey 逐日邏輯,與本線的
      releaseNotification / releaseReceiptSlots / receipt / nudge 並存

    驗證方式(不只靠測試)

    用 sqlite3 實際重放 0001 → 0007,模擬正式環境已有一筆帶 day-key 的 overdue 列:

    • 套用修正版 0007:event = 2026-08-29 保留 ✅
    • 反證:把該行換回 '' 再跑一次 → event = ''(確實會被清空)
    • nudge type 現已被 CHECK 接受 ✅

    誠實補充

    新增的 schema 測試無法捕捉這個 bug——測試環境先跑完 migrations 才有資料,重建的
    SELECT 永遠碰不到測試列。這點已寫成註解留在 test/schema.test.ts,避免後人誤以為有測試保護。
    真正的驗證是上面的 sqlite 重放。

    現況

    Worker tests 373 passed / 52 files,tsc --noEmit 綠。

    剩餘工作:Task 3–16(P0-5 回條主體、P0-6 中文化、C1–C9),共 14 項尚未開始。

  4. poterpan commented on Aug 29, 2026

    @poterpan
    OwnerAuthor

    ✅ 全部 16 個 task 完成(2026-08-29)

    分兩個 PR:

    完成清單

    編號 內容
    P0-5 退回一定在頻道 @ 當事人並附原因;確認可設定(預設關)
    P0-6 成員繳費頁 8 處英文技術錯誤中文化
    C1 CSV 匯入/新增訂閱都能通知新成員
    C2 成員×期別頁可單獨催繳這一位
    C3 /我的帳單 自助查詢
    C4 綁錯名字可自助解綁重綁
    C5a/b Discord 可取得網頁連結;失效頁改指真的走得通的路
    C6 繳費頁金額改讀本期帳單
    C7/C8 三個繳費入口的能力差異事先講清楚
    C9 未綁定成員在後台可見 + 同步彈窗說出 @ 不到的人

    順帶修掉 healthcheck P0-8(/繳費 指令描述的「可選」)——批次 D(#46)請跳過此項。

    驗收

    423 passed / 57 files(起始基準 355);pnpm -r typecheck 綠;admin + web build 綠。

    ⚠️ 部署後必做

    /繳費 指令描述有變更,且新增了 /我的帳單 — 部署後必須到後台 → 設定 → 工具 → 註冊 Discord 指令按一次,否則成員在 Discord 看到的仍是舊的指令清單與描述。

    待辦

    Codex 跨引擎收尾審查因額度用盡尚未執行,額度恢復後補送。

  5. poterpan commented on Aug 30, 2026

    @poterpan
    OwnerAuthor

    🔍 Codex 跨引擎審查完成(2026-08-30)— 判定 DO-NOT-MERGE,三項全部修正

    findings 與處置

    # 發現 驗證 處置
    1 core/nudge.ts 送失敗時以 key 刪除 slot:併發的兩次 force 之間,B 可能在 A 的 claim 與 release 之間換掉那一列,A 的 keyed DELETE 會毀掉 B 的有效 slot ✅ 屬實。這正是 #48 在 core/scheduled.ts 修過的同一缺陷,nudge.ts 又犯了一次 改用 claimNotificationSlot 回傳的 row id + releaseSlot
    2 沒有頻道/token 時在查詢收件人之前就返回,unbound 恆為 0——「沒設定 Discord」與「沒有人需要通知」在 UI 上長得一模一樣 ✅ 屬實 先算收件名單再返回,新增 transport 欄位(no_channel/no_bot_token),nudgeSummary 講出真正原因並仍列出未綁定的人
    3 0 byte 檔案走 size 分支,成員看到「檔案太大(上限 10 MB)」——與事實相反 ✅ 屬實 InvalidImage.reason 新增 empty,文案改為「這個檔案是空的(0 KB)」

    回歸測試有真的驗過

    第 1 項我沒有只寫測試就算數——把修法改回舊寫法,該測試確實會紅(expected +0 to be 1,B 的 slot 被毀)。改回正確版本後才綠。

    第 2、3 項也各補一條測試。

    驗收

    Worker 測試 423 → 426,pnpm -r typecheck 綠,admin + web build 綠。README 徽章同步為 426。

    commit d28e2d1

  6. added 2 commits that reference this issue on Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions