diff --git a/README.md b/README.md index 9cf062d..64f3a24 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-F38020?logo=cloudflare&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white) ![React](https://img.shields.io/badge/React-18-61DAFB?logo=react&logoColor=black) -![Vitest](https://img.shields.io/badge/tests-426%20passing-0f6e63?logo=vitest&logoColor=white) +![Vitest](https://img.shields.io/badge/tests-431%20passing-0f6e63?logo=vitest&logoColor=white) ![Serverless](https://img.shields.io/badge/100%25-serverless-074340)
@@ -65,23 +65,23 @@ pre-wired) and a multi-workspace-ready data model, so it generalizes well beyond plans it couldn't match, cancelled subs that need a human) before you apply it; idempotent re-runs. - 🧾 **Review queue + reconciliation** — an admin dashboard with per-plan / per-channel totals, a one-click verify queue, manual back-fill, single-payment delete, undo-verify, and frozen period - amounts (price changes never rewrite history). A **重新同步本期帳單** action re-aligns an opened + amounts (price changes never rewrite history). A **重新同步此期帳單** action re-aligns an opened period's bills to the current roster/price (preview before applying), optionally pinging newly-added members. Tapping a member's name (or a submission alert) opens the **成員×期別合併審核**: the shared - screenshot once, every settled row, and one 一鍵全部核准 button. The queue, that view and the + screenshot once, every settled row, and one 一鍵全部驗證 button. The queue, that view and the review dialog all work on a phone. - 🔔 **Customizable notifications** — editable templates (with live preview + validation) for the billing-opened notice, the batched overdue reminder, and the persistent pay message. - 📲 **Submission alerts** — when a member submits a payment, push the owner a Bark and/or webhook notice (Discord / Google Chat / Slack — body shape auto-detected by host) with a deep link straight to that member's period review — one submit settles every subscription, so the link - opens all of them together with a 一鍵全部核准 button. + opens all of them together with a 一鍵全部驗證 button. - ⏰ **Daily cron, idempotent** — opens billing, sends an overdue reminder **every day** until a period has no unpaid bills left, and enforces screenshot retention — all deduped through `notification_logs`. - 🛡️ **Access-gated admin** — the whole admin host sits behind Cloudflare Access (email OTP); the SPA and its API are same-origin so the Access JWT reaches the Worker. -- 🧪 **Real-runtime tests** — 426 Vitest cases run against actual Miniflare D1 + R2 (FK constraints +- 🧪 **Real-runtime tests** — 431 Vitest cases run against actual Miniflare D1 + R2 (FK constraints enforced), not mocks. ## How a payment flows @@ -226,7 +226,7 @@ payment screenshots) and fill in `wrangler.toml` accordingly — `database_id`, - **Review queue** — Payments → status pills → the **已繳待驗** queue floats to the top → one-click ✅ verify (or open a row for screenshots, channel, reject, amount override, delete proof, **撤回驗證** to undo a mistaken verify, or **delete the whole bill**). -- **重新同步本期帳單** — on Payments, re-align the selected opened period's bills to the current +- **重新同步此期帳單** — on Payments, re-align the selected opened period's bills to the current roster/price (add missing · remove de-subscribed · reprice pending · freeze settled), with a preview before applying and an option to ping newly-added members with the pay button. - **發起繳費** — confirm the selected period's per-plan amounts (any change becomes the plan's new @@ -244,11 +244,11 @@ payment screenshots) and fill in `wrangler.toml` accordingly — `database_id`, **重置催繳發送紀錄**. The first two show the exact recipients before sending; all three take a confirmation step and report the real counts — a non-2xx from Discord is reported as a failed send, so the billing notice's `sent_at` is never moved forward and the overdue dedup slot is - released so the next run (cron included) can try again. Reopening/closing a period lives in 收回本期開繳 on + released so the next run (cron included) can try again. Reopening/closing a period lives in 收回此期開繳 on the payments page, not here. - **Submission alerts** — set a Bark device key and/or a webhook (Discord / Google Chat / Slack) under Settings → 繳費通知; each new submission then pushes you a notice that opens that member's whole - period for review (shared screenshot once, every row listed, 一鍵全部核准), phone-friendly. Both + period for review (shared screenshot once, every row listed, 一鍵全部驗證), phone-friendly. Both are optional and best-effort (a slow or failing endpoint never blocks the payment). - **Daily cron** (01:00 UTC = 09:00 Asia/Taipei) — idempotently opens each period's bills, posts the billing-opened notice (tagging plan roles), sends **an overdue reminder every day** listing the diff --git a/README.zh-TW.md b/README.zh-TW.md index 581f06e..0b45bea 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -9,7 +9,7 @@ ![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-F38020?logo=cloudflare&logoColor=white) ![TypeScript](https://img.shields.io/badge/TypeScript-5-3178C6?logo=typescript&logoColor=white) ![React](https://img.shields.io/badge/React-18-61DAFB?logo=react&logoColor=black) -![Vitest](https://img.shields.io/badge/tests-426%20passing-0f6e63?logo=vitest&logoColor=white) +![Vitest](https://img.shields.io/badge/tests-431%20passing-0f6e63?logo=vitest&logoColor=white) ![Serverless](https://img.shields.io/badge/100%25-serverless-074340)
@@ -45,7 +45,7 @@ ChipPot 解決的是一個很具體的痛點:社團大量採購 OpenAI / Anthr - 💳 **Discord 內繳費** — 常駐「繳費」按鈕 → 選渠道 → 完成。一次送出就把該成員**當期所有訂閱** 一起結清(多方案加總)。 -- 🔗 **自助綁定** — 成員自己把 Discord 帳號接到名單:`/綁定`、繳費按鈕,或帳單頻道裡常駐的公開 +- 🔗 **自助綁定** — 成員自己把 Discord 帳號接到名單:`/綁定`、繳費按鈕,或繳費頻道裡常駐的公開 **綁定 Discord** 按鈕。名單超過 Discord 選單的 25 筆上限時改用搜尋(`/綁定` 的「名字」自動完成, 或按鈕跳出的搜尋彈窗)。管理員也能手動指定 ID,或**解除綁定**讓成員重新綁。 - 🧾 **審核結果會回到成員手上** — 退回一定在頻道 @ 當事人並附上原因(成員唯一知道要重繳的管道, @@ -59,19 +59,19 @@ ChipPot 解決的是一個很具體的痛點:社團大量採購 OpenAI / Anthr 代表訂閱(或恢復暫停中的訂閱),`FALSE` 代表暫停該訂閱,留空則完全不動。每次上傳都會先顯示完整 差異預覽(新成員、新增/暫停/恢復的訂閱、對不到的方案、需人工處理的已取消訂閱)再套用,可冪等重跑。 - 🧾 **審核佇列 + 對帳** — 後台看板有各方案/各渠道金額、一鍵驗證佇列、手動補登、單筆繳費刪除、 - **撤回驗證**(誤按的驗證可還原),且**當期金額凍結**(改價不會回頭改歷史帳)。**重新同步本期帳單** + **撤回驗證**(誤按的驗證可還原),且**當期金額凍結**(改價不會回頭改歷史帳)。**重新同步此期帳單** 會把已開期別的帳單重新對齊目前名單/現價(先看差異預覽再套用),並可順便 @ 通知新加進來的成員。 點成員名字(或從繳費推播點進來)會開啟**成員×期別合併審核**:共用截圖只出現一次、當期每一筆都列出, - 已繳待驗的可一鍵全部核准。佇列、這個畫面與審核彈窗在手機上都能用。 + 已繳待驗的可一鍵全部驗證。佇列、這個畫面與審核彈窗在手機上都能用。 - 🔔 **可自訂通知** — 開繳通知、整批逾期催繳、常駐繳費訊息三種文字皆可自訂模板(含即時預覽 + 格式驗證)。 - 📲 **繳費推播** — 成員送出繳費時,可推一則 Bark 和/或 webhook(Discord/Google Chat/Slack,body 格式依主機自動判斷)給擁有者,並帶上直接跳到該成員該期審核的深連結——一次送出會結清所有訂閱, - 所以連結會把它們一起打開,可一鍵全部核准。 -- ⏰ **每日 cron、冪等** — 自動開帳、進入催繳後**每天發一則**整批催繳(直到全部繳完)、執行 + 所以連結會把它們一起打開,可一鍵全部驗證。 +- ⏰ **每日 cron、冪等** — 自動開繳、進入催繳後**每天發一則**整批催繳(直到全部繳完)、執行 截圖保存期清理,全部經 `notification_logs` 去重。 - 🛡️ **Access 保護的後台** — 整個後台主機在 Cloudflare Access 後(email OTP);SPA 與其 API 同源, Access JWT 因此能到達 Worker。 -- 🧪 **真環境測試** — 426 個 Vitest 案例跑在真正的 Miniflare D1 + R2(強制 FK 約束),不是 mock。 +- 🧪 **真環境測試** — 431 個 Vitest 案例跑在真正的 Miniflare D1 + R2(強制 FK 約束),不是 mock。 ## 一筆繳費怎麼跑 @@ -81,10 +81,10 @@ ChipPot 解決的是一個很具體的痛點:社團大量採購 OpenAI / Anthr ├─ 還沒綁定? → 「選你的名字」下拉 → 綁定 Discord id 後,接著繳費 │ ▼ - ephemeral:列出本期各方案 + 總額 + 渠道下拉 + ephemeral:列出該期各方案 + 總額 + 渠道下拉 │ (/繳費 或網頁可另附截圖/備註) ▼ - settleUserPeriod() — 把每筆 pending/rejected 標記為「已繳」,共用同一張截圖 key + settleUserPeriod() — 把每筆 pending/rejected 標記為「已繳待驗」,共用同一張截圖 key │ ▼ 後台看板 → 審核佇列 → ✅ 驗證(自動帶入申報渠道) @@ -98,7 +98,7 @@ ChipPot 解決的是一個很具體的痛點:社團大量採購 OpenAI / Anthr ``` Discord ─┐ ┌─ D1 (chippot-db) — SQLite 帳本 網頁上傳 ─┼─► Cloudflare Worker ───────┤─ R2 (chippot-proofs) — 私有截圖 -後台 UI ─┤ core + adapters └─ Cron(每日 01:00 UTC) — 開帳 / 催繳 / 保存期 +後台 UI ─┤ core + adapters └─ Cron(每日 01:00 UTC) — 開繳 / 催繳 / 保存期 Cron ─┘ ``` @@ -199,6 +199,7 @@ pnpm --filter @chippot/worker register Discord guild/頻道 id、可發起繳費的管理員白名單(`admin_discord_ids`)、三種可自訂的通知模板, 以及選填的**繳費推播**(Bark 裝置金鑰和/或 incoming webhook;推播裡的審核深連結由 `ADMIN_ORIGIN` 組出,不必另外設定)。 + `timezone` 目前是死設定——時區固定為 Asia/Taipei(`core/time.ts`),改這個欄位沒有任何效果。 - **Discord** — 把 app 的 Interactions Endpoint 設成 Worker 的 `/interactions`,再用上面的腳本註冊 guild 指令。 ## 後台與營運 @@ -208,26 +209,26 @@ pnpm --filter @chippot/worker register **申報渠道**(對帳最常看的一欄,不必再點進詳情;「來源」移到詳情彈窗),沒設定 R2 時整欄隱藏「憑證」。 點開某筆可看截圖、渠道、退回、改金額、刪截圖,也可**撤回驗證**(還原誤按的驗證)或**刪除整筆帳單**。 - **成員×期別合併審核** — 點表格裡的成員名字(或從繳費推播的深連結進來),把該成員當期的帳單收在 - 同一頁:共用截圖只顯示一次、每一筆都列出且可單筆核准/退回,已繳待驗的可一鍵掃過去核准(還有 + 同一頁:共用截圖只顯示一次、每一筆都列出且可單筆驗證/退回,已繳待驗的可一鍵掃過去驗證(還有 待繳/已退回的會明講需要逐筆處理)。手機版把繳費表改成堆疊卡片、彈窗改成底部抽屜。 -- **重新同步本期帳單** — 在繳費審核頁,把目前篩選的**已開期別**帳單對齊現在的名單/價格 - (補缺漏 · 移除已退訂 · 待繳改現價 · 已繳/已驗證凍結);先出差異預覽再套用,並可勾選用繳費按鈕 +- **重新同步此期帳單** — 在繳費審核頁,把目前篩選的**已開期別**帳單對齊現在的名單/價格 + (補缺漏 · 移除訂閱已暫停/已取消者 · 待繳改現價 · 已繳待驗/已驗證凍結);先出差異預覽再套用,並可勾選用繳費按鈕 @ 通知新加進來的成員。選「全部期別」時按鈕停用。 - **發起繳費** — 確認所選期別各方案金額(任何更動就是該方案的新定價),先看「會建立/改價哪些帳單、 定價 before→after、是否會發通知」的預覽,確認後才送出。預設期別是**目前收款中的那一期**, 「預開下期」是要自己勾的次要選項。可從後台「設定」或 Discord 的 `/發起繳費` 觸發 (後者可帶 `期別`,方案超過 5 個時會直接請你改用後台——開表單與送出表單都會重驗一次)。 Discord 沒有回應成功時回報的是「通知發送失敗」而不是已發送:帳單與開繳狀態照實留著,可到推播狀態補送。 -- **綁定按鈕** — 設定 → 工具 → 在帳單頻道張貼一則常駐的公開**綁定 Discord** 按鈕,讓成員主動綁 +- **綁定按鈕** — 設定 → 工具 → 在繳費頻道張貼一則常駐的公開**綁定 Discord** 按鈕,讓成員主動綁 (第一次繳費時綁定仍然保留為 fallback)。 - **推播狀態** — 看板顯示開繳/逾期通知是否已發,並提供 **重發開繳通知**(只重貼公告,不建帳單、 不會讓期別短暫變回未開繳)、**催繳未繳成員**(@ 全部未繳者,與只 @ 逾期者的每日 cron 不同) 與 **重置催繳發送紀錄**。前兩者送出前會先列出實際名單,三者都要再確認一次,並回報真實筆數: Discord 沒回成功就說發送失敗——開繳通知的 `sent_at` 不會被往前推,催繳也會把佔用的去重名額還回去, 下次(含每日 cron)才送得出去。 - 要把期別改回未開繳請用「繳費審核 → 收回本期開繳」。 + 要把期別改回未開繳請用「繳費審核 → 收回此期開繳」。 - **繳費通知** — 在設定 → 繳費通知填 Bark 裝置金鑰和/或 webhook(Discord/Google Chat/Slack), - 之後每筆新送出都會推你一則,點開就是該成員當期的合併審核(共用截圖一次、每筆列出、可一鍵核准), + 之後每筆新送出都會推你一則,點開就是該成員當期的合併審核(共用截圖一次、每筆列出、可一鍵驗證), 手機上也好操作。存檔前可先按**送出測試**確認打得通。兩者都選填、best-effort(端點慢或掛掉都不會 卡住成員繳費)。 - **每日 cron**(01:00 UTC = 台北 09:00)— 冪等地開出各期帳單、發開繳通知(tag 方案身分組)、 diff --git a/docs/superpowers/plans/2026-07-31-ux-d-terminology.md b/docs/superpowers/plans/2026-07-31-ux-d-terminology.md new file mode 100644 index 0000000..98e8562 --- /dev/null +++ b/docs/superpowers/plans/2026-07-31-ux-d-terminology.md @@ -0,0 +1,1752 @@ +# UX-D 術語與文案統一 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make every zh-TW string in ChipPot use one word per concept — 驗證 (not 核准), 已繳待驗 (not 已繳), 繳費渠道 (not 支付渠道), 此期 (not 本期), 開繳 (not 開帳), 成員 (not 使用者) — and stop showing raw English enums and raw English worker errors to Chinese-reading users. + +**Architecture:** This is a copy sweep, not a refactor. §A below is the canonical-terms SPEC — one mapping table per concept. Each task takes one concept, **regenerates its own edit-site list with `git grep` at execution time**, applies the mapping, then proves the concept is gone with a zero-hits assertion. Nothing in this plan changes a DB value, an API request/response *shape*, an enum stored in D1, or a `custom_id`. Three tasks do change API response *strings* (`routes/admin.ts` errors, `core/payments.ts` race message) and Discord reply strings; those are TDD with real vitest assertions. + +**Tech Stack:** TypeScript, React 18 (admin + web SPAs, Vite 6), Cloudflare Workers, Vitest 4 with `@cloudflare/vitest-pool-workers` (real Miniflare D1/R2), pnpm workspaces. + +--- + +## Why this plan has no line numbers + +This batch runs **last** — after UX-A (danger actions, `#43`), UX-B (mobile/a11y, `#44`) and UX-C (member feedback, `#45`) have merged. Those batches rewrite `Dashboard.tsx`, `Payments.tsx`, `Manage.tsx`, `Settings.tsx`, `handler.ts` and `web/App.tsx`. **Every line number in the source audit (`.superpowers/sdd/ux-audit-copy.md`) is stale by the time you read this.** + +So: the audit's `file:line` lists are an *inventory of what existed on 2026-07-30*, reproduced in §A only to tell you roughly how many sites to expect. **Never navigate by line number.** Every task starts with a `git grep` that rebuilds the list from the tree you actually have, and ends with the same grep returning nothing. + +Three consequences you must internalise: + +1. **A site may already be fixed.** UX-A owns `A5` (the 發起繳費 「本期」 wording). If your grep shows it already reads `${period}`, that is success, not a missing file — move on. +2. **A site may have moved to a file this plan never names.** If `git grep` finds the term in a file not listed in §B, fix it there too. The grep is the authority; §B is a hint. +3. **A string may have been reworded.** Match on the *concept*, not on the exact old sentence. If UX-A rewrote a tooltip and it still says 「本期」, the 本期 sweep still owns that word. + +`docs/superpowers/plans/**` is a **historical archive**. It contains 核准, 已繳, 本期 in abundance. **Never edit it.** Every grep in this plan is pathspec-scoped to `packages README.md README.zh-TW.md` precisely to keep it out. + +--- + +## Global Constraints + +Every task's requirements implicitly include this section. + +- **Branch:** `ux/46-terminology`, cut from `main` after A/B/C have merged. One PR, body contains `Closes #46`. +- **Worker suite green after every task.** Baseline on 2026-07-31 (before A/B/C) was **300 passed, 41 files**. A/B/C will raise it. **Do not hardcode 300 anywhere** — capture the real baseline in Task 1 Step 2 and use that number. +- **`pnpm -r typecheck` green after every task** (worker + admin + web `tsc --noEmit`). +- **Admin and web must build:** `pnpm --filter @chippot/admin build` and `VITE_API_BASE=https://example.invalid pnpm --filter @chippot/web build`. Run both in Task 15; run them earlier too if a task touched `.tsx` and you want the signal. +- **`packages/worker/wrangler.toml` must not be touched.** It is `skip-worktree`'d locally and its committed copy holds placeholders. Do not `git add` it, do not open it to "check" something. +- **`docs/deploy-state.md` is gitignored (`.gitignore:10`) — local-only.** Append the entry (Task 15) but **never `git add` it**; `git add docs/` would fail to stage it anyway, so use explicit paths in every `git add`. +- **Conventional commits**, zh-TW subject line after the type/scope, matching repo history (e.g. `fix(admin-ui): 彈窗內文字不再繼承靠右對齊 (#33)`). +- **Display-layer only.** English enum *values* (`active`/`paused`/`cancelled`, `user_slash`/`admin_manual`/`cron`, `pending`/`paid`/`verified`/`rejected`) stay English in the DB, in `