Add Booking module: model, create/read/cancel API, Filament resource (T4.1-T4.7)
- Booking model with booking_vehicle_options line items (supports mixing vehicle options like front_seat + back_seat in one booking), price snapshot, status machine, and driver/car assignment fields - BookingService: front-seat max, disabled-option toggles, duplicate-option and whole-vehicle-exclusivity guards - CreateBookingAction, CancelBookingAction, AssignDriverAction - BookingRefGenerator: sequential EVB-AAAAA1-style refs via row lock - POST/GET/cancel booking API endpoints (Sanctum, ownership + admin policy) - BookingPlugin + Filament BookingResource: list, detail view, Cancel and Assign Driver actions (shared between table and detail page) - domain.md updated for multi-vehicle-option bookings (§2) and driver/ vehicle assignment (§5a)
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
<?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\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);
|
||||
|
||||
return DB::transaction(function () use ($data) {
|
||||
$route = EvRoute::findOrFail($data->evRouteId);
|
||||
|
||||
$lines = array_map(
|
||||
fn (VehicleSelectionData $selection) => $this->priceSelection($route, $selection),
|
||||
$data->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' => $data->evRouteId,
|
||||
'departure_time_slot_id' => $data->departureTimeSlotId,
|
||||
'travel_date' => $data->travelDate,
|
||||
'passenger_name' => $data->passengerName,
|
||||
'passenger_phone' => $data->passengerPhone,
|
||||
'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,
|
||||
'is_round_trip' => $data->isRoundTrip,
|
||||
'return_travel_date' => $data->returnTravelDate,
|
||||
'created_by_channel' => $data->createdByChannel,
|
||||
]);
|
||||
|
||||
$booking->vehicleOptions()->createMany($lines);
|
||||
|
||||
BookingCreated::dispatch($booking);
|
||||
|
||||
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),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user