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\Http\Resources\BookingResource;
|
||||||
use Modules\Booking\Models\Booking;
|
use Modules\Booking\Models\Booking;
|
||||||
use Modules\Payment\Enums\PaymentStatus;
|
use Modules\Payment\Enums\PaymentStatus;
|
||||||
|
use Modules\Payment\Enums\RefundStatus;
|
||||||
use Modules\Shared\Enums\VehicleOption;
|
use Modules\Shared\Enums\VehicleOption;
|
||||||
|
|
||||||
class BookingController extends Controller
|
class BookingController extends Controller
|
||||||
@@ -24,7 +25,7 @@ class BookingController extends Controller
|
|||||||
* @var list<string>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
private const EAGER_LOADS = [
|
private const EAGER_LOADS = [
|
||||||
'route', 'timeSlot', 'vehicleOptions', 'refunds',
|
'route', 'timeSlot', 'vehicleOptions',
|
||||||
'linkedBooking.route.company', 'linkedBooking.route.fromDestination', 'linkedBooking.route.toDestination',
|
'linkedBooking.route.company', 'linkedBooking.route.fromDestination', 'linkedBooking.route.toDestination',
|
||||||
'linkedBooking.timeSlot', 'linkedBooking.vehicleOptions',
|
'linkedBooking.timeSlot', 'linkedBooking.vehicleOptions',
|
||||||
];
|
];
|
||||||
@@ -34,6 +35,21 @@ class BookingController extends Controller
|
|||||||
private CancelBookingAction $cancelBookingAction,
|
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
|
public function index(Request $request): AnonymousResourceCollection
|
||||||
{
|
{
|
||||||
$openid = $request->attributes->get('fastapi_openid');
|
$openid = $request->attributes->get('fastapi_openid');
|
||||||
@@ -62,7 +78,7 @@ class BookingController extends Controller
|
|||||||
// return leg's full detail (including vehicle_options).
|
// return leg's full detail (including vehicle_options).
|
||||||
->where('is_return_leg', false)
|
->where('is_return_leg', false)
|
||||||
->when($request->filled('booking_ref'), fn ($q) => $q->where('booking_ref', 'ilike', '%'.$request->string('booking_ref').'%'))
|
->when($request->filled('booking_ref'), fn ($q) => $q->where('booking_ref', 'ilike', '%'.$request->string('booking_ref').'%'))
|
||||||
->with(self::EAGER_LOADS)
|
->with($this->eagerLoads())
|
||||||
->latest()
|
->latest()
|
||||||
->paginate();
|
->paginate();
|
||||||
|
|
||||||
@@ -79,7 +95,7 @@ class BookingController extends Controller
|
|||||||
Gate::authorize('view', $booking);
|
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
|
public function store(StoreBookingRequest $request): JsonResponse
|
||||||
@@ -145,7 +161,7 @@ class BookingController extends Controller
|
|||||||
returnSelections: $returnSelections,
|
returnSelections: $returnSelections,
|
||||||
));
|
));
|
||||||
|
|
||||||
return (new BookingResource($booking->load(self::EAGER_LOADS)))
|
return (new BookingResource($booking->load($this->eagerLoads())))
|
||||||
->response()
|
->response()
|
||||||
->setStatusCode(201);
|
->setStatusCode(201);
|
||||||
}
|
}
|
||||||
@@ -156,6 +172,6 @@ class BookingController extends Controller
|
|||||||
|
|
||||||
$this->cancelBookingAction->handle($booking, $request->user()?->id);
|
$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,
|
'label' => $this->timeSlot->label,
|
||||||
'time' => $this->timeSlot->time?->format('H:i'),
|
'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
|
// refund) — not payment-level detail, just what a customer/agent
|
||||||
// needs to see: status, amount, and when it was requested/settled.
|
// needs to see: amount, reason, and when it was requested/completed.
|
||||||
// Cancelling a pending_payment booking never creates one (no
|
// BookingController's eager load constrains this to status
|
||||||
// money moved yet — CancelBookingAction), so this stays empty.
|
// 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) => [
|
'refunds' => $this->whenLoaded('refunds', fn () => $this->refunds->map(fn ($refund) => [
|
||||||
'id' => $refund->id,
|
'id' => $refund->id,
|
||||||
'status' => $refund->status,
|
'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();
|
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 () {
|
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]);
|
$booking = Booking::factory()->create(['user_id' => $this->owner->id, 'status' => BookingStatus::PendingPayment]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user