This commit is contained in:
@@ -7,9 +7,11 @@ use Modules\Booking\Data\VehicleSelectionData;
|
||||
use Modules\Booking\Enums\BookingChannel;
|
||||
use Modules\Booking\Enums\BookingStatus;
|
||||
use Modules\Booking\Events\BookingCreated;
|
||||
use Modules\Booking\Exceptions\InvalidReturnRouteException;
|
||||
use Modules\Booking\Exceptions\InvalidVehicleSelectionException;
|
||||
use Modules\Booking\Models\Booking;
|
||||
use Modules\Catalog\Models\DepartureTimeSlot;
|
||||
use Modules\Routing\Exceptions\RoutePricingNotFoundException;
|
||||
use Modules\Routing\Models\EvRoute;
|
||||
use Modules\Routing\Models\RoutePricing;
|
||||
use Modules\Shared\Enums\VehicleOption;
|
||||
@@ -33,7 +35,7 @@ function makeBookableRoute(array $pricedOptions): array
|
||||
return [$route, $timeSlot];
|
||||
}
|
||||
|
||||
function bookingData(EvRoute $route, DepartureTimeSlot $timeSlot, array $selections): CreateBookingData
|
||||
function bookingData(EvRoute $route, DepartureTimeSlot $timeSlot, array $selections, array $roundTrip = []): CreateBookingData
|
||||
{
|
||||
return new CreateBookingData(
|
||||
evRouteId: $route->id,
|
||||
@@ -46,9 +48,38 @@ function bookingData(EvRoute $route, DepartureTimeSlot $timeSlot, array $selecti
|
||||
dropoffAddress: '456 Dropoff Ave',
|
||||
createdByChannel: BookingChannel::MiniApp,
|
||||
openid: 'mini-app-openid-123',
|
||||
returnEvRouteId: $roundTrip['route']->id ?? null,
|
||||
returnDepartureTimeSlotId: $roundTrip['timeSlot']->id ?? null,
|
||||
returnTravelDate: $roundTrip['travelDate'] ?? (isset($roundTrip['route']) ? now()->addDays(3)->toDateString() : null),
|
||||
returnSelections: $roundTrip['selections'] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same company as $outbound, from/to swapped — the true reverse route.
|
||||
*
|
||||
* @param array<int, array{0: VehicleOption, 1: string}> $pricedOptions
|
||||
*/
|
||||
function makeReverseRoute(EvRoute $outbound, array $pricedOptions): array
|
||||
{
|
||||
$route = EvRoute::factory()->create([
|
||||
'ev_company_id' => $outbound->ev_company_id,
|
||||
'from_destination_id' => $outbound->to_destination_id,
|
||||
'to_destination_id' => $outbound->from_destination_id,
|
||||
]);
|
||||
$timeSlot = DepartureTimeSlot::factory()->create();
|
||||
|
||||
foreach ($pricedOptions as [$vehicleOption, $price]) {
|
||||
RoutePricing::factory()->create([
|
||||
'ev_route_id' => $route->id,
|
||||
'vehicle_option' => $vehicleOption,
|
||||
'price' => $price,
|
||||
]);
|
||||
}
|
||||
|
||||
return [$route, $timeSlot];
|
||||
}
|
||||
|
||||
test('it persists a pending_payment booking with the price snapshotted from PricingService', function () {
|
||||
config(['booking.back_seat_enabled' => true]);
|
||||
|
||||
@@ -150,3 +181,144 @@ test('each booking created gets a unique, sequential booking_ref', function () {
|
||||
expect($first->booking_ref)->toBe('EVB-AAAAA1')
|
||||
->and($second->booking_ref)->toBe('EVB-AAAAA2');
|
||||
});
|
||||
|
||||
test('a plain one-way booking has no linked leg', function () {
|
||||
config(['booking.back_seat_enabled' => true]);
|
||||
|
||||
[$route, $timeSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);
|
||||
|
||||
$booking = app(CreateBookingAction::class)->handle(
|
||||
bookingData($route, $timeSlot, [new VehicleSelectionData(VehicleOption::BackSeat)])
|
||||
);
|
||||
|
||||
expect($booking->linked_booking_id)->toBeNull()
|
||||
->and($booking->is_round_trip)->toBeFalse()
|
||||
->and($booking->is_return_leg)->toBeFalse()
|
||||
->and(Booking::count())->toBe(1);
|
||||
});
|
||||
|
||||
test('a round trip creates two bookings linked bidirectionally, each priced independently', function () {
|
||||
config(['booking.back_seat_enabled' => true]);
|
||||
|
||||
[$outboundRoute, $outboundSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);
|
||||
[$returnRoute, $returnSlot] = makeReverseRoute($outboundRoute, [[VehicleOption::BackSeat, '11000.00']]);
|
||||
|
||||
$outbound = app(CreateBookingAction::class)->handle(bookingData(
|
||||
$outboundRoute,
|
||||
$outboundSlot,
|
||||
[new VehicleSelectionData(VehicleOption::BackSeat)],
|
||||
roundTrip: [
|
||||
'route' => $returnRoute,
|
||||
'timeSlot' => $returnSlot,
|
||||
'selections' => [new VehicleSelectionData(VehicleOption::BackSeat)],
|
||||
],
|
||||
));
|
||||
|
||||
expect(Booking::count())->toBe(2)
|
||||
->and($outbound->is_return_leg)->toBeFalse()
|
||||
->and($outbound->is_round_trip)->toBeTrue()
|
||||
->and($outbound->price)->toEqual('9000.00');
|
||||
|
||||
$return = $outbound->linkedBooking;
|
||||
|
||||
expect($return)->not->toBeNull()
|
||||
->and($return->is_return_leg)->toBeTrue()
|
||||
->and($return->is_round_trip)->toBeTrue()
|
||||
->and($return->linked_booking_id)->toBe($outbound->id)
|
||||
->and($return->ev_route_id)->toBe($returnRoute->id)
|
||||
->and($return->departure_time_slot_id)->toBe($returnSlot->id)
|
||||
->and($return->price)->toEqual('11000.00');
|
||||
});
|
||||
|
||||
test('a round trip dispatches BookingCreated for both legs', function () {
|
||||
Event::fake([BookingCreated::class]);
|
||||
config(['booking.back_seat_enabled' => true]);
|
||||
|
||||
[$outboundRoute, $outboundSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);
|
||||
[$returnRoute, $returnSlot] = makeReverseRoute($outboundRoute, [[VehicleOption::BackSeat, '9000.00']]);
|
||||
|
||||
$outbound = app(CreateBookingAction::class)->handle(bookingData(
|
||||
$outboundRoute,
|
||||
$outboundSlot,
|
||||
[new VehicleSelectionData(VehicleOption::BackSeat)],
|
||||
roundTrip: [
|
||||
'route' => $returnRoute,
|
||||
'timeSlot' => $returnSlot,
|
||||
'selections' => [new VehicleSelectionData(VehicleOption::BackSeat)],
|
||||
],
|
||||
));
|
||||
|
||||
Event::assertDispatched(BookingCreated::class, 2);
|
||||
Event::assertDispatched(BookingCreated::class, fn (BookingCreated $event) => $event->booking->is($outbound));
|
||||
Event::assertDispatched(BookingCreated::class, fn (BookingCreated $event) => $event->booking->is($outbound->linkedBooking));
|
||||
});
|
||||
|
||||
test('it rejects a return route that is not the reverse of the outbound route', function () {
|
||||
config(['booking.back_seat_enabled' => true]);
|
||||
|
||||
[$outboundRoute, $outboundSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);
|
||||
// Unrelated route — not from/to swapped.
|
||||
[$unrelatedRoute, $unrelatedSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);
|
||||
|
||||
expect(fn () => app(CreateBookingAction::class)->handle(bookingData(
|
||||
$outboundRoute,
|
||||
$outboundSlot,
|
||||
[new VehicleSelectionData(VehicleOption::BackSeat)],
|
||||
roundTrip: [
|
||||
'route' => $unrelatedRoute,
|
||||
'timeSlot' => $unrelatedSlot,
|
||||
'selections' => [new VehicleSelectionData(VehicleOption::BackSeat)],
|
||||
],
|
||||
)))->toThrow(InvalidReturnRouteException::class);
|
||||
|
||||
// The whole transaction rolls back — no orphan outbound-only booking.
|
||||
expect(Booking::count())->toBe(0);
|
||||
});
|
||||
|
||||
test('return leg selections are validated independently of the outbound leg', function () {
|
||||
config(['booking.back_seat_enabled' => true]);
|
||||
|
||||
[$outboundRoute, $outboundSlot] = makeBookableRoute([[VehicleOption::FrontSeat, '12000.00']]);
|
||||
[$returnRoute, $returnSlot] = makeReverseRoute($outboundRoute, [[VehicleOption::FrontSeat, '12000.00']]);
|
||||
|
||||
expect(fn () => app(CreateBookingAction::class)->handle(bookingData(
|
||||
$outboundRoute,
|
||||
$outboundSlot,
|
||||
[new VehicleSelectionData(VehicleOption::FrontSeat, 1)],
|
||||
roundTrip: [
|
||||
'route' => $returnRoute,
|
||||
'timeSlot' => $returnSlot,
|
||||
// Front seat max per booking is 1 — this should fail validation
|
||||
// for the return leg even though the outbound leg is valid.
|
||||
'selections' => [new VehicleSelectionData(VehicleOption::FrontSeat, 2)],
|
||||
],
|
||||
)))->toThrow(InvalidVehicleSelectionException::class);
|
||||
|
||||
expect(Booking::count())->toBe(0);
|
||||
});
|
||||
|
||||
test('a failed return-leg price lookup rolls back the outbound leg too', function () {
|
||||
config(['booking.back_seat_enabled' => true]);
|
||||
|
||||
[$outboundRoute, $outboundSlot] = makeBookableRoute([[VehicleOption::BackSeat, '9000.00']]);
|
||||
// Return route exists (true reverse) but has no pricing rows at all.
|
||||
$returnRoute = EvRoute::factory()->create([
|
||||
'ev_company_id' => $outboundRoute->ev_company_id,
|
||||
'from_destination_id' => $outboundRoute->to_destination_id,
|
||||
'to_destination_id' => $outboundRoute->from_destination_id,
|
||||
]);
|
||||
$returnSlot = DepartureTimeSlot::factory()->create();
|
||||
|
||||
expect(fn () => app(CreateBookingAction::class)->handle(bookingData(
|
||||
$outboundRoute,
|
||||
$outboundSlot,
|
||||
[new VehicleSelectionData(VehicleOption::BackSeat)],
|
||||
roundTrip: [
|
||||
'route' => $returnRoute,
|
||||
'timeSlot' => $returnSlot,
|
||||
'selections' => [new VehicleSelectionData(VehicleOption::BackSeat)],
|
||||
],
|
||||
)))->toThrow(RoutePricingNotFoundException::class);
|
||||
|
||||
expect(Booking::count())->toBe(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user