Files
a620906209 03f8caf3e9 feat:實作通知系統 — 站內通知、Email 通知、Polling 機制
後端
- 新增 6 個 Notification class(預約建立/確認/拒絕/取消/完成、收到評價),database + mail 雙 channel
- 新增 NotificationController(list / unread-count / markRead / markAllRead / destroy)
- 整合通知觸發至 MemberBookingController、ProviderBookingController、CompleteFinishedBookings、ReviewController
- 新增 notifications / jobs / failed_jobs migration
- Docker Compose 加入 queue-worker、mailpit service
- DivingOffer 補上 provider() 關聯

前端
- 新增 notificationStore(Polling 30s/60s 自適應 + Page Visibility API)
- 新增 NotificationBell(未讀 Badge)、NotificationDrawer(側邊通知中心)
- main.js:auth store init 前置於 router.use(),修正 beforeEach guard 時序問題
- notificationAxios:依路徑動態選擇 member/coach token
- NotificationDrawer:改用 new URL().pathname 提取 action_url 路徑

OpenSpec
- 歸檔 notification-system change
- 同步 notification-core / notification-email / notification-triggers specs 至主規格
- 更新 booking-lifecycle / review-lifecycle spec(補充通知觸發 requirement)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-17 22:26:14 +08:00

119 lines
6.1 KiB
Markdown
Raw Permalink 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.
### Requirement: Member 送出預約
Member SHALL 能選擇一個開放時段送出預約,系統記錄價格快照。pending 狀態不佔用時段名額。
#### Scenario: 成功建立預約
- **WHEN** 已登入 Member 送出 `POST /api/member/bookings`,指定 `schedule_id``participants`(≥1
- **THEN** 系統建立 Bookingstatus 為 `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 取消)
#### Scenario: 非法狀態轉換被拒絕
- **WHEN** 任何角色嘗試執行上述以外的狀態轉換
- **THEN** 系統回傳 422,說明當前狀態不允許此操作
### 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