Files
famous-ly4-ev/app-modules/booking/src/Actions/CreateBookingAction.php
T
Nyan Lin Paing fa908cdcaf
PHP Tests / php-tests (push) Has been cancelled
add notes/remark and refactor round-trip
2026-08-22 21:43:41 +07:00

149 lines
5.2 KiB
PHP

<?php
namespace Modules\Booking\Actions;
use Illuminate\Support\Facades\DB;
use Modules\Booking\Data\CreateBookingData;
use Modules\Booking\Data\VehicleSelectionData;
use Modules\Booking\Enums\BookingStatus;
use Modules\Booking\Events\BookingCreated;
use Modules\Booking\Exceptions\InvalidReturnRouteException;
use Modules\Booking\Models\Booking;
use Modules\Booking\Services\BookingRefGenerator;
use Modules\Booking\Services\BookingService;
use Modules\Routing\Models\EvRoute;
use Modules\Routing\Services\PricingService;
use Modules\Shared\Enums\VehicleOption;
class CreateBookingAction
{
public function __construct(
private BookingService $bookingService,
private PricingService $pricingService,
private BookingRefGenerator $bookingRefGenerator,
) {}
public function handle(CreateBookingData $data): Booking
{
$this->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<VehicleSelectionData> $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),
];
}
}