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,124 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\QueryException;
|
||||
use Modules\Booking\Enums\BookingChannel;
|
||||
use Modules\Booking\Enums\BookingStatus;
|
||||
use Modules\Booking\Models\Booking;
|
||||
use Modules\Booking\Models\BookingVehicleOption;
|
||||
use Modules\Catalog\Models\DepartureTimeSlot;
|
||||
use Modules\Routing\Models\EvRoute;
|
||||
use Modules\Routing\Models\RoutePricing;
|
||||
use Modules\Shared\Enums\VehicleOption;
|
||||
|
||||
test('a booking belongs to a route and a time slot', function () {
|
||||
$route = EvRoute::factory()->create();
|
||||
$timeSlot = DepartureTimeSlot::factory()->create();
|
||||
|
||||
$booking = Booking::factory()->create([
|
||||
'ev_route_id' => $route->id,
|
||||
'departure_time_slot_id' => $timeSlot->id,
|
||||
]);
|
||||
|
||||
expect($booking->route)->toBeInstanceOf(EvRoute::class)
|
||||
->and($booking->route->is($route))->toBeTrue()
|
||||
->and($booking->timeSlot)->toBeInstanceOf(DepartureTimeSlot::class)
|
||||
->and($booking->timeSlot->is($timeSlot))->toBeTrue();
|
||||
});
|
||||
|
||||
test('booking_ref is unique', function () {
|
||||
Booking::factory()->create(['booking_ref' => 'EVB-DUPLICATE']);
|
||||
|
||||
expect(fn () => Booking::factory()->create(['booking_ref' => 'EVB-DUPLICATE']))
|
||||
->toThrow(QueryException::class);
|
||||
});
|
||||
|
||||
test('status and created_by_channel cast to their enums', function () {
|
||||
$booking = Booking::factory()->create([
|
||||
'status' => BookingStatus::Confirmed,
|
||||
'created_by_channel' => BookingChannel::Android,
|
||||
]);
|
||||
|
||||
expect($booking->status)->toBe(BookingStatus::Confirmed)
|
||||
->and($booking->created_by_channel)->toBe(BookingChannel::Android);
|
||||
});
|
||||
|
||||
test('a booking defaults to pending_payment', function () {
|
||||
$booking = Booking::factory()->create();
|
||||
|
||||
expect($booking->status)->toBe(BookingStatus::PendingPayment);
|
||||
});
|
||||
|
||||
test('a booking can have multiple vehicle option lines, e.g. front seat and back seat together', function () {
|
||||
$booking = Booking::factory()->create();
|
||||
|
||||
BookingVehicleOption::factory()->create([
|
||||
'booking_id' => $booking->id,
|
||||
'vehicle_option' => VehicleOption::FrontSeat,
|
||||
'passenger_count' => 1,
|
||||
'unit_price' => 12000,
|
||||
'line_total' => 12000,
|
||||
]);
|
||||
BookingVehicleOption::factory()->create([
|
||||
'booking_id' => $booking->id,
|
||||
'vehicle_option' => VehicleOption::BackSeat,
|
||||
'passenger_count' => 2,
|
||||
'unit_price' => 9000,
|
||||
'line_total' => 18000,
|
||||
]);
|
||||
|
||||
expect($booking->vehicleOptions)->toHaveCount(2)
|
||||
->and($booking->vehicleOptions->pluck('vehicle_option')->map(fn ($option) => $option->value)->sort()->values()->all())
|
||||
->toEqual(['back_seat', 'front_seat']);
|
||||
});
|
||||
|
||||
test('a vehicle option line cannot be duplicated on the same booking', function () {
|
||||
$booking = Booking::factory()->create();
|
||||
|
||||
BookingVehicleOption::factory()->create([
|
||||
'booking_id' => $booking->id,
|
||||
'vehicle_option' => VehicleOption::BackSeat,
|
||||
]);
|
||||
|
||||
expect(fn () => BookingVehicleOption::factory()->create([
|
||||
'booking_id' => $booking->id,
|
||||
'vehicle_option' => VehicleOption::BackSeat,
|
||||
]))->toThrow(QueryException::class);
|
||||
});
|
||||
|
||||
test('price is snapshotted onto the booking and does not change when RoutePricing is edited later', function () {
|
||||
$route = EvRoute::factory()->create();
|
||||
|
||||
$pricing = RoutePricing::factory()->create([
|
||||
'ev_route_id' => $route->id,
|
||||
'vehicle_option' => VehicleOption::BackSeat,
|
||||
'price' => 15000,
|
||||
]);
|
||||
|
||||
$booking = Booking::factory()->create([
|
||||
'ev_route_id' => $route->id,
|
||||
'price' => $pricing->price,
|
||||
]);
|
||||
|
||||
BookingVehicleOption::factory()->create([
|
||||
'booking_id' => $booking->id,
|
||||
'vehicle_option' => VehicleOption::BackSeat,
|
||||
'unit_price' => $pricing->price,
|
||||
'line_total' => $pricing->price,
|
||||
]);
|
||||
|
||||
$pricing->update(['price' => 25000]);
|
||||
|
||||
expect($booking->refresh()->price)->toEqual('15000.00')
|
||||
->and($booking->vehicleOptions()->first()->unit_price)->toEqual('15000.00')
|
||||
->and($pricing->refresh()->price)->toEqual('25000.00');
|
||||
});
|
||||
|
||||
test('a booking can have a user or be guest-checked-out via mini app openid', function () {
|
||||
$guestBooking = Booking::factory()->create([
|
||||
'user_id' => null,
|
||||
'openid' => 'mini-app-openid-123',
|
||||
]);
|
||||
|
||||
expect($guestBooking->user_id)->toBeNull()
|
||||
->and($guestBooking->openid)->toBe('mini-app-openid-123');
|
||||
});
|
||||
Reference in New Issue
Block a user