diff --git a/app-modules/booking/src/Http/Controllers/BookingController.php b/app-modules/booking/src/Http/Controllers/BookingController.php index 50488b4..ceab72e 100644 --- a/app-modules/booking/src/Http/Controllers/BookingController.php +++ b/app-modules/booking/src/Http/Controllers/BookingController.php @@ -16,6 +16,7 @@ use Modules\Booking\Http\Requests\StoreBookingRequest; use Modules\Booking\Http\Resources\BookingResource; use Modules\Booking\Models\Booking; use Modules\Payment\Enums\PaymentStatus; +use Modules\Payment\Enums\RefundStatus; use Modules\Shared\Enums\VehicleOption; class BookingController extends Controller @@ -24,7 +25,7 @@ class BookingController extends Controller * @var list */ private const EAGER_LOADS = [ - 'route', 'timeSlot', 'vehicleOptions', 'refunds', + 'route', 'timeSlot', 'vehicleOptions', 'linkedBooking.route.company', 'linkedBooking.route.fromDestination', 'linkedBooking.route.toDestination', 'linkedBooking.timeSlot', 'linkedBooking.vehicleOptions', ]; @@ -34,6 +35,21 @@ class BookingController extends Controller private CancelBookingAction $cancelBookingAction, ) {} + /** + * Same relations as EAGER_LOADS, plus refunds constrained to completed + * only — a pending/failed attempt isn't something a customer/agent needs + * to see here; Filament's Refunds resource is where staff track those. + * + * @return array + */ + private function eagerLoads(): array + { + return [ + ...self::EAGER_LOADS, + 'refunds' => fn ($query) => $query->where('status', RefundStatus::Completed), + ]; + } + public function index(Request $request): AnonymousResourceCollection { $openid = $request->attributes->get('fastapi_openid'); @@ -62,7 +78,7 @@ class BookingController extends Controller // return leg's full detail (including vehicle_options). ->where('is_return_leg', false) ->when($request->filled('booking_ref'), fn ($q) => $q->where('booking_ref', 'ilike', '%'.$request->string('booking_ref').'%')) - ->with(self::EAGER_LOADS) + ->with($this->eagerLoads()) ->latest() ->paginate(); @@ -79,7 +95,7 @@ class BookingController extends Controller Gate::authorize('view', $booking); } - return new BookingResource($booking->load(self::EAGER_LOADS)); + return new BookingResource($booking->load($this->eagerLoads())); } public function store(StoreBookingRequest $request): JsonResponse @@ -145,7 +161,7 @@ class BookingController extends Controller returnSelections: $returnSelections, )); - return (new BookingResource($booking->load(self::EAGER_LOADS))) + return (new BookingResource($booking->load($this->eagerLoads()))) ->response() ->setStatusCode(201); } @@ -156,6 +172,6 @@ class BookingController extends Controller $this->cancelBookingAction->handle($booking, $request->user()?->id); - return new BookingResource($booking->load(self::EAGER_LOADS)); + return new BookingResource($booking->load($this->eagerLoads())); } } diff --git a/app-modules/booking/src/Http/Resources/BookingResource.php b/app-modules/booking/src/Http/Resources/BookingResource.php index 990e23f..5131407 100644 --- a/app-modules/booking/src/Http/Resources/BookingResource.php +++ b/app-modules/booking/src/Http/Resources/BookingResource.php @@ -63,11 +63,15 @@ class BookingResource extends JsonResource 'label' => $this->timeSlot->label, 'time' => $this->timeSlot->time?->format('H:i'), ]), - // This leg's own refund attempts (cancellation, or any partial + // This leg's own settled refunds (cancellation, or any partial // refund) — not payment-level detail, just what a customer/agent - // needs to see: status, amount, and when it was requested/settled. - // Cancelling a pending_payment booking never creates one (no - // money moved yet — CancelBookingAction), so this stays empty. + // needs to see: amount, reason, and when it was requested/completed. + // BookingController's eager load constrains this to status + // completed only — a pending/failed attempt isn't customer-facing + // (Filament's Refunds resource is where staff track those), so + // `status` here is always "completed" in practice. Cancelling a + // pending_payment booking never creates a refund at all (no money + // moved yet — CancelBookingAction), so this stays empty too. 'refunds' => $this->whenLoaded('refunds', fn () => $this->refunds->map(fn ($refund) => [ 'id' => $refund->id, 'status' => $refund->status, diff --git a/app-modules/booking/tests/Feature/BookingCancelApiTest.php b/app-modules/booking/tests/Feature/BookingCancelApiTest.php index e916bab..616d5ca 100644 --- a/app-modules/booking/tests/Feature/BookingCancelApiTest.php +++ b/app-modules/booking/tests/Feature/BookingCancelApiTest.php @@ -115,6 +115,22 @@ test('cancelling a confirmed booking returns the refund it created', function () expect($response->json('data.refunds.0.completed_at'))->not->toBeNull(); }); +test('a pending or failed refund is not returned — only completed refunds are exposed', function () { + $booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::Cancelled, 'price' => 15000]); + $payment = Payment::factory()->completed()->create([ + 'booking_id' => $booking->id, + 'gateway' => PaymentMethod::KbzMiniApp, + 'amount' => 15000, + ]); + $booking->refunds()->create(['payment_id' => $payment->id, 'status' => RefundStatus::Pending, 'amount' => 15000, 'reason' => 'Booking cancellation']); + $booking->refunds()->create(['payment_id' => $payment->id, 'status' => RefundStatus::Failed, 'amount' => 15000, 'reason' => 'Booking cancellation']); + + $this->withHeader('Authorization', "Bearer {$this->token}") + ->getJson("/api/v1/bookings/{$booking->booking_ref}") + ->assertSuccessful() + ->assertJsonPath('data.refunds', []); +}); + test('cancelling a pending_payment booking returns an empty refunds list — no money moved yet', function () { $booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]);