Files
CFDivePlatform/openspec/specs/booking-lifecycle/spec.md
T
a620906209 72daf9baa0
Run Tests / test (pull_request) Successful in 37s
chore(openspec): 歸檔 fix-booking-exception-handling 並同步主規格
- booking-lifecycle 主規格新增「預約交易錯誤分類」requirement
- tasks 3.2 正式環境驗證完成(建立、確認預約正常,未觀察到 500)
- 待辦文件標記 M7 完成

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 01:45:26 +08:00

9.4 KiB
Raw Blame History

booking-lifecycle Specification

Purpose

定義預約系統的七狀態機、建立/確認/拒絕/取消流程、防超賣的名額驗證、Scheduler 自動過期與完成,以及狀態轉換觸發的通知。

Requirements

Requirement: Member 送出預約

Member SHALL 能選擇一個開放時段送出預約,系統記錄價格快照。pending 狀態不佔用時段名額。

Scenario: 成功建立預約

  • WHEN 已登入 Member 送出 POST /api/member/bookings,指定 schedule_id 與 participants(≥1)
  • THEN 系統建立 Booking,status 為 pending,total_price 快照為 diving_offer.price × participants,回傳 201

Scenario: 時段已滿無法預約

  • WHEN 指定時段 status 為 full 或 cancelled
  • THEN 系統回傳 422,告知時段不可用

Scenario: 超過剩餘名額(API 層快速驗證)

  • WHEN participants 大於時段當前剩餘名額(max_participants - current_participants),在進入 DB transaction 前
  • THEN 系統回傳 422,告知人數超過上限,不進入 lockForUpdate 流程

Scenario: 超過剩餘名額(DB 層二次驗證)

  • WHEN API 層通過但 lockForUpdate 後重新計算剩餘名額仍不足(race condition 情境)
  • THEN 系統 rollback transaction,回傳 422,告知名額不足

Scenario: 不可重複預約同一時段

  • WHEN Member 對同一 schedule_id 已有 pending 或 confirmed 狀態的 Booking
  • THEN 系統回傳 422,告知已有預約(取消後可重新預約)

Requirement: 預約狀態機

系統 SHALL 維護七個合法狀態,且只允許以下轉換:

  • pending → confirmed(Provider 確認)
  • pending → rejected(Provider 拒絕)
  • pending → member_cancelled(Member 取消)
  • pending → expired(Scheduler 超時)
  • confirmed → completed(Scheduler 課程後自動)
  • confirmed → member_cancelled(Member 取消)
  • confirmed → provider_cancelled(Provider 取消)

各狀態對應訊息頻道語義:

  • confirmed:presence-booking.{id} 頻道開放,可讀寫訊息
  • completed:頻道關閉,訊息歷史封存唯讀
  • 其餘狀態(pending、rejected、expired、member_cancelled、provider_cancelled):無訊息頻道

Scenario: 非法狀態轉換被拒絕

  • WHEN 任何角色嘗試執行上述以外的狀態轉換
  • THEN 系統回傳 422,說明當前狀態不允許此操作

Scenario: confirmed 轉 completed 時封存訊息頻道

  • WHEN Scheduler 將 confirmed 預約轉為 completed
  • THEN 對應 presence-booking.{id} 頻道不再授權新連線;現有訊息歷史保留,後續 POST 訊息回傳 403

Requirement: Provider 確認或拒絕預約

Provider SHALL 能對自己課程的 pending 預約執行確認或拒絕。確認時才真正佔用時段名額。

Scenario: 確認預約

  • WHEN Provider 送出 PUT /api/provider/bookings/{id}/confirm
  • THEN Booking status 改為 confirmed,時段 current_participants 增加對應人數

Scenario: 拒絕預約

  • WHEN Provider 送出 PUT /api/provider/bookings/{id}/reject
  • THEN Booking status 改為 rejected,current_participants 不變(pending 未佔位)

Scenario: 只能操作自己課程的預約

  • WHEN Provider 嘗試操作不屬於自己課程的 Booking
  • THEN 系統回傳 403 Forbidden

Requirement: Provider 取消已確認預約

Provider SHALL 能取消 confirmed 狀態的預約(例如天氣因素)。

Scenario: Provider 取消確認中預約

  • WHEN Provider 送出 PUT /api/provider/bookings/{id}/cancel
  • THEN Booking status 改為 provider_cancelled,時段名額釋放

Requirement: Member 取消預約

Member SHALL 能取消自己的 pending 或 confirmed 預約,但須在課程開始前 24 小時之前提出。

Scenario: 取消 pending 預約(期限內)

  • WHEN Member 送出 DELETE /api/member/bookings/{id},Booking status 為 pending,且當前時間早於 scheduled_date + start_time - 24h
  • THEN Booking status 改為 member_cancelled,current_participants 不變

Scenario: 取消 confirmed 預約(期限內)

  • WHEN Member 送出 DELETE /api/member/bookings/{id},Booking status 為 confirmed,且當前時間早於 scheduled_date + start_time - 24h
  • THEN Booking status 改為 member_cancelled,時段名額釋放

