ddbf956144
新增 BookingRuleViolationException,MemberBookingController::store 與 ProviderBookingController::confirm 只 catch 此型別並回 422(訊息不變)。 QueryException 等其他例外不再被當成 422 回傳 SQL,改由 Laravel 回 500 並回報 Sentry。對應 OpenSpec change fix-booking-exception-handling(M7)。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.7 KiB
4.7 KiB
Tasks
測試集中在新檔 tests/Feature/BookingExceptionHandlingTest.php(建立 provider/時段的輔助方法參考 tests/Feature/BookingOversellTest.php)。每個測試的 docblock 要寫明「為什麼」:資料庫錯誤曾以 422 把 SQL 回給使用者,且不會送到 Sentry。觸發與斷言方式見 design.md D3。
1. 業務例外與 Controller
- 1.1 [後端] 新增
app/Exceptions/BookingRuleViolationException.php(final class,繼承\RuntimeException,不加額外方法);驗證:2.x 測試通過 - 1.2 [後端]
app/Http/Controllers/API/MemberBookingController.php的store:交易閉包內兩處throw new \RuntimeException(...)改為throw new BookingRuleViolationException(...),訊息文字不變;catch (\RuntimeException $e)改為catch (BookingRuleViolationException $e) - 1.3 [後端]
app/Http/Controllers/API/ProviderBookingController.php的confirm:同 1.2,處理名額不足,無法確認此預約 - 1.4 [後端] 確認
grep -rn "RuntimeException" app/只剩BookingRuleViolationException本身的繼承宣告
2. 測試
- 2.1 [整合測試]
test_duplicate_booking_returns_422_with_rule_message:member 已有該時段pending預約,再次POST /api/member/bookings,回 422,assertExactJson(['status' => false, 'message' => '您已預約此時段']),該 member 在此時段的預約仍為 1 筆;測試開頭Exceptions::fake(),最後Exceptions::assertNotReported(BookingRuleViolationException::class)(對應 Scenario「重複預約仍回 422 與既定訊息」) - 2.2 [整合測試]
test_capacity_lost_inside_transaction_returns_422_with_rule_message:以CourseSchedule::retrieved事件在交易內把current_participants設為滿額(事件只能改動交易內lockForUpdate()->find()那次讀取;交易前的第一次讀取若被改動,會被前置檢查以「人數超過剩餘名額」攔下,測不到交易內路徑),POST /api/member/bookings回 422,assertExactJson(['status' => false, 'message' => '名額不足,請重新選擇']),bookings筆數為 0;同 2.1 以Exceptions::fake()斷言未回報(對應 Scenario「交易內名額不足仍回 422 與既定訊息」) - 2.3 [整合測試]
test_confirm_without_capacity_returns_422_with_rule_message:provider 確認人數大於剩餘名額的pending預約,回 422,assertExactJson(['status' => false, 'message' => '名額不足,無法確認此預約']),預約仍為pending、current_participants不變;同 2.1 以Exceptions::fake()斷言未回報(對應 Scenario「確認預約時名額不足仍回 422 與既定訊息」) - 2.4 [整合測試]
test_database_error_on_create_returns_500_without_sql:config(['app.debug' => false])、Exceptions::fake(),Booking::created(INSERT 已執行之後)拋出 SQL 含insert into "bookings"與欄位名稱的QueryException;合法POST /api/member/bookings回 500,assertExactJson(['message' => 'Server Error']),body 不含insert into、bookings、member_id,Exceptions::assertReported(QueryException::class),bookings筆數為 0(INSERT 已發生,筆數為 0 才能證明 rollback)(對應 Scenario「建立預約時資料庫錯誤回 500 且不洩漏細節」) - 2.5 [整合測試]
test_database_error_on_confirm_returns_500_without_sql:同 2.4 的設定,CourseSchedule::updating(在交易內預約狀態已更新、increment執行時觸發)拋出 SQL 含update "course_schedules"的QueryException;provider 確認名額足夠的pending預約,回 500,assertExactJson(['message' => 'Server Error']),body 不含update、course_schedules、current_participants,Exceptions::assertReported(QueryException::class),預約仍為pending(狀態更新已執行,仍為pending才能證明 rollback)、current_participants不變(對應 Scenario「確認預約時資料庫錯誤回 500 且不洩漏細節」) - 2.6 [整合測試] 執行
php artisan test --filter='BookingLifecycleTest|BookingOversellTest',確認既有 422 行為不變;完整php artisan test需在有 GD 的環境執行(見cfdive-test-gdimage 的做法)
3. 手動驗證
- 3.1 [手動驗證] 變異驗證(只在本機執行,不提交):暫時把 1.2、1.3 的 catch 改回
\RuntimeException,確認 2.4、2.5 失敗後再還原,證明測試抓得到原本的問題 - 3.2 [手動驗證] 部署後在正式環境建立一筆預約、確認一筆預約,確認業務流程正常(201/200);Sentry 是否收到預約端點的 500 只能等實際錯誤發生,若期間沒有錯誤則記錄為「未觀察到」,不視為失敗(自動化驗證以 2.4、2.5 為準)