bookingService->validateSelections($data->selections); $isRoundTrip = $data->returnEvRouteId !== null; if ($isRoundTrip) { $this->bookingService->validateSelections($data->returnSelections); } return DB::transaction(function () use ($data, $isRoundTrip) { $outboundRoute = EvRoute::findOrFail($data->evRouteId); $outboundBooking = $this->createLeg( data: $data, route: $outboundRoute, selections: $data->selections, travelDate: $data->travelDate, timeSlotId: $data->departureTimeSlotId, isReturnLeg: false, ); if (! $isRoundTrip) { BookingCreated::dispatch($outboundBooking); return $outboundBooking; } $returnRoute = EvRoute::findOrFail($data->returnEvRouteId); if (! $returnRoute->isReverseOf($outboundRoute)) { throw InvalidReturnRouteException::notReverseOfOutbound($returnRoute, $outboundRoute); } $returnBooking = $this->createLeg( data: $data, route: $returnRoute, selections: $data->returnSelections, travelDate: $data->returnTravelDate, timeSlotId: $data->returnDepartureTimeSlotId, isReturnLeg: true, ); // Linked bidirectionally after both rows exist — a single // `linked_booking_id` FK can't be set on either row at create // time since the other side doesn't have an id yet. $returnBooking->update(['linked_booking_id' => $outboundBooking->id]); $outboundBooking->update(['linked_booking_id' => $returnBooking->id]); // No registered listeners on BookingCreated today, so firing it // twice per round-trip creation has no side effects — flagged // here for whoever adds the first listener. BookingCreated::dispatch($outboundBooking); BookingCreated::dispatch($returnBooking); return $outboundBooking->refresh(); }); } /** * @param list $selections */ private function createLeg( CreateBookingData $data, EvRoute $route, array $selections, string $travelDate, int $timeSlotId, bool $isReturnLeg, ): Booking { $lines = array_map( fn (VehicleSelectionData $selection) => $this->priceSelection($route, $selection), $selections, ); $totalPrice = array_reduce( $lines, fn (string $carry, array $line) => bcadd($carry, $line['line_total'], 2), '0.00', ); $booking = Booking::create([ 'booking_ref' => $this->bookingRefGenerator->generate(), 'user_id' => $data->userId, 'openid' => $data->openid, 'ev_route_id' => $route->id, 'is_return_leg' => $isReturnLeg, 'departure_time_slot_id' => $timeSlotId, 'travel_date' => $travelDate, 'passenger_name' => $data->passengerName, 'passenger_phone' => $data->passengerPhone, 'notes' => $data->notes, 'pickup_address' => $data->pickupAddress, 'pickup_lat' => $data->pickupLat, 'pickup_lng' => $data->pickupLng, 'dropoff_address' => $data->dropoffAddress, 'dropoff_lat' => $data->dropoffLat, 'dropoff_lng' => $data->dropoffLng, 'price' => $totalPrice, 'status' => BookingStatus::PendingPayment, 'created_by_channel' => $data->createdByChannel, ]); $booking->vehicleOptions()->createMany($lines); return $booking; } /** * @return array{vehicle_option: VehicleOption, passenger_count: int, unit_price: string, line_total: string} */ private function priceSelection(EvRoute $route, VehicleSelectionData $selection): array { $quote = $this->pricingService->quote($route, $selection->vehicleOption); return [ 'vehicle_option' => $selection->vehicleOption, 'passenger_count' => $selection->passengerCount, 'unit_price' => $quote->price, 'line_total' => bcmul($quote->price, (string) $selection->passengerCount, 2), ]; } }