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

159 lines
9.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 不變