BookingController's eager load now constrains the refunds relation to status completed only (a new eagerLoads() layering that onto the existing EAGER_LOADS list, applied uniformly across index/show/store/cancel). A pending or failed refund attempt isn't customer-facing — staff still track those via Filament's Refunds resource, which loads the relation unconstrained.
This commit is contained in:
@@ -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<string>
|
||||
*/
|
||||
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<int|string, string|\Closure>
|
||||
*/
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user