fix: refresh live room availability during create flow

This commit is contained in:
Björn Fromme committed 2026-07-14 11:25:23 +02:00
1 parent a73e8a593f
commit cdce7d57e0
26 files changed
+1976 -54

No files matched your search

@@ -158,6 +158,34 @@ class BookingConfiguratorStatusTest extends TestCase
$this->bookingService->startFreshBooking($request, 123, 456);
}
public function testStartFreshBookingRejectsRoomsThatGoUnavailableDuringLiveRefresh(): void
{
$travel = $this->createTravelWithRooms([
['status' => Constants::STATUS_AVAILABLE, 'available' => 1],
]);
$this->travelDataService
->expects($this->once())
->method('getTravelData')
->willReturn($travel);
$this->travelDataService
->expects($this->once())
->method('enrichWithFreshAvailabilities')
->with($travel, true, true, true)
->willReturnCallback(static function (Travel $travel): bool {
$travel->rooms[1]->available = 0;
return true;
});
$request = $this->createRequestWithSession();
$this->expectException(NoRoomsAvailableException::class);
$this->bookingService->startFreshBooking($request, 123, 456);
}
public function testStartFreshBookingWithApiRestrictionsForcesInquiry(): void
{
// Even with Frei rooms, if API says only 'A' allowed, booking status should be 'A'
+114 -2
View File
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Tests\Service;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel;
use App\Exception\BookingSessionNotFoundException;
use App\Form\Model\BookingDto;
@@ -52,6 +53,12 @@ class BookingSessionManagerTest extends TestCase
$hydratedTravel->id = 12;
$hydratedTravel->hotelId = 34;
$hydratedTravel->label = 'Hydrated travel';
$hydratedRoom = new Room();
$hydratedRoom->id = 7;
$hydratedRoom->available = 0;
$hydratedRoom->status = 'Frei';
$hydratedTravel->dateTo = new \DateTimeImmutable('2030-01-06');
$hydratedTravel->rooms = [$hydratedRoom];
$this->travelDataService
->expects($this->once())
@@ -59,12 +66,99 @@ class BookingSessionManagerTest extends TestCase
->with(12, 34)
->willReturn($hydratedTravel);
$this->travelDataService
->expects($this->once())
->method('enrichWithFreshAvailabilities')
->with($hydratedTravel, true, false, true)
->willReturnCallback(static function (Travel $travel): bool {
$travel->rooms[0]->available = 0;
return true;
});
$this->service->markCreateAvailabilityRefreshed($request);
$this->service->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE);
$loadedDto = $this->service->getBookingDto($request, BookingDto::MODE_CREATE);
$this->assertNotNull($loadedDto);
$this->assertSame($hydratedTravel, $loadedDto?->travel);
$this->assertSame($hydratedTravel, $loadedDto?->booking?->travelData);
$this->assertSame(0, $loadedDto?->travel->rooms[0]->available);
}
public function testSaveAndLoadBookingDtoForcesRefreshWhenCreateAvailabilityIsStale(): void
{
$request = $this->createRequestWithSession();
$storedTravel = new Travel();
$storedTravel->id = 12;
$storedTravel->hotelId = 34;
$bookingDto = new BookingDto($storedTravel, 34);
$hydratedTravel = new Travel();
$hydratedTravel->id = 12;
$hydratedTravel->hotelId = 34;
$hydratedTravel->dateTo = new \DateTimeImmutable('2030-01-06');
$request->getSession()->set(
BookingSessionManager::BOOKING_CREATE_AVAILABILITY_REFRESHED_AT_KEY,
time() - 601
);
$this->travelDataService
->expects($this->once())
->method('getTravelData')
->with(12, 34)
->willReturn($hydratedTravel);
$this->travelDataService
->expects($this->once())
->method('enrichWithFreshAvailabilities')
->with($hydratedTravel, true, true, true)
->willReturn(true);
$this->service->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE);
$loadedDto = $this->service->getBookingDto($request, BookingDto::MODE_CREATE);
$this->assertNotNull($loadedDto);
$this->assertSame($hydratedTravel, $loadedDto?->travel);
}
public function testRefreshCreateAvailabilityForcesLiveRefreshAndResetsUnavailableSelections(): void
{
$request = $this->createRequestWithSession();
$travel = new Travel();
$travel->id = 12;
$travel->hotelId = 34;
$room = new Room();
$room->id = 7;
$room->available = 1;
$room->status = 'Frei';
$travel->rooms = [$room];
$bookingDto = new BookingDto($travel, 34);
$selection = new RoomSelectionDto();
$selection->id = 7;
$selection->quantity = 2;
$bookingDto->roomSelections = [7 => $selection];
$this->travelDataService
->expects($this->once())
->method('enrichWithFreshAvailabilities')
->with($travel, true, true, true)
->willReturn(true);
$result = $this->service->refreshCreateAvailability($request, $bookingDto, true);
$this->assertTrue($result->availabilityRefreshed);
$this->assertTrue($result->roomSelectionQuantityReset);
$this->assertTrue($result->roomSelectionSnapshotChanged);
$this->assertTrue($result->hasAvailableRooms);
$this->assertSame(0, $bookingDto->roomSelections[7]->quantity);
$this->assertSame($bookingDto, $request->getSession()->get(BookingSessionManager::BOOKING_CREATE_KEY));
}
public function testBaselineSnapshotIsCreatedAndReused(): void
@@ -77,13 +171,31 @@ class BookingSessionManagerTest extends TestCase
$bookingDto->roomSelections = [$selection];
$first = $this->service->getOrCreateBaselineSnapshot($request, $bookingDto);
$this->assertSame([[1, 2]], $first);
$this->assertSame([[1, 2, 0.0]], $first);
$selection->quantity = 5;
$second = $this->service->getOrCreateBaselineSnapshot($request, $bookingDto);
$this->assertSame($first, $second);
$this->assertSame([[1, 2]], $second);
$this->assertSame([[1, 2, 0.0]], $second);
}
public function testSaveBaselineSnapshotReplacesExistingSnapshot(): void
{
$request = $this->createRequestWithSession();
$bookingDto = new BookingDto(new Travel(), 99);
$selection = new RoomSelectionDto();
$selection->id = 1;
$selection->quantity = 2;
$bookingDto->roomSelections = [$selection];
$this->service->saveBaselineSnapshot($request, $bookingDto);
$selection->quantity = 4;
$updated = $this->service->saveBaselineSnapshot($request, $bookingDto);
$this->assertSame([[1, 4, 0.0]], $updated);
$this->assertSame([[1, 4, 0.0]], $request->getSession()->get(BookingSessionManager::BOOKING_CREATE_BASELINE_KEY));
}
public function testStoreReturnUrlFallsBackToDefaultForInvalidInput(): void
+221
View File
@@ -5,6 +5,9 @@ declare(strict_types=1);
namespace App\Tests\Service;
use App\BusProNet\ApiClient;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\ServiceAvailabilityResponse;
use App\BusProNet\Model\Travel;
use App\BusProNet\XmlLoader\TravelLoader;
use App\Exception\TravelNotFoundException;
@@ -440,4 +443,222 @@ class TravelDataProviderTest extends TestCase
$this->assertSame($travel, $result);
}
public function testEnrichWithFreshAvailabilitiesRefreshesRoomInventory(): void
{
$dateId = 12345;
$hotelId = 67890;
$travel = new Travel();
$travel->id = $dateId;
$travel->hotelId = $hotelId;
$travel->dateTo = new \DateTimeImmutable('2030-01-06');
$room1 = new Room();
$room1->id = 1;
$room1->label = 'Doppelzimmer';
$room1->available = 2;
$room1->status = 'Frei';
$room2 = new Room();
$room2->id = 2;
$room2->label = 'Familienzimmer';
$room2->available = 1;
$room2->status = 'Frei';
$travel->rooms = [$room1, $room2];
$liveRoom = new Room();
$liveRoom->id = 1;
$liveRoom->available = 0;
$liveRoom->status = 'Frei';
$liveRoom->price = 199.5;
$liveRoom2 = new Room();
$liveRoom2->id = 2;
$liveRoom2->available = 1;
$liveRoom2->status = 'Frei';
$liveRoom2->price = 249.0;
$this->apiClient
->expects($this->once())
->method('getAvailabilities')
->with($dateId)
->willReturn(new ServiceAvailabilityResponse([]));
$this->apiClient
->expects($this->once())
->method('getHotelAvailability')
->with($dateId, $hotelId, $travel->dateTo)
->willReturn(new BaseData([
1 => $liveRoom,
2 => $liveRoom2,
]));
$this->travelLoader
->expects($this->once())
->method('patchRoomAvailabilities')
->with($travel, $this->isInstanceOf(BaseData::class))
->willReturnCallback(static function (Travel $travel, BaseData $roomAvailabilities): void {
$liveRooms = $roomAvailabilities->getItems();
foreach ($travel->rooms as $room) {
$liveRoom = $liveRooms[$room->id] ?? null;
if (!($liveRoom instanceof Room)) {
continue;
}
$room->available = $liveRoom->available;
$room->price = $liveRoom->price;
}
});
$this->service->enrichWithFreshAvailabilities($travel, false, refreshRooms: true);
$this->assertSame(0, $travel->rooms[0]->available);
$this->assertSame(199.5, $travel->rooms[0]->price);
$this->assertSame(1, $travel->rooms[1]->available);
$this->assertSame(249.0, $travel->rooms[1]->price);
$this->assertSame([2 => $travel->rooms[1]], $travel->getAvailableRooms());
}
public function testPatchRoomAvailabilitiesLogsMissingRoomIdsAsContractIssue(): void
{
$travel = new Travel();
$travel->id = 12345;
$travel->hotelId = 67890;
$room1 = new Room();
$room1->id = 1;
$room1->available = 2;
$room1->status = 'Frei';
$room2 = new Room();
$room2->id = 2;
$room2->available = 1;
$room2->status = 'Frei';
$travel->rooms = [$room1, $room2];
$liveRoom = new Room();
$liveRoom->id = 1;
$liveRoom->available = 0;
$liveRoom->status = 'Frei';
$this->logger
->expects($this->once())
->method('warning')
->with(
'Hotel availability response is missing room IDs from the travel snapshot',
$this->callback(static function (array $context): bool {
return [2] === $context['missingRoomIds']
&& 12345 === $context['travelId']
&& 67890 === $context['hotelId'];
})
);
$this->travelLoader
->expects($this->once())
->method('patchRoomAvailabilities')
->with($travel, $this->isInstanceOf(BaseData::class));
$this->service->patchRoomAvailabilities($travel, new BaseData([
1 => $liveRoom,
]));
}
public function testEnrichWithFreshAvailabilitiesDoesNotForceRefreshHotelAvailabilityByDefault(): void
{
$travel = new Travel();
$travel->id = 12345;
$travel->hotelId = 67890;
$travel->dateTo = new \DateTimeImmutable('2030-01-06');
$this->cache
->expects($this->never())
->method('delete');
$this->cache
->expects($this->exactly(2))
->method('get')
->willReturnCallback(static fn (string $key, callable $callback) => $callback(new \Symfony\Component\Cache\CacheItem()));
$this->apiClient
->expects($this->once())
->method('getAvailabilities')
->with($travel->id)
->willReturn(new ServiceAvailabilityResponse([]));
$this->apiClient
->expects($this->once())
->method('getHotelAvailability')
->with($travel->id, $travel->hotelId, $travel->dateTo)
->willReturn(new BaseData([]));
$this->service->enrichWithFreshAvailabilities($travel, refreshRooms: true);
}
public function testEnrichWithFreshAvailabilitiesDoesNotRefreshRoomsByDefault(): void
{
$travel = new Travel();
$travel->id = 12345;
$travel->hotelId = 67890;
$travel->dateTo = new \DateTimeImmutable('2030-01-06');
$this->cache
->expects($this->once())
->method('get')
->willReturnCallback(static fn (string $key, callable $callback) => $callback(new \Symfony\Component\Cache\CacheItem()));
$this->apiClient
->expects($this->once())
->method('getAvailabilities')
->with($travel->id)
->willReturn(new ServiceAvailabilityResponse([]));
$this->apiClient
->expects($this->never())
->method('getHotelAvailability');
$this->travelLoader
->expects($this->never())
->method('patchRoomAvailabilities');
$this->service->enrichWithFreshAvailabilities($travel);
}
public function testEnrichWithFreshAvailabilitiesForceRefreshInvalidatesHotelAvailabilityCache(): void
{
$travel = new Travel();
$travel->id = 12345;
$travel->hotelId = 67890;
$travel->dateTo = new \DateTimeImmutable('2030-01-06');
$this->cache
->expects($this->exactly(2))
->method('delete')
->with($this->logicalOr(
$this->equalTo('availability_12345'),
$this->equalTo('hotel_availability_12345_67890_20300106')
));
$this->cache
->expects($this->exactly(2))
->method('get')
->willReturnCallback(static fn (string $key, callable $callback) => $callback(new \Symfony\Component\Cache\CacheItem()));
$this->apiClient
->expects($this->once())
->method('getAvailabilities')
->with($travel->id)
->willReturn(new ServiceAvailabilityResponse([]));
$this->apiClient
->expects($this->once())
->method('getHotelAvailability')
->with($travel->id, $travel->hotelId, $travel->dateTo)
->willReturn(new BaseData([]));
$this->service->enrichWithFreshAvailabilities($travel, true, true, refreshRooms: true);
}
}