feat(verification): 教練審核流程完整版——狀態機/證照送審/Admin 裁決/通知
Run Tests / test (pull_request) Successful in 2m3s
Run Tests / test (pull_request) Successful in 2m3s
實作 openspec change provider-verification-workflow(25/25 tasks): - is_verified 升級為 verification_status 四狀態機(unsubmitted/pending/ approved/rejected),migration 自動轉換既有資料,API 以 accessor 保留 is_verified 相容輸出 - 教練端:證照上傳(≤3 張、複用 CompressesImages)、送審、駁回原因 顯示與重送(/coach/verification 新頁面) - Admin 端:審核佇列、通過/駁回(原因必填)、撤銷 approved;移除 toggle-verified 端點(防繞過狀態機);裁決後 flush 課程快取 - 通知:審核通過/駁回(站內+Email,駁回含原因) - 可見性與預約入口改判 approved(行為等價) - 測試 +18(ProviderVerificationTest 10、AdminVerificationTest 8), 既有 4 個測試檔 helper 遷移;Swagger 同步 - 規格同步:provider-verification 全面改寫、admin-user-management toggle 段落改為移除聲明 容器內全套件 173 passed / 439 assertions。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-06-11
|
||||
@@ -0,0 +1,57 @@
|
||||
# Design — provider-verification-workflow
|
||||
|
||||
## D1:四狀態而非三狀態
|
||||
|
||||
路線圖原列 pending/approved/rejected 三態,但「註冊後補件送審」決策(Hank 2026-06-11 拍板)使「註冊完還沒送審」與「已送審待審」必須區分——Admin 佇列只該出現後者。故:
|
||||
|
||||
```
|
||||
unsubmitted ──submit──> pending ──approve──> approved
|
||||
↑ │ │
|
||||
│ reject reject(撤銷,原因必填)
|
||||
│ ↓ ↓
|
||||
└──(重新上傳)── rejected ──submit──> pending
|
||||
```
|
||||
|
||||
- 合法轉移:`unsubmitted→pending`、`pending→approved|rejected`、`rejected→pending`、`approved→rejected`(撤銷)
|
||||
- `rejected→pending` 重送時清空 `rejection_reason`
|
||||
- 欄位用 string + 應用層常數(與 BookingStatus enum 同模式,建 `App\Enums\VerificationStatus`)
|
||||
|
||||
## D2:取代而非並存 boolean
|
||||
|
||||
`is_verified` 欄位**移除**(migration: true→approved、false→unsubmitted),不留同步欄位——兩個真實來源必然漂移(Rule 7)。API 相容由 `ProviderProfile::$appends` 的 `is_verified` accessor 提供(`= status === approved`),前端/Swagger 既有讀取點不破壞;查詢端(`visibleToPublic`、預約入口)改查 `verification_status`。
|
||||
|
||||
## D3:證照獨立資料表
|
||||
|
||||
`provider_certifications`(id, user_id, image_path, created_at)。不塞 JSON 欄位:要逐張刪除、要 FK 清理、上限 3 張需 count 查詢。圖片複用 `CompressesImages::compressToJpeg`(O3.1 的 trait,第二個使用者,驗證抽共用的價值),存 `providers/{user_id}/certifications/`。刪除政策:`unsubmitted`/`rejected` 可增刪;`pending`/`approved` 鎖定(審核依據不可變動)。
|
||||
|
||||
## D4:端點設計
|
||||
|
||||
**Provider(auth:sanctum + provider prefix)**
|
||||
- `GET /api/provider/verification`——status、rejection_reason、certifications[]
|
||||
- `POST /api/provider/verification/certifications`——上傳(≤3 張、≤10MB、壓縮);僅 unsubmitted/rejected
|
||||
- `DELETE /api/provider/verification/certifications/{id}`——僅 unsubmitted/rejected、僅本人
|
||||
- `POST /api/provider/verification/submit`——至少 1 張證照;unsubmitted/rejected → pending
|
||||
|
||||
**Admin(auth:sanctum + admin)**
|
||||
- `GET /api/admin/verifications?status=pending`——佇列(預設 pending,可查全部狀態),含教練資料與證照 URL
|
||||
- `PUT /api/admin/verifications/{userId}/approve`——pending → approved
|
||||
- `PUT /api/admin/verifications/{userId}/reject`——pending/approved → rejected,`reason` 必填(max 500)
|
||||
|
||||
approve/reject 皆 flush `diving_offers` 快取 tag(可見性立即生效,沿用 toggle 的既有做法)。`toggle-verified` 端點與 `AdminUserController::toggleProviderVerified` 移除——保留會提供繞過狀態機的後門。
|
||||
|
||||
控制器:新建 `ProviderVerificationController` 與 `AdminVerificationController`(單一職責),不塞進 AuthController/AdminUserController。
|
||||
|
||||
## D5:通知
|
||||
|
||||
`ProviderVerificationApprovedNotification`、`ProviderVerificationRejectedNotification`(含原因),照 Booking* 模式(database + mail channel,try/catch 包裹不阻斷主流程)。送審時**不**通知 Admin(Admin 有佇列頁;避免通知氾濫),列為未來選項。
|
||||
|
||||
## D6:前端
|
||||
|
||||
- **教練端** `views/coach/VerificationView.vue`(route `/coach/verification`):狀態卡(四態 + 駁回原因)、證照上傳/刪除(沿用 OfferFormView 的圖片上傳模式)、送審按鈕;CoachLayout 側欄加入口與狀態 badge
|
||||
- **Admin 端** `views/admin/ProvidersView.vue` 擴充:狀態 badge 四態化、「待審核」filter、展開查看證照圖、通過/駁回操作(駁回原因用 inline 輸入,沿用該頁既有操作風格);不另開新頁(教練列表即審核入口,避免兩頁資料重複)
|
||||
|
||||
## D7:Demo 資料與測試遷移
|
||||
|
||||
- DemoSeeder:3 位已驗證 → `approved`;1 位未驗證 → 給 1 張證照 + `pending`(展示審核佇列)
|
||||
- 既有測試 helper(DivingOfferVisibility / BookingLifecycle / BookingOversell / ReviewTest):`is_verified => true/false` 改 `verification_status => approved/unsubmitted`
|
||||
- 新測試:送審狀態機(含非法轉移)、證照上限與鎖定、Admin 審核權限邊界、通知發送、approve/reject 後可見性立即生效
|
||||
@@ -0,0 +1,26 @@
|
||||
## Why
|
||||
|
||||
目前教練驗證只有一顆 Admin 的 `is_verified` 開關(2026-06-11 修補後具備可見性約束力),但**沒有審核「流程」**:教練無從提交資質證明、Admin 沒有審核依據與佇列、駁回無原因可言、結果無通知。路線圖(`docs/analysis/2026-06-11-future-roadmap-feasibility.md` §2.1)將此列為金流前的信任基礎建設。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **資料模型**:`provider_profiles.is_verified`(boolean)升級為 `verification_status` 狀態機——`unsubmitted`(註冊預設)→ `pending`(已送審)→ `approved` / `rejected`(含 `rejection_reason`);新增 `provider_certifications` 資料表存證照圖片
|
||||
- **教練端**:後台新增「驗證申請」頁——上傳證照(1~3 張,複用 `CompressesImages` 壓縮管線)、送審、查看駁回原因、重新送審(產品決策:**註冊後補件送審**,不擋註冊流程)
|
||||
- **Admin 端**:審核佇列(pending 優先)、查看證照、通過/駁回(駁回原因必填);移除舊 `toggle-verified` 開關端點(與狀態機衝突)
|
||||
- **通知**:審核通過/駁回通知教練(站內 + Email,複用 notification 管線)
|
||||
- **相容性**:`ProviderProfile` 以 accessor 保留 API 輸出的 `is_verified` 欄位(= status 是否為 approved);`visibleToPublic` scope 與預約入口檢查改查 `verification_status = 'approved'`,語意不變
|
||||
|
||||
## Capabilities
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `provider-verification`:boolean 語意升級為四狀態機;新增教練送審端點、Admin 審核端點、通知 requirements;可見性規則改以 `approved` 判定(行為等價)
|
||||
- `admin-user-management`:`toggle-verified` 端點移除,由 approve / reject 取代
|
||||
|
||||
## Impact
|
||||
|
||||
- **資料庫**:migration 轉換既有資料(`is_verified=true → approved`、`false → unsubmitted`)、新表 `provider_certifications`
|
||||
- **後端**:新增 `ProviderVerificationController`、`AdminVerificationController`、2 個 Notification class;修改 `DivingOffer::visibleToPublic`、`MemberBookingController`、`AdminUserController`(移除 toggle)、`ProviderProfile`、兩個 Seeder
|
||||
- **前端**:新增 `coach/VerificationView.vue`;改 `admin/ProvidersView.vue`(狀態 badge、審核操作)
|
||||
- **測試**:既有用到 `is_verified` 的 4 個測試檔改用 `verification_status`;新增送審流程與審核權限測試
|
||||
- **行為變更**:Admin 不再能單鍵切換驗證,必須走審核流(撤銷 = 駁回既有 approved 教練並附原因);既有預約不受狀態變動影響(沿用 provider-verification 規格既定政策)
|
||||
@@ -0,0 +1,14 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 管理員切換教練驗證狀態
|
||||
(本 requirement 由 provider-verification 的「Admin 審核佇列與裁決」取代)
|
||||
|
||||
`PUT /api/admin/providers/{id}/toggle-verified` SHALL 移除——單鍵切換允許繞過審核狀態機(無原因駁回、未送審直接通過)。教練驗證狀態的變更一律透過 `PUT /api/admin/verifications/{userId}/approve|reject`。
|
||||
|
||||
#### Scenario: toggle 端點回 404
|
||||
- **WHEN** Admin 請求 `PUT /api/admin/providers/{id}/toggle-verified`
|
||||
- **THEN** 回傳 HTTP 404
|
||||
|
||||
#### Scenario: 教練列表仍顯示驗證狀態
|
||||
- **WHEN** Admin 請求 `GET /api/admin/providers`
|
||||
- **THEN** 每筆教練資料包含 `verification_status`(與相容用 `is_verified` accessor)
|
||||
@@ -0,0 +1,84 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 教練驗證狀態機
|
||||
`provider_profiles.verification_status` SHALL 為四狀態字串:`unsubmitted`(註冊預設)、`pending`、`approved`、`rejected`。合法轉移僅限:`unsubmitted→pending`(送審)、`pending→approved`(通過)、`pending→rejected`(駁回)、`rejected→pending`(重新送審,同時清空 `rejection_reason`)、`approved→rejected`(撤銷,原因必填)。原 boolean `is_verified` 欄位移除,API 輸出以 accessor 保留 `is_verified`(= status 為 approved)。
|
||||
|
||||
#### Scenario: 新教練註冊預設未送審
|
||||
- **WHEN** 教練完成註冊
|
||||
- **THEN** `verification_status = unsubmitted`,不出現在 Admin 待審佇列
|
||||
|
||||
#### Scenario: 非法轉移被拒絕
|
||||
- **WHEN** 嘗試對 `unsubmitted` 教練執行 approve,或對 `approved` 教練執行 approve
|
||||
- **THEN** 回傳 HTTP 422,狀態不變
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 教練上傳證照與送審
|
||||
教練 SHALL 能於後台上傳 1~3 張證照圖片(jpeg/png/webp、≤10MB,伺服器端壓縮為 `.jpg`,存 `providers/{user_id}/certifications/`)並送出審核。證照僅於 `unsubmitted` / `rejected` 狀態可增刪;`pending` / `approved` 狀態鎖定。送審需至少 1 張證照。
|
||||
|
||||
#### Scenario: 上傳證照
|
||||
- **WHEN** `unsubmitted` 教練 `POST /api/provider/verification/certifications` 上傳合法圖片且現有 < 3 張
|
||||
- **THEN** 壓縮儲存並建立 `provider_certifications` 紀錄,回傳圖片資訊
|
||||
|
||||
#### Scenario: 超過 3 張上限
|
||||
- **WHEN** 教練已有 3 張證照再上傳
|
||||
- **THEN** 回傳 HTTP 422
|
||||
|
||||
#### Scenario: 送審成功
|
||||
- **WHEN** 教練至少有 1 張證照,`POST /api/provider/verification/submit`
|
||||
- **THEN** 狀態轉為 `pending`,`rejection_reason` 清空
|
||||
|
||||
#### Scenario: 無證照不可送審
|
||||
- **WHEN** 教練無任何證照即送審
|
||||
- **THEN** 回傳 HTTP 422,狀態不變
|
||||
|
||||
#### Scenario: pending 期間證照鎖定
|
||||
- **WHEN** `pending` 或 `approved` 教練嘗試上傳或刪除證照
|
||||
- **THEN** 回傳 HTTP 422
|
||||
|
||||
#### Scenario: 查詢自身驗證狀態
|
||||
- **WHEN** 教練 `GET /api/provider/verification`
|
||||
- **THEN** 回傳 `status`、`rejection_reason`、`certifications[]`(id、url)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Admin 審核佇列與裁決
|
||||
Admin SHALL 能查詢審核佇列(`GET /api/admin/verifications?status=pending`,預設 pending,含教練資料與證照 URL),並對 `pending` 教練執行通過(`PUT /api/admin/verifications/{userId}/approve`)或駁回(`PUT /api/admin/verifications/{userId}/reject`,`reason` 必填、max 500)。Admin SHALL 能駁回 `approved` 教練(撤銷驗證,原因必填)。裁決後 SHALL flush `diving_offers` 快取(可見性立即生效)。原 `PUT /api/admin/providers/{id}/toggle-verified` 端點 SHALL 移除。
|
||||
|
||||
#### Scenario: 通過審核
|
||||
- **WHEN** Admin 對 pending 教練執行 approve
|
||||
- **THEN** 狀態轉 `approved`,教練課程立即恢復公開可見性
|
||||
|
||||
#### Scenario: 駁回需附原因
|
||||
- **WHEN** Admin 執行 reject 未帶 `reason`
|
||||
- **THEN** 回傳 HTTP 422,狀態不變
|
||||
|
||||
#### Scenario: 撤銷已通過教練
|
||||
- **WHEN** Admin 對 approved 教練執行 reject 並附原因
|
||||
- **THEN** 狀態轉 `rejected`,課程立即從公開列表消失;既有預約照常(沿用既定政策)
|
||||
|
||||
#### Scenario: 舊 toggle 端點已移除
|
||||
- **WHEN** 任何人請求 `PUT /api/admin/providers/{id}/toggle-verified`
|
||||
- **THEN** 回傳 HTTP 404
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 審核結果通知
|
||||
系統 SHALL 於 approve / reject 後通知該教練(站內 + Email):通過通知告知課程已可公開曝光;駁回通知包含駁回原因。通知失敗不阻斷審核主流程。
|
||||
|
||||
#### Scenario: 通過通知
|
||||
- **WHEN** Admin approve
|
||||
- **THEN** 教練收到站內通知與 Email,內容含審核通過訊息
|
||||
|
||||
#### Scenario: 駁回通知含原因
|
||||
- **WHEN** Admin reject 並附原因
|
||||
- **THEN** 教練收到的通知內容包含該原因
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 未驗證教練的課程不對公開端點曝光
|
||||
公開課程端點(列表、詳情、schedules、reviews)SHALL 僅曝光 `provider_id` 為 null 或教練 `verification_status = 'approved'` 的課程(原以 `is_verified = true` 判定,語意等價)。`POST /api/member/bookings` 的可預約檢查同步改以 `approved` 判定。其餘行為(404、422、既有預約不受影響)不變。
|
||||
|
||||
#### Scenario: 非 approved 狀態一律不曝光
|
||||
- **WHEN** 課程教練的狀態為 unsubmitted / pending / rejected
|
||||
- **THEN** 公開端點不回傳該課程,新預約被拒
|
||||
@@ -0,0 +1,47 @@
|
||||
## 1. 資料模型
|
||||
|
||||
- [x] 1.1 新增 `App\Enums\VerificationStatus`(Unsubmitted / Pending / Approved / Rejected,含 `VALID_TRANSITIONS` 與 `canTransitionTo()`,仿 BookingStatus 模式)
|
||||
- [x] 1.2 Migration:`provider_profiles` 加 `verification_status`(string, default 'unsubmitted')與 `rejection_reason`(text nullable);資料轉換 `is_verified=true→approved`、`false→unsubmitted`;drop `is_verified`
|
||||
- [x] 1.3 Migration:新表 `provider_certifications`(id, user_id FK cascade, image_path, timestamps)
|
||||
- [x] 1.4 `ProviderProfile`:fillable/casts 更新、`is_verified` accessor(appends,= approved)、`certifications` 由 User 關聯
|
||||
- [x] 1.5 新 Model `ProviderCertification`(belongsTo User、url accessor,仿 CourseImage)
|
||||
|
||||
## 2. 教練端 API
|
||||
|
||||
- [x] 2.1 `ProviderVerificationController`:`show`(status + reason + certifications)、`uploadCertification`(≤3 張、複用 CompressesImages、僅 unsubmitted/rejected)、`deleteCertification`(僅本人 + 僅 unsubmitted/rejected)、`submit`(≥1 張證照、unsubmitted/rejected→pending、清 rejection_reason)
|
||||
- [x] 2.2 routes:`/api/provider/verification*` 四端點(auth:sanctum + provider group)
|
||||
|
||||
## 3. Admin 端 API
|
||||
|
||||
- [x] 3.1 `AdminVerificationController`:`index`(?status= 預設 pending,含教練資料與證照)、`approve`(pending→approved)、`reject`(pending|approved→rejected,reason 必填 max 500);approve/reject flush diving_offers 快取 + 通知教練
|
||||
- [x] 3.2 routes:`/api/admin/verifications*` 三端點;移除 toggle-verified route 與 `AdminUserController::toggleProviderVerified`
|
||||
- [x] 3.3 `AdminUserController::providers` 列表回應含 `verification_status`
|
||||
|
||||
## 4. 可見性判定切換
|
||||
|
||||
- [x] 4.1 `DivingOffer::visibleToPublic`:`is_verified=true` → `verification_status='approved'`
|
||||
- [x] 4.2 `MemberBookingController::store` 可預約檢查同步改判 approved
|
||||
|
||||
## 5. 通知
|
||||
|
||||
- [x] 5.1 `ProviderVerificationApprovedNotification` + `ProviderVerificationRejectedNotification`(含原因;database + mail,仿 Booking* 模式)
|
||||
|
||||
## 6. Seeder
|
||||
|
||||
- [x] 6.1 DemoSeeder / DevelopmentSeeder:`is_verified` 改 `verification_status`;demo 第 4 位教練給 1 張證照 + pending(展示佇列)
|
||||
|
||||
## 7. 前端
|
||||
|
||||
- [x] 7.1 `views/coach/VerificationView.vue` + route `/coach/verification`:狀態卡(四態 + 駁回原因)、證照上傳/刪除、送審;CoachLayout 側欄入口
|
||||
- [x] 7.2 `views/admin/ProvidersView.vue`:badge 四態化、待審 filter、查看證照、通過/駁回(原因輸入);移除 toggle 呼叫
|
||||
|
||||
## 8. 測試
|
||||
|
||||
- [x] 8.1 既有測試遷移:DivingOfferVisibilityTest / BookingLifecycleTest / BookingOversellTest / ReviewTest 的 helper 改 `verification_status`
|
||||
- [x] 8.2 `ProviderVerificationTest`(新):狀態機合法/非法轉移、證照上限/鎖定/本人限定、送審前置條件、重送清空原因
|
||||
- [x] 8.3 `AdminVerificationTest`(新):佇列過濾、approve/reject 權限(非 admin 403)、reject 無原因 422、撤銷 approved、裁決後可見性立即生效(快取)、通知發送斷言、toggle 端點 404
|
||||
- [x] 8.4 容器內全套件綠(基準 155 passed)
|
||||
|
||||
## 9. 規格同步
|
||||
|
||||
- [x] 9.1 specs 增量套用至主規格 `provider-verification` 與 `admin-user-management`
|
||||
@@ -57,13 +57,13 @@
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 管理員驗證教練
|
||||
後端 SHALL 提供 `PUT /api/admin/providers/{id}/toggle-verified`,反轉 ProviderProfile.is_verified 狀態。
|
||||
### Requirement: 管理員驗證教練(已由審核狀態機取代)
|
||||
`PUT /api/admin/providers/{id}/toggle-verified` 已於 2026-06-12 移除——單鍵切換允許繞過審核狀態機(無原因駁回、未送審直接通過)。教練驗證狀態的變更一律透過 `PUT /api/admin/verifications/{userId}/approve|reject`(見 provider-verification 規格「Admin 審核佇列與裁決」)。
|
||||
|
||||
#### Scenario: 驗證教練
|
||||
- **WHEN** 管理員對 is_verified=false 的教練發送請求
|
||||
- **THEN** 將 is_verified 設為 true,回傳 HTTP 200,`{ status: true, message: "教練已驗證", data: { is_verified: true } }`
|
||||
#### Scenario: toggle 端點回 404
|
||||
- **WHEN** 管理員請求 `PUT /api/admin/providers/{id}/toggle-verified`
|
||||
- **THEN** 回傳 HTTP 404
|
||||
|
||||
#### Scenario: 取消驗證教練
|
||||
- **WHEN** 管理員對 is_verified=true 的教練發送請求
|
||||
- **THEN** 將 is_verified 設為 false,回傳 HTTP 200,`{ status: true, message: "已取消驗證", data: { is_verified: false } }`
|
||||
#### Scenario: 教練列表仍顯示驗證狀態
|
||||
- **WHEN** 管理員請求 `GET /api/admin/providers`
|
||||
- **THEN** 每筆教練的 provider_profile 包含 `verification_status` 與相容用 `is_verified`(accessor,= approved)
|
||||
|
||||
@@ -2,77 +2,119 @@
|
||||
|
||||
## Purpose
|
||||
|
||||
定義 `provider_profiles.is_verified` 的最小業務語意:未通過平台驗證的教練,其課程不對公開端點曝光。在完整教練審核流程(證照上傳、審核佇列、駁回原因)實作前,先以此約束堵住「未審核教練可公開曝光」的風險。
|
||||
定義教練資質審核的完整生命週期:教練上傳證照送審、Admin 審核裁決(通過/駁回含原因)、結果通知,以及「未通過審核(approved)之教練的課程不對公開端點曝光、不可接受新預約」的可見性約束。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 未驗證教練的課程不對公開端點曝光
|
||||
公開課程端點(`GET /api/diving-offers`、`GET /api/diving-offers/{id}`)SHALL 僅回傳符合以下任一條件的課程:(a) `provider_id` 為 null(平台自有資料);(b) 課程所屬 Provider 的 `provider_profiles.is_verified = true`。未驗證教練的課程在列表中 SHALL 被排除,在詳情端點 SHALL 回傳 404。
|
||||
### Requirement: 教練驗證狀態機
|
||||
`provider_profiles.verification_status` SHALL 為四狀態字串:`unsubmitted`(註冊預設)、`pending`、`approved`、`rejected`。合法轉移僅限:`unsubmitted→pending`(送審)、`pending→approved`(通過)、`pending→rejected`(駁回)、`rejected→pending`(重新送審,同時清空 `rejection_reason`)、`approved→rejected`(撤銷,原因必填)。原 boolean `is_verified` 欄位移除,API 輸出以 accessor 保留 `is_verified`(= status 為 approved)。
|
||||
|
||||
#### Scenario: 已驗證教練的課程正常曝光
|
||||
- **WHEN** 匿名使用者請求 `GET /api/diving-offers`
|
||||
- **THEN** 已驗證教練(is_verified=true)的課程出現在結果中
|
||||
#### Scenario: 新教練註冊預設未送審
|
||||
- **WHEN** 教練完成註冊
|
||||
- **THEN** `verification_status = unsubmitted`,不出現在 Admin 待審佇列
|
||||
|
||||
#### Scenario: 未驗證教練的課程從列表排除
|
||||
- **WHEN** 匿名使用者請求 `GET /api/diving-offers`
|
||||
- **THEN** 未驗證教練(is_verified=false 或無 ProviderProfile)的課程不出現在結果中
|
||||
#### Scenario: 非法轉移被拒絕
|
||||
- **WHEN** 嘗試對 `unsubmitted` 教練執行 approve,或對 `approved` 教練執行 approve
|
||||
- **THEN** 回傳 HTTP 422,狀態不變
|
||||
|
||||
#### Scenario: 未驗證教練的課程詳情回 404
|
||||
- **WHEN** 匿名使用者請求 `GET /api/diving-offers/{id}`,該課程屬於未驗證教練
|
||||
---
|
||||
|
||||
### Requirement: 教練上傳證照與送審
|
||||
教練 SHALL 能於後台上傳 1~3 張證照圖片(jpeg/png/webp、≤10MB,伺服器端壓縮為 `.jpg`,存 `providers/{user_id}/certifications/`)並送出審核。證照僅於 `unsubmitted` / `rejected` 狀態可增刪;`pending` / `approved` 狀態鎖定(審核依據不可變動)。送審需至少 1 張證照。
|
||||
|
||||
#### Scenario: 上傳證照
|
||||
- **WHEN** `unsubmitted` 教練 `POST /api/provider/verification/certifications` 上傳合法圖片且現有 < 3 張
|
||||
- **THEN** 壓縮儲存並建立 `provider_certifications` 紀錄,回傳圖片資訊
|
||||
|
||||
#### Scenario: 超過 3 張上限
|
||||
- **WHEN** 教練已有 3 張證照再上傳
|
||||
- **THEN** 回傳 HTTP 422
|
||||
|
||||
#### Scenario: 送審成功
|
||||
- **WHEN** 教練至少有 1 張證照,`POST /api/provider/verification/submit`
|
||||
- **THEN** 狀態轉為 `pending`,`rejection_reason` 清空
|
||||
|
||||
#### Scenario: 無證照不可送審
|
||||
- **WHEN** 教練無任何證照即送審
|
||||
- **THEN** 回傳 HTTP 422,狀態不變
|
||||
|
||||
#### Scenario: pending 期間證照鎖定
|
||||
- **WHEN** `pending` 或 `approved` 教練嘗試上傳或刪除證照
|
||||
- **THEN** 回傳 HTTP 422
|
||||
|
||||
#### Scenario: 查詢自身驗證狀態
|
||||
- **WHEN** 教練 `GET /api/provider/verification`
|
||||
- **THEN** 回傳 `status`、`rejection_reason`、`certifications[]`(id、url)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Admin 審核佇列與裁決
|
||||
Admin SHALL 能查詢審核佇列(`GET /api/admin/verifications?status=pending`,預設 pending,含教練資料與證照 URL),並對 `pending` 教練執行通過(`PUT /api/admin/verifications/{userId}/approve`)或駁回(`PUT /api/admin/verifications/{userId}/reject`,`reason` 必填、max 500)。Admin SHALL 能駁回 `approved` 教練(撤銷驗證,原因必填)。裁決後 SHALL flush `diving_offers` 快取(可見性立即生效)。原 `PUT /api/admin/providers/{id}/toggle-verified` 端點已移除。
|
||||
|
||||
#### Scenario: 通過審核
|
||||
- **WHEN** Admin 對 pending 教練執行 approve
|
||||
- **THEN** 狀態轉 `approved`,教練課程立即恢復公開可見性(不受 180 秒快取影響)
|
||||
|
||||
#### Scenario: 駁回需附原因
|
||||
- **WHEN** Admin 執行 reject 未帶 `reason`
|
||||
- **THEN** 回傳 HTTP 422,狀態不變
|
||||
|
||||
#### Scenario: 撤銷已通過教練
|
||||
- **WHEN** Admin 對 approved 教練執行 reject 並附原因
|
||||
- **THEN** 狀態轉 `rejected`,課程立即從公開列表消失;既有預約照常
|
||||
|
||||
#### Scenario: 舊 toggle 端點已移除
|
||||
- **WHEN** 任何人請求 `PUT /api/admin/providers/{id}/toggle-verified`
|
||||
- **THEN** 回傳 HTTP 404
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 審核結果通知
|
||||
系統 SHALL 於 approve / reject 後通知該教練(站內 + Email):通過通知告知課程已可公開曝光;駁回通知包含駁回原因。通知失敗不阻斷審核主流程。
|
||||
|
||||
#### Scenario: 通過通知
|
||||
- **WHEN** Admin approve
|
||||
- **THEN** 教練收到站內通知與 Email,內容含審核通過訊息
|
||||
|
||||
#### Scenario: 駁回通知含原因
|
||||
- **WHEN** Admin reject 並附原因
|
||||
- **THEN** 教練收到的通知內容包含該原因
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 未通過審核教練的課程不對公開端點曝光
|
||||
公開課程端點(`GET /api/diving-offers` 列表、`GET /api/diving-offers/{id}` 詳情、`/{id}/schedules`、`/{id}/reviews`)SHALL 僅回傳符合以下任一條件的課程:(a) `provider_id` 為 null(平台自有資料);(b) 課程所屬教練 `verification_status = 'approved'`。其餘狀態(unsubmitted / pending / rejected,或無 ProviderProfile)的課程在列表中排除、詳情與子端點回傳 404。
|
||||
|
||||
#### Scenario: approved 教練的課程正常曝光
|
||||
- **WHEN** 匿名使用者請求公開課程端點
|
||||
- **THEN** approved 教練的課程出現在結果中,詳情/時段/評價端點照常回傳
|
||||
|
||||
#### Scenario: 非 approved 教練的課程不曝光
|
||||
- **WHEN** 課程教練狀態為 unsubmitted / pending / rejected
|
||||
- **THEN** 列表排除該課程,詳情/時段/評價端點回傳 404
|
||||
|
||||
#### Scenario: provider_id 為 null 的課程不受限
|
||||
- **WHEN** 匿名使用者請求公開課程端點,課程的 `provider_id` 為 null
|
||||
- **THEN** 課程正常曝光
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 驗證狀態切換立即生效
|
||||
管理員透過 `PUT /api/admin/providers/{id}/toggle-verified` 切換驗證狀態後,公開課程列表的快取(`diving_offers` cache tag)SHALL 立即失效,下次請求反映最新可見性。
|
||||
### Requirement: 未通過審核教練的課程不可建立新預約
|
||||
`POST /api/member/bookings` SHALL 在建立預約前驗證 schedule 所屬課程可預約(`provider_id` 為 null 或教練 `verification_status = 'approved'`),不符時回傳 HTTP 422,不建立預約。既有預約(pending / confirmed / completed)SHALL 不受教練驗證狀態變動影響:教練仍可處理 pending、confirmed 的聊天與完課流程照常、completed 可正常評價。
|
||||
|
||||
#### Scenario: 取消驗證後課程立即從公開列表消失
|
||||
- **WHEN** 管理員將教練 is_verified 由 true 切為 false
|
||||
- **THEN** 下一次 `GET /api/diving-offers` 請求不包含該教練的課程(不受 180 秒快取影響)
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 公開子端點套用相同可見性
|
||||
課程的公開子端點(`GET /api/diving-offers/{id}/schedules`、`GET /api/diving-offers/{id}/reviews`)SHALL 套用與課程詳情相同的可見性規則:課程屬未驗證教練時回傳 HTTP 404。
|
||||
|
||||
#### Scenario: 隱藏課程的時段查詢回 404
|
||||
- **WHEN** 匿名使用者請求 `GET /api/diving-offers/{id}/schedules`,該課程屬未驗證教練
|
||||
- **THEN** 回傳 HTTP 404
|
||||
|
||||
#### Scenario: 隱藏課程的評價查詢回 404
|
||||
- **WHEN** 匿名使用者請求 `GET /api/diving-offers/{id}/reviews`,該課程屬未驗證教練
|
||||
- **THEN** 回傳 HTTP 404
|
||||
|
||||
#### Scenario: 可見課程的子端點正常
|
||||
- **WHEN** 課程屬已驗證教練或 `provider_id` 為 null
|
||||
- **THEN** 時段與評價端點照常回傳資料
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 未驗證教練的課程不可建立新預約
|
||||
`POST /api/member/bookings` SHALL 在建立預約前驗證 schedule 所屬課程可預約(`provider_id` 為 null 或教練 `is_verified = true`),不符時回傳 HTTP 422,不建立預約。既有預約(pending / confirmed / completed)SHALL 不受教練驗證狀態變動影響:教練仍可處理 pending、confirmed 的聊天與完課流程照常、completed 可正常評價。
|
||||
|
||||
#### Scenario: 未驗證教練課程的新預約被拒絕
|
||||
- **WHEN** 會員以未驗證教練課程的 schedule_id 送出預約
|
||||
#### Scenario: 未通過審核教練課程的新預約被拒絕
|
||||
- **WHEN** 會員以非 approved 教練課程的 schedule_id 送出預約
|
||||
- **THEN** 回傳 HTTP 422,`{ status: false, message: "此課程目前不開放預約" }`,不建立 Booking
|
||||
|
||||
#### Scenario: 教練被取消驗證後既有預約照常
|
||||
- **WHEN** 教練在預約 confirmed 之後被取消驗證
|
||||
#### Scenario: 教練被撤銷驗證後既有預約照常
|
||||
- **WHEN** 教練在預約 confirmed 之後被撤銷驗證(approved→rejected)
|
||||
- **THEN** 該預約的聊天、完課、評價流程照常運作;僅新預約被擋
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 教練自有管理端點不受可見性限制
|
||||
Provider 對自己課程的管理端點(`/api/provider/offers*`)與 Admin 管理端點(`/api/admin/offers*`)SHALL 不受公開可見性過濾影響,未驗證教練仍可登入、編輯與管理自己的課程。
|
||||
Provider 對自己課程的管理端點(`/api/provider/offers*`)與 Admin 管理端點(`/api/admin/offers*`)SHALL 不受公開可見性過濾影響,未通過審核的教練仍可登入、編輯與管理自己的課程、上傳證照與送審。
|
||||
|
||||
#### Scenario: 未驗證教練仍可管理自己的課程
|
||||
- **WHEN** 未驗證教練以有效 token 請求 `GET /api/provider/offers`
|
||||
#### Scenario: 未通過審核教練仍可管理自己的課程
|
||||
- **WHEN** 未通過審核教練以有效 token 請求 `GET /api/provider/offers`
|
||||
- **THEN** 回傳該教練的全部課程
|
||||
|
||||
## Notes
|
||||
|
||||
完整審核流程(verification_status enum、證照上傳、審核佇列)見 `docs/analysis/2026-06-11-future-roadmap-feasibility.md` §2.1。
|
||||
|
||||
Reference in New Issue
Block a user