Aquaturbo V2 行為規格文檔
📖 正典定義(Canonical Definitions)
用途:
所有 features 共用的術語、角色、數據常量、狀態機。任何 scenario 內出現的概念都應以此為準。
需求方確認此區塊後,後續 features 不再對這些定義做局部變更。
A. 角色與人物
- Given 王女士(Wang) — 家長 / Parent,名下有學員小明、小華
- And 小明 — 王女士的兒子,當前 Level L3,Burnaby 校區
- And 小華 — 王女士的女兒,當前 Level L1,Burnaby 校區
- And 陳先生 — 另一聯絡人(用於跨家長對照場景)
- And Lisa — 客服 / Admin,Burnaby 校區
- And Tom — 客服 / Admin,Coquitlam 校區(用於跨校區場景)
- And Mark — 主管 / Super Admin,跨校區
- And Coach Luke — 教練,Level 4,可帶 L3-L4 課程
- And Coach Eric — 教練,Level 2,可帶 L1-L2 課程
B. 場域與資源
- Given 校區:Burnaby(主校區)、Coquitlam、Surrey(範例)
- And 泳池:Pool A(25m)、Pool B(50m)
- And 泳道:Lane 1、Lane 2、Lane 3(每條泳道為獨立排課單位)
- And 季度(Term/Season):2026 Spring(3-6月)、2026 Summer(7-9月)、2026 Fall(10-12月)、2026 Winter(1-3月)
- And 課程類型:1on1、1on2、Group、長訓(Long-Training)
- And Level:L1(初學)、L2、L3、L4、L5(進階)
- And 典型上課時段:週二 17:00-18:00(用於範例)
C. 金錢與費率
- Given 貨幣單位:CAD(加幣)
- And GST 稅率:5%(聯邦)
- And 註冊費:30 CAD per 學員,一次性,不退款(用於泳帽、袋子、證書、人工成本)
- And Trial 折扣:正式價格的 80%(即 8 折),折價部分以「優惠」名義呈現於 Invoice
- And 金額計算:所有金額用 BigDecimal,HALF_UP 保留 2 位小數
- And 價格快照:訂單建立時鎖定當時價格表,後續價格表變動「不」回溯
待確認 QC.1:註冊費 30 CAD 是「per 學員」一次性(一個學員生命週期一次),還是「per 訂單」?我預設 per 學員一次性(首次報名時收,之後同學員所有報名都不再收)。請確認。
待確認 QC.2:學員退款後再回來,註冊費是否重收?我預設 不重收(既然標榜「一次性、不退款」就保持不重收)。
D. 訂單狀態機(V2 核心)
- Given 訂單共 6 種狀態,狀態機如下:
- And 待付款 — 客服已生成訂單,課程名額暫時保留,等待客戶付款
- And 待確認 — 客戶已宣告完成付款,等待客服 / 金流系統確認
- And 已完成 — 付款已確認,學員正式加入課表,開始扣費
- And 已取消 — 超時未付款 / 客戶取消(capacity 立即釋放)
- And 退款中 — 已申請退款,等待財務 / 金流處理
- And 已退款 — 退款完成並確認
- And 合法 transitions:
- 待付款 → 待確認(客戶宣告付款)
- 待付款 → 已取消(超時 / 客戶取消)
- 待確認 → 已完成(客服 / 金流確認)
- 已完成 → 退款中(SUPER_ADMIN 發起退款)
- 退款中 → 已退款(金流系統確認退款完成)
待確認 QD.1:「待確認」是否能轉「已取消」?例如客戶宣告付款但實際沒付,48 小時後客服查無入帳,是否能取消?預設 能,加 transition 待確認 → 已取消。
待確認 QD.2:退款是否支持「部分退款」?訂單中只退一部分課程的金額。預設 支持(V1 schema 已有 PARTIAL_REFUNDED),請確認 V2 是否保留此機制。
E. 餘額模型 — 雙層餘額(V2.1 拍板版,2026-05-08 會議)
- Given 每位學員有「雙層餘額」:
- 1. 不可自由消費餘額(Locked Balance) — 來自訂單付款,綁定訂單,必須用於該訂單對應的課程
- 2. 可自由消費餘額(Free Balance) — 來自任意金額付款 / 退款找零 / 訂單對應課程全部結束後剩餘的轉入
- And 兩種餘額在客戶端分別展示,合計 = 總餘額
- And 付款全部先進餘額,再從餘額扣費(不直接扣訂單剩餘)
- And 付款進入餘額的規則:
- • 訂單付款 → 「不可消費餘額」(綁該訂單 ID)
- • 任意金額付款(客服在後台代操作) → 「自由消費餘額」
- • 退款找零 → 「自由消費餘額」(原訂單已退無法綁)
- And 扣費規則(系統自動,無人工點名):
- • 課程開始時系統觸發自動扣費(cron 每分鐘掃)
- • 該堂課對應訂單的「不可消費餘額」優先扣(單堂價格快照)
- • 若不足(補課差價、Trial 折扣等情境)→ 從「自由消費餘額」補
- • 若都不足 → 系統 alert 客服 + 該堂課狀態標記為「待處理(缺費)」
- And 不消費餘額轉自由的時機:
- • 該訂單對應的所有 sessions 全部結束(已扣完 / 取消 / 過期)
- • 系統 cron 自動轉換(每天 00:00 掃)
- • 例:12 堂季度課,10 堂已扣 + 2 堂提前請假無補上 → 訂單對應 sessions 全結束 → 剩 0;若有 1 堂換到便宜課剩 30 → 季度結束後 30 → 自由
- And 請假與扣費關係:
- • 24 小時前請假 → 該堂課從 enrollment 直接移除,不扣費(產生補課額度,僅季度課)
- • 24 小時內請假 → 不可補課,仍照常扣費(系統當日自動扣)
- And 客戶端展示:
- • Dashboard:「不可消費餘額 $X / 自由消費餘額 $Y / 總計 $X+Y」
- • 詳情頁:每筆訂單剩餘 + 自由餘額明細 + 完整流水
- • 文案強調:「不可消費 = 您為某組課程預付,按堂扣完即止;自由 = 您可用於任意課程」
已拍板 E.1:採用雙層餘額(不可消費 + 自由消費)。先進餘額再扣費。
已拍板 E.2:任意金額付款由客服後台代操作,進入自由消費餘額。
已拍板 E.3:不消費餘額轉自由的時機 = 訂單對應所有 sessions 全部結束。
已拍板 E.4:退款找零 → 自由消費餘額(不再綁訂單)。
待確認 QE.5:餘額不足時,alert 客服的具體做法 — 預設「該堂課狀態為『待處理(缺費)』,客服收到通知後手動處理(要求家長充值或聯繫退課)」。請確認此 fallback 行為。
F. 時間與時區
- Given 後端儲存時間統一用 UTC(LocalDateTime 存 UTC)
- And 前端顯示與輸入用 Vancouver Local Time(PST/PDT)
- And 文檔中的時間描述(如「17:00」、「2026-04-07」)均指 Vancouver Local
- And 時區轉換在 Controller 邊界處理
G. 通用業務規則
- Given 軟刪除:所有業務表用 deleted_at 軟刪;刪除後資料保留但不顯示於日常查詢
- And 歷史紀錄保留:請假/補課/扣款/退款/Audit 紀錄永久保留,即使學員或訂單軟刪
- And Audit Log:刪除、退款、Credit 手動調整、跨季補課特批 → 必須留下 audit log
- And 不可變表:t_credit_log / t_audit_log / t_refund 只允許 INSERT,禁止 UPDATE/DELETE
- And API 錯誤訊息:英文(前端按錯誤碼映射為雙語顯示)
- And UI 介面語言:繁體中文 / 英文,可切換,登入後延續
01. 登入 / 註冊 / 忘記密碼(V2.1:開放註冊)已完成 家長端 + 客服端
⚠️ V2.1 重大變更(2026-05-08 拍板):
取消「註冊碼」機制,改為開放註冊(任何人都可在家長端註冊)。
因此 Scenarios 4-6(註冊碼相關)和 Scenario 12(綁定碼衝突)不再適用, 請忽略或視為過時。實作時 Scenario 7(6 步註冊向導)變成「無需註冊碼即可進入」。
其他流程(登入、忘記密碼、員工帳號、教練)保持不變。
因此 Scenarios 4-6(註冊碼相關)和 Scenario 12(綁定碼衝突)不再適用, 請忽略或視為過時。實作時 Scenario 7(6 步註冊向導)變成「無需註冊碼即可進入」。
其他流程(登入、忘記密碼、員工帳號、教練)保持不變。
主要角色:家長(王女士)、客服(Lisa)、主管(Mark)、教練(Coach Luke)、系統
Background 共用前置條件
- Given 系統已預先建立 SUPER_ADMIN「Mark」、ADMIN「Lisa」(綁定 Burnaby)、Coach「Luke」三個內部帳號
- And 系統的家長端網址為
https://parent.aquaturboswim.ca - And 系統的客服端網址為
https://admin.aquaturboswim.ca - And 客服可在後台「聯絡人管理」頁面派發註冊碼
Scenario 1 家長以 Email + 密碼登入家長端
- Given 王女士已完成註冊,Email 為
wang@example.com,密碼為Wang2026! - When 她在家長端登入頁輸入 Email + 密碼,點擊「登入」
- Then 系統驗證通過
- And 跳轉到家長端 Dashboard
- And 顯示「歡迎回來,王女士」
- And 介面語言沿用她上次選擇的語言(繁中或英文)
Scenario 2 密碼錯誤 — 統一錯誤訊息
- Given 王女士的帳號為
wang@example.com - When 她輸入正確 Email 但錯誤密碼
- Then 系統返回「Email 或密碼錯誤」
- And 不洩露是 Email 不存在還是密碼不正確(防帳號探測)
- And 該失敗事件被記錄到登入失敗日誌
待確認 Q01.1:是否啟用「連續 5 次錯誤鎖定 15 分鐘」防爆破機制?文檔未提,我預設 啟用。請確認。
Scenario 3 忘記密碼 — Email 驗證碼重設
- Given 王女士在登入頁點擊「忘記密碼」
- When 她輸入
wang@example.com - Then 系統發送 6 位數字驗證碼至該 Email
- And 顯示提示「驗證碼已發送,10 分鐘內有效」
- And 該驗證碼僅可使用一次
- When 她在驗證頁輸入正確的 6 位數碼 + 新密碼
Wang2027New! - Then 系統重置密碼並自動登入
- And 通知中心新增「密碼已重設」通知
待確認 Q01.2:驗證碼長度(6 位)+ 有效期(10 分鐘)+ 一次使用 — 預設這樣,請確認規格。
待確認 Q01.3:密碼複雜度要求?預設 ≥ 8 字元、至少 1 字母 + 1 數字。是否要求大小寫 / 特殊符號?
Scenario 4 註冊 — 持有客服派發的註冊碼
- Given 客服 Lisa 在後台為新客戶王女士派發註冊碼
AQUA-X8K9-2026 - And 該註冊碼未被使用、未過期(有效期 30 天)
- When 王女士在家長端註冊頁輸入該註冊碼
- Then 系統驗證通過
- And 進入註冊向導 Step 1(關係選擇)
待確認 Q01.4:註冊碼有效期?我預設 30 天。是否每組註冊碼也只能用 1 次?預設 是。請確認。
待確認 Q01.5:註冊碼派發方式?我預設 客服在後台手動產生 + 透過 WeChat / Email 告知客戶。是否要批次產生?是否要 QR Code?
Scenario 5 註冊 — 註冊碼無效
- Given 王女士輸入無效的註冊碼(已使用 / 已過期 / 不存在)
- When 點擊「下一步」
- Then 系統顯示「註冊碼無效或已使用」
- And 顯示「請聯繫客服取得新註冊碼」
- And 顯示 4 種聯繫方式:WeChat / WhatsApp / Email / Phone
Scenario 6 註冊 — 沒有註冊碼,顯示客服聯繫方式
- Given 王女士第一次接觸系統,沒有註冊碼
- When 她在註冊頁點擊「我沒有註冊碼」
- Then 系統顯示說明:「為避免無效帳號,所有家長需透過客服取得註冊碼」
- And 顯示 4 種聯繫方式(WeChat / WhatsApp / Email / Phone)含 QR Code 或 deep link
- And 不允許在沒有註冊碼的情況下進入註冊流程
Scenario 7 註冊向導 6 步驟
- Given 王女士已通過註冊碼驗證
- When 她依序填寫註冊向導 6 步:
- Step 1 關係:父母 / 家人 / 其他 → 選「父母」
- Step 2 聯絡人資料:姓名「王女士」、Email「wang@example.com」、電話「604-xxx-xxxx」、密碼「Wang2026!」
- Step 3 註冊對象:自己 / 孩子 → 選「孩子」
- Step 4 主校區:Burnaby
- Step 5 學員資料(可多位):小明(生日 2018-05-01、初始 Level L2)+ 小華(生日 2019-08-12、L1)
- Step 6 備註:「小明對氯敏感,建議戴護目鏡」
- Then 系統建立聯絡人「王女士」
- And 在王女士名下建立 2 位學員「小明」「小華」
- And 註冊碼
AQUA-X8K9-2026標記為已使用 - And 自動登入到 Dashboard
- And 通知中心新增「歡迎使用 Aquaturbo,註冊完成」通知
- And 兩位學員的初始 Level(L2 / L1)不進入 Level 調整歷史
待確認 Q01.6:Level 由家長自選 — 文檔說「系統提供 Level 說明供參考」。是否每個 Level 都有圖文說明(如「L1 = 不會浮、不會閉氣」)?這份說明文案誰來提供?
Scenario 8 註冊 — Email 已被註冊
- Given Email
wang@example.com已存在於系統 - When 新註冊嘗試使用同一 Email
- Then 系統提示「此 Email 已註冊,如忘記密碼請使用『忘記密碼』功能」
- And 不消耗註冊碼(讓使用者改用其他 Email 或聯繫客服)
Scenario 9 員工登入 — 角色決定跳轉與權限
- Given 系統有以下三個員工帳號
- When 各自登入客服端
- Then 跳轉與可見資料如下表所示
Examples:
| 角色 | 帳號 | 登入後跳轉 | 可見資料 |
|---|---|---|---|
| SUPER_ADMIN | mark@aquaturbo.com | 後台 Dashboard | 所有校區 |
| ADMIN | lisa@aquaturbo.com | 後台 Dashboard | 僅本校區(Burnaby) |
| COACH | luke@aquaturbo.com | 個人課表頁 | 僅自己負責的 sessions |
Scenario 10 教練登入 — 唯讀視圖
- Given Coach Luke 已建立教練帳號
- When Luke 登入客服端
- Then 跳轉到「我的課表」頁,僅顯示自己負責的 sessions
- And 介面隱藏所有「新增課程」「編輯」「刪除」「報名學員」按鈕
- And 介面隱藏所有財務 / 退款 / 報表 / 帳號管理選單
- And 介面顯示「點名」按鈕(可逐生點名 + 批量點名)
待確認 Q01.7:系統端文檔說「教練不可修改任何資料」,但點名是修改 attendance 紀錄。教練可以點名嗎?我預設 可以點名(含逐生與批量),但不可修改其他資料。請確認。
Scenario 11 客服手動建立家長帳號 — 預設密碼 123456
- Given 客服 Lisa 在後台「聯絡人管理」頁
- When Lisa 為陳先生建立聯絡人:姓名、Email「chen@example.com」、電話、不設定密碼
- Then 系統自動建立帳號,密碼為
123456 - And Lisa 看到提示「請告知客戶首次登入密碼為 123456」
- When 陳先生首次以該 Email + 123456 登入
- Then 系統強制要求修改密碼後才能進入 Dashboard
Scenario 12 客服已建檔 + 家長持註冊碼自助註冊 — 衝突處理
- Given 客服 Lisa 已手動建立王女士的聯絡人記錄(Email
wang@example.com,密碼 123456) - And 同時 Lisa 也派發了一組註冊碼給王女士
- When 王女士不知客服已建檔,自己拿註冊碼上來「註冊」
- And 在 Step 2 填入相同 Email
wang@example.com - Then 系統檢測到 Email 已存在
- And 顯示提示「此 Email 已有帳號,請以該 Email + 預設密碼 123456 登入;或聯繫客服協助」
- And 註冊碼不標記為已使用(保留供綁定用)
- And 不重複建立聯絡人
待確認 Q01.8:「綁定碼」與「註冊碼」是否同一機制?我預設 同一個碼:客服建檔時派發一個碼,家長拿這碼來:(a) 若帳號不存在 → 完整註冊;(b) 若帳號已存在 → 跳到登入提示。請確認。
Scenario 13 系統清理過期密碼重設碼 內部行為
- Given 系統存在多筆已超過 10 分鐘有效期的密碼重設碼
- When 系統於每天 02:00 執行清理任務
- Then 所有過期重設碼從 DB 中刪除
- And 清理動作記錄到 audit log
Scenario 14 系統清理過期未使用註冊碼 內部行為
- Given 一組註冊碼派發後 30 天未被使用
- When 系統於每天 02:00 執行清理任務
- Then 該註冊碼被標記為 EXPIRED
- And 客服在「註冊碼管理」頁可看到過期紀錄
- And 後台支援批次重新派發已過期的註冊碼給同一客戶
02. 帳號 / 學員管理 已完成 家長端
業務價值:
一個帳號管理多位學員(家庭場景);家長可自助修改聯絡人 / 學員的基本資料,
但 Level 由教練評估後由客服統一調整,確保專業判斷不被家長主觀干擾。
主要角色:家長(王女士)、客服(Lisa)、系統
Background 共用前置條件
- Given 王女士帳號下有兩位學員:「小明」(L3)、「小華」(L1)
- And 王女士已登入家長端
- And 當前語言設定為「繁體中文」
Scenario 1 在學員之間切換
- Given 王女士當前正在查看「小明」的 Dashboard
- When 她點擊頂部學員切換器,選擇「小華」
- Then Dashboard 立即切換為「小華」的資料
- And 顯示小華的下一堂課、Credit、待補課數、剩餘請假額度
- And 後續所有頁面(課表 / 帳單 / 通知)均以小華為視角
- And 切換器在所有頁面右上角始終可見
Scenario 2 修改聯絡人姓名與電話
- Given 王女士當前的電話為
604-xxx-1111 - When 她在「我的資料」頁修改電話為
778-xxx-2222並點擊「保存」 - Then 系統更新聯絡人電話
- And 顯示 Toast「資料已更新」
- And 客服後台的聯絡人記錄即時更新
- And 不需要重新登入
Scenario 3 修改 Email — 需驗證新 Email
- Given 王女士當前 Email 為
wang@example.com - When 她在「我的資料」頁修改為
wang.new@example.com並提交 - Then 系統發送驗證碼到新 Email
- And 在驗證前舊 Email 仍可登入、新 Email 不可登入
- When 王女士在新 Email 收到驗證碼,回到設定頁輸入並確認
- Then 系統切換為新 Email
- And 通知中心新增「Email 已變更」通知
待確認 Q02.1:修改 Email 是否需要驗證新 Email?我預設 需要(防誤輸 + 防被冒用)。是否同時要求舊 Email 確認?
Scenario 4 修改密碼 — 需要舊密碼
- Given 王女士在「修改密碼」頁
- When 她輸入舊密碼
Wang2026!+ 新密碼Wang2027New! - Then 系統驗證舊密碼正確
- And 更新為新密碼
- And 顯示提示「密碼已更新,請重新登入」
- And 自動登出,跳轉到登入頁
- And 通知中心新增「密碼已修改」通知
Scenario 5 修改密碼 — 舊密碼錯誤
- Given 王女士在「修改密碼」頁
- When 她輸入錯誤的舊密碼 + 任意新密碼
- Then 系統拒絕並顯示「舊密碼錯誤」
- And 不更新任何資料
- And 不發通知
Scenario 6 修改學員基本資料
- Given 王女士當前查看「小明」的學員資料頁
- When 她修改備註:「對氯敏感,需戴護目鏡」並保存
- Then 系統更新學員備註
- And 顯示成功提示
- And 客服後台查看小明資料時可見此備註
待確認 Q02.2:家長可修改的學員欄位有哪些?預設:姓名、生日、緊急聯絡人、過敏資訊、備註。不可改:Level、所屬聯絡人、主校區。請確認。
Scenario 7 學員 Level 唯讀 — 家長端不可修改
- Given 王女士在「小明」(L3)的學員資料頁
- When 她查看 Level 欄位
- Then Level 顯示為 L3(唯讀)
- And 旁邊顯示說明:「Level 由教練評估後由客服調整」
- And 不存在編輯按鈕
- And 直接在 URL 或 API 嘗試提交 Level 變更會被後端拒絕
Scenario 8 查看 Level 調整歷史
- Given「小明」初始註冊 Level 為 L1,後續客服 Lisa 進行了 2 次調整:
| 日期 | 從 | 到 | 操作人 | 原因 |
|---|---|---|---|---|
| 2026-04-10 | L1 | L2 | Lisa | 教練 Eric 期中評估 |
| 2026-05-15 | L2 | L3 | Lisa | 學期末綜合評估 |
- When 王女士進入「小明」的 Level 紀錄頁
- Then 顯示 2 筆紀錄(按日期倒序)
- And 每筆顯示日期、變化、操作人、原因
- And 初始 L1 不顯示在紀錄中(符合需求文檔「初始 Level 不納入紀錄」)
Scenario 9 客服調整 Level — 自動寫入歷史 + 通知家長
- Given Coach Luke 評估「小明」可升至 L4
- And 客服 Lisa 在後台將「小明」Level 從 L3 改為 L4,原因:「教練評估提升」
- When Lisa 點擊保存
- Then 系統建立 1 筆 t_student_level_log,含時間 / from / to / 操作人 / 原因
- And 王女士的家長端鈴鐺新增「小明 Level 已調整為 L4」通知
- And 學員資料頁的 Level 欄位即時更新
- And 後續報名時,「小明」可選 L3-L5 區間的課程(即使原本不能)
Scenario 10 家長端不可新增學員
- Given 王女士想為新出生的女兒建立第三位學員
- When 她在「學員管理」頁尋找「新增學員」按鈕
- Then 介面不顯示新增按鈕
- And 顯示提示:「如需新增學員,請聯繫客服」並附 4 種聯繫方式
待確認 Q02.3:家長註冊時可填多位學員,但註冊後是否還能新增?V2 文檔未明說。我預設 不可,需聯繫客服(符合 V2「客服統一管理」精神)。請確認。
Scenario 11 家長端不可刪除學員
- Given 王女士想停用「小華」(學業繁忙暫不上課)
- When 她在學員管理頁尋找「刪除」按鈕
- Then 介面不顯示刪除按鈕
- And 提示「如需暫停或刪除,請聯繫客服」
Scenario 12 客服軟刪學員 — 歷史保留
- Given 客服 Lisa 在後台軟刪「小華」
- When Lisa 提交刪除
- Then「小華」的 deleted_at 被標記
- And 王女士的家長端「小華」從學員切換器消失
- And 但「小華」的歷史 attendance / payment / level_log 全部保留
- And 系統建立 1 筆 audit log,記錄刪除操作
Scenario 13 Level 變更時系統發出通知 內部行為
- Given 任意客服在後台調整某學員的 Level
- When 系統建立 t_student_level_log 紀錄
- Then 系統觸發 1 筆通知到該學員所屬聯絡人的家長端
- And 通知標題:「[學員名] Level 已調整為 [新 Level]」
- And 通知內含教練評估原因(若有填寫)
- And 通知狀態:未讀
03. 後台設定(校區/泳池/泳道/價格/帳號)已完成 客服端 / SUPER_ADMIN
業務價值:
後台設定是系統運營的基礎:校區 / 泳池 / 泳道 / 價格表 / 員工帳號。
這些資料為 SUPER_ADMIN 唯一管轄;ADMIN 可讀但不可寫,避免分校區客服誤改全局配置。
主要角色:主管(Mark)、客服(Lisa)、教練(Coach Luke)、系統
Background 共用前置條件
- Given SUPER_ADMIN「Mark」已登入後台
- And 系統已有預設校區「Burnaby」「Coquitlam」
- And Mark 的角色可看 / 可寫所有設定
Scenario 1 創建新校區
- Given Mark 在「校區管理」頁
- When 他點擊「新增校區」並填入名稱「Surrey」、地址、電話、營業時間
- And 點擊保存
- Then 系統建立 t_branch 紀錄
- And 校區列表新增 Surrey
- And 報名流程的校區下拉選單立即可選 Surrey
Scenario 2 在校區下創建泳池與泳道
- Given Mark 進入 Burnaby 校區詳情頁
- When 他新增泳池:名稱「Pool A」、長度 25m、深度 1.2-1.8m、最大同時開課數 6
- And 進入 Pool A 詳情,批量新增 6 條泳道:Lane 1 ~ Lane 6
- Then 系統建立 1 筆 t_pool + 6 筆 t_lane
- And 每條泳道為獨立排課單位
- And 泳道編輯頁可獨立啟用 / 停用某條 lane(例如維修中)
Scenario 3 ADMIN 嘗試新增校區 — 被拒絕
- Given 客服 Lisa(ADMIN,Burnaby)登入後台
- When 她嘗試進入「校區管理」頁
- Then 介面隱藏該選單
- And 直接訪問該 URL 跳到「無權限」頁
- And API 拒絕並返回 403
Scenario 4 嘗試刪除有關聯課程的校區 — 阻擋
- Given Surrey 校區下有 1 個 session 已建立
- When Mark 嘗試刪除 Surrey 校區
- Then 系統阻擋
- And 顯示提示「該校區下有 1 個課程,請先移除課程或使用『停用』」
- And 不變更任何資料
待確認 Q03.1:校區是否真有「刪除」需求?預設 軟刪除(停用)為主,硬刪除僅限完全無關聯資料的新建校區。請確認。
Scenario 5 停用校區 — 既有課程不受影響
- Given Surrey 校區下有進行中的課程
- When Mark 將 Surrey 設為「停用」
- Then Surrey 不再出現在新報名流程的校區選擇
- And 既有課程繼續運行直到自然結束
- And 既有家長 / 學員不受影響
- And 報表中該校區仍可被選為篩選條件(看歷史數據)
Scenario 6 建立價格表(教練等級 × 課程類型)
- Given Mark 在「價格表」頁
- When 他建立以下價格行(皆為單堂未稅 CAD):
| 課程類型 | 教練等級 | 單堂價格 |
|---|---|---|
| 1on1 | L4-L5 | 80 |
| 1on1 | L1-L3 | 60 |
| 1on2 | L4-L5 | 50 |
| 1on2 | L1-L3 | 40 |
| Group | 任意 | 30 |
| 長訓 | L4-L5 | 35 |
- Then 系統儲存 6 條價格
- And 之後新建課程時,根據 課程類型 × 該節課指派的教練等級 自動帶出單堂價格
- And 報名 Step 5 費用明細顯示此價格作為基準價
Scenario 7 修改既有價格 — 已報名訂單不受影響(價格快照)
- Given「1on1 + L4-L5」當前單堂價格 80 CAD
- And 王女士已為小明以此價格報名 12 堂課(訂單 #123,總額已鎖定)
- When Mark 將該價格調整為 90 CAD,於 2026-05-08 生效
- Then 訂單 #123 的單堂價格快照仍為 80 CAD(不影響既有訂單的扣費 / 退款金額)
- And 之後(含 2026-05-08 起)新建的「1on1 + L4-L5」報名,單堂為 90 CAD
- And 系統建立 1 筆價格變更 audit log
Scenario 8 建立 ADMIN 員工帳號 — 必須綁校區
- Given Mark 在「員工帳號」頁
- When 他新增員工:姓名「Tom」、Email「tom@aquaturbo.com」、角色「ADMIN」、不選校區
- And 點擊保存
- Then 系統阻擋
- And 顯示「ADMIN 角色必須指定所屬校區」
Scenario 9 建立 COACH 員工帳號 — 必須關聯一名教練實體
- Given 系統已有教練 Coach Luke 的 t_coach 紀錄
- When Mark 新增員工:姓名「Luke」、角色 COACH、關聯教練:Coach Luke
- Then 系統建立 sys_user 紀錄並綁定 coach_id
- And Luke 登入後僅能看到自己的課表
- When Mark 嘗試建立 COACH 帳號但不關聯任何教練
- Then 系統阻擋並提示「COACH 角色必須關聯一名教練」
Scenario 10 建立 SUPER_ADMIN 員工帳號 — 跨校區
- Given Mark 新增員工:姓名「Alice」、角色「SUPER_ADMIN」
- When 他不指定校區,直接保存
- Then 系統建立成功
- And Alice 登入後可看 / 可操作所有校區
- And Alice 可發起退款、修改已結束的點名、查所有財務
Scenario 11 修改員工角色 — 觸發權限重評
- Given Tom 當前是 ADMIN(綁定 Burnaby)
- When Mark 將 Tom 的角色改為 SUPER_ADMIN
- Then 系統清除 Tom 的 branch_id 綁定
- And Tom 下次登入即為 SUPER_ADMIN,可看所有校區
- And Tom 在當前已登入的 session 會被強制登出(因 JWT 中的角色變了)
- And 系統建立 1 筆角色變更 audit log
Scenario 12 停用員工帳號 — 既有紀錄保留
- Given 員工 Tom 即將離職
- When Mark 將 Tom 的帳號設為「停用」
- Then Tom 無法登入(包含當前 session 強制失效)
- And Tom 過往的所有操作紀錄保留
- And Tom 經手的訂單 / 退款 / 點名紀錄不受影響
- And 報表查詢中可仍以「Tom」為操作人篩選歷史
Scenario 13 教練等級設定與排課限制
- Given Mark 在「教練管理」頁建立教練「Coach Henry」
- And 設定 Coach Henry 的 Level 為 L3
- When Mark 嘗試將 Coach Henry 排到一節 L4-L5 的 1on1 課程
- Then 系統顯示警告「教練等級(L3)低於課程 Level 區間(L4-L5)」
- And 預設允許強制排課(彈性需求),但記錄到 audit log
Q03.2:教練等級限制 — 「警告但允許強制」 vs 「完全禁止」?我預設 警告但允許(教學現場可能臨時換人)。請確認。
Scenario 14 國定假日清單管理
- Given Mark 在「國定假日」頁,2026 年清單預設包含:
- • 2026-01-01 New Year
- • 2026-04-03 Good Friday
- • 2026-05-18 Victoria Day
- • 2026-07-01 Canada Day
- • 2026-09-07 Labour Day
- • 2026-10-12 Thanksgiving
- • 2026-12-25 Christmas Day
- When Mark 將 2026-04-06 也加入「館內活動日(休課)」
- Then 該日期被加入校區的 closed_dates
- And 報名 Step 4 上課日期選擇時,該日期顯示為「國定日(休)」且不可選
Q03.3:國定假日是全系統一份還是每校區一份?我預設 全系統一份(加拿大 BC 省假日表),各校區可額外加自己的休館日。
09. 請假(V2.1:24 小時前 / 內 規則)已完成 家長端
⚠️ V2.1 重大變更(2026-05-08 拍板):
請假分界從 12 小時 → 24 小時。
≥ 24hr 請假:學員從 enrollment 直接移除、不扣費、僅季度課產生補課額度。
< 24hr 請假:仍在 enrollment、照常扣費、無補課(懲罰晚請假)。
「No-show」概念已廢除(由 F08 自動扣費取代;24hr 內請假等同 No-show 扣費)。
≥ 24hr 請假:學員從 enrollment 直接移除、不扣費、僅季度課產生補課額度。
< 24hr 請假:仍在 enrollment、照常扣費、無補課(懲罰晚請假)。
「No-show」概念已廢除(由 F08 自動扣費取代;24hr 內請假等同 No-show 扣費)。
業務價值:
作為家長,我希望能為孩子在課程列表中提交請假,
以便管理孩子無法到場的時段,並在符合條件時自動取得補課機會,
同時讓客服掌握出席狀況。
主要角色:家長(Parent)、客服(Admin)、系統(System)
Background 共用前置條件
- Given 學員「小明」是聯絡人「王女士」名下的學員
- And「小明」在 Burnaby 校區已報名 2026 Spring 季度週二 17:00 的 1on2 課程,共 12 堂
- And「小明」當前的請假額度為 4 次/季度
- And「小明」目前尚未使用任何請假額度
- And 王女士已登入家長端
待確認 Q09.1:請假額度 4 次/季度 — 這個數字是固定全系統一致,還是 per 學員/per 等級可配置?文檔未明說。
Scenario 1 24 小時前請假(≥ 24hr)— 從 enrollment 移除 + 不扣費 + 補課額度(僅季度課)
- Given 距離下堂課(2026-04-07 17:00)還有 25 小時
- And 該堂課屬於季度課程
- When 王女士在課程列表點擊該堂課的「請假」並確認
- Then 該堂課狀態 → 「正常請假」
- And 系統立即將小明從該 session 的 enrollment 移除(capacity 釋放)
- And 系統不對該堂課扣費(4/7 17:00 自動扣費 cron 觸發時,小明已不在 enrollment)
- And 系統為小明建立 1 筆「待補課」紀錄
- And 補課額度 -1(從 2 變 1,僅季度課有額度)
- And 請假額度 -1(從 4 變 3)
- And Toast 提示「請假成功,可申請補課(同等級及以上空檔)」
- And 通知中心新增「請假成功」
Scenario 2 24 小時內請假(< 24hr)— 仍 enrollment、照常扣費、無補課
- Given 距離下堂課還有 8 小時(< 24hr)
- When 王女士提交請假
- Then 介面先彈確認框:「距離上課不足 24 小時,本次請假將不可補課,且該堂課照常扣費。確定提交?」
- When 王女士確認
- Then 該堂課狀態 → 「晚請假(不可補課)」
- And 小明仍在該 session 的 enrollment 中
- And 系統不建立補課紀錄
- And 補課額度不變、請假額度不變(晚請假無實際權益,不消耗)
- And 課程開始時系統照常扣 80 CAD(依 F08 自動扣費)
待確認 Q09.2:晚請假是否消耗請假額度?我預設 不消耗(沒拿到任何權益不應再扣額度)。請確認。
Scenario Outline 請假距離 vs 後果對照表(V2.1:24 小時為界)
- Given 距離下堂課還有 <小時> 小時
- When 王女士提交請假
- Then 請假類型 = <類型>
- And 從 enrollment 移除 = <移除>
- And 是否扣費 = <扣費>
- And 補課額度變動 = <補課>(僅季度課)
- And 請假額度變動 = <請假>
Examples:
| 小時 | 類型 | 移除 | 扣費 | 補課 | 請假 |
|---|---|---|---|---|---|
| 72 | 正常請假 | 是 | 否 | −1 | −1 |
| 24.01 | 正常請假 | 是 | 否 | −1 | −1 |
| 24.00 | 正常請假 | 是 | 否 | −1 | −1 |
| 23.99 | 晚請假 | 否 | 是 | 0 | 0 |
| 1 | 晚請假 | 否 | 是 | 0 | 0 |
| 0 | 晚請假 | 否 | 是 | 0 | 0 |
待確認 Q09.4:「24 小時前」邊界是否含 24:00:00 整點?我預設 含(≥ 24hr → 正常請假)。請確認。
Scenario 3 請假額度已用完,自助請假被阻擋
- Given「小明」當前剩餘請假額度為 0
- When 王女士嘗試對該堂課提交請假
- Then 系統阻擋此次請假
- And 顯示提示「請假額度已用完,如需請假請聯繫客服」
- And 該堂課狀態維持為「已報名」
- And 不產生任何請假紀錄、不扣額度、不通知
Scenario 4 撤回請假(補課名額未被佔用)— 成功
- Given 王女士已對 2026-04-07 的課提交「正常請假」
- And 該堂課對應的「待補課」名額尚未被任何學員選走
- When 王女士點擊「撤回請假」
- Then 該堂課狀態恢復為「已報名」
- And「小明」剩餘請假額度回到請假前的值(恢復為 4 次)
- And 對應的「待補課」紀錄被移除
- And 通知中心新增「撤回請假成功」通知
Scenario 5 撤回請假(名額已被他人補走)— 阻擋
- Given 王女士已對該堂課提交請假
- And 學員「小華」已選擇該名額補課並完成
- When 王女士點擊「撤回請假」
- Then 系統阻擋撤回
- And 顯示提示「該名額已被他人補走,無法撤回。如需協助請聯繫客服」
- And 該堂課狀態維持為「正常請假」
Scenario 6 客服特批超額請假
- Given「小明」剩餘請假額度為 0
- And 客服「Lisa」已登入後台
- When Lisa 在學員詳細頁為「小明」對某堂課手動添加「特批請假」
- Then 該堂課狀態顯示為「超額請假(特批)」
- And「小明」剩餘請假額度仍為 0(特批不扣額度)
- And 系統建立 1 筆 audit log,記錄操作人 Lisa、時間、原因
- And 王女士的家長端收到「客服已為您安排請假」通知
待確認 Q09.5:特批請假是否同時提供補課資格?我預設「是」(特批的意義在於恢復家長權益)。請確認。
Scenario 7 24hr 內請假與 F08 自動扣費的整合 內部行為
- Given 小明對 2026-04-07 17:00 提交「晚請假」(< 24hr)
- And 小明仍在該 session enrollment 中
- When 17:00 系統觸發 F08 自動扣費 cron
- Then 系統檢測到小明仍在 enrollment(晚請假不移除)
- And 從訂單對應餘額扣 80(單堂價)
- And t_credit_log change_type = LATE_LEAVE_DEDUCT(區別於正常 SESSION_AUTO_DEDUCT)
- And 不建立補課紀錄
- And session.status → PROCESSED
Scenario 8 24hr 前請假時 enrollment 立即移除 內部行為
- Given 王女士提交 24hr 前請假
- When 系統處理請假請求(單一事務)
- Then 系統執行:
- • DELETE FROM t_enrollment WHERE student_id = X AND session_id = Y
- • INSERT t_leave_record(紀錄請假歷史,永久保留)
- • 季度課:INSERT t_makeup_quota(補課額度)
- • UPDATE t_student.leave_quota -= 1(季度範圍)
- • 通知家長
- And 之後 F08 自動扣費 cron 觸發時,該 student 不在 enrollment 列表中,跳過扣費
04. 課表查看(多視角) 已完成 家長端 + 客服端 + 教練
⚠️ V2.1 變更:
家長端課表規則調整為「只顯示有空位可填 Waitlist 的課程 + 自家學員已報名的課程」。
結合 F13 V2.1(Waitlist 綁 session),家長從課表上看到滿員課可點「加入 Waitlist」。
客服 / 教練端不變。
主要角色:家長(王女士)、客服 ADMIN(Lisa, Burnaby)、客服 SUPER_ADMIN(Mark)、教練(Coach Luke)、系統
Background 共用前置條件
- Given 2026 Spring 季度(2026-03-01 ~ 2026-06-15)已建立
- And Burnaby 校區的 Pool A 已建立(含 Lane 1, 2, 3)
- And Burnaby 課表包含以下課程:
- • 1on2 週二 17:00-18:00 Pool A Lane 1 by Coach Luke (L4),Level 區間 L3-L4,2 left
- • 1on1 週四 18:00-19:00 Pool A Lane 2 by Coach Luke,Level 區間 L4-L5,1 left
- • Group 週六 10:00-11:00 Pool A Lane 3 by Coach Eric,Level 區間 L1-L2,4 left
- And 小明(L3)已報名週二 17:00 那節 1on2
- And 王女士已登入家長端
- And Lisa(ADMIN, Burnaby)已登入後台
- And Coach Luke 已登入
Scenario 1 家長查看自家學員的課表(當前季度)
- Given 王女士當前查看「小明」的視角
- And 當前是 2026-04-15(在 2026 Spring 季度內)
- When 她進入家長端「課表」頁
- Then 預設顯示週視圖(本週週一到週日)
- And 課表只顯示 2026 Spring 季度的 sessions
- And 看到 3 種狀態的課程:
- • 已報名(藍色框):週二 17:00 1on2 by Coach Luke
- • 可補(橙色框):之前請假符合補課條件的課(若無則不顯示)
- • Available(淡灰色虛線):尚未設定類型的空堂
- And 不顯示其他學員(如小華)的課程
- And 不顯示其他校區(如 Coquitlam)的課程
Scenario 2 家長端視圖切換 — 日 / 週 + 月份切換器
- Given 王女士在課表頁
- When 她點擊頂部「日」/「週」切換器
- Then 切換對應視圖
- And 月份切換器在頂部,可前後翻月
- And 但只能在 2026 Spring 季度範圍內切換(2026-03 至 2026-06)
- And 嘗試切到季度外的月份時提示「下一季度尚未開放查看」
待確認 Q04.1:家長端是否提供「月視圖」(grid 顯示一整月)?文檔說「日 / 週兩種視圖,並可切換月份」 — 我解讀為「日 + 週兩種視圖,月份是切換器」(沒有真正的月 grid view)。請確認。
Scenario 3 家長僅看符合學員 Level 的課(+ 所有 Available 空堂)
- Given 小明 Level = L3
- And Burnaby 課表有以下課程(與 Background 列表)
- When 王女士查看小明的課表
- Then 看到 1on2 L3-L4(符合 Level)
- And 不看到 1on1 L4-L5(Level 不符)
- And 不看到 Group L1-L2(Level 不符)
- And 所有 Available 空堂仍顯示(不論 Level — 因為空堂沒有預設 Level)
Scenario 4 家長無法查看歷史 / 未來季度
- Given 當前是 2026 Spring 季度進行中
- And 2025 Fall(已結束)和 2026 Summer(未開始)的課表也都存在
- When 王女士嘗試切換月份到 2026-07 或 2026-01
- Then 系統提示「僅可查看當前季度(2026 Spring)」
- And 不顯示這些月份的課程
待確認 Q04.2:家長端是否能查看「已結束的歷史季度」?我預設 不可(在課表頁僅當前季度),但「學習紀錄」頁可以看歷史已上完課程。請確認。
Scenario 5 家長點擊 Available 空堂 — 不能直接報名
- Given 課表存在 Available 空堂(週三 17:00 Pool A Lane 1)
- When 王女士點擊該空堂
- Then 開啟說明卡片:「此時段尚未開課,如有興趣請聯繫客服」
- And 卡片提供 2 個入口:「聯繫客服」+「加入 Waitlist」
- And 完全沒有「立即報名」按鈕(V2 規定家長不可自助報名)
Scenario 6 家長點擊已報名課程 — 進入詳情面板
- Given 小明已報名週二 17:00 課
- When 王女士點擊該課程卡片
- Then 開啟詳情面板,顯示:
- • 課程資訊(時間、教練、Level、泳道、課程類型)
- • 該日課程狀態(待點名 / 出席 / 請假 / No-show)
- •「請假」按鈕(依 12 小時規則決定行為,詳見 F09)
- •「查看課程說明」入口(教練介紹、課程目標等)
- And 不顯示其他學員的個人資訊(即使同節 1on2 還有另一位學員)
Scenario 7 家長點擊「可補」標記 — 進入補課流程
- Given 小明前一週請假了 1 堂 1on2 課(正常請假,產生補課額度)
- And 課表上有另一堂同類型同教練等級同價格的 1on2 課程,未滿員
- When 王女士點擊該「可補」標記的課程
- Then 進入補課確認流程(詳見 F10 補課)
Scenario 8 ADMIN 僅看本校區的課表
- Given Lisa 是 ADMIN,綁定 Burnaby 校區
- When 她進入後台課表
- Then 校區下拉選擇器只顯示「Burnaby」(唯一可選)
- And 直接調整 URL 或 API 嘗試切到 Coquitlam → 返回 403
- And 後端會強制注入 branchId 過濾條件,無法繞過
Scenario 9 SUPER_ADMIN 跨校區查看
- Given Mark 是 SUPER_ADMIN,無校區綁定
- When 他進入後台課表
- Then 校區下拉可選 Burnaby / Coquitlam / Surrey 等
- And 切換校區後課表立即更新
- And 可選「全部校區」做跨校區比較(如報表用途)
Scenario 10 客服 11 維度篩選器
- Given Lisa 在後台課表頁
- When 她展開頂部固定篩選列
- Then 看到以下 11 個篩選欄位:
| # | 欄位 | 取值 |
|---|---|---|
| 1 | 課程種類(Kind) | 正課 / 補課 / 單次(One-Time)/ Trial |
| 2 | 課程類型(Type) | 1on1 / 1on2 / Group / 長訓 |
| 3 | 教練 | 下拉選一個或多個 |
| 4 | 日期 | 單日 / 範圍 |
| 5 | 季度 | 2026 Spring / Summer / Fall / Winter |
| 6 | 泳道 | Lane 1 / Lane 2 / ... |
| 7 | 泳池 | Pool A / Pool B |
| 8 | 學員標籤 | WAITLIST / ACTIVE_CREDIT / NEW / ...(詳見 F12) |
| 9 | 等級(Level 區間) | L1 / L2 / L3 / L4 / L5 |
| 10 | 時間段 | 例:14:00-18:00 |
| 11 | 搜尋欄 | 關鍵字模糊搜尋(教練名 / 學員名 / 備註) |
待確認 Q04.3:「課程種類」與「課程類型」是否為兩個不同欄位?我解讀為:「種類」= 報名性質(正課 / 補課 / 一次性 / Trial),「類型」= 教學形式(1on1 / 1on2 / Group / 長訓)。請確認。
Scenario 11 客服建立新課程 — 設定教練 / 泳道 / 時間 / Level / 人數
- Given Lisa 在週四 17:00 的空白時段
- When 她點擊「新增課程」並填寫:
- • 課程類型: 1on2
- • 教練: Coach Luke
- • 泳道: Pool A Lane 2
- • 時間: 週四 17:00-18:00
- • Level 區間: L3-L4(自定義)
- • 人數上下限(1on2 自動鎖 = 2/2)
- And 點擊保存
- Then 系統建立 1 個 RecurrenceGroup(季度週期性紀錄)
- And 系統自動展開該季度所有上課日期的 sessions(扣除國定假日)
- And 課表立即顯示新增課程
Scenario 12 衝突檢測 — 泳道時間衝突(V2 必須阻擋)
- Given 週二 17:00-18:00 Pool A Lane 1 已被 Coach Luke 的 1on2 課程使用
- When Lisa 嘗試在同時段同泳道安排另一節課
- Then 系統阻擋
- And 顯示「Pool A Lane 1 在週二 17:00-18:00 已被 Coach Luke 的 1on2 課程使用」
- And 不建立新課程
待確認 Q04.4:泳道衝突在 V1 是「未阻擋」,文檔明確要求 V2 阻擋。但實際使用是否需要「強制覆蓋」逃生門?例如同時間同泳道可拆兩 lane(如 Lane 1A / Lane 1B)?預設 嚴格阻擋,無逃生門。
Scenario 13 衝突檢測 — 教練時間衝突
- Given Coach Luke 在週二 17:00-18:00 已有 1on2 課程(Pool A Lane 1)
- When Lisa 嘗試在同時段安排 Coach Luke 的另一節 1on1 課(在 Pool A Lane 3)
- Then 系統阻擋
- And 顯示「Coach Luke 在週二 17:00-18:00 已有課程」
Scenario 14 衝突檢測 — 學員時間衝突(報名階段)
- Given 小明已報名週二 17:00 的 1on2 課程
- When Lisa 嘗試將小明加入週二同時段的另一節課
- Then 系統阻擋
- And 顯示「學員小明在週二 17:00 已有課程」
- And 報名流程的學員選擇步驟即時提示衝突,不必等到 Step 7
Scenario 15 1on1 / 1on2 為固定人數
- Given Lisa 建立課程,課程類型: 1on2
- When 系統初始化人數設定
- Then 系統自動鎖定人數上限為 2
- And 不允許客服修改該數字(欄位變灰)
- And 同理 1on1 鎖定為 1
Scenario 16 Group / 長訓 自定義人數上下限
- Given Lisa 建立課程,課程類型: Group
- When 系統提示設定人數
- Then 允許輸入下限(warning_capacity,例 4)和上限(hard_capacity,例 10)
- And 報名超過 hard_capacity 會被阻擋
- And 報名數量低於 warning_capacity 時,課表顯示「人數不足」黃色標記
待確認 Q04.5:「人數不足」是否影響開課?例如下限 4 但只報 3 人,是否自動取消開課?預設 不自動取消,僅提示客服,由客服人工決定是否取消或鼓勵 Waitlist。
Scenario 17 ADMIN 不可跨校區點名
- Given Lisa 是 ADMIN,綁定 Burnaby
- And 系統設定允許 ADMIN 看到全部校區的課表(純查看)
- When Lisa 嘗試對 Coquitlam 校區的某節課點名
- Then 系統阻擋
- And 顯示「您僅能對 Burnaby 校區的學員點名」
- And 但仍可查看 Coquitlam 課表(如需 Burnaby 學員到 Coquitlam 上課的情境)
待確認 Q04.6:ADMIN 是否能「看」其他校區的課表?文檔說「ADMIN 可操作所有校區的課表與學員安排」+「不可跨校區進行點名」。看來是 可看全部、僅本校區可點名。請確認。
Scenario 18 教練只看自己的課表
- Given Coach Luke 已登入
- When 他進入課表頁
- Then 只看到自己負責的 sessions
- And 完全看不到其他教練(如 Coach Eric)的課程
- And 跨校區(如他同時在 Burnaby 和 Coquitlam 教課)的課全部一起顯示,並標註校區
Scenario 19 教練篩選器(校區 / 等級 / 週幾)
- Given Coach Luke 同時在 Burnaby 和 Coquitlam 帶 4 個 sessions
- When Luke 點擊頂部篩選器
- Then 提供 3 個篩選維度:
- • 校區: Burnaby / Coquitlam(限他帶過課的校區)
- • 等級: L3 / L4(限他帶過的 sessions Level 區間)
- • 週幾: 一 ~ 日
- And 不提供其他維度(如教練、泳道、學員標籤)— 教練視角是課程而非學員
Scenario 20 教練不可修改任何資料(除點名)
- Given Coach Luke 在某節 session 詳情頁
- When 他查看介面
- Then 介面隱藏「編輯課程」「刪除」「報名學員」「取消報名」等按鈕
- But「點名」按鈕保留可用(含逐生點名 + 批量點名)
- And 即使 API 直接呼叫修改 / 刪除類接口會返回 403
Scenario 21 系統自動展開季度週期性課程 內部行為
- Given Lisa 建立週二 17:00 1on2 課程,季度為 2026 Spring(2026-03-01 ~ 2026-06-15)
- And 季度區間共有 15 個週二
- And 其中 2026-04-07(Good Friday)為國定假日
- When 系統建立 RecurrenceGroup 並執行展開
- Then 系統建立 14 個 t_session 紀錄(15 - 1 國定假日)
- And 每個 session 對應一個具體日期
- And 國定假日不建立 session
- And 報名 Step 4 顯示這 14 個日期 + 國定假日(標記「國定日(休)」不可選)
Scenario 22 季度跨月份切換時的 session 展示 內部行為
- Given 課表查詢請求要看 2026-04 月的課程
- When 後端收到請求
- Then 從 t_session 中查詢 session_date 在 2026-04-01 ~ 2026-04-30 範圍內的紀錄
- And 應用該角色的篩選(家長: 自己學員; ADMIN: 校區; COACH: 自己)
- And 應用該角色的可見性(家長: Level 過濾; 客服 / 教練: 全部可見)
- And 返回 sessions + 每節的當前報名數 + 每節的學員列表
05. 餘額 / 訂單模型(V2.1:雙層餘額)已完成 內部行為 + 客服端 + 家長端
⚠️ V2.1 重大變更(2026-05-08 拍板):
架構從「訂單剩餘額度直接扣」 → 「付款先進餘額,再從餘額扣」。
餘額分兩層:(1) 不可消費餘額(訂單付款,綁訂單);(2) 自由消費餘額(任意金額付款 / 退款找零 / 訂單對應課程結束後剩餘)。
扣費先扣訂單對應的不可消費餘額,不足從自由消費補;都不足 alert 客服。
訂單對應 sessions 全部結束後,剩餘自動轉自由消費餘額(cron 每天)。
詳見正典定義 E。Scenarios 中的「訂單剩餘額度」應理解為「訂單對應的不可消費餘額」(語義等價)。
新增功能:任意金額付款(客服在後台代家長充值,進自由消費餘額),FIFO 抵扣概念由「訂單建立時間」改為「整個餘額池中先扣不可消費(按訂單時間)→ 再扣自由消費」。
餘額分兩層:(1) 不可消費餘額(訂單付款,綁訂單);(2) 自由消費餘額(任意金額付款 / 退款找零 / 訂單對應課程結束後剩餘)。
扣費先扣訂單對應的不可消費餘額,不足從自由消費補;都不足 alert 客服。
訂單對應 sessions 全部結束後,剩餘自動轉自由消費餘額(cron 每天)。
詳見正典定義 E。Scenarios 中的「訂單剩餘額度」應理解為「訂單對應的不可消費餘額」(語義等價)。
新增功能:任意金額付款(客服在後台代家長充值,進自由消費餘額),FIFO 抵扣概念由「訂單建立時間」改為「整個餘額池中先扣不可消費(按訂單時間)→ 再扣自由消費」。
主要角色:系統、客服(Lisa)、SUPER_ADMIN(Mark)、家長(王女士)
Background 共用前置條件
- Given 王女士名下有學員小明(L3)
- And 小明當前無任何訂單,Credit Pool 為空
- And 系統已配置:1on2 + L3-L4 教練 = 80 CAD/堂(未稅)
- And GST = 5%
- And 註冊費 = 30 CAD(一次性,已收於小明首次報名)
Scenario 1 訂單完整生命週期:待付款 → 待確認 → 已完成(Online)
- Given Lisa 為小明生成訂單 #001:12 堂 1on2,總額 1008 CAD(80×12×1.05)
- When Lisa 點擊「生成訂單」
- Then 訂單 #001 狀態 = 待付款
- And 12 個 sessions 的 capacity 暫時保留(小明被視為「待付款佔位」,不算正式報名)
- And 系統設定 60 分鐘倒數計時
- And 王女士的家長端立即看到該待付款訂單
- When 30 分鐘後王女士在家長端完成 Online 付款
- Then 訂單狀態 → 待確認
- And Online 倒數計時停止
- When 金流系統 webhook 確認 Stripe 收款成功(或 Lisa 在後台手動標記確認)
- Then 訂單狀態 → 已完成
- And 小明正式加入 12 個 sessions 的學員列表
- And 訂單剩餘額度 = 12 堂 / 960 CAD(未稅課程價值 = 1008 - 48 GST)
- And 通知中心:「訂單 #001 已完成,學員已加入課表」
待確認 Q05.1:訂單剩餘額度的單位 — 是「堂數」還是「金額」?我預設兩個都記錄(堂數 = 主鍵;金額 = 單堂價格快照 × 堂數)。每節課單價可能因「教練等級」「課程類型」而異,所以堂數+單價快照組合最準確。請確認。
待確認 Q05.2:Credit Pool 計入的金額是「未稅」還是「含稅」?我預設 未稅(GST 屬政府不該流入學員餘額池)。退款時 GST 部分按比例退回原付款方式。請確認。
Scenario 2 訂單超時自動取消(Online 60 分鐘)
- Given 訂單 #002 在 14:00 生成,待付款狀態
- And 12 個 sessions 的 capacity 暫時保留
- When 60 分鐘後(15:00)王女士仍未完成 Online 付款
- Then 系統自動將訂單 #002 狀態改為 已取消
- And capacity 立即釋放(其他學員可報名同 session)
- And 王女士的家長端待付款訂單從列表消失
- And 通知中心新增「訂單 #002 已自動取消(超時未付款)」
- And 該訂單不可再被使用 / 復活
Scenario 3 Cash 訂單超時自動取消(48 小時)
- Given 訂單 #003 為 Cash 付款方式,2026-04-01 14:00 生成
- When 48 小時後(2026-04-03 14:00)小明 / 王女士都未到客服處交現金
- Then 系統自動將訂單 #003 改為已取消
- And capacity 釋放
- And 家長端通知「Cash 訂單 #003 已逾時取消,如仍需報名請聯繫客服重新生成訂單」
Scenario 4 已完成訂單退款流程(已完成 → 退款中 → 已退款)
- Given 訂單 #001 已完成,已上 4 堂課
- And 訂單剩餘 = 8 堂 / 640 CAD(未稅)
- And 王女士因小明受傷申請退款
- When SUPER_ADMIN Mark 在學員詳情頁發起退款
- And 填寫退款明細:未上 8 堂 = 640;扣手續費 10;可退 = 630(不退註冊費,因為註冊費已隱含在歷史訂單中)
- Then 訂單 #001 狀態 → 退款中
- And 訂單剩餘額度凍結(不可再扣費,從 Credit Pool 移除)
- And 8 個未來 sessions 中的小明被移除(capacity 釋放)
- When 金流系統確認已退款 630 CAD 到原付款方式
- And Mark 在後台手動標記「金流已確認」
- Then 訂單狀態 → 已退款
- And 通知中心:「退款 630 CAD 已完成」
- And 學員標籤更新為 REFUNDED(詳見 F12)
Scenario 5 不允許的訂單狀態轉換 — 防呆
- Given 系統定義合法 transitions:
- • 待付款 → 待確認 / 已取消
- • 待確認 → 已完成 / 已取消
- • 已完成 → 退款中
- • 退款中 → 已退款
- When 任何 service / 直接 SQL 嘗試非法 transition
- Then 系統拒絕並丟出錯誤碼 ORDER_INVALID_TRANSITION
非法 transitions 對照表:
| 從 | 到 | 合法? | 原因 |
|---|---|---|---|
| 待付款 | 已完成 | 否 | 必須經過待確認 |
| 已取消 | 任何 | 否 | 取消後不可復活 |
| 已退款 | 任何 | 否 | 終態 |
| 已完成 | 已取消 | 否 | 已完成只能走退款 |
| 待確認 | 已取消 | 是 (Q) | 客服查無入帳可取消 |
Scenario 6 已完成訂單形成 Credit Pool
- Given 訂單 #001 已完成,總價值 960 CAD(12 堂 × 80 未稅)
- And 已上 4 堂 → 扣 320 CAD
- When 系統查詢小明的 Credit Pool 餘額
- Then Pool = 960 - 320 = 640 CAD(剩 8 堂)
- And 來源明細顯示:「訂單 #001:剩 8 堂 1on2 / 640 CAD」
- And 該餘額可用於該訂單的後續 8 堂課扣費
- And 該餘額可用於 FIFO 抵扣下一筆同學員的新訂單
Scenario 7 退款找零回 Credit Pool
- Given 訂單 #001 申請退款
- And 應退 = 540 CAD(已扣 4 堂 + 註冊費不退 + 手續費 + 一些奇數調整)
- And 但金流系統因規則限制只能整數退 530 CAD
- When Mark 在退款表單裡分配「退到金流 530,找零 10 進 Credit Pool」
- Then 訂單 #001 狀態 → 已退款
- And Credit Pool 增加 10 CAD(來源:退款找零)
- And Credit 詳情頁新增一行:「退款找零 +10 CAD(來自訂單 #001 退款)」
- And 該 10 CAD 可用於下次報名 FIFO 抵扣
待確認 Q05.3:找零的決定權 — 是「金流退不到自動入池」還是「Mark 手動分配」?我預設 Mark 手動分配(更靈活,可以選擇全退金流 / 部分入池)。請確認。
Scenario 8 點名觸發扣款 — 從訂單剩餘扣(綁定 order_id)
- Given 小明訂單 #001 剩 8 堂 / 640 CAD
- And 4 月 14 日的課程屬於該訂單
- When Lisa 對 4/14 課程點名小明為「出席」
- Then 系統從訂單 #001 扣除 1 堂(80 CAD 未稅)
- And 訂單 #001 剩餘 = 7 堂 / 560 CAD
- And t_credit_log 新增 1 筆紀錄:
- • student_id = 小明
- • amount = -80
- • change_type = SESSION_DEDUCT
- • session_id = 4/14 課程的 session
- • order_id = #001 ← 綁定到具體訂單
- • balance_after = 560
- And 訂單仍為「已完成」狀態
Scenario 9 No-show 自動扣款(與出席同等)
- Given 小明訂單 #001 剩 7 堂額度
- And 4/21 課程結束後沒人點名小明
- When 系統於課程結束(19:00)自動結算
- Then 小明標記為 No-show
- And 從訂單 #001 扣除 1 堂(與正常出席相同)
- And t_credit_log 新增 change_type = NO_SHOW_DEDUCT
Scenario 10 訂單耗盡時的處理
- Given 小明訂單 #001 剩 1 堂 / 80 CAD
- When 點名最後一堂為出席
- Then 訂單 #001 剩餘 = 0 堂 / 0 CAD
- And 訂單狀態仍為「已完成」(不變)
- And 訂單在「Credit Pool 來源明細」中標記為「已耗盡」
- And 不再出現在「可抵扣下次報名」的可用列表中
Scenario 11 Dashboard 顯示總 Credit 餘額(多訂單合計)
- Given 小明的 Credit 來源:
- • 訂單 #001:剩 7 堂 1on2 / 560 CAD
- • 訂單 #002:剩 3 堂 1on1 / 240 CAD(不同類型)
- • 退款找零:10 CAD
- When 王女士在家長端 Dashboard 查看小明
- Then 顯示主要數字:「Credit 餘額:810 CAD」
- And 下方備註:「來自 2 筆訂單 + 退款找零」
- And 點擊「詳情」進入逐筆明細頁
Scenario 12 詳情頁逐筆訂單明細(強調「對應課程」)
- Given 王女士進入 Credit 詳情頁
- Then 顯示 3 筆來源(按 FIFO 順序):
| 來源 | 內容 | 對應課程 |
|---|---|---|
| 訂單 #001 | 剩 7 堂 1on2 / 560 CAD | 連結 → 看 7 個未來 sessions |
| 訂單 #002 | 剩 3 堂 1on1 / 240 CAD | 連結 → 看 3 個未來 sessions |
| 退款找零 | 10 CAD(任意類型可抵扣) | 來源訂單 #003 退款 |
- And 訊息語氣強調「對應課程」而非「自由餘額」
- And 介面顯示說明文字:「此 Credit 對應您已付款的課程,可用於後續扣費或抵扣新訂單」
Scenario 13 Credit 流水紀錄(每筆綁訂單)
- Given 小明已上完 5 堂 1on2 課
- When 王女士查看 Credit 流水頁
- Then 顯示流水紀錄(按時間倒序):
| 日期 | 動作 | 金額 | 關聯訂單 | 扣後餘額 |
|---|---|---|---|---|
| 2026-04-21 | 出席扣費 | -80 | #001 | 560 |
| 2026-04-14 | 出席扣費 | -80 | #001 | 640 |
| 2026-04-07 | 出席扣費 | -80 | #001 | 720 |
| 2026-03-31 | 出席扣費 | -80 | #001 | 800 |
| 2026-03-24 | 出席扣費 | -80 | #001 | 880 |
| 2026-03-17 | 訂單建立 | +960 | #001 | 960 |
- And 每筆顯示日期、動作(出席 / 退款 / 找零 / 抵扣)、金額、訂單關聯、扣後餘額
Scenario 14 FIFO 抵扣下次報名
- Given 小明 Credit Pool 來源:
- • 訂單 #001(已完成 2026-03-17):剩 200 CAD
- • 訂單 #002(已完成 2026-04-25):剩 100 CAD
- • 退款找零(產生 2026-05-15):50 CAD
- And Lisa 為小明生成新訂單 #004:總額 250 CAD(含稅)
- When Lisa 在 Step 6 輸入「使用 250 Credit 抵扣」
- Then 系統按 FIFO 順序扣:
- • 從訂單 #001 扣 200(耗盡)
- • 從訂單 #002 扣 50(剩 50)
- And 退款找零未被動到
- And 訂單 #004 待付款金額 = 0 CAD(純 Credit 抵扣)
- And Credit 流水新增 2 筆「抵扣訂單 #004」紀錄
待確認 Q05.4:FIFO 是按「訂單完成時間」還是「訂單建立時間」?我預設 訂單完成時間(更貼近「先收到的錢先用」的會計邏輯)。請確認。
待確認 Q05.5:退款找零是否與訂單剩餘額度走同一個 Pool?或是優先 / 最後扣?我預設 同 Pool 但 FIFO 在最後(找零產生時間通常較晚)。請確認。
Scenario 15 抵扣超過 Credit Pool 餘額 — 阻擋
- Given 小明 Credit Pool 總計 350 CAD
- When Lisa 在 Step 6 嘗試輸入「抵扣 400 CAD」
- Then 系統阻擋
- And 顯示「Credit 餘額不足,最多可使用 350 CAD」
- And 自動將輸入框值修正為 350
Scenario 16 抵扣範例(文檔指定範例)
- Given 訂單總額 621.18 CAD(含稅)
- And 小明 Credit Pool = 220 CAD
- When Lisa 在 Step 6 輸入「使用 220 Credit」
- Then 系統即時計算「待付款 = 401.18 CAD」(621.18 - 220)
- And 點擊生成訂單後,220 CAD 從 Pool 扣除
- And 待付款訂單顯示應付 401.18
Scenario 17 Credit 不可跨學員共用(兄弟姐妹獨立池)
- Given 王女士有 2 個學員:小明(Credit 200)、小華(Credit 0)
- When Lisa 為小華生成訂單時嘗試使用「家庭合計 Credit」
- Then 系統不提供此選項
- And Credit 抵扣輸入欄位顯示「小華 Credit Pool: 0 CAD」
- And 小明的 Credit 不可用於小華的訂單
待確認 Q05.6:同家庭兄弟姐妹之間 Credit 是否可共享?我預設 不共享(每學員獨立池,避免會計混亂)。如果需要共享,可由 SUPER_ADMIN 手動發起「Credit 轉移」(特批操作 + audit log)。請確認。
Scenario 18 跨季度延續(Credit 不過期)
- Given 2026 Spring 結束時小明 Credit Pool 還有 200 CAD
- And 進入 2026 Summer 季度
- When Lisa 為小明報名 2026 Summer 課程
- Then 上季剩餘 200 CAD 仍可用於本次報名(FIFO 抵扣)
- And Credit Pool 永不過期
待確認 Q05.7:Credit Pool 真的永不過期?實務上有些學校會設「12 個月內未使用即過期,作為公司收入」。我預設 永不過期(V2 文檔未提,且更符合用戶權益)。請確認。
Scenario 19 部分退款的處理
- Given 訂單 #001 已完成,剩 8 堂未上 / 640 CAD
- And 王女士想退 4 堂(保留 4 堂繼續上)
- When Mark 發起部分退款,退 4 堂 = 320 CAD
- Then 訂單 #001 狀態仍為「已完成」(不變)
- But 在訂單詳情標註「部分退款 320 CAD(2026-05-08)」
- And 訂單剩餘額度從 8 堂 / 640 變為 4 堂 / 320
- And 對應的 4 個未來 sessions 中小明被移除
- And t_refund 新增 1 筆部分退款紀錄(refund_type = PARTIAL)
- And t_credit_log 新增 1 筆 PARTIAL_REFUND 流水
待確認 Q05.8:部分退款後訂單狀態保持「已完成」是否合理?或要新增「部分退款」狀態?我預設 保持已完成 + 詳情標註(避免狀態機過度複雜)。請確認。
Scenario 20 t_credit_log / t_audit_log / t_refund 不可修改 內部行為
- Given 系統有 100 筆 credit_log 紀錄
- When 任何 Service / Mapper 嘗試 UPDATE 或 DELETE 一筆 credit_log
- Then 操作被 Mapper 層阻擋(編譯期不存在這兩個方法)
- And 唯一允許的操作是 INSERT 新紀錄
- And 同樣規則適用於 t_audit_log 與 t_refund(refund 不可 DELETE)
- And 即使 SUPER_ADMIN 也無法繞過 — 想修正錯誤紀錄只能 INSERT 一筆「沖正」紀錄
Scenario 21 訂單超時自動取消任務(系統定時) 內部行為
- Given 系統有大量待付款訂單
- When 系統每 1 分鐘執行一次「訂單超時掃描」
- Then 找出所有 created_at 超過 60 分鐘的 Online 待付款訂單,標記為已取消
- And 找出所有 created_at 超過 48 小時的 Cash 待付款訂單,標記為已取消
- And 釋放對應的 capacity
- And 對受影響的家長發出通知
- And 系統 audit log 紀錄取消批次
06. 報名 7 步流程 已完成 客服端
⚠️ V2.1 變更:
(1) 季度課必須先排好課(建立 RecurrenceGroup + 展開 sessions)才能生成訂單(流程不變,但概念明確)。
(2) 新增「任意金額付款」捷徑(不走 7 步,直接客服在後台「為學員充值」彈窗,進自由消費餘額)— 詳見 F07 新增 scenario。
(3) Step 6 Credit 抵扣的「Credit Pool」改為「自由消費餘額」(不可消費餘額不可作為抵扣下次報名的工具,仍綁原訂單)。
主要角色:客服(Lisa)、SUPER_ADMIN(Mark)、家長(王女士)、學員(小明 L3 / 小華 L1)、系統
Background 共用前置條件
- Given Lisa(ADMIN, Burnaby)已登入後台
- And 2026 Spring 季度(2026-03-01 ~ 2026-06-15)已建立
- And Burnaby 課表包含:1on2 週二 17:00 by Coach Luke (L3-L4)、1on1 週四 18:00 by Coach Luke (L4-L5)、Group 週六 10:00 by Coach Eric (L1-L2)
- And 價格表:1on2 + L3-L4 = 80 CAD/堂;1on1 + L4-L5 = 80 CAD/堂;Group 任意 = 30 CAD/堂
- And 註冊費 = 30 CAD(per 學員生命週期一次)
- And GST = 5%
- And 小明(L3)尚無任何訂單;小華(L1)首次接觸系統
Scenario 1 入口 1:從課表的某個課程進入「學員報名」
- Given 課表上有「週二 17:00 1on2 by Coach Luke」課程,1 left
- When Lisa 在課表點擊該課程的「學員報名」按鈕
- Then 進入報名嚮導 Step 1
- And Step 1 自動帶入:校區 = Burnaby
- And Step 2 預設選「季度(Seasonal)」+ 季度 = 2026 Spring
- And Step 3 自動帶入:課程類型 = 1on2、週幾 = 週二、時間 = 17:00、教練 = Coach Luke、泳道 = Lane 1、Level 區間 = L3-L4
- And Lisa 只需 Step 1 選學員、Step 4 確認日期、後續走完即可
Scenario 2 入口 2:從 Waitlist 進入「報名」
- Given Waitlist 上有小華的候補需求:Burnaby、2026 Spring、週六、10:00、Group
- When Lisa 在 Waitlist 列表點擊小華那筆紀錄的「報名」按鈕
- Then 進入 Step 1
- And 自動帶入:學員 = 小華、聯絡人 = 王女士、Level = L1、校區 = Burnaby
- And Step 2 預設選「季度」+ 季度 = 2026 Spring(依 Waitlist 偏好)
- And Step 3 預設課程類型 = Group、週幾 = 週六、時間提示 ≈ 10:00(仍由 Lisa 確認)
Scenario 3 入口 3:從學員管理頁進入「報名」
- Given Lisa 在學員列表點擊小明那行的「報名」按鈕
- When 進入報名流程
- Then Step 1 自動帶入:學員 = 小明、聯絡人 = 王女士、Level = L3
- And 校區欄位顯示小明主校區 = Burnaby(但可改)
- And Step 2-7 全部需 Lisa 手動選擇
Scenario 4 Step 1:校區可重新選擇(學員可能跨校區上課)
- Given Lisa 在 Step 1,預設校區 = 小明主校區 Burnaby
- When Lisa 將校區改為 Coquitlam
- Then 系統允許切換
- And 後續 Step 3 課程列表將顯示 Coquitlam 校區的課程
- And 訂單最終建立在 Coquitlam 校區下(branch_id = Coquitlam)
- And 小明的「主校區」欄位不變(仍為 Burnaby)
Scenario 5 Step 2:選擇 Seasonal — 需指定具體季度
- Given Lisa 在 Step 2
- When 她選「季度(Seasonal)」
- Then 顯示季度下拉:2026 Spring(進行中)/ 2026 Summer / 2026 Fall
- And 預設選擇進行中或下一個將開放的季度
- And 已結束的季度(如 2025 Fall)不在選項中
Scenario 6 Step 2:選擇 Trial — 自動套用 8 折
- Given 小明從未上過 Coach Luke 的正式課
- And 小明從未對 Coach Luke 申請過 Trial
- When Lisa 在 Step 2 選「Trial」
- Then 系統允許繼續
- And Step 5 費用明細的單堂計算將自動套用 80% × 80 = 64 CAD
- And 折價 16 CAD 顯示在 Invoice 的「折扣」欄位(呈現原價 + 折扣)
Scenario 7 Step 2:Trial 規則 — 每教練名下僅一次
- Given 小明已對 Coach Luke 申請過 Trial 並已上完
- When Lisa 在 Step 2 為小明選「Trial」並在 Step 3 選 Coach Luke 的課
- Then 系統阻擋
- And 顯示「該學員已對 Coach Luke 使用過 Trial,不可再 Trial」
- But Lisa 可改選 Coach Eric 或其他教練的 Trial(前提是該學員從未對該教練 Trial 或上正式課)
Scenario 8 Step 2:Trial 規則 — 已上過正式課則不可
- Given 小明已上過 Coach Luke 的 1on2 季度課(已 attendance)
- When Lisa 嘗試為小明選 Trial + Coach Luke 的課
- Then 系統阻擋
- And 顯示「學員已上過 Coach Luke 的正式課程,無法使用 Trial」
待確認 Q06.1:「上過正式課」的判定 — 是「曾被任意點名為出席」,還是「曾完成過該教練名下的訂單付款」?我預設 曾被點名為出席(attendance status = ATTENDED)。請確認。
Scenario 9 Step 3:列表只顯示符合學員 Level 的課程
- Given 小明 Level = L3
- And Burnaby 課程:1on2 L3-L4、1on1 L4-L5、Group L1-L2
- When Lisa 在 Step 3 選「課程類型 = 1on2」+「週幾 = 週二」
- Then 列表顯示「17:00-18:00 Lane 1 L3-L4 Coach Luke 1 left」
- And 不顯示 1on1 L4-L5(Level 不符)
- And 不顯示 Group(course type 不符)
Scenario 10 Step 3:滿員的課程不顯示
- Given 「週二 17:00 1on2」已滿員(2/2)
- When Lisa 在 Step 3 選 1on2 + 週二
- Then 該時段不在列表中
- And 列表只顯示其他週二的 1on2(未滿員)
- And 若無任何符合課程,顯示「無可報名課程,請考慮其他週幾或加入 Waitlist」
Scenario 11 Step 3:空堂(Available)不在報名列表中
- Given 課表有「週三 19:00-20:00 Lane 1」空堂(尚未設定課程類型)
- When Lisa 在 Step 3 選擇任何課程類型
- Then 空堂不在列表中
- And 顯示提示:「如需開新課程,請先於課表建立後再報名」
Scenario 12 Step 4:季度課自動展開所有日期 + 國定假日標記
- Given Lisa 在 Step 4,季度課 = 2026 Spring,週二 17:00
- And 季度區間共 14 個週二
- And 其中 2026-04-07(Good Friday)為國定假日
- When 系統載入日期列表
- Then 顯示 14 個日期
- And 2026-04-07 標記「國定日(休)」且 checkbox 不可勾選
- And 預設其他 13 個日期全部勾選
- And Lisa 可逐個取消其他日期(例如已知小明 4/14 將出國)
Scenario 13 Step 4:全選 / 重置功能
- Given Lisa 已逐個取消了一些日期
- When 她點擊「重置」
- Then 所有日期被取消勾選(除已標記國定假日)
- When 她點擊「全選」
- Then 所有非假日日期被勾選
Scenario 14 Step 4:單次課僅選單一日期
- Given Lisa 在 Step 2 選了「One-Time」
- When 進到 Step 4
- Then 介面顯示日期挑選器(single date picker)
- And 一旦選了某日期,「下一步」按鈕可用
- And 國定假日不可選
Scenario 15 Step 5:費用明細完整展示(含註冊費首次收)
- Given Lisa 在 Step 5
- And 季度報名 12 堂 1on2(80 CAD/堂未稅)
- And 小明首次報名(尚未繳註冊費)
- When 系統載入費用明細
- Then 顯示課程資訊區:
- • 學員:小明 / 校區:Burnaby / 種類:Seasonal / 季度:2026 Spring
- • 課程類型:1on2 / 週幾:週二 / 時間:17:00 / 課堂數量:12 堂
- • 所有上課日期列表(12 個日期,國定假日已排除)
- And 顯示費用明細:
| 項目 | 金額(CAD) | 備註 |
|---|---|---|
| 一次性註冊費 | 30.00 | 首次收,不退款 |
| 課程費用 | 960.00 | 12 × 80 |
| 折扣 | 0.00 | 無折扣 |
| 小計(未稅) | 990.00 | |
| GST (5%) | 49.50 | 對小計徵稅 |
| 分期手續費 | 0.00 | 預留欄位 |
| 總計 | 1039.50 | 含稅 |
Scenario 16 Step 5:註冊費 — 已收過則不重複收
- Given 小明歷史紀錄中已有 1 筆「已完成」訂單,包含註冊費
- When Lisa 為小明再次報名(任何類型:Seasonal / One-Time / Trial)
- Then Step 5 費用明細「註冊費」項顯示為 0 CAD
- And 旁邊標註「已收(2026-03-17 訂單 #001)」
待確認 Q06.2:註冊費判定 — 我預設「per 學員生命週期一次」(同一個 student_id 只收一次)。是否應改為「per 聯絡人」(家庭只交一次)?我預設 per 學員(資料隔離更清楚)。請確認。
Scenario 17 Step 5:註冊費 — 退款後再回來不重收
- Given 小明的歷史訂單 #001 已退款
- And 註冊費 30 CAD 在退款流程中明確「不退」
- When Lisa 為小明再次報名
- Then 註冊費仍為 0(歷史已收,不重收)
- And 旁邊標註「已收(含於已退款訂單 #001)」
Scenario 18 Step 5:Trial 折扣呈現為「優惠」
- Given Lisa 在 Step 5,類型 = Trial
- And 1on2 單堂正式價 80 CAD
- When 系統計算 Trial 費用
- Then 費用明細顯示:
| 項目 | 金額(CAD) |
|---|---|
| 註冊費(首次) | 30.00 |
| 課程費用(原價 80) | 80.00 |
| 折扣(Trial 8 折) | -16.00 |
| 小計(未稅) | 94.00 |
| GST (5%) | 4.70 |
| 總計 | 98.70 |
Scenario 19 Step 5:優惠碼 — 4 種類型
- Given 系統有以下優惠碼:
| 類型 | 範例代碼 | 行為 |
|---|---|---|
| 固定金額折扣 | WELCOME50 | 課程費用 - 50 CAD |
| 百分比折扣 | SAVE10 | 課程費用 × 10% 折扣 |
| 免註冊費 | WAIVE-REG | 註冊費歸 0 |
| 特定課程優惠 | GROUP-PROMO | 僅 Group 類型可用,特殊折扣 |
- When Lisa 在 Step 5 輸入優惠碼「WELCOME50」
- Then 系統驗證優惠碼有效
- And 「折扣」欄顯示 -50 CAD
- And 總計即時更新
待確認 Q06.3:優惠碼是否第一版就要實作?V2 文檔說「需支持未來優惠碼功能」 — 預設 第一版只實作後台「優惠碼管理」CRUD + Step 5 的代碼輸入欄位(驗證 + 套用),但管理後台先不上線,等業務確定具體優惠策略再啟用。請確認。
Scenario 20 Step 6:Online + Credit 抵扣 — 即時計算
- Given Step 5 計算總計 = 1039.50 CAD
- And 小明 Credit Pool = 220 CAD
- When Lisa 在 Step 6 選「Online」付款方式
- And 在「使用 Credit 金額」欄輸入 220
- Then 系統即時計算「待付款金額 = 819.50 CAD」(1039.50 - 220)
- And Invoice Detail 顯示完整明細:總額、Credit 抵扣、待付款
- And Lisa 可在 Step 6 看到「點生成訂單後家長將在 60 分鐘內完成付款」提示
Scenario 21 Step 6:Cash + Credit 抵扣
- Given Step 5 計算總計 = 1039.50 CAD
- And 小明 Credit Pool = 220 CAD
- When Lisa 在 Step 6 選「Cash」+ 輸入 Credit 220
- Then 系統計算「Cash 應付 = 819.50 CAD」
- And 提示「點生成訂單後家長將在 48 小時內到場交現金」
Scenario 22 Step 6:Credit 抵扣超過總額 — 阻擋
- Given Step 5 總計 = 100 CAD
- And 小明 Credit Pool = 200 CAD
- When Lisa 嘗試輸入 Credit 抵扣 = 150
- Then 系統阻擋
- And 顯示「Credit 抵扣金額不可超過訂單總額(最多 100 CAD)」
- And 自動修正輸入為 100
Scenario 23 Step 6:純 Credit 抵扣(待付款 = 0)
- Given 訂單總額 100 CAD
- And Credit Pool = 200 CAD
- When Lisa 輸入 Credit 抵扣 = 100(剛好抵完)
- Then 待付款 = 0 CAD
- And Step 7 生成訂單時,訂單建立後直接跳到「待確認」狀態(跳過付款步驟)
- And Lisa 可在後台直接「確認」(因為沒有現金 / Online 流入)
待確認 Q06.4:純 Credit 抵扣的訂單流程 — 我預設 跳過待付款,直接進待確認,由 Lisa 一鍵確認 → 已完成。是否要更簡化(直接已完成)?預設保留待確認步驟以維持流程一致性。
Scenario 24 Step 7:Online 訂單生成 — capacity 暫時保留 + 60 分鐘倒數
- Given Lisa 完成 Step 6(Online 付款方式)
- When 她點擊「生成訂單」
- Then 系統建立訂單 #001(狀態:待付款)
- And 12 個 sessions 中小明被加入為「待付款佔位」狀態(capacity 已扣 1)
- And 設定 60 分鐘倒數(自訂單建立時刻起算)
- And 王女士的家長端立即收到「待付款訂單 #001」通知
- And 訂單卡片顯示倒數計時:「請於 60 分鐘內完成付款,逾時自動取消」
Scenario 25 Step 7:Cash 訂單生成 — 48 小時倒數
- Given Lisa 完成 Step 6(Cash 付款方式)
- When 她點擊「生成訂單」
- Then 系統建立訂單(狀態:待付款 - Cash)
- And capacity 同樣保留
- And 48 小時倒數啟動
- And 客戶端訂單卡片顯示「請於 48 小時內到客服處交現金」
Scenario 26 未解決邏輯:單次課插入季度課程的名額計算
- Given 季度課「週二 17:00 1on2」共 12 個 sessions
- And 第 5 週某日(2026-04-14)有人以「One-Time」單次報名加入
- When Lisa 為新學員嘗試季度報名同一節課
- Then ❓ 第 5 週的容量是否應視為「已用 = 季度學員數 + One-Time 加入數」?
待確認 Q06.5:此為文檔明確標示的「尚未解決邏輯」。我預設方案:「per session 動態計算 capacity」 — 每個 session 的剩餘 = hard_capacity - 該 session 當前的所有 enrollments(含季度 + 單次 + 補課)。Lisa 看到剩餘為 0 就無法把新季度學員塞進去(即使其他週仍有空)。請確認此邏輯。
Scenario 27 Step 7 內部:capacity 暫時保留的數據結構 內部行為
- Given Lisa 點擊「生成訂單」
- When 系統建立訂單與佔位
- Then 為訂單裡每個 session 建立 1 筆 t_enrollment 紀錄
- And enrollment.status = PENDING_PAYMENT(區別於 ACTIVE)
- And 計算 session capacity 時,PENDING_PAYMENT enrollments 也要計入「已用」
- And 訂單超時取消時,所有 PENDING_PAYMENT enrollments 改為 CANCELLED 並釋放 capacity
- And 訂單付款確認時,所有 PENDING_PAYMENT enrollments 改為 ACTIVE
Scenario 28 「不可付款 Lisa 開錯訂單」的撤回機制
- Given Lisa 剛生成的訂單 #001(待付款)
- And 她發現選錯課程或學員
- When Lisa 在訂單列表點擊「取消訂單」(針對待付款狀態)
- Then 訂單狀態 → 已取消
- And capacity 立即釋放
- And 家長端待付款訂單消失
- And Lisa 可重新走 7 步流程
待確認 Q06.6:客服主動取消「待付款」訂單是否需要原因記錄?我預設 需要(audit log + 簡短備註欄)。請確認。
07. 付款(Online / Cash + 任意金額付款)已完成 家長端 + 客服端
⚠️ V2.1 新增:「任意金額付款」流程 — 客服在後台「學員詳細頁 → 充值」彈窗發起,
選擇付款方式(Online / Cash),金額自由填,建立 order_type = TOPUP 的訂單。
完成後直接進入自由消費餘額(不綁特定課程)。
其他內容(訂單付款 60min/48h 倒數、Stripe webhook、純 Credit 抵扣)保持不變。
其他內容(訂單付款 60min/48h 倒數、Stripe webhook、純 Credit 抵扣)保持不變。
主要角色:家長(王女士)、客服(Lisa)、SUPER_ADMIN(Mark)、金流系統(Stripe)、系統
Background 共用前置條件
- Given Lisa 已透過 F06 為小明生成訂單 #001(Online,總額 1039.50 CAD,待付款)
- And 訂單建立時刻 = 2026-05-08 14:00
- And 12 個 sessions 中小明為 PENDING_PAYMENT 佔位
- And 王女士已收到「待付款訂單 #001」通知
- And 王女士已登入家長端
Scenario 1 Online 付款成功 — 完整流程
- Given 王女士在家長端「待付款訂單」列表點擊訂單 #001
- When 她查看訂單詳情
- Then 顯示完整 Invoice:學員、課程資訊、費用明細、待付款 1039.50 CAD
- And 顯示倒數計時器(如剩 47:23)
- When 她點擊「立即付款」
- Then 跳轉到 Stripe 付款頁面
- When 她輸入信用卡資訊並完成付款
- Then Stripe 返回成功
- And 訂單 #001 狀態 → 待確認
- And 倒數計時停止(已不需再倒數)
- And 王女士看到「付款成功,等待客服確認」
- When Stripe webhook 回調系統,確認 charge.succeeded
- Then 訂單 #001 狀態 → 已完成
- And 12 個 sessions 中小明從 PENDING_PAYMENT 改為 ACTIVE
- And Credit Pool 增加:訂單 #001 剩 12 堂 / 960 CAD(未稅)
- And 王女士收到「訂單 #001 已完成,學員已加入課表」通知
待確認 Q07.1:金流選擇 — 預設 Stripe(北美主流,支援 SCA、退款、webhook)。是否要支援 Square / Moneris 等加拿大本地金流?
待確認 Q07.2:「待確認」狀態的觸發 — 我預設 客戶完成付款後 Stripe 還沒 webhook 回來時為「待確認」中間態。Webhook 回來後變「已完成」。如果 webhook 30 秒內沒回來,是否要 polling Stripe API 主動確認?
Scenario 2 Online 付款失敗(信用卡被拒)
- Given 王女士點擊「立即付款」並進入 Stripe 頁
- When 她輸入信用卡,但 Stripe 回應「銀行拒絕」
- Then 訂單狀態仍為「待付款」(不變)
- And 倒數計時繼續(不重置)
- And 王女士看到錯誤提示「付款失敗:銀行拒絕,請更換卡片或聯繫銀行」
- And 她可在倒數結束前再次嘗試付款
Scenario 3 Online 倒數計時器 — 視覺提示
- Given 訂單 #001 待付款,建立 14:00
- When 王女士查看訂單卡片
- Then 顯示倒數時鐘(mm:ss 格式)
- And 倒數 ≥ 30 分鐘 → 綠色
- And 倒數 30 ~ 10 分鐘 → 黃色
- And 倒數 ≤ 10 分鐘 → 紅色 + 閃爍
- And 倒數 = 0 自動消失,訂單卡片變為「已取消」狀態
Scenario 4 Online 超時自動取消(60 分鐘)
- Given 訂單 #001 建立於 14:00,待付款
- When 60 分鐘後(15:00)王女士仍未完成付款
- Then 系統自動將訂單狀態 → 已取消
- And 12 個 PENDING_PAYMENT enrollments 改為 CANCELLED
- And capacity 立即釋放
- And 王女士收到通知「訂單 #001 已自動取消(超時未付款)」
- And 訂單卡片從家長端待付款區消失,移到「歷史訂單」
- And 該訂單不可再被付款 / 復活
- And 若仍需報名,需 Lisa 重新走 F06 流程
Scenario 5 王女士主動取消待付款訂單
- Given 訂單 #001 待付款,王女士改變主意不報了
- When 她在訂單卡片點擊「取消訂單」
- And 確認彈窗
- Then 訂單狀態 → 已取消
- And capacity 釋放
- And 不收任何違約金
- And 通知中心新增「訂單 #001 已取消」
Scenario 6 Cash 付款流程 — 王女士到客服處交現金
- Given 訂單 #002 為 Cash 方式,建立 2026-05-08 14:00(待付款)
- And 48 小時倒數中
- When 王女士於 2026-05-09 11:00 到 Burnaby 客服處交現金 819.50 CAD
- And 在家長端訂單卡片點擊「我已付款」
- Then 訂單狀態 → 待確認
- And 倒數計時停止
- And Lisa 後台「待確認訂單」列表新增訂單 #002
- When Lisa 收到現金後,在後台點擊訂單 #002 的「確認收款」
- And 填寫收款備註(可選)
- Then 訂單狀態 → 已完成
- And 學員加入課表 / Credit Pool 入帳
- And 王女士收到「訂單 #002 已確認,學員已加入課表」通知
Scenario 7 Cash 倒數 48 小時
- Given 訂單 #002 Cash,建立 14:00
- When 王女士查看訂單卡片
- Then 顯示倒數時鐘(hh:mm 格式)
- And 倒數 ≥ 24 小時 → 綠色
- And 倒數 24 ~ 6 小時 → 黃色
- And 倒數 ≤ 6 小時 → 紅色
- And 訂單顯示「請於 48 小時內到 Burnaby 客服處交現金 819.50 CAD」
Scenario 8 Cash 超時自動取消(48 小時)
- Given 訂單 #002 建立於 2026-05-08 14:00,待付款 (Cash)
- When 48 小時後(2026-05-10 14:00)王女士仍未到客服處付款
- Then 系統自動將訂單 → 已取消
- And capacity 釋放
- And 王女士收到通知「訂單 #002 已自動取消(超時未到客服處付款)」
Scenario 9 Cash 客戶宣告已付但客服查無入帳 — 取消處理
- Given 訂單 #002 Cash,王女士已點「我已付款」(狀態:待確認)
- And 但 Lisa 在現金箱中查無對應金額
- When Lisa 與王女士確認,發現王女士記憶有誤實際未交
- And Lisa 在後台點擊訂單 #002 的「拒絕並取消」
- And 填寫原因「查無入帳,客戶確認未實際付款」
- Then 訂單狀態 → 已取消
- And capacity 釋放
- And 系統記錄 audit log(操作人 Lisa、時間、原因)
- And 王女士收到「訂單 #002 因查無入帳被取消,如有疑問請聯繫客服」
待確認 Q07.3:「待確認 → 已取消」的轉換是否合法?正典 D 的 transition 表中我已預設此 transition 合法(QD.1)。此 scenario 確認該操作。
Scenario 10 純 Credit 抵扣訂單 — 跳過付款步驟
- Given Lisa 為小明生成訂單 #003:總額 100 CAD
- And 在 Step 6 用 100 Credit 全額抵扣(待付款 = 0)
- When Lisa 點擊「生成訂單」
- Then 訂單建立後狀態直接為 待確認(跳過待付款)
- And Credit Pool 立即扣 100(FIFO 從最早訂單剩餘扣)
- And Lisa 在後台點擊「確認」一鍵將訂單變為已完成
- And 不需金流介入
Scenario 11 部分 Credit + Online 補差額
- Given 訂單 #004 總額 500 CAD
- And Credit 抵扣 200,待付款 300
- And 付款方式 = Online
- When Lisa 點擊「生成訂單」
- Then 訂單狀態 = 待付款 (Online)
- And Credit Pool 立即扣 200(鎖定,不可被另一筆訂單再用)
- And 60 分鐘倒數啟動,待付 300
- When 王女士完成 Online 付款 300
- Then 訂單 → 待確認 → 已完成
- When 訂單超時取消
- Then 200 Credit 退回 Pool
Scenario 12 部分 Credit + Cash 補差額
- Given 訂單 #005 總額 500,Credit 抵 200,Cash 應付 300
- When Lisa 生成訂單(Cash)
- Then 狀態 = 待付款 (Cash)
- And Credit 立即鎖定 200
- And 48 小時倒數,Cash 應付 300
- When 王女士交現金 300 + 王女士點「已付」+ Lisa 確認
- Then 訂單已完成
Scenario 13 已退款訂單不可復活
- Given 訂單 #006 已退款
- When 任何人嘗試將訂單 #006 改為任何其他狀態
- Then 系統阻擋(已退款是終態)
- And 即使 SUPER_ADMIN 也無法繞過
- And 如需恢復報名,需走 F06 重新生成新訂單
Scenario 14 已取消訂單不可復活
- Given 訂單 #007 因超時被自動取消
- When 王女士在歷史訂單頁點擊「重新付款」(介面不應有此按鈕)
- Then 介面不存在重新付款按鈕
- And 若 API 直接呼叫,系統返回錯誤「已取消訂單不可恢復」
- And 介面提示「如仍需報名,請聯繫客服重新生成訂單」
Scenario 15 倒數中客服緊急延長付款時限
- Given 訂單 #008 待付款,倒數剩 5 分鐘
- And 王女士打電話給 Lisa 說「我銀行剛被風控,正在解鎖,可否多給我時間」
- When Lisa 在後台點擊訂單的「延長 60 分鐘」
- Then 倒數重置為 60:00
- And 系統記錄 audit log(操作人、原訂單建立時間、新到期時間、原因)
待確認 Q07.4:是否提供「客服延長付款時限」功能?我預設 是,但僅 SUPER_ADMIN 可操作,且延長有上限(最多延 1 次 + 60 分鐘)。請確認。
Scenario 16 訂單超時掃描定時任務 內部行為
- Given 系統運行中
- When 系統每 1 分鐘觸發一次「訂單超時掃描」cron
- Then 找出所有 created_at 超過 60 分鐘的 Online 待付款訂單,標記為已取消
- And 找出所有 created_at 超過 48 小時的 Cash 待付款訂單,標記為已取消
- And 對每筆受影響的訂單:
- • 釋放 capacity(PENDING_PAYMENT enrollments → CANCELLED)
- • 退回鎖定的 Credit(若有部分 Credit 抵扣)
- • 通知家長
- • Audit log 記錄
Scenario 17 Stripe webhook 處理 內部行為
- Given 系統暴露 webhook 端點
POST /api/webhooks/stripe - And Stripe 已配置該 webhook URL
- When Stripe 推送 charge.succeeded 事件
- Then 系統驗證 webhook 簽名(防偽造)
- And 取出對應 payment_intent_id 找到關聯訂單
- And 訂單狀態若為「待確認」→ 改為「已完成」
- And 訂單狀態若為其他(如已取消)→ 觸發「重複付款處理」流程:自動退款給客戶 + audit log
- And 對 charge.refunded 事件做對應處理
待確認 Q07.5:若 webhook 收到 charge.succeeded 但訂單已自動取消(極端競態),系統應 自動全額退款 + 通知客服 + 通知客戶。請確認此處理。
Scenario 18 「以小明角度」聲明:客戶端永遠看到「訂單對應課程」 家長端
- Given 王女士的家長端「我的訂單」頁
- When 她查看任意訂單卡片
- Then 卡片顯示:
- • 訂單編號 + 狀態
- • 「對應課程:12 堂 1on2 / Coach Luke / 週二 17:00 / 2026 Spring 季度」
- • 費用明細
- • 付款時間
- And 文案語氣強調「您支付的是這組課程」而非「儲值餘額」
- And 客戶端任何地方都不出現「請充值 X CAD 到您的帳戶」這類字眼
08. 自動扣費(V2.1:廢除點名)已完成 內部行為 + 客服端
業務價值(V2.1 變更):
會議拍板廢除「人工點名」概念。改為「課程開始時系統自動扣費」:只要該堂課的學員未在 24 小時前請假,系統就在課程開始時自動從其餘額扣 1 堂的價格。
24 小時前請假的學員此時已從 enrollment 移除,不會被扣;24 小時內請假的學員仍在 enrollment 中,照常扣費(懲罰晚請假)。
這個改動極大簡化了客服日常工作(不需點名 / 不需 No-show 處理 / 不需課後修改),並避免人工誤操作。
主要角色:系統(自動扣費 cron)、客服(Lisa)、SUPER_ADMIN(Mark)、家長(王女士)
主要角色:客服(Lisa)、SUPER_ADMIN(Mark)、教練(Coach Luke)、系統
Background 共用前置條件
- Given 小明訂單 #001 已完成(12 堂 1on2,不可消費餘額 960 CAD)
- And 該訂單對應 12 個 sessions,當前已上 4 堂、剩 8 堂未來課程
- And 4/14 17:00 是其中一個未來 session
- And 該 session 還有同學員「小強」也已報名(也是訂單付款)
Scenario 1 課程開始時系統自動扣費(標準路徑)
- Given 當前時刻 4/14 17:00(課程開始時刻)
- And 小明仍在該 session 的 enrollment 中(沒有 24hr 前請假)
- When 系統 cron(每分鐘掃)檢測到該 session 開始
- Then 系統對 session 中的每位 enrolled 學員逐一扣費
- And 對小明:從訂單 #001 對應的「不可消費餘額」扣 80 CAD(單堂價)
- And 訂單 #001 對應餘額 = 960 - 80 = 880
- And t_credit_log 新增 1 筆 SESSION_AUTO_DEDUCT 紀錄(綁 session_id + order_id)
- And 王女士的家長端餘額頁 / 流水頁即時更新
- And session 狀態標記為 PROCESSED(已扣費)
- And 不需要任何點名操作
Scenario 2 24hr 前請假學員 — 不在 enrollment 中,跳過扣費
- Given 4/13 16:00 王女士為小明請假 4/14 17:00 那堂課(提前 25 小時)
- And 系統在請假時刻立即將小明從該 session 的 enrollment 移除
- When 4/14 17:00 系統觸發自動扣費
- Then 該 session 的 enrolled 學員列表不含小明(已被請假時移除)
- And 系統不對小明扣費
- And 訂單 #001 餘額不變(仍 960 - 已扣4堂 = 640)
- And 系統為小明保留 1 個補課額度(依 F10 補課)
Scenario 3 24hr 內請假學員 — 仍在 enrollment 中,照常扣費
- Given 4/14 10:00 王女士才為小明請假當天 17:00 課(僅提前 7 小時)
- And 系統認定為「24 小時內請假」(依 F09)
- And 小明仍在該 session 的 enrollment 中(未被移除)
- And 該堂課狀態標記為「LEAVE_LATE(晚請假)」
- When 4/14 17:00 系統觸發自動扣費
- Then 系統照常對小明扣 80 CAD(懲罰晚請假)
- And 訂單 #001 餘額減 80
- And t_credit_log change_type = LATE_LEAVE_DEDUCT
- And 不產生補課額度
- And 王女士已在請假提交時收到「不可補課且仍扣費」的提示,不再重複通知
Scenario 4 訂單對應餘額不足 — 從自由消費餘額補
- Given 小明的「補課差價」訂單 #003 對應某 session(單堂 80)
- And 訂單 #003 的不可消費餘額為 60(不足以扣 80,因為先前情境扣過)
- And 小明的自由消費餘額 = 50
- When 系統觸發該 session 自動扣費
- Then 系統先從訂單 #003 對應餘額扣 60(耗盡)
- And 不足部分 20 從「自由消費餘額」扣(自由餘額 50 - 20 = 30)
- And t_credit_log 寫入 2 筆紀錄(分別綁 source)
- And 客戶端流水頁顯示「課程扣費 -80(從訂單 #003 扣 60 + 自由餘額補 20)」
Scenario 5 訂單與自由餘額都不足 — alert 客服
- Given 訂單 #003 對應餘額為 0
- And 自由消費餘額為 30(不足 80)
- When 系統觸發扣費
- Then 系統不強制扣(避免負餘額)
- And 該 session 的小明狀態標記為「待處理(缺費)」
- And 系統發 alert 通知該校區所有 ADMIN:「小明 4/14 17:00 課程缺費 80 CAD,請聯繫家長」
- And 王女士的家長端顯示「該堂課缺費,請聯繫客服」
- And Lisa 可手動處理:(a) 要求家長充值 / (b) 從家長處收現金後客服任意金額充值 / (c) 取消該堂課
待確認 Q08.1:「待處理(缺費)」的後續處理 — 我預設客服手動跟進,48 小時內未處理則該堂課狀態 → 自動取消(學員視為缺席不扣費)。請確認此 fallback 政策。
Scenario 6 課程結束後不再扣費(防重複)
- Given 4/14 17:00 課程已被自動扣費(session.status = PROCESSED)
- When 系統 cron 再次掃到該 session(避免重複處理)
- Then 系統檢測到 status = PROCESSED 跳過
- And 不重複扣費
Scenario 7 客服 / 教練查看課程「出席情況」(替代點名介面)
- Given 4/14 17:00 課程已開始
- When Lisa 或 Coach Luke 進入該 session 詳情頁
- Then 顯示學員出席狀態總覽:
- • 已扣費學員(24hr 內請假 / 未請假)— 顯示「應出席(已扣費)」
- • 24hr 前請假學員 — 顯示「正常請假」+ 補課額度標記
- And 介面沒有「點名」「No-show」「出席」按鈕
- And 教練 / 客服只看資訊,不做操作
- And 教練只能查看自己負責 sessions 的出席情況
Scenario 8 SUPER_ADMIN 修正錯誤扣費
- Given 系統因某 bug 對小明錯誤扣了 4/14 17:00 的 80 CAD(小明實際 25 小時前已請假)
- When Mark 在學員詳細頁的「扣費紀錄」找到此筆
- And 點擊「沖正」並填寫原因「系統誤扣,已 24hr 前請假」
- Then 系統 INSERT 1 筆「沖正紀錄」(金額 +80,change_type = MANUAL_REVERSAL)
- And 訂單 #001 對應餘額回升 80
- And 補建補課額度 +1(如果原請假應產生補課但未生)
- And Audit log 記錄
- And 原扣費紀錄不修改(t_credit_log 不可變表)
Scenario 9 自動扣費 cron 詳細邏輯 內部行為
- Given 系統 cron
SessionAutoDeductScanner - When 每分鐘觸發
- Then 找出 session.start_at <= now() AND status = SCHEDULED 的 sessions
- And 對每個 session:
- • 取出當前 enrolled 學員列表(24hr 前請假已被移除)
- • 對每位學員逐筆扣費(依該 enrollment.order_id 找對應餘額)
- • 寫 t_credit_log + 更新餘額
- • 不足時觸發 alert 流程
- And session.status → PROCESSED
- And 整個 session 處理在單一事務中(避免部分扣費部分失敗)
Scenario 10 任意金額付款流入的「自由消費餘額」也能用於扣費
- Given 王女士先前找客服任意金額充值了 100 CAD(進自由消費餘額)
- And 小明訂單 #001 對應餘額剩 60(不足扣下節課 80)
- When 系統扣費下節課
- Then 從訂單 #001 餘額扣 60(耗盡)
- And 從自由消費餘額補 20(剩 80)
- And 該 session 順利扣費完成
10. 補課(V2.1:24hr 請假 / 同等級及以上 / 僅季度課)已完成 家長端 + 客服端
⚠️ V2.1 重大變更(2026-05-08 拍板):
• 請假時間:12 小時 → 24 小時(必須提前 24 小時請假才產生補課額度)
• 補課範圍:「同等級」→ 「同等級及以上」(L3 學員可補 L3 / L4 / L5 課)
• 補課額度:僅季度課有,每季 2 次免費(單次課和 Trial 無補課機會)
• 價差處理:如果補到較貴的課程,提示聯繫客服 → 前台生成價差付款訂單給家長付款
• 取消窗口:12 小時 → 24 小時(與請假規則一致)
• 請假時間:12 小時 → 24 小時(必須提前 24 小時請假才產生補課額度)
• 補課範圍:「同等級」→ 「同等級及以上」(L3 學員可補 L3 / L4 / L5 課)
• 補課額度:僅季度課有,每季 2 次免費(單次課和 Trial 無補課機會)
• 價差處理:如果補到較貴的課程,提示聯繫客服 → 前台生成價差付款訂單給家長付款
• 取消窗口:12 小時 → 24 小時(與請假規則一致)
主要角色:家長(王女士)、客服(Lisa)、學員(小明、小華、小強)、系統
Background 共用前置條件(V2.1)
- Given 小明(L3)提前 25 小時請假了 4/14 17:00 那堂 1on2(依 F09,已從 enrollment 移除、不扣費、產生 1 個補課額度)
- And 該課程是季度課(補課僅適用季度課)
- And 小明剩餘補課額度:1 / 2 (本季)
- And 課表上有以下備選(V2.1 規則:同等級 L3 及以上):
- • 4/16 18:00 1on2 by Coach Luke (L4),L3-L4,2 left(同類 + 同價)✅
- • 4/17 17:00 1on1 by Coach Luke (L4),L4-L5,1 left(類型不同,跳過)
- • 4/18 18:00 1on2 by Coach Eric (L2),L1-L2,2 left(教練等級 L2 < L3,跳過)
- • 4/20 19:00 1on2 by Coach Diana (L5),L4-L5,2 left(教練 L5 > L3,同類型 + 等級更高,但價格 100 > 80,需聯繫客服補差價)
- • 4/19 19:00 Available 空堂 by Coach Luke(同教練等級)
- And 王女士已登入家長端
Scenario 1 家長端「可補」標記顯示(自動篩選符合條件)
- Given 小明有 1 個補課額度
- When 王女士在小明課表上瀏覽
- Then 4/16 18:00 那堂 1on2 顯示「可補」橙色標記
- And 4/17 1on1 不顯示「可補」(類型不符)
- And 4/18 1on2 by Coach Eric 不顯示「可補」(教練等級不符)
- And 4/19 空堂顯示「可補」(依空堂規則,價格相同則可補)
Scenario 2 家長自助補課 — 條件完全符合
- Given 王女士在課表上看到 4/16 18:00 1on2 「可補」
- When 她點擊該課程的「補課」按鈕
- And 系統提示「請選擇對應的請假紀錄」
- And 王女士選擇「4/14 17:00 1on2 請假」
- And 點擊「確認補課」
- Then 系統驗證 3 條件全部符合(同類型 + 同等級 + 同價)
- And 鎖定該 session 的 1 個名額(capacity 從 2 left 變 1 left)
- And 4/16 18:00 那堂課加入小明(補課狀態 MAKEUP)
- And 4/14 那堂的「待補課」紀錄狀態 → MATCHED(已成功補上)
- And 不扣 Credit(補課不扣費,因為原請假已扣或將扣)
- And 王女士收到「補課成功」通知
待確認 Q10.1:補課的 Credit 規則 — 我預設 「補上的這堂課不扣 Credit」+「原請假課照常從 Credit Pool 扣(因為訂單剩餘額度本就計入這堂)」。換言之:學員報名 12 堂 → 訂單剩 12 → 請假 1 + 補上 1 → 剩 11 已扣 + 1 待扣(補上的那堂在補上日才扣)。請確認此邏輯。
Scenario 3 補課條件不符 — 課程類型不同
- Given 王女士嘗試將 4/14 1on2 請假補到 4/17 1on1
- When 她點擊「補課」
- Then 系統顯示「該課程類型(1on1)與您請假的課程(1on2)不符,無法自助補課」
- And 顯示「如有特殊需求請聯繫客服」
Scenario 4 (V2.1) 補課到「同等級及以上」教練 — 同價直接補課
- Given 小明請假的原課程:1on2 by Coach Luke (L4),單堂價 80
- And 4/16 18:00 1on2 by Coach Diana (L5),價格也是 80
- When 王女士嘗試補到該課程
- Then 系統檢查:類型同 ✓ + 教練等級 L5 ≥ L4 ✓ + 同價 ✓
- And 補課成功(直接占位)
Scenario 4b (V2.1) 補課到等級更低教練 — 阻擋
- Given 小明請假的原課程教練 Coach Luke (L4)
- And 嘗試補到 4/18 1on2 by Coach Eric (L2),L2 < L4
- When 王女士點擊「補課」
- Then 系統阻擋
- And 顯示「目標教練等級(L2)低於原教練(L4),無法補課」
Scenario 5 (V2.1) 補課到較貴課程 — 提示聯繫客服 + 前台生成價差訂單
- Given 小明原課程單堂 80 CAD
- And 嘗試補到 4/20 19:00 1on2 by Coach Diana (L5),單堂 100 CAD(同類型 + 等級更高)
- When 王女士點擊「補課」
- Then 系統顯示「該課程單堂 100,比原課程 80 高 20 CAD。需聯繫客服由前台處理價差,前台會生成 20 CAD 的付款訂單給您」
- And 介面提供「聯繫客服」按鈕
- And 補課額度不立即扣(須等價差訂單付款後才扣)
- When Lisa 收到家長請求後,在後台「補課管理」為小明手動安排
- And 系統依 F06 流程生成 1 筆 ONE_TIME 補課差價訂單(20 CAD)
- Then 訂單狀態 = 待付款(依 F07)
- And 王女士付款後,補課正式生效(小明加入 4/20 19:00 課程)
- And 補課額度 -1
- And 該堂課開始時,從 Diana 訂單對應的不可消費餘額扣 100(依 F08 自動扣費)
Scenario 6 空堂補課 — 價格相同可直接補
- Given 4/19 19:00 是 Available 空堂,by Coach Luke (L4)
- And 系統依「該空堂泳道 + 教練等級」自動推算,若改為 1on2 課單堂價 = 80(與請假同價)
- When 王女士嘗試補到該空堂
- Then 系統允許
- And 後台自動建立 1 個 One-Time 課程(類型 = 1on2,依小明 Level)
- And 小明加入該 session 為 MAKEUP 狀態
- And 該空堂變為 1on2 課(1 left 給其他學員可加入)
待確認 Q10.2:空堂自動轉為「One-Time 課程」是否合理?或者該空堂應先由客服建立正式課程,家長才能補?我預設 家長可直接補空堂並自動建立 One-Time,但需 Lisa 在後台補一個課程資訊(課程類型 / Level 區間)。請確認。
Scenario 7 空堂補課 — 有價差需聯繫客服
- Given 4/19 空堂 by Coach Eric (L2)
- And Coach Eric L2 的 1on2 課單堂價 = 50(小明原請假是 80)
- When 王女士嘗試補到該空堂
- Then 系統顯示「該時段價格(50)與您請假課程(80)不符,請聯繫客服協助處理價差」
- And 提示「客服會處理 30 CAD 的差額(退回 Credit 或抵扣下次)」
Scenario 8 FCFS — 名額已被他人補走
- Given 4/16 1on2 課僅剩 1 left
- And 王女士在補課介面停留時,學員小華的家長同時補同一節
- When 小華家長先點擊「確認補課」
- And 王女士隨後點擊「確認補課」
- Then 系統檢測到該 session capacity 已滿
- And 王女士看到「該名額已被他人選擇,請選擇其他時段」
- And 王女士的補課額度仍未消耗(可重新挑時段)
Scenario 9 (V2.1) 補課取消 — 距離上課 ≥ 24 小時可取消,補課額度恢復
- Given 王女士已補課到 4/16 18:00
- And 當前時刻 4/14 18:00(距離上課還有 48 小時)
- When 王女士點擊該補課的「取消補課」
- Then 系統允許
- And 4/16 那節課小明被從 enrollment 移除(capacity 釋放)
- And 對應「待補課」紀錄狀態 → AVAILABLE(重新可用)
- And 王女士可再選其他時段補課
- And 通知「補課取消成功,補課額度已恢復」
Scenario 10 (V2.1) 補課取消 — 距離上課 < 24 小時視為已使用
- Given 王女士已補課到 4/16 18:00
- And 當前時刻 4/15 22:00(距離上課僅 20 小時)
- When 王女士點擊「取消補課」
- Then 系統提示「距離上課不足 24 小時,取消後將視為已使用該補課額度,且仍照常扣費」+ 二次確認
- When 王女士確認
- Then 4/16 該節課小明被移除 enrollment
- But 對應「待補課」紀錄狀態 → CONSUMED(已使用,不退補課額度)
- And 補課額度不恢復(懲罰晚取消)
- And 該堂課照常扣費(依 F08 自動扣費)
待確認 Q10.5:晚取消補課的扣費 — 是「從原請假對應訂單扣」還是「不扣(因為已從 enrollment 移除)」?我預設 仍扣(懲罰晚取消,與晚請假同等對待)。請確認。
Scenario 11 客服協助補課 — 處理價差
- Given 王女士聯繫客服,希望將 4/14 1on2 (80) 請假補到 4/19 1on2 by Coach Eric (50)
- When Lisa 在後台「補課管理」頁為小明手動安排
- And 選擇來源請假紀錄 + 目標 session
- And 選擇價差處理方式:「退回 30 CAD 到 Credit Pool」
- Then 系統建立補課紀錄
- And 從訂單 #001 退回 30 CAD 到 Credit Pool(綁定來源訂單)
- And Audit log 紀錄
- And 王女士收到「補課成功,價差 30 CAD 已退回您的 Credit」通知
Scenario 12 超過免費補課額度 — 客服特批時 V1 機制保留 vs V2 改造
- Given 小明本季已用過 2 次免費補課額度
- And 第 3 次請假產生「超額待補」紀錄
- When 王女士嘗試自助補課該紀錄
- Then 系統顯示「您已超出本季免費補課額度,請聯繫客服安排(將收取補課費)」
- When Lisa 在後台為小明安排補課並選擇「收取補課費 30 CAD」
- Then 系統建立補課紀錄
- And 從小明 Credit Pool 扣 30 CAD(change_type = MAKEUP_FEE,綁定訂單 FIFO)
- And 若 Credit 不足則 Lisa 必須生成新訂單收取補課費
待確認 Q10.3:補課額度上限 — 我預設 每季 2 次免費(V1 沿用)。是否改為「無上限但每次都收費」?或「上限可配置 per 等級」?
待確認 Q10.4:超額補課費金額 — 我預設 30 CAD/次。是否依課程類型不同收費?
Scenario 13 跨季度補課特批 — SUPER_ADMIN only
- Given 小明在 2026 Spring 末尾請假 1 次(額度未使用)
- And 2026 Spring 季度結束,小明未報名 2026 Summer
- When 王女士聯繫客服希望保留該補課額度到 2026 Summer 開課時使用
- And Lisa 認為合理,請示 Mark
- And Mark 在後台對該紀錄選「跨季度補課特批」
- Then 該補課紀錄在 2026 Summer 仍有效
- And Audit log 記錄特批操作(操作人、原因)
- And ADMIN 無法做此特批(僅 SUPER_ADMIN)
Scenario 14 補課的 Race Condition 內部行為
- Given 4/16 1on2 課僅剩 1 left
- And 王女士和小華媽媽幾乎同時點擊「確認補課」
- When 兩個請求到達後端
- Then 系統使用悲觀鎖(SELECT FOR UPDATE)對 session 加鎖
- And 先到的請求扣減 capacity 並提交
- And 後到的請求檢測 capacity 已滿,返回錯誤 SESSION_FULL
- And 後到的家長端顯示「該名額已被他人選擇」
11. 退款(V2.1:表單簡化)已完成 客服端 / SUPER_ADMIN
⚠️ V2.1 變更:
表單簡化為三項:退款金額 / 退款路徑(Online or Cash)/ 退款原因。
其他欄位(已扣 / 未扣 / 手續費 / 退款後餘額)由系統自動計算顯示,不需 SUPER_ADMIN 手動填。
兩段狀態(退款中 → 已退款)保留。
退款找零進入「自由消費餘額」(依 V2.1 雙層餘額模型)。
其他規則(註冊費不退、部分退款、僅 SUPER_ADMIN)保持不變。
兩段狀態(退款中 → 已退款)保留。
退款找零進入「自由消費餘額」(依 V2.1 雙層餘額模型)。
其他規則(註冊費不退、部分退款、僅 SUPER_ADMIN)保持不變。
主要角色:SUPER_ADMIN(Mark)、客服(Lisa)、家長(王女士)、金流系統、系統
Background 共用前置條件
- Given 訂單 #001 已完成(小明,12 堂 1on2,總額 1039.50 含稅 / 990 未稅)
- And 已上 4 堂課(已扣 320 CAD 未稅)
- And 訂單剩餘 = 8 堂 / 640 CAD 未稅
- And Mark(SUPER_ADMIN)已登入後台
- And 小明因受傷需要全額退剩餘課程
Scenario 1 退款入口 — 僅在學員詳細頁
- Given Mark 在後台
- When 他在訂單列表頁尋找「退款」按鈕
- Then 訂單列表頁不提供退款按鈕
- When Mark 進入小明的學員詳細頁
- Then 在「訂單與付款」區塊看到每筆已完成訂單旁的「退款」按鈕
- And 只有訂單狀態 = 已完成 的才有退款按鈕(其他狀態如待付款 / 已取消無此按鈕)
Scenario 2 僅 SUPER_ADMIN 可發起退款
- Given Lisa 是 ADMIN
- When 她進入小明的學員詳細頁
- Then 介面隱藏所有「退款」按鈕
- And 直接 API 呼叫退款端點返回 403
- And Lisa 看到提示:「退款請聯繫主管處理」
Scenario 3 退款表單必填項
- Given Mark 點擊訂單 #001 的「退款」按鈕
- When 退款表單彈出
- Then 表單包含以下欄位:
| 欄位 | 必填 | 說明 |
|---|---|---|
| 退款原因 | 是 | 下拉選 + 自由文字(如「受傷 / 搬家 / 不適合」) |
| 退款金額(未稅) | 是 | 系統自動計算建議值,可手動調整 |
| 退款方式 | 是 | EMT / Cash / Debit / Credit Pool(找零) |
| 已扣金額 | 顯示 | 已上課的金額(不可編輯,自動計算) |
| 未扣金額 | 顯示 | 剩餘額度(自動) |
| 手續費 | 否 | 金流退款手續費(預設 0,可填) |
| 退款後 Credit Pool 餘額 | 顯示 | 計算結果,唯讀 |
| 備註 | 否 | 給家長的訊息 |
- And 任一必填欄位空白時「提交」按鈕禁用
Scenario 4 全額退款流程(已完成 → 退款中 → 已退款)
- Given Mark 對訂單 #001 發起全額退款
- And 退款明細:未稅可退 = 640(8 堂 × 80);註冊費不退;金流手續費 10;可退淨額 = 630
- When Mark 提交退款表單
- Then 訂單 #001 狀態 → 退款中
- And 訂單剩餘 8 堂從 Credit Pool 凍結(不可再扣費)
- And 8 個未來 sessions 中小明被移除(capacity 釋放給其他學員)
- And 王女士的家長端顯示「訂單 #001 退款處理中」
- And Mark 透過 EMT 推送 630 到王女士的 Email
- When Mark 確認金流系統已成功推送(手動標記 or webhook)
- Then 訂單 #001 狀態 → 已退款
- And t_refund 紀錄補上 completed_at 時間
- And 王女士收到通知「退款 630 CAD 已完成」
- And 學員標籤更新為 REFUNDED(依 F12 標籤系統)
待確認 Q11.1:「退款中 → 已退款」的觸發 — 文檔說「金流系統顯示已完成退款後...狀態轉為已退款」+「客戶端顯示為待確認,客戶確認後 → 等待金流系統顯示已完成」。我理解為兩種觸發機制:(a) Stripe webhook 自動觸發;(b) 非 Stripe 退款(如 EMT / Cash)由 Mark 手動標記。客戶端的「待確認」是純資訊性提示(讓家長知道有處理中的退款),不是阻擋性節點。請確認此解讀。
Scenario 5 退款金額計算 — 註冊費不退
- Given 訂單 #001 含 30 CAD 註冊費
- And 已上 4 堂 = 320
- And 剩 8 堂 = 640
- When Mark 開啟退款表單
- Then 系統自動計算建議退款金額 = 640 CAD(未稅)
- And 表單明確顯示「註冊費 30 不退」
- And Mark 可手動調整金額(但有上限為 640)
Scenario 6 部分退款(保留部分課程)
- Given 訂單 #001 剩 8 堂 / 640,王女士想退 4 堂保留 4 堂
- When Mark 在退款表單選「部分退款」並輸入金額 320
- And 選擇「保留哪 4 堂」(提供未來 8 個 sessions 的清單)
- And 提交
- Then 訂單 #001 狀態仍為「已完成」(依 Q05.8 預設)
- And 訂單剩餘從 8 堂變為 4 堂
- And 被退款的 4 個 sessions 中小明移除
- And 訂單詳情顯示「部分退款 320 CAD(2026-05-08)」標記
- And t_refund.refund_type = PARTIAL
- And Credit Pool 中該訂單剩餘從 640 → 320
Scenario 7 退款方式 — 4 種選項
- Given Mark 在退款表單
- When 他展開「退款方式」
- Then 4 種選項:
| 方式 | 適用 | 後續處理 |
|---|---|---|
| EMT (Email Transfer) | 退到原 Email | Mark 手動發起 + 標記 |
| Cash | 退現金 | 家長到場領取,Mark 標記完成 |
| Debit / Credit Card | 原路徑退回 | 透過 Stripe API 發起,webhook 確認 |
| Credit Pool(找零) | 進入該學員 Credit Pool | 立即生效,無需金流 |
- And Mark 可組合多種方式(如「主退 EMT 600 + 找零 30 入 Pool」)
Scenario 8 找零回 Credit Pool
- Given 訂單 #001 退款明細:應退 640;金流退 600(手續費 + 整數限制);找零 40
- When Mark 在退款方式選「EMT 600 + Credit Pool 40」
- And 提交
- Then 訂單 #001 → 已退款
- And Credit Pool 增加 40(來源:退款找零,from_order = #001)
- And Credit 詳情頁顯示「退款找零 +40 CAD(來自訂單 #001 退款)」
- And 該 40 CAD 可用於下次報名(FIFO 順序)
- And t_refund 紀錄含 amount_to_credit_pool = 40
Scenario 9 退款完成後標籤更新為 REFUNDED
- Given 訂單 #001 已退款
- And 小明該季度沒有其他「已完成」訂單
- When 系統異步重算學員標籤(依 F12 規則)
- Then 小明的標籤變更為 REFUNDED
- And 該標籤在客服的學員列表中顯示
- And 列表篩選「REFUNDED」可找到此學員
Scenario 10 退款後學員回來繼續上課 — REFUNDED 標籤自動移除
- Given 小明上次退款後當前 REFUNDED 標籤
- When Lisa 為小明再次生成新訂單 #005 並走完付款流程(已完成)
- Then 系統重算標籤
- And REFUNDED 標籤自動移除
- And 替換為 ACTIVE_CREDIT(依新訂單)
Scenario 11 退款金額超過訂單剩餘 — 阻擋
- Given 訂單 #001 剩 8 堂 / 640 CAD
- When Mark 在退款表單嘗試輸入退款金額 800(超過 640)
- Then 系統阻擋
- And 顯示「退款金額不可超過訂單剩餘額度(最多 640 CAD)」
- And 自動修正為 640
Scenario 12 已退款訂單再次嘗試退款 — 阻擋
- Given 訂單 #001 已退款
- When Mark 在學員詳細頁查看訂單 #001
- Then 訂單卡片顯示狀態「已退款」
- And 「退款」按鈕被「已退款」標籤替代(無動作)
- And API 直接呼叫返回「該訂單已退款,無法再次退款」
Scenario 13 退款全流程 audit log 內部行為
- Given Mark 發起退款
- When 系統執行退款
- Then 寫入 audit log:
- • 操作人:Mark(user_id, role=SUPER_ADMIN)
- • 操作:REFUND_INITIATED
- • 對象:order_id = #001
- • 詳情 JSON:含退款金額、方式、原因、明細
- • 時間戳
- And 退款完成時再寫一筆 REFUND_COMPLETED audit log
- And t_refund 表為不可變表(INSERT only,不可 UPDATE / DELETE)
Scenario 14 客戶端展示退款進度
- Given 訂單 #001 處於「退款中」狀態
- When 王女士在家長端「我的訂單」頁查看 #001
- Then 訂單卡片顯示:
- • 狀態徽章:「退款處理中」(藍色)
- • 退款明細:未稅可退 640、註冊費不退(30)、手續費 10、淨退 630
- • 退款方式:EMT 600 + Credit Pool 40
- • 預計到帳時間:「3-5 個工作日」
- When 退款狀態 → 已退款
- Then 訂單卡片狀態徽章變為「已退款」(綠色)
- And 顯示完成時間
Scenario 15 退款撤銷 — 緊急情況下 SUPER_ADMIN 操作
- Given 訂單 #001 處於「退款中」狀態
- And 王女士改變主意,打電話請求繼續上課
- When Mark 在退款管理頁點擊「撤銷退款」(僅在「退款中」階段可用)
- And 確認「金流尚未推送」
- Then 訂單 #001 狀態 → 已完成(恢復原狀)
- And Credit Pool 中該訂單剩餘解凍
- And 8 個未來 sessions 中小明重新加入(若 capacity 允許)
- And 若 capacity 不足,提示 Mark「[date] 該節課已被別人補入,需要協調」
- And Audit log 記錄撤銷操作
待確認 Q11.2:「撤銷退款」是否提供?實務上家長改主意是真實情境。我預設 提供,但僅在「退款中」階段且金流未推送前可用。請確認。
12. 學員標籤系統(V2.1:互斥規則 + 手動 CONTACTED)已完成 內部行為 + 客服端
⚠️ V2.1 重大變更(2026-05-08 拍板):
• 5 個標籤互斥(同時只能持有其中一個):
• 2 個標籤可獨立並列(與上述 5 個互斥標籤可共存):
• 學員最多同時持有 3 個標籤(5 互斥之 1 + WAITLIST + CONTACTED)
• CONTACTED 改為手動標記(客服打過電話跟進後手動勾選)
• 計算策略:每天 00:00 cron 重算(每天 1 次),不再使用即時 / 觸發式重算
• 5 個標籤互斥(同時只能持有其中一個):
ACTIVE_CREDIT / INACTIVE_CREDIT / INACTIVE_NO_CREDIT / REFUNDED / NEW• 2 個標籤可獨立並列(與上述 5 個互斥標籤可共存):
WAITLIST / CONTACTED• 學員最多同時持有 3 個標籤(5 互斥之 1 + WAITLIST + CONTACTED)
• CONTACTED 改為手動標記(客服打過電話跟進後手動勾選)
• 計算策略:每天 00:00 cron 重算(每天 1 次),不再使用即時 / 觸發式重算
主要角色:系統、客服(Lisa)、SUPER_ADMIN(Mark)
Background 共用前置條件
- Given 系統有以下測試學員(用於各標籤情境):
- • 小明:剛完成註冊 5 天,有 Credit Pool 200,本季已上 3 堂課
- • 小華:3 個月前註冊,有 Credit Pool 100,本季因受傷未上任何課
- • 小強:2 個月前註冊,Credit Pool = 0,本季未上課
- • 小強的姐姐 Bobby:上月退款,至今未再報名
- • 小新:3 天前註冊,尚未報名任何課
- • Tom:1 個月前註冊,至今無動作(未報名 / 未登入)
- • Lily:在 Burnaby Waitlist 中
Scenario 1 WAITLIST 標籤 — 學員在 t_waitlist 有 ACTIVE 紀錄
- Given Lily 在 Burnaby Waitlist 有 1 筆紀錄,狀態 = ACTIVE
- When 系統重算 Lily 的標籤
- Then Lily 持有 WAITLIST 標籤
- When Lily 報名了該校區該季度的季度課(依 F13 自動移除 Waitlist)
- Then 系統重算後 Lily 的 WAITLIST 標籤移除
Scenario 2 ACTIVE_CREDIT — 有餘額且當季有上課
- Given 小明 Credit Pool = 200(> 0)
- And 當前季度(2026 Spring)小明有 ≥ 1 筆 attendance 紀錄為 ATTENDED
- When 系統重算標籤
- Then 小明持有 ACTIVE_CREDIT 標籤
Scenario 3 INACTIVE_CREDIT — 有餘額但當季未上課(受傷例)
- Given 小華 Credit Pool = 100(> 0)
- And 當前季度小華無 ATTENDED 紀錄(全部請假或 0 紀錄)
- When 系統重算
- Then 小華持有 INACTIVE_CREDIT 標籤
- And 客服可篩選此標籤主動聯繫了解狀況
待確認 Q12.1:「當季未上課」判定 — 是「無 ATTENDED」還是「無任何 attendance 紀錄(含 LEAVE / NO_SHOW)」?我預設 無 ATTENDED(請假 / no-show 都視為未上)。請確認。
Scenario 4 INACTIVE_NO_CREDIT — 無餘額且未上課
- Given 小強 Credit Pool = 0
- And 當前季度小強無 ATTENDED 紀錄
- When 系統重算
- Then 小強持有 INACTIVE_NO_CREDIT 標籤
- And 此標籤是「沉睡客戶」的指標,營運可主動接觸
Scenario 5 REFUNDED — 已退款且未繼續上課
- Given Bobby 上月有 1 筆「已退款」訂單
- And 退款後 Bobby 沒有任何「已完成」新訂單
- When 系統重算
- Then Bobby 持有 REFUNDED 標籤
- When Bobby 後續被 Lisa 重新報名並完成新訂單付款
- Then 系統重算後 REFUNDED 標籤移除(替換為 ACTIVE_CREDIT 或其他適用標籤)
Scenario 6 NEW — 新註冊客戶(2 週內)
- Given 小新的 contact.created_at 是 3 天前
- When 系統重算
- Then 小新持有 NEW 標籤(< 14 天)
- When 14 天後系統重算
- Then NEW 標籤自動移除
- And 依「2 週內是否有動作」決定是否替換為 CONTACTED
待確認 Q12.2:NEW 起算點 — 我預設 contact.created_at(聯絡人建立時間)。是否該用「首次成功登入」?預設聯絡人建立更穩定(可能客服建檔但客戶沒登入)。請確認。
Scenario 7 CONTACTED — 2 週內無任何動作
- Given Tom 的 contact.created_at 是 1 個月前
- And Tom 從未報名 / 從未登入 / 從未有任何動作
- When 系統 cron 在 14 天後檢測
- Then Tom 持有 CONTACTED 標籤(NEW 已移除)
- And 此標籤是「需主動聯繫」的指標
待確認 Q12.3:「動作」定義 — 我預設包含:登入家長端、客服成功電話聯繫(需手動標記)、報名訂單、進過家長端任何頁面(access log)。請確認。
Scenario 8 多標籤同存
- Given 小新註冊 3 天(NEW),剛報名第一筆並完成(ACTIVE_CREDIT)
- When 系統重算
- Then 小新同時持有 NEW + ACTIVE_CREDIT 兩個標籤
- And 列表顯示時兩個標籤都顯示
- And 各標籤之間無互斥規則(除了部分自然互斥,如 ACTIVE_CREDIT 與 INACTIVE_NO_CREDIT 不會同時持有)
Scenario 9 列表篩選使用標籤
- Given Lisa 在學員列表頁
- When 她在篩選器選「INACTIVE_CREDIT」
- Then 列表只顯示有 INACTIVE_CREDIT 標籤的學員
- And 可同時選多個標籤(如「INACTIVE_CREDIT OR INACTIVE_NO_CREDIT」找所有未上課的)
Scenario 10 標籤統計面板(後台 Dashboard)
- Given Mark 進入 SUPER_ADMIN Dashboard
- When 他查看「學員標籤分布」widget
- Then 顯示各標籤的當前計數(餅圖 + 數字)
- And 點擊任一標籤跳到該標籤的學員列表
- And 顯示趨勢(過去 30 天 NEW / ACTIVE_CREDIT 變化)
Scenario 11 標籤計算策略:cron + 觸發式 內部行為
- Given 系統採用混合策略
- When 標籤需更新
- Then 觸發機制如下:
- • 事件觸發:訂單狀態變更 / 報名 / 退款 / Waitlist 變動 / 點名 → 同步重算受影響學員的所有標籤
- • 每天 00:00 全量 cron:掃所有學員,重算所有標籤(補正觸發遺漏 + NEW→CONTACTED 時間到期轉換)
- And 重算結果寫入 t_student.tags(JSON 陣列欄位)
- And 列表查詢直接讀此欄位(不臨時計算)
待確認 Q12.4:計算策略偏好 — 我預設 「事件觸發 + 每日 cron 兜底」混合策略。是否改為「純 cron」(簡單但延遲)?預設混合最平衡。
Scenario 12 NEW → CONTACTED 自動轉換 cron 內部行為
- Given 系統運行 cron 「標籤重算」每天 00:00
- When 系統檢測有 NEW 標籤超過 14 天且 14 天內無任何動作的學員
- Then 系統將其 NEW 移除,加上 CONTACTED
- And 不發通知(內部標籤變更,家長無感)
- And 客服列表立即看到該學員從 NEW 列表轉到 CONTACTED 列表
Scenario 13 標籤的資料儲存模型 內部行為
- Given 學員小明同時持有 NEW + ACTIVE_CREDIT
- When 系統儲存標籤
- Then t_student.tags 欄位(JSON 陣列)= ["NEW", "ACTIVE_CREDIT"]
- And 列表查詢使用 JSON_CONTAINS 索引以保證效能
- And 標籤值為列舉常量(不允許自由輸入)
- And tags_updated_at 欄位記錄最後重算時間
13. Waitlist 候補名單(V2.1:綁具體 session 的排隊)已完成 家長端 + 客服端
⚠️ V2.1 重大變更(2026-05-08 拍板):
Waitlist 從原先的「需求登記簿(不綁 session)」改為「綁定具體 session 的排隊」。
主要場景:家長在排課表上點擊某堂特定課程的「加入 Waitlist」(多用於該課已滿員的情況); 前台亦可在課表上代家長添加(例如電話請求)。
不參與排課流程 — 純記錄,由前台 / 教練在課程詳情頁查看誰排在前面,FIFO 順序聯繫。
家長僅看到自己的 Waitlist 條目;前台 / 教練可看某天某課的完整排隊列表。
主要場景:家長在排課表上點擊某堂特定課程的「加入 Waitlist」(多用於該課已滿員的情況); 前台亦可在課表上代家長添加(例如電話請求)。
不參與排課流程 — 純記錄,由前台 / 教練在課程詳情頁查看誰排在前面,FIFO 順序聯繫。
家長僅看到自己的 Waitlist 條目;前台 / 教練可看某天某課的完整排隊列表。
主要角色:家長(王女士)、客服(Lisa)、教練(Coach Luke)、系統
Background 共用前置條件(V2.1)
- Given Burnaby 課表上的 4/16 18:00 1on2 by Coach Luke 已滿員(2/2)
- And 小華(王女士的女兒,L1)想加入該課,但已滿
- And 該 session 已有 2 筆現存 Waitlist 紀錄:
- 1. Tina(媽媽 2026-05-01 10:00 加入)
- 2. Bobby(爸爸 2026-05-02 14:00 加入)
- And 王女士已登入家長端
Scenario 1 (V2.1) 家長在排課表上點擊某堂滿員課的「加入 Waitlist」
- Given 王女士在家長端排課表查看 4/16 18:00 1on2 by Coach Luke(顯示為已滿)
- And 該 session 已有 2 筆 Waitlist 紀錄(Tina, Bobby)
- When 王女士點擊該堂課的「加入 Waitlist」按鈕
- And 確認對話框中選擇學員「小華」並提交
- Then 系統建立 t_waitlist 紀錄(綁 student_id + session_id + created_at)
- And 紀錄狀態 = ACTIVE
- And 小華在該 session Waitlist 排第 3 位(FIFO)
- And 小華的標籤新增 WAITLIST
- And Toast 提示「已加入 4/16 18:00 1on2 候補名單,您是第 3 位」
Scenario 2 家長查看 / 編輯 / 刪除自己的 Waitlist
- Given 王女士在家長端 Waitlist 頁
- When 她查看小華的 Waitlist 紀錄
- Then 顯示她填的所有資訊 + 提交時間 + 排序位置(如「您是 Burnaby 校區週四下午 Group 的第 X 位候補」)
- And 提供「修改」和「刪除」按鈕
- When 她點擊「刪除」並確認
- Then 該 Waitlist 紀錄狀態 → CANCELLED
- And 小華的 WAITLIST 標籤移除(若無其他 Waitlist 紀錄)
Scenario 3 客服查看 per 校區 Waitlist + FIFO 排序
- Given Lisa 在後台「Waitlist 管理」頁
- When 她選擇校區 = Burnaby
- Then 顯示 Burnaby 所有 ACTIVE Waitlist 紀錄
- And 按提交時間升序排列(FIFO,最早提交在最上)
- And 每行顯示:學員姓名 / 標籤 / 等級 / 季度 / 週幾 / 時間段 / 希望課程類型 / 備註 / 「報名」按鈕
待確認 Q13.1:排序是嚴格 FIFO 還是允許客服自訂優先順位?我預設 FIFO 為預設,但客服可手動「置頂」某筆紀錄(用於 VIP / 特殊情況)+ audit log。請確認。
Scenario 4 客服從 Waitlist 進入報名流程(捷徑)
- Given Lisa 在 Waitlist 列表,看到 Tina 想要週四下午 Group
- And Burnaby 課表有 4/17(週四)15:00 Group 課,1 left
- When Lisa 點擊 Tina 行的「報名」按鈕
- Then 進入 F06 報名嚮導 Step 1
- And 自動帶入:學員 = Tina、聯絡人 = Tina 媽媽、校區 = Burnaby、Level = Tina 當前 Level
- And Step 2 預設 Seasonal + 2026 Spring(依 Waitlist 偏好)
- And Step 3 預設 Group + 週四(依 Waitlist 偏好;具體時間需 Lisa 選)
Scenario 5 報名完成自動移除 Waitlist(同校區同季度)
- Given Tina 在 Burnaby Waitlist 有「2026 Spring 週四下午 Group」紀錄
- When Lisa 為 Tina 在 Burnaby 完成 2026 Spring 的季度報名(任何週幾、任何類型)
- And 訂單狀態變為「已完成」
- Then 系統自動將 Tina 的「Burnaby + 2026 Spring」Waitlist 紀錄狀態 → MATCHED
- And 該紀錄從 ACTIVE Waitlist 列表移除
- And Tina 的 WAITLIST 標籤移除(若該校區季度只有這一筆)
待確認 Q13.2:「自動移除」的觸發時機 — 是「訂單狀態變為已完成」?還是「訂單建立(待付款)」就移除?我預設 已完成才移除(訂單超時取消的話 Waitlist 應保留)。請確認。
Scenario 6 單次課報名不移除 Waitlist
- Given Bobby 在 Burnaby Waitlist 有「2026 Spring 週六上午 1on2」紀錄
- When Lisa 為 Bobby 安排了一次 One-Time 補課(單次課)
- And 該 One-Time 訂單已完成
- Then Bobby 的 Waitlist 紀錄不被移除(仍 ACTIVE)
- And 因為「單次課不算滿足候補需求」
Scenario 9 客服手動為學員加入 Waitlist(從學員頁)
- Given Lisa 在小華的學員詳細頁
- And 王女士致電請客服幫忙加入 Waitlist
- When Lisa 點擊「加入 Waitlist」按鈕並填寫
- Then 系統建立 Waitlist 紀錄(建立人 = Lisa,標記為「客服代填」)
- And Audit log 記錄
- And 王女士可在家長端查看該紀錄
Scenario 10 客服開新課時推薦 Waitlist 候選人
- Given Lisa 剛在 Burnaby 課表上建立一節新 Group 課(週四 15:00)
- When 系統檢測到 Burnaby Waitlist 有匹配紀錄(季度 / 週幾 / 時段 / 類型)
- Then 課表上該節課顯示「Waitlist 中有 2 位匹配候選」標記
- And Lisa 點擊標記可看 FIFO 順序的候選清單(含 Tina、Lily)
- And 一鍵為候選人發送通知(Email / 系統通知)「您候補的時段已開放,請聯繫客服報名」
待確認 Q13.3:新課開放時是否自動發通知給匹配的 Waitlist 候選人?預設 系統自動發通知 + 客服手動 follow-up 雙管齊下。是否要 SMS 通知?
Scenario 11 Waitlist 過期清理(6 個月無動作)內部行為
- Given 系統有大量 ACTIVE Waitlist 紀錄
- When 系統每天 00:00 執行「Waitlist 清理」cron
- Then 找出 created_at 超過 6 個月且狀態仍為 ACTIVE 的紀錄
- And 標記為 EXPIRED
- And 對應家長收到通知「您的 Waitlist 紀錄已超過 6 個月未排到,已自動清理。如仍需要請重新登記」
待確認 Q13.4:過期時長 — 我預設 6 個月。是否更短(如 3 個月)或更長(季度結束自動清)?
Scenario 12 排序邊界:兩筆紀錄提交時間相同
- Given Tina 和 Bobby 在同一秒提交 Waitlist
- When 系統排序
- Then 以 created_at + id 升序作為次序(id 較小的在前)
- And 兩人都看到自己的明確排位
Scenario 13 (V2.1) Session 有人退出時,系統通知 Waitlist FIFO 第 1 位
- Given 4/16 18:00 1on2 滿員,Waitlist 有 3 位(Tina #1、Bobby #2、小華 #3)
- When 該 session 的某學員 24hr 前請假(依 F09,從 enrollment 移除,session 變 1 left)
- Then 系統發通知給 Tina 媽媽:「您候補的 4/16 18:00 1on2 課程已有空位,請聯繫客服安排報名」
- And Tina 的 Waitlist 紀錄狀態 → NOTIFIED
- And 此時 Tina 在 24 小時內聯繫客服則可報名;若無動作 24 小時後系統自動 fallback 通知 Bobby(FIFO 下一位)
- And 客服收到報名請求後走 F06 報名流程
- And Tina 報名完成後 Waitlist 紀錄狀態 → MATCHED 並移除
待確認 Q13.5:Waitlist 第 1 位 24hr 內未動作 → 自動通知第 2 位。我預設 24hr 為 fallback 時間。請確認此時長。
Scenario 14 (V2.1) 教練 / 客服查看某 session 的完整 Waitlist 排隊
- Given Coach Luke 在課程詳情頁查看 4/16 18:00 1on2
- When 他展開「候補名單」
- Then 顯示 3 位排隊者(FIFO 順序):
- • #1 Tina(5/1 10:00 加入)— 標記「已通知」
- • #2 Bobby(5/2 14:00 加入)
- • #3 小華(5/8 16:00 加入)
- And 同樣資訊客服 Lisa 在課表上點擊該 session 也可看到
- And 家長端看不到其他排隊者,只看到自己「您是第 X 位」
Scenario 15 (V2.1) Waitlist 過期 — Session 已開始或已結束
- Given Waitlist 紀錄綁某 session(4/16 18:00)
- When 4/16 18:00 該 session 已開始(無論最終是否有人補進來)
- Then 系統自動將該 session 所有未匹配的 Waitlist 紀錄狀態 → EXPIRED
- And 對應家長收到「Waitlist 已過期(課程已開始)」通知
⚠️ 過時 Scenarios 提示
上方 Scenarios 1-12 中,部分原本針對「需求登記簿」設計的內容(如多校區、多時段、季度自動移除等)在 V2.1 下不再適用。 建議實作時以本 feature 頂部 V2.1 變更說明 + Scenarios 1, 13, 14, 15 為主要依據。 其餘 Scenarios(2 家長自查/編輯、4 客服捷徑、9 客服代填、12 排序邊界)的核心邏輯仍適用,僅資料結構從「校區/季度/週幾」改為「session_id」。
14. 通知系統 已完成 家長端
業務價值:
通知系統讓家長即時掌握帳號狀態(付款結果、補課成功、課程變更等),減少反覆查詢。
三層機制:Toast(即時短暫)+ 鈴鐺(持久未讀)+ 歷史列表(完整紀錄)。
主要角色:家長(王女士)、系統
Background 共用前置條件
- Given 王女士已登入家長端
- And 通知中心當前有 3 條未讀紀錄
- And 鈴鐺圖標顯示紅點 + 數字「3」
Scenario 1 Toast 即時提示 — 操作完成
- Given 王女士剛完成請假操作
- When 後端返回成功
- Then 螢幕右上角出現 Toast「請假成功」
- And Toast 3 秒後自動消失
- And 同時通知中心新增 1 筆持久通知
- And 鈴鐺紅點 +1(變為「4」)
Scenario 2 鈴鐺通知 — 即時更新(WebSocket / Polling)
- Given 王女士在家長端任意頁面
- When 後台有新事件觸發通知(如 Lisa 為小明調整 Level)
- Then 鈴鐺圖標的紅點數字 +1(無需重整)
- And 不顯示 Toast(因為非當前用戶操作觸發)
- And 點擊鈴鐺打開下拉,看到新通知標記為「未讀」
待確認 Q14.1:即時更新機制 — 我預設 WebSocket(推送)。是否退回 polling(每 30 秒拉一次)?WebSocket 即時但增加複雜度。請確認。
Scenario 3 點擊鈴鐺打開通知下拉
- Given 鈴鐺紅點顯示「4」
- When 王女士點擊鈴鐺
- Then 下拉面板顯示最近 10 條通知(時間倒序)
- And 未讀紀錄左邊有藍色點標記
- And 每條顯示:標題 / 摘要 / 時間 / 操作按鈕(如「查看訂單」)
- And 底部「查看全部」按鈕跳到完整通知頁
- And 「全部標記為已讀」按鈕
Scenario 4 通知標記為已讀
- Given 王女士在通知下拉
- When 她點擊任一未讀通知
- Then 該通知標記為已讀
- And 鈴鐺紅點數字 -1
- And 跳轉到通知對應的詳情頁(如「查看訂單」跳到該訂單)
Scenario 5 6 種通知類型
- Given 系統定義以下通知類型:
| 類型 | 觸發點 | 標題範例 |
|---|---|---|
| PAYMENT_SUCCESS | 訂單已完成 | 訂單 #001 已完成,學員已加入課表 |
| REFUND_SUCCESS | 退款已退款 | 訂單 #001 退款 630 CAD 已完成 |
| MAKEUP_SUCCESS | 補課完成 | 小明 4/16 18:00 補課成功 |
| LEAVE_SUCCESS | 請假完成 | 小明 4/14 17:00 請假成功,已保留補課額度 |
| SESSION_CHANGED | 課程變更(時間 / 教練 / 取消) | 4/14 17:00 課程已調整為 4/14 18:00 |
| REGISTRATION_DONE | 註冊完成 | 歡迎使用 Aquaturbo,註冊完成 |
Scenario 6 通知偏好設定(哪些要 Email 推送)
- Given 王女士在「我的資料 → 通知偏好」頁
- When 她設定:
- • 付款 / 退款 → 鈴鐺 + Email
- • 補課 / 請假 → 鈴鐺(不 Email)
- • 課程變更 → 鈴鐺 + Email
- • 註冊完成 → 鈴鐺(不 Email)
- And 點擊保存
- Then 之後對應通知按設定發送
待確認 Q14.2:Email 推送是否第一版就要實作?預設 第一版只實作系統內通知(鈴鐺 / Toast / 歷史),Email 推送 V2.1 再加。請確認。
Scenario 7 全部標記為已讀
- Given 王女士有 10 條未讀通知
- When 她點擊「全部標記為已讀」
- Then 所有未讀紀錄標記為已讀
- And 鈴鐺紅點消失(顯示為空狀態)
- And 通知列表內容不變(紀錄仍可見,只是不再標為未讀)
Scenario 8 歷史通知列表頁
- Given 王女士進入完整通知頁
- When 介面載入
- Then 顯示所有歷史通知(無時間限制)
- And 可按通知類型篩選
- And 分頁載入(每頁 20 條)
Scenario 9 通知系統觸發點集合 內部行為
- Given 系統各業務模組需發通知
- When 觸發以下事件之一:
- • 訂單狀態 → 已完成 / 已取消 / 已退款(PAYMENT/REFUND)
- • 請假成功 / 撤回 / 特批
- • 補課成功 / 取消
- • 點名為 No-show(給家長知曉)
- • Level 調整
- • 課程變更(時間 / 教練 / 取消)
- • Waitlist 排到通知
- Then 對應 Service 呼叫 NotificationService.send()
- And NotificationService 寫入 t_notification + 推送 WebSocket(若用戶在線)
- And 依用戶偏好決定是否同步 Email
15. 報表 已完成 客服端 / SUPER_ADMIN
業務價值:
報表給管理層運營決策依據:哪個校區賺錢?哪個時段空閒?哪個教練最受歡迎?哪個 Level 段學員最多?
多維度篩選 + 匯出 Excel 支援深度分析。
主要角色:SUPER_ADMIN(Mark)、客服 ADMIN(限本校區)
Background 共用前置條件
- Given Mark(SUPER_ADMIN)已登入後台
- And 系統有 2026 Q1(Spring)的完整課程 / 收款 / 點名數據
Scenario 1 各校區收入報表
- Given Mark 進入「報表 → 收入分析」
- When 他選擇時間範圍 = 2026-03-01 ~ 2026-05-31
- Then 顯示各校區收入:
| 校區 | 已完成訂單數 | 含稅總收入 | 未稅淨收入 | 退款金額 |
|---|---|---|---|---|
| Burnaby | 120 | 125,640 | 119,657 | 3,200 |
| Coquitlam | 85 | 89,750 | 85,476 | 1,500 |
| 合計 | 205 | 215,390 | 205,133 | 4,700 |
Scenario 2 不同時段課程表現分析
- Given Mark 在「報表 → 時段分析」
- When 選擇時間範圍 + 校區
- Then 系統將時段分組(早、中、晚):
- • 早(9:00-12:00):sessions 數 / 平均出席率 / 平均報名率
- • 中(12:00-17:00):同上
- • 晚(17:00-22:00):同上
待確認 Q15.1:時段定義 — 是「早 / 中 / 晚」三段,還是 hourly bucket,或允許自訂?我預設 早 9-12 / 中 12-17 / 晚 17-22 三段。請確認。
Scenario 3 課程類型分析
- Given Mark 在「報表 → 課程類型」
- When 篩選條件設定後
- Then 顯示各類型統計:1on1 / 1on2 / Group / 長訓 各自的:sessions 數 / 報名數 / 收入 / 平均出席率
Scenario 4 學員年齡與等級分布
- Given Mark 在「報表 → 學員分布」
- When 介面載入
- Then 顯示:
- • 年齡分布:4-6 / 7-9 / 10-12 / 13-15 / 16+ 各區段人數
- • Level 分布:L1-L5 各 Level 人數(圖表)
- • 交叉分析:每個年齡段各 Level 的學員數(heatmap)
待確認 Q15.2:年齡計算 — 以「當前日期」還是「報名時學員年齡」? V2 schema 沒有 birthdate 欄位,需新增(QC.3 含義已暗示)。請確認需新增 birthdate 欄位 + 年齡以當前日期計算。
Scenario 5 教練表現報表
- Given Mark 在「報表 → 教練表現」
- When 介面載入
- Then 顯示每位教練的:
- • 教學時數(總分鐘)
- • 帶過 sessions 數
- • 帶過學員數(去重)
- • 平均出席率
- • 學員續報率(學員上完後是否繼續報名同教練的下個課程)
- • 平均單堂收入(收入 / sessions 數)
待確認 Q15.3:教練績效是否影響薪資計算?我預設 不影響(純展示),薪資由 HR 系統處理。請確認。
Scenario 6 多維度篩選
- Given 任一報表頁
- When Mark 套用組合篩選:時間範圍 + 校區 + 課程類型 + 教練
- Then 報表即時更新
- And 篩選器在頂部固定,方便快速調整
- And 「重置篩選」按鈕
Scenario 7 匯出 Excel
- Given Mark 在任一報表頁,已套用篩選
- When 他點擊「匯出 Excel」
- Then 系統產生 .xlsx 檔,含當前篩選條件下的完整數據
- And 檔名格式:「
Aquaturbo_收入報表_2026-03_2026-05_Burnaby.xlsx」
Scenario 8 ADMIN 限本校區
- Given Lisa 是 ADMIN(Burnaby)
- When 她進入報表頁
- Then 校區篩選器只顯示「Burnaby」(鎖定)
- And 不可看其他校區資料
- And 部分敏感報表(如全公司教練績效對比)對 ADMIN 隱藏
Scenario 9 月度自動報表 Email 內部行為
- Given 系統 cron 每月 1 日 09:00 觸發
- When 任務執行
- Then 系統產生「上月運營月報」PDF
- And Email 給所有 SUPER_ADMIN
待確認 Q15.4:月報 Email 是否第一版實作?預設 V2.1 再加,第一版只做頁面查詢 + Excel 匯出。請確認。
Scenario 10 報表查詢效能 內部行為
- Given 系統有 10000+ 個 sessions / 50000+ 個 attendances
- When Mark 查詢 1 年範圍報表
- Then 響應時間 < 3 秒
- And 資料庫查詢使用適當索引(branch_id, session_date, status)
- And 大型聚合用 view 或物化(materialized view)
16. 換課 / 換時間(V2.1:拍板版 — 前台操作)已完成 客服端
✅ V2.1 拍板(2026-05-08 會議):
會議決策方案是「前台操作 + 自動生成價差訂單」(接近原方案 B 但更簡)。
• 家長不可自助換課:必須通知前台處理。
• 同價換課:前台一鍵在後台將 enrollment 從原 session 移到新 session,無金流變動。
• 換到較貴課程:前台立即生成「補差價」付款訂單給家長,付款完成後 enrollment 移轉。
• 換到較便宜課程:差價退到「不可消費餘額」(綁原訂單),等該訂單對應 sessions 全部結束後自動轉自由餘額。
下方 Scenarios 中的方案 A/B/C 並列段落已過時 — 直接看「拍板版」執行 scenarios。
• 家長不可自助換課:必須通知前台處理。
• 同價換課:前台一鍵在後台將 enrollment 從原 session 移到新 session,無金流變動。
• 換到較貴課程:前台立即生成「補差價」付款訂單給家長,付款完成後 enrollment 移轉。
• 換到較便宜課程:差價退到「不可消費餘額」(綁原訂單),等該訂單對應 sessions 全部結束後自動轉自由餘額。
下方 Scenarios 中的方案 A/B/C 並列段落已過時 — 直接看「拍板版」執行 scenarios。
主要角色:客服(Lisa)、SUPER_ADMIN(Mark)、家長(王女士)
Background 業務場景
- Given 小明已報名訂單 #001:12 堂 1on2 by Coach Luke 週二 17:00(已上 4 堂,剩 8 堂)
- And 王女士因家庭時程變動,希望剩下 8 堂改為週四 17:00(同教練、同類型、同價)
- And 4/16 18:00 起的週四 1on2 課程有空位
核心待確認 Q16.0:需要從以下三方案中拍板選一(或混合):
方案 A 訂單項目級換課 — 不退款、不新訂單(最輕量)
- Given Lisa 在訂單 #001 詳情頁
- When 她點擊「批量換課」
- And 在 8 個未來 sessions 中勾選要換的(如全選)
- And 選擇目標 RecurrenceGroup(週四 17:00 1on2 by Coach Luke)
- And 系統驗證:同類型 + 同教練 + 同單堂價(嚴格符合)
- Then 8 個 enrollments 從原 sessions 移到目標 sessions
- And 訂單 #001 不變(金額不變、狀態不變)
- And 不產生退款 / 不產生新訂單
- And Audit log 記錄此換課操作
- And 王女士收到「課程已調整」通知
方案 A 評估:
優點:操作最簡、家長無感、無金流變動、capacity 直接互換。
缺點:審計鏈混亂(一個訂單對應的 sessions 中途換了);如果新 session 價格不同無法處理。
方案 B 「換課申請」狀態 + 客服一鍵切換(中等)
- Given 王女士致電客服請求換課
- When Lisa 在後台發起「換課申請」(新狀態 / 工作流)
- And 填寫:來源 sessions、目標 sessions、價差處理(同價 / 補差價 / 退差價)
- And 提交
- Then 系統建立 1 筆 t_class_change 紀錄(新表)
- And 同價 → 直接執行(同方案 A)
- And 補差價 → 自動建一筆「One-Time 補差價」訂單,待付款
- And 退差價 → 將差價退到 Credit Pool(找零模式)
- And 王女士收到通知 / 確認
方案 B 評估:
優點:流程明確 + 處理價差 + 審計鏈清晰(換課動作獨立記錄)。
缺點:需新增 t_class_change 表 + 新狀態機 + 較複雜的工作流。
方案 C 退款 + 新訂單(最嚴格 / 當前 V1 方式)
- Given 王女士致電請求換課
- When Mark 對訂單 #001 發起部分退款(剩 8 堂 = 640)
- And 退款方式選「Credit Pool」(直接入找零池)
- And Lisa 走 F06 為小明新建訂單 #002(週四 17:00 8 堂)
- And Step 6 用 640 Credit 全額抵扣(待付款 = 0)
- Then 訂單 #001 → 已退款(部分退款情境見 F11 S6)
- And 訂單 #002 → 已完成(純 Credit 抵扣)
- And 對家長透明(看到一進一出)
方案 C 評估:
優點:審計清晰(每筆訂單獨立)、現有 F06/F11 已支援,無需新功能。
缺點:耗時(兩個操作)、家長疑惑(為什麼要退又付)、文檔明確抱怨「太麻煩」。
推薦 我的建議:方案 B(中等)
- Given 三方案各有取捨
- When 從「業務便捷度 × 審計清晰度 × 工程量」綜合考量
- Then 我建議選 方案 B,理由:
- • 文檔抱怨「方案 C 太麻煩」 → 排除 C
- • 方案 A 審計鏈不清晰 + 無法處理價差 → 不夠完整
- • 方案 B 的 t_class_change 表只是 1 個新表,工程量可控
- • 方案 B 處理同價 / 補差價 / 退差價 三種情境
場景 1(方案 B) 同價換時間 — 直接執行
- Given 方案 B 已被採用
- And 王女士想將 8 堂週二 17:00 1on2 換為 8 堂週四 17:00 1on2(同教練同類型同價)
- When Lisa 在後台發起「換課申請」
- And 系統檢測同類型 + 同教練 + 同價
- Then 自動執行(無需 SUPER_ADMIN 審批)
- And 8 個 enrollments 移轉
- And 訂單 #001 金額不變
場景 2(方案 B) 換到不同價格 — 補差價
- Given 王女士想將 8 堂 80/堂 換為 8 堂 90/堂(教練升等)
- And 差額 = 8 × 10 = 80 CAD
- When Lisa 發起換課並選「補差價」
- Then 系統建立「One-Time 補差價」訂單,總額 80 + GST = 84 CAD
- And 訂單為「待付款」狀態,60 分鐘倒數
- And 待付款完成後,enrollments 才實際移轉到新 sessions
- And 補差價訂單超時取消的話,原訂單 sessions 保持不變
場景 3(方案 B) 換到較便宜課程 — 退差價到 Credit Pool
- Given 王女士想將 8 堂 80/堂 換為 8 堂 50/堂(換教練降等)
- And 差額 = 8 × 30 = 240 CAD
- When Lisa 發起換課並選「退差價到 Credit Pool」
- Then 8 個 enrollments 移轉
- And 240 CAD 進入小明 Credit Pool(來源:換課退差價,from_order = #001)
- And 訂單 #001 標記「換課退差 240」
17. 系統定時任務(V2.1 調整)已完成 內部行為
⚠️ V2.1 變更:
• 新增 SessionAutoDeductScanner(每分鐘)— F08 自動扣費(取代點名 + No-show)
• 新增 LockedBalanceTransferScanner(每天 00:00)— 訂單對應 sessions 全結束後,剩餘不可消費餘額轉自由餘額
• 移除 NoShowAutoCloser(取消點名後不再需要)
• StudentTagRecalculator 仍保留每天 00:00(V2.1 不再支援即時重算,純 cron)
其他 cron(訂單超時 / Waitlist 清理 / 認證碼清理 / 上課提醒)保持不變。
• 新增 SessionAutoDeductScanner(每分鐘)— F08 自動扣費(取代點名 + No-show)
• 新增 LockedBalanceTransferScanner(每天 00:00)— 訂單對應 sessions 全結束後,剩餘不可消費餘額轉自由餘額
• 移除 NoShowAutoCloser(取消點名後不再需要)
• StudentTagRecalculator 仍保留每天 00:00(V2.1 不再支援即時重算,純 cron)
其他 cron(訂單超時 / Waitlist 清理 / 認證碼清理 / 上課提醒)保持不變。
主要角色:系統
Background Cron 任務拓撲
- Given 系統定時任務集中在 Spring Scheduler(@Scheduled)
- And 採用分散式鎖(Redis)防止多實例重複執行
- And 失敗紀錄寫入 audit log + Sentry alert
Scenario 1 訂單超時自動取消(最高頻)
- Given 系統定時任務
OrderTimeoutScanner - When 每 1 分鐘觸發
- Then 找出 Online 待付款 + created_at > 60 分鐘的訂單 → 自動取消
- And 找出 Cash 待付款 + created_at > 48 小時的訂單 → 自動取消
- And 對應 capacity 釋放 + 鎖定 Credit 解凍 + 通知家長
- And 詳見 F07 Scenario 16
Scenario 2 No-show 自動結算(每 15 分鐘)
- Given 定時任務
NoShowAutoCloser - When 每 15 分鐘觸發
- Then 找出 session.end_at + 1 小時 < now() 且狀態 = OPEN 且仍有 PENDING attendance 的 sessions
- And 將所有 PENDING attendance 標記為 NO_SHOW
- And 從各自訂單扣費
- And session.status → CLOSED
- And 詳見 F08 Scenario 11
Scenario 3 標籤重算 + NEW → CONTACTED 轉換(每天 00:00)
- Given 定時任務
StudentTagRecalculator - When 每天 00:00 觸發
- Then 對所有學員執行 7 個標籤的重新計算
- And 補正觸發式重算的遺漏
- And 處理 NEW → CONTACTED 時間到期轉換
- And 詳見 F12 Scenarios 11, 12
Scenario 4 Waitlist 過期清理(每天 00:00)
- Given 定時任務
WaitlistCleaner - When 每天 00:00 觸發
- Then 找出 created_at > 6 個月且狀態 = ACTIVE 的紀錄
- And 標記為 EXPIRED
- And 通知家長
- And 詳見 F13 Scenario 11
Scenario 5 過期密碼重設碼 / 註冊碼清理(每天 02:00)
- Given 定時任務
AuthCodeCleaner - When 每天 02:00 觸發
- Then 刪除超過 10 分鐘的過期密碼重設碼
- And 標記超過 30 天的未使用註冊碼為 EXPIRED
- And 詳見 F01 Scenarios 13, 14
Scenario 6 課程上課提醒(每天 19:00 — 隔天提醒)
- Given 定時任務
UpcomingSessionReminder - When 每天 19:00 觸發
- Then 找出明天有課的學員
- And 給家長發提醒通知(鈴鐺 + 可選 Email)
待確認 Q17.1:是否需要「課前 24 小時提醒」?文檔未明示,但實務上是常見功能。請確認是否第一版包含。
Scenario 7 季度開始時自動展開 sessions(事件觸發)
- Given Mark 建立新季度(如 2026 Summer)
- When 客服建立 RecurrenceGroup
- Then 系統立即同步展開該季度範圍內所有 sessions(依 weekday 規則)
- And 排除國定假日 + 校區自訂休館日
- And 寫入 t_session 表
- And 詳見 F04 Scenario 21
Scenario 8 月度報表自動產出(每月 1 日 09:00 — V2.1)
- Given 定時任務
MonthlyReportGenerator(V2.1 含) - When 每月 1 日 09:00 觸發
- Then 產生上月運營月報 PDF
- And Email 給所有 SUPER_ADMIN
- And 詳見 F15 Scenario 9
Scenario 9 系統健康檢查(每分鐘)
- Given 定時任務
HealthCheck - When 每分鐘觸發
- Then 檢查:DB 連線 / Redis / Stripe API / 各 cron 上次執行時間
- And 異常時 alert 給運維
Scenario 10 Cron 任務拓撲總表
- Given 完整 cron 表如下
| 頻率 | 任務 | 關聯 Feature |
|---|---|---|
| 每分鐘 | OrderTimeoutScanner | F07 |
| 每分鐘 | HealthCheck | F17 |
| 每 15 分鐘 | NoShowAutoCloser | F08 |
| 每天 00:00 | StudentTagRecalculator | F12 |
| 每天 00:00 | WaitlistCleaner | F13 |
| 每天 02:00 | AuthCodeCleaner | F01 |
| 每天 19:00 | UpcomingSessionReminder | F14 |
| 每月 1 日 09:00 | MonthlyReportGenerator (V2.1) | F15 |
| 事件觸發 | SessionExpander(季度建立時) | F04 |
| 事件觸發 | StudentTagRecalculator (per 學員) | F12 |