Generalize per-vehicle-option booking rules to Front Seat too
Front Seat previously had a hardcoded max-per-booking check with no enable toggle; now every Vehicle Option (front_seat/back_seat/whole_vehicle) gets the same config-driven pair — an on/off toggle and a max passenger_count — surfaced via new BOOKING_*_ENABLED/BOOKING_*_MAX_PER_BOOKING env vars, editable from ManageAppSettings, and exposed on route pricing as max_per_booking.
This commit is contained in:
+5
-2
@@ -53,9 +53,12 @@ REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
BOOKING_BACK_SEAT_ENABLED=true
|
||||
BOOKING_WHOLE_VEHICLE_ENABLED=true
|
||||
BOOKING_FRONT_SEAT_ENABLED=true
|
||||
BOOKING_FRONT_SEAT_MAX_PER_BOOKING=1
|
||||
BOOKING_BACK_SEAT_ENABLED=true
|
||||
BOOKING_BACK_SEAT_MAX_PER_BOOKING=3
|
||||
BOOKING_WHOLE_VEHICLE_ENABLED=true
|
||||
BOOKING_WHOLE_VEHICLE_MAX_PER_BOOKING=4
|
||||
|
||||
BOOKING_ADMIN_EMAILS="example@gmail.com"
|
||||
|
||||
|
||||
@@ -9,9 +9,9 @@ use RuntimeException;
|
||||
|
||||
class InvalidVehicleSelectionException extends RuntimeException
|
||||
{
|
||||
public static function frontSeatLimitExceeded(int $requested, int $max): self
|
||||
public static function passengerLimitExceeded(VehicleOption $vehicleOption, int $requested, int $max): self
|
||||
{
|
||||
return new self("Front seat request [{$requested}] exceeds the max of [{$max}] per booking.");
|
||||
return new self("Vehicle option [{$vehicleOption->value}] passenger count [{$requested}] exceeds the max of [{$max}] per booking.");
|
||||
}
|
||||
|
||||
public static function optionDisabled(VehicleOption $vehicleOption): self
|
||||
|
||||
@@ -9,11 +9,11 @@ use Modules\Shared\Enums\VehicleOption;
|
||||
class BookingService
|
||||
{
|
||||
/**
|
||||
* Enforces the only v1 inventory rule (max Front Seats per booking), the
|
||||
* blunt config toggles for Back Seat / Whole Vehicle availability, and
|
||||
* shape rules around combining options in one booking (no duplicate
|
||||
* option lines, Whole Vehicle can't be mixed with anything else since it
|
||||
* already covers the whole car).
|
||||
* Enforces the same two blunt, config-driven rules for every Vehicle
|
||||
* Option — an on/off toggle and a max passenger_count per booking (see
|
||||
* domain.md §2) — plus shape rules around combining options in one
|
||||
* booking (no duplicate option lines, Whole Vehicle can't be mixed with
|
||||
* anything else since it already covers the whole car).
|
||||
*
|
||||
* Deliberately does not check real capacity/availability — that's an
|
||||
* explicitly deferred future phase (domain.md §2, §7).
|
||||
@@ -43,26 +43,23 @@ class BookingService
|
||||
|
||||
private function validateOption(VehicleOption $vehicleOption, int $passengerCount): void
|
||||
{
|
||||
match ($vehicleOption) {
|
||||
VehicleOption::FrontSeat => $this->validateFrontSeat($passengerCount),
|
||||
VehicleOption::BackSeat => $this->validateEnabled($vehicleOption, 'booking.back_seat_enabled'),
|
||||
VehicleOption::WholeVehicle => $this->validateEnabled($vehicleOption, 'booking.whole_vehicle_enabled'),
|
||||
};
|
||||
$this->validateEnabled($vehicleOption);
|
||||
$this->validateMax($vehicleOption, $passengerCount);
|
||||
}
|
||||
|
||||
private function validateFrontSeat(int $passengerCount): void
|
||||
private function validateEnabled(VehicleOption $vehicleOption): void
|
||||
{
|
||||
$max = config('booking.front_seat_max_per_booking');
|
||||
|
||||
if ($passengerCount > $max) {
|
||||
throw InvalidVehicleSelectionException::frontSeatLimitExceeded($passengerCount, $max);
|
||||
}
|
||||
}
|
||||
|
||||
private function validateEnabled(VehicleOption $vehicleOption, string $configKey): void
|
||||
{
|
||||
if (! config($configKey)) {
|
||||
if (! config("booking.{$vehicleOption->value}_enabled")) {
|
||||
throw InvalidVehicleSelectionException::optionDisabled($vehicleOption);
|
||||
}
|
||||
}
|
||||
|
||||
private function validateMax(VehicleOption $vehicleOption, int $passengerCount): void
|
||||
{
|
||||
$max = config("booking.{$vehicleOption->value}_max_per_booking");
|
||||
|
||||
if ($passengerCount > $max) {
|
||||
throw InvalidVehicleSelectionException::passengerLimitExceeded($vehicleOption, $passengerCount, $max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,37 @@ test('front-seat-limit rejection surfaces as 422', function () {
|
||||
['vehicle_option' => 'front_seat', 'passenger_count' => 2],
|
||||
]))
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('message', 'Front seat request [2] exceeds the max of [1] per booking.');
|
||||
->assertJsonPath('message', 'Vehicle option [front_seat] passenger count [2] exceeds the max of [1] per booking.');
|
||||
|
||||
expect(Booking::count())->toBe(0);
|
||||
});
|
||||
|
||||
test('back-seat-limit rejection surfaces as 422', function () {
|
||||
config(['booking.back_seat_max_per_booking' => 1]);
|
||||
|
||||
[$route, $timeSlot] = bookableRouteAndSlot([[VehicleOption::BackSeat, '9000.00']]);
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$this->token}")
|
||||
->postJson('/api/v1/bookings', bookingPayload($route, $timeSlot, [
|
||||
['vehicle_option' => 'back_seat', 'passenger_count' => 2],
|
||||
]))
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('message', 'Vehicle option [back_seat] passenger count [2] exceeds the max of [1] per booking.');
|
||||
|
||||
expect(Booking::count())->toBe(0);
|
||||
});
|
||||
|
||||
test('whole-vehicle-limit rejection surfaces as 422', function () {
|
||||
config(['booking.whole_vehicle_max_per_booking' => 1]);
|
||||
|
||||
[$route, $timeSlot] = bookableRouteAndSlot([[VehicleOption::WholeVehicle, '30000.00']]);
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$this->token}")
|
||||
->postJson('/api/v1/bookings', bookingPayload($route, $timeSlot, [
|
||||
['vehicle_option' => 'whole_vehicle', 'passenger_count' => 2],
|
||||
]))
|
||||
->assertStatus(422)
|
||||
->assertJsonPath('message', 'Vehicle option [whole_vehicle] passenger count [2] exceeds the max of [1] per booking.');
|
||||
|
||||
expect(Booking::count())->toBe(0);
|
||||
});
|
||||
|
||||
@@ -30,40 +30,52 @@ test('front seat and back seat can be selected together in one booking', functio
|
||||
]))->not->toThrow(InvalidVehicleSelectionException::class);
|
||||
});
|
||||
|
||||
test('requesting more front seats than the configured max is rejected', function () {
|
||||
test('requesting more passengers than the configured max is rejected', function (VehicleOption $option) {
|
||||
config(["booking.{$option->value}_max_per_booking" => 1]);
|
||||
|
||||
$service = new BookingService;
|
||||
|
||||
expect(fn () => $service->validateSelections([new VehicleSelectionData($option, 2)]))
|
||||
->toThrow(InvalidVehicleSelectionException::class);
|
||||
})->with([
|
||||
'front_seat' => [VehicleOption::FrontSeat],
|
||||
'back_seat' => [VehicleOption::BackSeat],
|
||||
'whole_vehicle' => [VehicleOption::WholeVehicle],
|
||||
]);
|
||||
|
||||
test('requesting passengers up to the configured max passes', function (VehicleOption $option) {
|
||||
config(["booking.{$option->value}_max_per_booking" => 2]);
|
||||
|
||||
$service = new BookingService;
|
||||
|
||||
expect(fn () => $service->validateSelections([new VehicleSelectionData($option, 2)]))
|
||||
->not->toThrow(InvalidVehicleSelectionException::class);
|
||||
})->with([
|
||||
'front_seat' => [VehicleOption::FrontSeat],
|
||||
'back_seat' => [VehicleOption::BackSeat],
|
||||
'whole_vehicle' => [VehicleOption::WholeVehicle],
|
||||
]);
|
||||
|
||||
test('an option is rejected when disabled via config', function (VehicleOption $option) {
|
||||
config(["booking.{$option->value}_enabled" => false]);
|
||||
|
||||
$service = new BookingService;
|
||||
|
||||
expect(fn () => $service->validateSelections([new VehicleSelectionData($option)]))
|
||||
->toThrow(InvalidVehicleSelectionException::class, "Vehicle option [{$option->value}] is not currently available for booking.");
|
||||
})->with([
|
||||
'front_seat' => [VehicleOption::FrontSeat],
|
||||
'back_seat' => [VehicleOption::BackSeat],
|
||||
'whole_vehicle' => [VehicleOption::WholeVehicle],
|
||||
]);
|
||||
|
||||
test('exceeding the max produces the expected message', function () {
|
||||
config(['booking.front_seat_max_per_booking' => 1]);
|
||||
|
||||
$service = new BookingService;
|
||||
|
||||
expect(fn () => $service->validateSelections([new VehicleSelectionData(VehicleOption::FrontSeat, 2)]))
|
||||
->toThrow(InvalidVehicleSelectionException::class);
|
||||
});
|
||||
|
||||
test('requesting front seats up to the configured max passes', function () {
|
||||
config(['booking.front_seat_max_per_booking' => 2]);
|
||||
|
||||
$service = new BookingService;
|
||||
|
||||
expect(fn () => $service->validateSelections([new VehicleSelectionData(VehicleOption::FrontSeat, 2)]))
|
||||
->not->toThrow(InvalidVehicleSelectionException::class);
|
||||
});
|
||||
|
||||
test('back seat is rejected when disabled via config', function () {
|
||||
config(['booking.back_seat_enabled' => false]);
|
||||
|
||||
$service = new BookingService;
|
||||
|
||||
expect(fn () => $service->validateSelections([new VehicleSelectionData(VehicleOption::BackSeat)]))
|
||||
->toThrow(InvalidVehicleSelectionException::class);
|
||||
});
|
||||
|
||||
test('whole vehicle is rejected when disabled via config', function () {
|
||||
config(['booking.whole_vehicle_enabled' => false]);
|
||||
|
||||
$service = new BookingService;
|
||||
|
||||
expect(fn () => $service->validateSelections([new VehicleSelectionData(VehicleOption::WholeVehicle)]))
|
||||
->toThrow(InvalidVehicleSelectionException::class);
|
||||
->toThrow(InvalidVehicleSelectionException::class, 'Vehicle option [front_seat] passenger count [2] exceeds the max of [1] per booking.');
|
||||
});
|
||||
|
||||
test('the same vehicle option cannot be selected twice in one booking', function () {
|
||||
|
||||
@@ -60,9 +60,12 @@ class ManageAppSettings extends Page
|
||||
'support_phone' => config('app.support_phone'),
|
||||
'timezone' => config('app.timezone'),
|
||||
'currency' => config('app.currency'),
|
||||
'back_seat_enabled' => (bool) config('booking.back_seat_enabled'),
|
||||
'whole_vehicle_enabled' => (bool) config('booking.whole_vehicle_enabled'),
|
||||
'front_seat_enabled' => (bool) config('booking.front_seat_enabled'),
|
||||
'front_seat_max_per_booking' => config('booking.front_seat_max_per_booking'),
|
||||
'back_seat_enabled' => (bool) config('booking.back_seat_enabled'),
|
||||
'back_seat_max_per_booking' => config('booking.back_seat_max_per_booking'),
|
||||
'whole_vehicle_enabled' => (bool) config('booking.whole_vehicle_enabled'),
|
||||
'whole_vehicle_max_per_booking' => config('booking.whole_vehicle_max_per_booking'),
|
||||
'booking_admin_emails' => config('booking.admin_emails'),
|
||||
'sms_enabled' => (bool) config('services.sms.enabled'),
|
||||
'sms_server' => config('services.sms.sms_poh.server'),
|
||||
@@ -106,23 +109,39 @@ class ManageAppSettings extends Page
|
||||
->columns(2),
|
||||
Tab::make('Booking')
|
||||
->schema([
|
||||
Toggle::make('back_seat_enabled')
|
||||
->label('Back Seat Enabled')
|
||||
->helperText('Whether customers can select Back Seat at all right now.'),
|
||||
Toggle::make('whole_vehicle_enabled')
|
||||
->label('Whole Vehicle Enabled')
|
||||
->helperText('Whether customers can select Whole Vehicle at all right now.'),
|
||||
Toggle::make('front_seat_enabled')
|
||||
->label('Front Seat Enabled')
|
||||
->helperText('Whether customers can select Front Seat at all right now.'),
|
||||
TextInput::make('front_seat_max_per_booking')
|
||||
->label('Front Seat Max Per Booking')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->required()
|
||||
->helperText('Max Front Seats a single booking may request.'),
|
||||
Toggle::make('back_seat_enabled')
|
||||
->label('Back Seat Enabled')
|
||||
->helperText('Whether customers can select Back Seat at all right now.'),
|
||||
TextInput::make('back_seat_max_per_booking')
|
||||
->label('Back Seat Max Per Booking')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->required()
|
||||
->helperText('Max Back Seats a single booking may request.'),
|
||||
Toggle::make('whole_vehicle_enabled')
|
||||
->label('Whole Vehicle Enabled')
|
||||
->helperText('Whether customers can select Whole Vehicle at all right now.'),
|
||||
TextInput::make('whole_vehicle_max_per_booking')
|
||||
->label('Whole Vehicle Max Per Booking')
|
||||
->numeric()
|
||||
->minValue(1)
|
||||
->required()
|
||||
->helperText('Max Whole Vehicle passenger count a single booking may request.'),
|
||||
TagsInput::make('booking_admin_emails')
|
||||
->label('Admin Emails')
|
||||
->required()
|
||||
->helperText('Notified on booking events. Press enter after each address.'),
|
||||
]),
|
||||
])
|
||||
->columns(2),
|
||||
Tab::make('SMS')
|
||||
->schema([
|
||||
Toggle::make('sms_enabled')
|
||||
@@ -170,9 +189,12 @@ class ManageAppSettings extends Page
|
||||
'SUPPORT_PHONE' => $state['support_phone'],
|
||||
'APP_TIMEZONE' => $state['timezone'],
|
||||
'APP_CURRENCY' => $state['currency'],
|
||||
'BOOKING_BACK_SEAT_ENABLED' => (bool) $state['back_seat_enabled'],
|
||||
'BOOKING_WHOLE_VEHICLE_ENABLED' => (bool) $state['whole_vehicle_enabled'],
|
||||
'BOOKING_FRONT_SEAT_ENABLED' => (bool) $state['front_seat_enabled'],
|
||||
'BOOKING_FRONT_SEAT_MAX_PER_BOOKING' => (int) $state['front_seat_max_per_booking'],
|
||||
'BOOKING_BACK_SEAT_ENABLED' => (bool) $state['back_seat_enabled'],
|
||||
'BOOKING_BACK_SEAT_MAX_PER_BOOKING' => (int) $state['back_seat_max_per_booking'],
|
||||
'BOOKING_WHOLE_VEHICLE_ENABLED' => (bool) $state['whole_vehicle_enabled'],
|
||||
'BOOKING_WHOLE_VEHICLE_MAX_PER_BOOKING' => (int) $state['whole_vehicle_max_per_booking'],
|
||||
'BOOKING_ADMIN_EMAILS' => implode(',', $state['booking_admin_emails'] ?? []),
|
||||
'SMS_ENABLED' => (bool) $state['sms_enabled'],
|
||||
'SMS_SERVER' => $state['sms_server'],
|
||||
|
||||
@@ -40,9 +40,12 @@ test('a super_admin can view and save app settings, writing them to .env', funct
|
||||
'support_phone' => '+95912345678',
|
||||
'timezone' => 'Asia/Yangon',
|
||||
'currency' => 'MMK',
|
||||
'back_seat_enabled' => false,
|
||||
'whole_vehicle_enabled' => true,
|
||||
'front_seat_enabled' => false,
|
||||
'front_seat_max_per_booking' => 2,
|
||||
'back_seat_enabled' => false,
|
||||
'back_seat_max_per_booking' => 5,
|
||||
'whole_vehicle_enabled' => true,
|
||||
'whole_vehicle_max_per_booking' => 6,
|
||||
'booking_admin_emails' => ['ops@evbooking.test', 'dispatch@evbooking.test'],
|
||||
'sms_enabled' => true,
|
||||
'sms_server' => 'https://sms.example.test/send',
|
||||
@@ -59,9 +62,12 @@ test('a super_admin can view and save app settings, writing them to .env', funct
|
||||
->toContain('SUPPORT_EMAIL=help@evbooking.test')
|
||||
->toContain('APP_TIMEZONE=Asia/Yangon')
|
||||
->toContain('APP_CURRENCY=MMK')
|
||||
->toContain('BOOKING_BACK_SEAT_ENABLED=false')
|
||||
->toContain('BOOKING_WHOLE_VEHICLE_ENABLED=true')
|
||||
->toContain('BOOKING_FRONT_SEAT_ENABLED=false')
|
||||
->toContain('BOOKING_FRONT_SEAT_MAX_PER_BOOKING=2')
|
||||
->toContain('BOOKING_BACK_SEAT_ENABLED=false')
|
||||
->toContain('BOOKING_BACK_SEAT_MAX_PER_BOOKING=5')
|
||||
->toContain('BOOKING_WHOLE_VEHICLE_ENABLED=true')
|
||||
->toContain('BOOKING_WHOLE_VEHICLE_MAX_PER_BOOKING=6')
|
||||
->toContain('BOOKING_ADMIN_EMAILS=ops@evbooking.test,dispatch@evbooking.test')
|
||||
->toContain('SMS_ENABLED=true')
|
||||
->toContain('SMS_SERVER=https://sms.example.test/send')
|
||||
@@ -80,13 +86,17 @@ test('sms server and token are required once sms is enabled', function () {
|
||||
->assertHasFormErrors(['sms_server', 'sms_token']);
|
||||
});
|
||||
|
||||
test('front seat max per booking must be at least 1', function () {
|
||||
test('max per booking fields must be at least 1', function (string $field) {
|
||||
$superAdmin = User::factory()->create();
|
||||
$superAdmin->assignRole('super_admin');
|
||||
$this->actingAs($superAdmin);
|
||||
|
||||
Livewire::test(ManageAppSettings::class)
|
||||
->fillForm(['front_seat_max_per_booking' => 0])
|
||||
->fillForm([$field => 0])
|
||||
->call('save')
|
||||
->assertHasFormErrors(['front_seat_max_per_booking']);
|
||||
});
|
||||
->assertHasFormErrors([$field]);
|
||||
})->with([
|
||||
'front_seat_max_per_booking',
|
||||
'back_seat_max_per_booking',
|
||||
'whole_vehicle_max_per_booking',
|
||||
]);
|
||||
|
||||
@@ -18,6 +18,7 @@ class RoutePricingResource extends JsonResource
|
||||
'vehicle_option' => $this->vehicle_option->value,
|
||||
'price' => (string) $this->price,
|
||||
'is_blocked' => $this->is_blocked,
|
||||
'max_per_booking' => config("booking.{$this->vehicle_option->value}_max_per_booking"),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ test('searches active routes with nested company, destinations, time slots and p
|
||||
->assertJsonPath('routes.data.0.time_slots.0.is_active', true)
|
||||
->assertJsonPath('routes.data.0.pricing.0.vehicle_option', 'front_seat')
|
||||
->assertJsonPath('routes.data.0.pricing.0.price', '12000.00')
|
||||
->assertJsonPath('routes.data.0.pricing.0.max_per_booking', config('booking.front_seat_max_per_booking'))
|
||||
->assertJsonCount(0, 'return_routes.data');
|
||||
});
|
||||
|
||||
@@ -311,8 +312,17 @@ test('lists a route\'s pricing including blocked options', function () {
|
||||
->getJson("/api/v1/routes/{$route->id}/pricing")
|
||||
->assertSuccessful()
|
||||
->assertJsonCount(2, 'data')
|
||||
->assertJsonFragment(['vehicle_option' => 'front_seat', 'price' => '12000.00', 'is_blocked' => false])
|
||||
->assertJsonFragment(['vehicle_option' => 'whole_vehicle', 'is_blocked' => true]);
|
||||
->assertJsonFragment([
|
||||
'vehicle_option' => 'front_seat',
|
||||
'price' => '12000.00',
|
||||
'is_blocked' => false,
|
||||
'max_per_booking' => config('booking.front_seat_max_per_booking'),
|
||||
])
|
||||
->assertJsonFragment([
|
||||
'vehicle_option' => 'whole_vehicle',
|
||||
'is_blocked' => true,
|
||||
'max_per_booking' => config('booking.whole_vehicle_max_per_booking'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('lists a route\'s time slots with the pivot active flag', function () {
|
||||
|
||||
+10
-16
@@ -4,30 +4,24 @@ return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Vehicle Option Toggles
|
||||
| Vehicle Option Rules
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Blunt on/off switches for Vehicle Options that have no real inventory
|
||||
| tracking in v1 (see domain.md §2). Front Seat has no toggle — it is
|
||||
| always selectable, only capped per-booking by the max below.
|
||||
| Every Vehicle Option gets the same two blunt, config-driven rules — no
|
||||
| real inventory tracking in v1 (see domain.md §2): an on/off switch for
|
||||
| whether it can be selected at all right now, and a max passenger_count
|
||||
| a single booking may request for it.
|
||||
|
|
||||
*/
|
||||
|
||||
'front_seat_enabled' => env('BOOKING_FRONT_SEAT_ENABLED', true),
|
||||
'front_seat_max_per_booking' => env('BOOKING_FRONT_SEAT_MAX_PER_BOOKING', 1),
|
||||
|
||||
'back_seat_enabled' => env('BOOKING_BACK_SEAT_ENABLED', true),
|
||||
'back_seat_max_per_booking' => env('BOOKING_BACK_SEAT_MAX_PER_BOOKING', 3),
|
||||
|
||||
'whole_vehicle_enabled' => env('BOOKING_WHOLE_VEHICLE_ENABLED', true),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Front Seat Limit
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The only inventory rule enforced in v1: max Front Seats a single
|
||||
| booking may request.
|
||||
|
|
||||
*/
|
||||
|
||||
'front_seat_max_per_booking' => env('BOOKING_FRONT_SEAT_MAX_PER_BOOKING', 1),
|
||||
'whole_vehicle_max_per_booking' => env('BOOKING_WHOLE_VEHICLE_MAX_PER_BOOKING', 4),
|
||||
|
||||
// comma-separated list of admin emails to notify on booking events
|
||||
'admin_emails' => explode(',', env('BOOKING_ADMIN_EMAILS', 'admin@example.com')),
|
||||
|
||||
@@ -27,11 +27,10 @@ Unlike a bus-booking system, **there is no seat map and no capacity tracking in
|
||||
- A Booking can select **more than one Vehicle Option** in the same booking (e.g. `front_seat` + `back_seat` for a customer traveling with a companion) — stored as one row per selected option in `booking_vehicle_options` (`booking_id`, `vehicle_option`, `passenger_count`, `unit_price`, `line_total`), not a single column on `bookings`. `bookings.price` is the sum of every line's `line_total`.
|
||||
- Each Vehicle Option can appear **at most once per booking** (no two separate `front_seat` lines — bump `passenger_count` instead). `whole_vehicle` cannot be combined with any other option in the same booking, since it already covers the entire vehicle.
|
||||
- Any number of *different bookings* can book the same Route + Date + Time Slot. The system does not check whether a "Whole Vehicle" or "Back Seat" is already taken by someone else.
|
||||
- The **only inventory rule enforced in code** is: **max Front Seats per booking**, checked against `passenger_count` on the `front_seat` line (`BOOKING_FRONT_SEAT_MAX_PER_BOOKING`, currently `1` — a per-booking constraint, not a per-trip inventory check).
|
||||
- Back Seat and Whole Vehicle availability are controlled by **blunt config toggles**, not database rows:
|
||||
- `BOOKING_BACK_SEAT_ENABLED` — whether Back Seat can be selected at all right now.
|
||||
- `BOOKING_WHOLE_VEHICLE_ENABLED` — whether Whole Vehicle can be selected at all right now.
|
||||
- `BOOKING_FRONT_SEAT_MAX_PER_BOOKING` — currently `1`, expressed as config in case it ever needs to change.
|
||||
- Every Vehicle Option is governed by the same two **blunt config-driven rules**, not database rows: an on/off toggle for whether it can be selected at all right now, and a max `passenger_count` a single booking may request for it (a per-booking constraint, not a per-trip inventory check):
|
||||
- `BOOKING_FRONT_SEAT_ENABLED` / `BOOKING_FRONT_SEAT_MAX_PER_BOOKING` (currently `1`)
|
||||
- `BOOKING_BACK_SEAT_ENABLED` / `BOOKING_BACK_SEAT_MAX_PER_BOOKING` (currently `3`)
|
||||
- `BOOKING_WHOLE_VEHICLE_ENABLED` / `BOOKING_WHOLE_VEHICLE_MAX_PER_BOOKING` (currently `4`)
|
||||
- This is a **deliberate v1 simplification**, not an oversight. Real per-route/date/time-slot capacity holding (e.g. "only 1 Whole Vehicle booking allowed per trip") is an explicitly deferred future phase — see §7. `booking_vehicle_options` is deliberately shaped so that phase can be built as a new query against it (`sum(passenger_count) group by vehicle_option` for a route/date/time-slot) rather than a schema rework.
|
||||
- Consequence: double-booking of "Whole Vehicle" is possible by design until that future phase ships. Admins reconcile manually via the Filament Booking list (filterable by route + date + time).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user