0cd1047b10
Run Tests / test (pull_request) Successful in 30s
repo 內 35/38 份既有 spec 都是舊版歸檔流程留下的原始 delta 內容(`## ADDED Requirements`,缺標題與 Purpose),openspec CLI 現版本嚴格驗證 (--strict)會報錯或警告。逐一補上: - 缺標題/Purpose 的 30 份:加上 `# <name> Specification` 標題 + 依內容 撰寫的 Purpose 段落,`## ADDED Requirements` 改回 `## Requirements` - compose-cloud-baseline、scheduler-container:已有標題與 Requirements,只補 Purpose 包裝 - provider-verification:已有標題與 Purpose,只需把 `## ADDED Requirements` 改回 `## Requirements` - admin-user-management、notification-email:各有一條 requirement 缺 SHALL/MUST 關鍵字(純敘述句或表格),補上規範用語,內容不變 `openspec validate --specs --strict`:38/38 全過(原本 4/38)。純文件 補齊,未變更任何 requirement 的實質內容或行為描述。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011LpcY9b7y9x4fusHBTXciQ
136 lines
6.9 KiB
Markdown
136 lines
6.9 KiB
Markdown
# 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
|