fix(booking): 交易內只攔截業務例外,資料庫錯誤改交由例外處理器

新增 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>
This commit is contained in:
2026-10-18 01:35:38 +08:00
parent dcbf3e4a9a
commit ddbf956144
5 changed files with 224 additions and 16 deletions
@@ -0,0 +1,5 @@
<?php
namespace App\Exceptions;
final class BookingRuleViolationException extends \RuntimeException {}
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\API;
use App\Enums\BookingStatus;
use App\Enums\ScheduleStatus;
use App\Exceptions\BookingRuleViolationException;
use App\Http\Controllers\Controller;
use App\Models\Booking;
use App\Models\CourseSchedule;
@@ -68,7 +69,7 @@ class MemberBookingController extends Controller
// Layer 2:lockForUpdate 後二次驗證
$schedule = CourseSchedule::lockForUpdate()->find($schedule->id);
if ($data['participants'] > $schedule->remainingSpots()) {
throw new \RuntimeException('名額不足,請重新選擇');
throw new BookingRuleViolationException('名額不足,請重新選擇');
}
// 重複預約檢查
@@ -77,7 +78,7 @@ class MemberBookingController extends Controller
->whereIn('status', [BookingStatus::Pending->value, BookingStatus::Confirmed->value])
->exists();
if ($duplicate) {
throw new \RuntimeException('您已預約此時段');
throw new BookingRuleViolationException('您已預約此時段');
}
return Booking::create([
@@ -89,7 +90,7 @@ class MemberBookingController extends Controller
'notes' => $data['notes'] ?? null,
]);
});
} catch (\RuntimeException $e) {
} catch (BookingRuleViolationException $e) {
return response()->json(['status' => false, 'message' => $e->getMessage()], 422);
}
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\API;
use App\Enums\BookingStatus;
use App\Enums\ScheduleStatus;
use App\Exceptions\BookingRuleViolationException;
use App\Http\Controllers\Controller;
use App\Models\Booking;
use App\Notifications\BookingCancelledNotification;
@@ -43,7 +44,7 @@ class ProviderBookingController extends Controller
$remaining = $schedule->max_participants - $schedule->current_participants;
if ($booking->participants > $remaining) {
throw new \RuntimeException('名額不足,無法確認此預約');
throw new BookingRuleViolationException('名額不足,無法確認此預約');
}
$booking->update(['status' => BookingStatus::Confirmed]);
@@ -54,7 +55,7 @@ class ProviderBookingController extends Controller
$schedule->update(['status' => ScheduleStatus::Full]);
}
});
} catch (\RuntimeException $e) {
} catch (BookingRuleViolationException $e) {
return response()->json(['status' => false, 'message' => $e->getMessage()], 422);
}