Scenario: 課程開始前 24h 內不可取消

  • WHEN Member 送出 DELETE /api/member/bookings/{id},但當前時間距 scheduled_date + start_time 不足 24 小時
  • THEN 系統回傳 422,告知「距課程開始不足 24 小時,無法取消,請聯繫教練」;Booking 狀態不變

Scenario: 不可取消已終態預約

  • WHEN Booking status 為 completed、rejected、expired、provider_cancelled
  • THEN 系統回傳 422,告知無法取消

Requirement: 系統自動過期 pending 預約

Scheduler SHALL 每小時掃描 pending 超過 48 小時的 Booking 並標記為 expired。

Scenario: 過期觸發

  • WHEN Booking status 為 pending 且 created_at 早於 48 小時前
  • THEN Scheduler 將 status 改為 expired,current_participants 不變(pending 未佔位)

Requirement: 系統自動完成 confirmed 預約

Scheduler SHALL 每日掃描課程日期已過的 confirmed Booking 並標記為 completed。

Scenario: 自動完成

  • WHEN Booking status 為 confirmed,對應 course_schedule.scheduled_date 早於今天
  • THEN Scheduler 將 status 改為 completed

Requirement: Member 查看自己的預約列表

Member SHALL 能查詢自己所有預約的列表及詳情,含課程連結與完整課程資訊。

Scenario: 取得預約列表

  • WHEN 已登入 Member 送出 GET /api/member/bookings
  • THEN 系統回傳該 Member 所有 Booking,含 offer_id、課程名稱、地點、時段日期、狀態、金額

Scenario: 取得單一預約詳情

  • WHEN 已登入 Member 送出 GET /api/member/bookings/{id}
  • THEN 系統回傳該 Booking 詳情;若非本人預約則回傳 403

Requirement: 預約狀態轉換觸發通知

預約七狀態機(pending / confirmed / completed / rejected / expired / member_cancelled / provider_cancelled)的每個轉換點,系統 SHALL 在狀態成功更新後觸發對應通知(詳見 notification-triggers spec)。通知觸發 MUST 在主業務 transaction commit 之後執行,且以 try/catch 包裹,不影響主業務結果。

Scenario: 狀態轉換後通知觸發

  • WHEN BookingService 中任一狀態轉換方法成功執行
  • THEN 對應的 Notification class 被觸發,不論通知是否成功主業務均正常回傳

Scenario: 通知失敗不影響主業務

  • WHEN notify 呼叫拋出例外
  • THEN 預約狀態已正確儲存,HTTP response 成功回傳,錯誤記錄至 Laravel log

Requirement: 預約交易錯誤分類

建立預約(POST /api/member/bookings)與確認預約(PUT /api/provider/bookings/{id}/confirm)的交易中,系統 SHALL 區分業務規則失敗與伺服器端故障:業務規則失敗(名額不足、重複預約)回傳 422 與既定訊息;其他任何例外(含資料庫錯誤)MUST NOT 以 422 回傳,MUST 回傳 500,回應內容不得包含 SQL、資料表或欄位名稱,且該例外 MUST 交由例外回報機制記錄(正式環境會送至 Sentry)。兩種情況下交易都 MUST rollback,不留下部分寫入。

Scenario: 重複預約仍回 422 與既定訊息

  • WHEN 已登入 member 對同一時段已有 pending 預約,再次送出 POST /api/member/bookings(schedule_id 相同、participants 1)
  • THEN 系統回傳 422,body 為 {"status": false, "message": "您已預約此時段"},該 member 在此時段的預約筆數不變

Scenario: 交易內名額不足仍回 422 與既定訊息

  • WHEN 已登入 member 送出 POST /api/member/bookings 時通過交易前的名額檢查,但交易內鎖定時段後重新計算的剩餘名額小於 participants
  • THEN 系統回傳 422,body 為 {"status": false, "message": "名額不足,請重新選擇"},不建立預約

Scenario: 確認預約時名額不足仍回 422 與既定訊息

  • WHEN 已登入 provider 對自己課程的 pending 預約送出 PUT /api/provider/bookings/{id}/confirm,而該預約人數大於時段剩餘名額
  • THEN 系統回傳 422,body 為 {"status": false, "message": "名額不足,無法確認此預約"},預約維持 pending,current_participants 不變

Scenario: 建立預約時資料庫錯誤回 500 且不洩漏細節

  • WHEN APP_DEBUG 為 false,已登入 member 送出合法的 POST /api/member/bookings,而交易內寫入預約之後發生資料庫例外
  • THEN 系統回傳 500,body 為通用錯誤訊息且不含 SQL 語句、bookings 等資料表名稱與欄位名稱,資料庫例外被回報,bookings 筆數不變

Scenario: 確認預約時資料庫錯誤回 500 且不洩漏細節

  • WHEN APP_DEBUG 為 false,已登入 provider 對自己課程的 pending 預約送出 PUT /api/provider/bookings/{id}/confirm(名額足夠),而交易內預約狀態已更新、更新時段名額時發生資料庫例外
  • THEN 系統回傳 500,body 為通用錯誤訊息且不含 SQL 語句、資料表名稱與欄位名稱,資料庫例外被回報,預約維持 pending,current_participants 不變