*/ private const EAGER_LOADS = ['route', 'timeSlot', 'vehicleOptions']; public function __construct( private CreateBookingAction $createBookingAction, private CancelBookingAction $cancelBookingAction, ) {} public function index(Request $request): AnonymousResourceCollection { $openid = $request->attributes->get('fastapi_openid'); $query = Booking::query(); if ($openid !== null) { // FastAPI agent (JWT auth, no Laravel user) — scoped to the // verified token's own openid, never a client-supplied value, // so one agent session can't list another customer's bookings. $query->where('openid', $openid); } else { Gate::authorize('viewAny', Booking::class); $query->where('user_id', $request->user()->id); } $bookings = $query ->with(self::EAGER_LOADS) ->latest() ->paginate(); return BookingResource::collection($bookings); } public function show(Request $request, Booking $booking): BookingResource { $openid = $request->attributes->get('fastapi_openid'); if ($openid !== null) { abort_if($booking->openid !== $openid, 404); } else { Gate::authorize('view', $booking); } return new BookingResource($booking->load(self::EAGER_LOADS)); } public function store(StoreBookingRequest $request): JsonResponse { $validated = $request->validated(); $openid = $request->attributes->get('fastapi_openid'); if ($openid === null) { Gate::authorize('create', Booking::class); } $selections = array_map( fn (array $selection) => new VehicleSelectionData( vehicleOption: VehicleOption::from($selection['vehicle_option']), passengerCount: $selection['passenger_count'], ), $validated['selections'], ); // The agent's own auth path always wins over anything a header could // claim; customer channels come from Device-Type, not a // client-supplied body field (BookingChannel::fromDeviceTypeHeader // already refuses to hand back Agent/Admin from a header value). $channel = $openid !== null ? BookingChannel::Agent : BookingChannel::fromDeviceTypeHeader($request->header('Device-Type')); $booking = $this->createBookingAction->handle(new CreateBookingData( evRouteId: $validated['ev_route_id'], departureTimeSlotId: $validated['departure_time_slot_id'], travelDate: $validated['travel_date'], selections: $selections, passengerName: $validated['passenger_name'], passengerPhone: $validated['passenger_phone'], pickupAddress: $validated['pickup_address'], dropoffAddress: $validated['dropoff_address'], createdByChannel: $channel, // A verified FastAPI JWT's own openid always wins over a // client-supplied one — a request can never claim a different // customer's identity than its own token proves. userId: $request->user()?->id, openid: $openid ?? $validated['openid'] ?? null, pickupLat: $validated['pickup_lat'] ?? null, pickupLng: $validated['pickup_lng'] ?? null, dropoffLat: $validated['dropoff_lat'] ?? null, dropoffLng: $validated['dropoff_lng'] ?? null, isRoundTrip: $validated['is_round_trip'] ?? false, returnTravelDate: $validated['return_travel_date'] ?? null, )); return (new BookingResource($booking->load(self::EAGER_LOADS))) ->response() ->setStatusCode(201); } public function cancel(Request $request, Booking $booking): BookingResource { Gate::authorize('cancel', $booking); $this->cancelBookingAction->handle($booking, $request->user()?->id); return new BookingResource($booking->load(self::EAGER_LOADS)); } }