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

+33
View File
@@ -82,6 +82,39 @@ class TravelTest extends TestCase
$this->assertCount(2, $result);
}
public function testGetAvailableRoomsExcludesSoldOutRooms(): void
{
$travel = new Travel();
$availableRoom = new \App\BusProNet\Model\Room();
$availableRoom->id = 1;
$availableRoom->label = 'Doppelzimmer';
$availableRoom->available = 2;
$availableRoom->status = 'Frei';
$soldOutRoom = new \App\BusProNet\Model\Room();
$soldOutRoom->id = 2;
$soldOutRoom->label = 'Einzelzimmer';
$soldOutRoom->available = 0;
$soldOutRoom->status = 'Frei';
$requestRoom = new \App\BusProNet\Model\Room();
$requestRoom->id = 3;
$requestRoom->label = 'Familienzimmer';
$requestRoom->available = 1;
$requestRoom->status = 'Anfrage';
$travel->rooms = [$availableRoom, $soldOutRoom, $requestRoom];
$result = $travel->getAvailableRooms();
$this->assertSame([
1 => $availableRoom,
3 => $requestRoom,
], $result);
$this->assertArrayNotHasKey(2, $result);
}
/**
* Test that services are sorted by subType.
*/
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace App\Tests\BusProNet\XmlLoader;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel;
use App\BusProNet\XmlLoader\HotelLoader;
use App\BusProNet\XmlLoader\TravelLoader;
use App\BusProNet\XmlParser\TravelParser;
use League\Flysystem\FilesystemOperator;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
class TravelLoaderTest extends TestCase
{
public function testPatchRoomAvailabilitiesAppliesLiveRoomDataToMatchingRooms(): void
{
$loader = $this->createLoader();
$travel = new Travel();
$patchedRoom = new Room();
$patchedRoom->id = 1;
$patchedRoom->available = 2;
$patchedRoom->status = 'Frei';
$patchedRoom->price = 100.0;
$otherRoom = new Room();
$otherRoom->id = 2;
$otherRoom->available = 1;
$otherRoom->status = 'Frei';
$otherRoom->price = 150.0;
$travel->rooms = [$patchedRoom, $otherRoom];
$liveRoom = new Room();
$liveRoom->id = 1;
$liveRoom->available = 0;
$liveRoom->status = 'Anfrage';
$liveRoom->price = 110.0;
$liveRoom->label = 'Live room';
$liveRoom->category = 'Live category';
$loader->patchRoomAvailabilities($travel, new BaseData([
1 => $liveRoom,
]));
$this->assertSame(0, $travel->rooms[0]->available);
$this->assertSame('Anfrage', $travel->rooms[0]->status);
$this->assertSame(110.0, $travel->rooms[0]->price);
$this->assertSame('Live room', $travel->rooms[0]->label);
$this->assertSame('Live category', $travel->rooms[0]->category);
$this->assertSame(1, $travel->rooms[1]->available);
$this->assertSame('Frei', $travel->rooms[1]->status);
$this->assertSame(150.0, $travel->rooms[1]->price);
}
private function createLoader(): TravelLoader
{
return new TravelLoader(
$this->createMock(HotelLoader::class),
$this->createMock(TravelParser::class),
'https://example.com',
new ArrayAdapter(),
$this->createMock(FilesystemOperator::class),
);
}
}
@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace App\Tests\BusProNet\XmlParser;
use App\BusProNet\Constants;
use App\BusProNet\Model\Room;
use App\BusProNet\XmlParser\AvailabilitiesParser;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\DomCrawler\Crawler;
class AvailabilitiesParserTest extends TestCase
{
private AvailabilitiesParser $parser;
protected function setUp(): void
{
$this->parser = new AvailabilitiesParser();
}
public function testParseRoomsNormalizesHotelAvailabilityStatuses(): void
{
$xml = <<<'XML'
<ergebnis>
<unterbringungen>
<unterbringung id_zimmer="1" bezeichnung="Room 1" kategorie="A" verpflegung="Board" frei="1" status="buchbar" preis="749,00" />
<unterbringung id_zimmer="2" bezeichnung="Room 2" kategorie="B" verpflegung="Board" frei="-900" status="anfrage" preis="849,00" />
<unterbringung id_zimmer="3" bezeichnung="Room 3" kategorie="C" verpflegung="Board" frei="0" status="stopp" preis="949,00" />
</unterbringungen>
</ergebnis>
XML;
$rooms = $this->parser->parseRooms(new Crawler($xml))->getItems();
$this->assertContainsOnlyInstancesOf(Room::class, $rooms);
$this->assertSame(Constants::STATUS_AVAILABLE, $rooms[1]->status);
$this->assertSame(Constants::STATUS_ON_REQUEST, $rooms[2]->status);
$this->assertSame(Constants::STATUS_BLOCKED, $rooms[3]->status);
}
public function testParseRoomsLogsUnrecognizedStatus(): void
{
$logger = $this->createMock(LoggerInterface::class);
$logger->expects($this->once())
->method('warning')
->with(
'Unrecognized room availability status from BusProNet XML',
['status' => 'foo']
);
$parser = new AvailabilitiesParser($logger);
$xml = <<<'XML'
<ergebnis>
<unterbringungen>
<unterbringung id_zimmer="1" bezeichnung="Room 1" kategorie="A" verpflegung="Board" frei="1" status="foo" preis="749,00" />
</unterbringungen>
</ergebnis>
XML;
$rooms = $parser->parseRooms(new Crawler($xml))->getItems();
$this->assertSame('foo', $rooms[1]->status);
}
}
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace App\Tests\Controller\Api;
use App\BusProNet\ApiClient;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\Room;
use App\Controller\Api\TravelController;
use App\Service\TravelDataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
use Symfony\Component\Serializer\Mapping\Loader\AttributeLoader;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
class TravelControllerTest extends TestCase
{
public function testHotelAvailabilityOnlySerializesApiGroupFields(): void
{
$room = new Room();
$room->id = 1;
$room->label = 'Doppelzimmer';
$room->status = 'Frei';
$room->available = 2;
$room->price = 199.0;
// booking-only fields that must NOT appear in the API response
$room->dateFrom = new \DateTimeImmutable('2030-01-01');
$room->totalCount = 42;
$room->board = 'Halbpension';
$apiClient = $this->createMock(ApiClient::class);
$apiClient->expects($this->once())
->method('getHotelAvailability')
->willReturn(new BaseData([1 => $room]));
$controller = new TravelController($apiClient, $this->createMock(TravelDataProvider::class));
$classMetadataFactory = new ClassMetadataFactory(new AttributeLoader());
$serializer = new Serializer(
[new DateTimeNormalizer(), new ObjectNormalizer($classMetadataFactory)],
[new JsonEncoder()],
);
$container = new Container();
$container->set('serializer', $serializer);
$controller->setContainer($container);
$response = $controller->hotelAvailability(12345, 67890, new \DateTimeImmutable('2030-01-06'));
$payload = json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);
$roomPayload = $payload['1'] ?? null;
$this->assertNotNull($roomPayload);
$this->assertSame('Frei', $roomPayload['status']);
$this->assertArrayNotHasKey('dateFrom', $roomPayload);
$this->assertArrayNotHasKey('totalCount', $roomPayload);
$this->assertArrayNotHasKey('board', $roomPayload);
}
public function testHotelAvailabilityReturnsErrorPayloadForApiNotification(): void
{
$apiClient = $this->createMock(ApiClient::class);
$apiClient->expects($this->once())
->method('getHotelAvailability')
->willReturn(new Notification(999, 'Hotel not found'));
$controller = new TravelController($apiClient, $this->createMock(TravelDataProvider::class));
$container = new Container();
$container->set('serializer', new Serializer(
[new ObjectNormalizer(new ClassMetadataFactory(new AttributeLoader()))],
[new JsonEncoder()],
));
$controller->setContainer($container);
$response = $controller->hotelAvailability(12345, 67890, new \DateTimeImmutable('2030-01-06'));
$payload = json_decode((string) $response->getContent(), true, 512, JSON_THROW_ON_ERROR);
$this->assertSame(['error' => 'Hotel not found', 'type' => 'api_error'], $payload);
}
}
@@ -0,0 +1,242 @@
<?php
declare(strict_types=1);
namespace App\Tests\Controller\Booking\Create;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel;
use App\Controller\Booking\Create\Step1Controller;
use App\Form\Model\BookingCreateContext;
use App\Form\Model\BookingDto;
use App\Form\Model\BookingSummaryDto;
use App\Form\Model\RoomGroupsDto;
use App\Form\Model\RoomSelectionDto;
use App\Service\BookingAvailabilityRefreshResult;
use App\Service\BookingConfigurator;
use App\Service\BookingCreateContextFactory;
use App\Service\BookingSessionManager;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
class Step1ControllerTest extends TestCase
{
public function testIndexRefreshesBaselineAfterAutoPruningUnavailableRooms(): void
{
$request = Request::create('/bookings/create/rooms', 'GET');
$bookingDto = $this->createBookingDtoWithUnavailableSelection();
$bookingSessionService = $this->createMock(BookingSessionManager::class);
$bookingSessionService->expects($this->once())
->method('getOrCreateBookingCreateDto')
->with($request)
->willReturn($bookingDto);
$bookingSessionService->expects($this->once())
->method('getOrCreateBaselineSnapshot')
->with($request, $bookingDto)
->willReturn([[2, 1]]);
$bookingSessionService->expects($this->once())
->method('refreshCreateAvailability')
->with($request, $bookingDto)
->willReturn(new BookingAvailabilityRefreshResult(
availabilityRefreshed: true,
roomSelectionQuantityReset: true,
roomSelectionSnapshotChanged: true,
hasAvailableRooms: true,
));
$bookingSessionService->expects($this->once())
->method('saveBaselineSnapshot')
->with($request, $bookingDto);
$bookingService = $this->createMock(BookingConfigurator::class);
$form = $this->createMock(FormInterface::class);
$form->expects($this->once())
->method('handleRequest')
->with($request)
->willReturnSelf();
$form->expects($this->once())
->method('isSubmitted')
->willReturn(false);
$createContextFactory = $this->createMock(BookingCreateContextFactory::class);
$createContextFactory->expects($this->once())
->method('create')
->with($bookingDto, $this->anything())
->willReturn(new BookingCreateContext(
bookingDto: $bookingDto,
summaryData: $this->createMock(BookingSummaryDto::class),
groupedRooms: new RoomGroupsDto([], []),
));
$controller = new TestableStep1Controller(
$bookingService,
$bookingSessionService,
$createContextFactory,
$form,
);
$response = $controller->index($request);
$this->assertInstanceOf(Response::class, $response);
$this->assertSame(['warning', 'Mindestens eine Zimmerauswahl ist nicht mehr verfügbar und wurde zurückgesetzt.'], $controller->flashes[0]);
}
public function testIndexUsesPrunedBaselineBeforeSubmittingUnchangedSelections(): void
{
$request = Request::create('/bookings/create/rooms', 'POST');
$bookingDto = $this->createBookingDtoWithUnavailableSelection();
$bookingDto = $this->getMockBuilder(BookingDto::class)
->setConstructorArgs([$bookingDto->travel, $bookingDto->hotelId])
->onlyMethods(['hasRoomSelectionChanged', 'resetParticipantAssignments'])
->getMock();
$bookingDto->roomSelections = $this->createBookingDtoWithUnavailableSelection()->roomSelections;
$bookingDto->currentStep = 1;
$bookingSessionService = $this->createMock(BookingSessionManager::class);
$bookingSessionService->expects($this->once())
->method('getOrCreateBookingCreateDto')
->with($request)
->willReturn($bookingDto);
$bookingSessionService->expects($this->once())
->method('getOrCreateBaselineSnapshot')
->with($request, $bookingDto)
->willReturn([[1, 0], [2, 1]]);
$bookingSessionService->expects($this->once())
->method('refreshCreateAvailability')
->with($request, $bookingDto)
->willReturn(new BookingAvailabilityRefreshResult(
availabilityRefreshed: true,
roomSelectionQuantityReset: true,
roomSelectionSnapshotChanged: true,
hasAvailableRooms: true,
));
$bookingSessionService->expects($this->once())
->method('saveBookingDto')
->with($request, $bookingDto, BookingDto::MODE_CREATE);
$bookingSessionService->expects($this->once())
->method('saveBaselineSnapshot')
->with($request, $bookingDto)
->willReturn([[1, 0]]);
$bookingSessionService->expects($this->once())
->method('clearBaselineSnapshot')
->with($request);
$bookingService = $this->createMock(BookingConfigurator::class);
$bookingService->expects($this->once())
->method('updateBookingStatusFromRoomSelection')
->with($bookingDto);
$bookingDto->expects($this->never())
->method('resetParticipantAssignments');
$bookingDto->expects($this->once())
->method('hasRoomSelectionChanged')
->with([[1, 0]])
->willReturn(false);
$form = $this->createMock(FormInterface::class);
$form->expects($this->once())
->method('handleRequest')
->with($request)
->willReturnSelf();
$form->expects($this->once())
->method('isSubmitted')
->willReturn(true);
$form->expects($this->once())
->method('isValid')
->willReturn(true);
$createContextFactory = $this->createMock(BookingCreateContextFactory::class);
$createContextFactory->expects($this->never())
->method('create');
$controller = new TestableStep1Controller(
$bookingService,
$bookingSessionService,
$createContextFactory,
$form,
);
$response = $controller->index($request);
$this->assertInstanceOf(Response::class, $response);
}
private function createBookingDtoWithUnavailableSelection(): BookingDto
{
$travel = new Travel();
$travel->id = 1234;
$availableRoom = new Room();
$availableRoom->id = 1;
$availableRoom->label = 'Doppelzimmer';
$availableRoom->available = 2;
$availableRoom->status = 'Frei';
$travel->rooms = [$availableRoom];
$bookingDto = new BookingDto($travel, 77);
$selection = new RoomSelectionDto();
$selection->id = 2;
$selection->quantity = 1;
$bookingDto->roomSelections = [$selection];
return $bookingDto;
}
}
final class TestableStep1Controller extends Step1Controller
{
/**
* @var array<int, array{string, mixed}>
*/
public array $flashes = [];
/**
* @param FormInterface<mixed> $form
*/
public function __construct(
BookingConfigurator $bookingService,
BookingSessionManager $bookingSessionService,
BookingCreateContextFactory $createContextFactory,
private readonly FormInterface $form,
) {
parent::__construct($bookingService, $bookingSessionService, $createContextFactory);
}
/**
* @return FormInterface<mixed>
*/
protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface
{
return $this->form;
}
protected function addFlash(string $type, mixed $message): void
{
$this->flashes[] = [$type, $message];
}
/**
* @param array<string, mixed> $parameters
*/
protected function render(string $view, array $parameters = [], ?Response $response = null): Response
{
return new Response('rendered:'.$view);
}
protected function redirectToRoute(string $route, array $parameters = [], int $status = 302): RedirectResponse
{
return new RedirectResponse('redirect:'.$route, $status);
}
protected function generateUrl(string $route, array $parameters = [], int $referenceType = 0): string
{
return $route;
}
}
@@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
namespace App\Tests\Controller\Booking\Create;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel;
use App\Controller\Booking\Create\Step2ParticipantController;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Model\RoomSelectionDto;
use App\Htmx\HxRedirectResponse;
use App\Service\BookingAvailabilityRefreshResult;
use App\Service\BookingConfigurator;
use App\Service\BookingCreateContextFactory;
use App\Service\BookingSessionManager;
use App\Service\ParticipantDataPrefiller;
use App\Service\ParticipantFormSupport;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
class Step2ParticipantControllerTest extends TestCase
{
public function testEditParticipantPersistsBookingStatusOnPlainGetRequest(): void
{
$request = Request::create('/bookings/create/participants/0', 'GET');
$bookingDto = $this->createBookingDto();
$bookingSessionService = $this->createMock(BookingSessionManager::class);
$bookingSessionService->expects($this->once())
->method('getBookingDto')
->with($request, BookingDto::MODE_CREATE)
->willReturn($bookingDto);
$bookingSessionService->expects($this->once())
->method('refreshCreateAvailability')
->with($request, $bookingDto, false)
->willReturn(new BookingAvailabilityRefreshResult(
availabilityRefreshed: true,
roomSelectionQuantityReset: false,
roomSelectionSnapshotChanged: false,
hasAvailableRooms: true,
));
$bookingSessionService->expects($this->once())
->method('saveBookingDto')
->with($request, $bookingDto, BookingDto::MODE_CREATE);
$bookingService = $this->createMock(BookingConfigurator::class);
$bookingService->expects($this->once())
->method('updateBookingStatusFromRoomSelection')
->with($bookingDto);
$participantFormSupportService = $this->createMock(ParticipantFormSupport::class);
$participantFormSupportService->method('ensureParticipantExists')->willReturn(new ParticipantDto());
$participantFormSupportService->method('createParticipantEditDto')->willReturn($this->createMock(\App\Form\Model\ParticipantEditDto::class));
$participantFormSupportService->method('getParticipantFormOptions')->willReturn([]);
$participantFormSupportService->method('collectAndClearNotifications')->willReturn([]);
$prepopulationService = $this->createMock(ParticipantDataPrefiller::class);
$prepopulationService->method('isDummyDataFillRequested')->willReturn(false);
$form = $this->createMock(FormInterface::class);
$form->expects($this->once())->method('handleRequest')->with($request)->willReturnSelf();
$form->expects($this->once())->method('isSubmitted')->willReturn(false);
$bookingDto->participants[0] = new ParticipantDto();
$createContextFactory = $this->createMock(BookingCreateContextFactory::class);
$createContextFactory->method('create')->willReturn(new \App\Form\Model\BookingCreateContext(
bookingDto: $bookingDto,
summaryData: $this->createMock(\App\Form\Model\BookingSummaryDto::class),
groupedRooms: new \App\Form\Model\RoomGroupsDto([], []),
));
$controller = new TestableStep2ParticipantController(
$bookingService,
$bookingSessionService,
$createContextFactory,
$prepopulationService,
$participantFormSupportService,
);
$controller->form = $form;
$response = $controller->editParticipant(0, $request);
$this->assertSame('rendered', $response->getContent());
}
public function testRefreshParticipantFormReturnsHxRedirectForHtmxRequestWhenSelectionStale(): void
{
$request = Request::create('/bookings/create/participants/0/refresh', 'POST');
$request->headers->set('HX-Request', 'true');
$response = $this->invokeRefreshWithStaleSelection($request);
$this->assertInstanceOf(HxRedirectResponse::class, $response);
$this->assertSame('app_booking_create_step_1', $response->headers->get('HX-Redirect'));
}
public function testRefreshParticipantFormReturnsPlainRedirectForNonHtmxRequestWhenSelectionStale(): void
{
$request = Request::create('/bookings/create/participants/0/refresh', 'POST');
$response = $this->invokeRefreshWithStaleSelection($request);
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertNotInstanceOf(HxRedirectResponse::class, $response);
$this->assertSame('app_booking_create_step_1', $response->getTargetUrl());
}
private function invokeRefreshWithStaleSelection(Request $request): \Symfony\Component\HttpFoundation\Response
{
$bookingDto = $this->createBookingDto();
$bookingSessionService = $this->createMock(BookingSessionManager::class);
$bookingSessionService->expects($this->once())
->method('getBookingDto')
->with($request, BookingDto::MODE_CREATE)
->willReturn($bookingDto);
$bookingSessionService->expects($this->once())
->method('refreshCreateAvailability')
->with($request, $bookingDto, false)
->willReturn(new BookingAvailabilityRefreshResult(
availabilityRefreshed: true,
roomSelectionQuantityReset: true,
roomSelectionSnapshotChanged: true,
hasAvailableRooms: true,
));
$bookingService = $this->createMock(BookingConfigurator::class);
$bookingService->expects($this->never())->method('updateBookingStatusFromRoomSelection');
$controller = new TestableStep2ParticipantController(
$bookingService,
$bookingSessionService,
$this->createMock(BookingCreateContextFactory::class),
$this->createMock(ParticipantDataPrefiller::class),
$this->createMock(ParticipantFormSupport::class),
);
return $controller->refreshParticipantForm(0, $request);
}
private function createBookingDto(): BookingDto
{
$travel = new Travel();
$travel->id = 1234;
$travel->hotelId = 77;
$room = new Room();
$room->id = 1;
$room->label = 'Doppelzimmer';
$room->available = 2;
$room->status = 'Frei';
$travel->rooms = [$room];
$bookingDto = new BookingDto($travel, 77);
$bookingDto->currentStep = 2;
$selection = new RoomSelectionDto();
$selection->id = 1;
$selection->quantity = 1;
$bookingDto->roomSelections = [$selection];
return $bookingDto;
}
}
final class TestableStep2ParticipantController extends Step2ParticipantController
{
/**
* @var array<int, array{string, mixed}>
*/
public array $flashes = [];
public ?FormInterface $form = null;
protected function addFlash(string $type, mixed $message): void
{
$this->flashes[] = [$type, $message];
}
protected function generateUrl(string $route, array $parameters = [], int $referenceType = 0): string
{
return $route;
}
protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface
{
return $this->form;
}
protected function render(string $view, array $parameters = [], ?\Symfony\Component\HttpFoundation\Response $response = null): \Symfony\Component\HttpFoundation\Response
{
return new \Symfony\Component\HttpFoundation\Response('rendered');
}
}
@@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
namespace App\Tests\Controller\Booking\Create;
use App\BusProNet\ApiClient;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel;
use App\Controller\Booking\Create\Step4Controller;
use App\Form\Model\BookingCreateContext;
use App\Form\Model\BookingDto;
use App\Form\Model\BookingSummaryDto;
use App\Form\Model\RoomGroupsDto;
use App\Form\Model\RoomSelectionDto;
use App\Service\BookingAvailabilityRefreshResult;
use App\Service\BookingConfigurator;
use App\Service\BookingCreateContextFactory;
use App\Service\BookingSessionManager;
use App\Service\NewsletterManager;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Cache\CacheInterface;
class Step4ControllerTest extends TestCase
{
public function testIndexRerendersWithErrorWhenForcedRefreshFails(): void
{
$request = Request::create('/bookings/create/confirmation', 'POST');
$bookingDto = $this->createBookingDto();
$bookingSessionService = $this->createMock(BookingSessionManager::class);
$bookingSessionService->expects($this->once())
->method('getOrCreateBookingCreateDto')
->with($request)
->willReturn($bookingDto);
$bookingSessionService->expects($this->exactly(2))
->method('refreshCreateAvailability')
->willReturnOnConsecutiveCalls(
new BookingAvailabilityRefreshResult(
availabilityRefreshed: true,
roomSelectionQuantityReset: false,
roomSelectionSnapshotChanged: false,
hasAvailableRooms: true,
),
new BookingAvailabilityRefreshResult(
availabilityRefreshed: false,
roomSelectionQuantityReset: false,
roomSelectionSnapshotChanged: false,
hasAvailableRooms: true,
),
);
$bookingService = $this->createMock(BookingConfigurator::class);
$bookingService->expects($this->never())->method('applyCreateBookingStatusRules');
$apiClient = $this->createMock(ApiClient::class);
$apiClient->expects($this->never())->method('createBooking');
$form = $this->createMock(FormInterface::class);
$form->expects($this->once())->method('handleRequest')->with($request)->willReturnSelf();
$form->expects($this->once())->method('isSubmitted')->willReturn(true);
$form->expects($this->once())->method('isValid')->willReturn(true);
$createContextFactory = $this->createMock(BookingCreateContextFactory::class);
$createContextFactory->expects($this->once())
->method('createWithParticipantPrices')
->with($bookingDto)
->willReturn(new BookingCreateContext(
bookingDto: $bookingDto,
summaryData: $this->createMock(BookingSummaryDto::class),
groupedRooms: new RoomGroupsDto([], []),
));
$controller = new TestableStep4Controller(
$bookingService,
$bookingSessionService,
$createContextFactory,
$apiClient,
$this->createMock(CacheInterface::class),
$this->createMock(NewsletterManager::class),
new NullLogger(),
$form,
);
$response = $controller->index($request);
$this->assertInstanceOf(Response::class, $response);
$this->assertNotInstanceOf(RedirectResponse::class, $response);
$this->assertSame('rendered:booking/create/step_4.html.twig', $response->getContent());
$this->assertSame(['error', 'Die Verfügbarkeit konnte gerade nicht aktualisiert werden. Bitte versuche es erneut.'], $controller->flashes[0]);
}
public function testIndexRedirectsWithWarningWhenPriceChangesDuringForcedRefresh(): void
{
$request = Request::create('/bookings/create/confirmation', 'POST');
$bookingDto = $this->createBookingDto();
$bookingSessionService = $this->createMock(BookingSessionManager::class);
$bookingSessionService->expects($this->once())
->method('getOrCreateBookingCreateDto')
->with($request)
->willReturn($bookingDto);
$bookingSessionService->expects($this->exactly(2))
->method('refreshCreateAvailability')
->willReturnOnConsecutiveCalls(
new BookingAvailabilityRefreshResult(
availabilityRefreshed: true,
roomSelectionQuantityReset: false,
roomSelectionSnapshotChanged: false,
hasAvailableRooms: true,
),
new BookingAvailabilityRefreshResult(
availabilityRefreshed: true,
roomSelectionQuantityReset: false,
roomSelectionSnapshotChanged: true,
hasAvailableRooms: true,
),
);
$bookingService = $this->createMock(BookingConfigurator::class);
$bookingService->expects($this->never())->method('applyCreateBookingStatusRules');
$apiClient = $this->createMock(ApiClient::class);
$apiClient->expects($this->never())->method('createBooking');
$form = $this->createMock(FormInterface::class);
$form->expects($this->once())->method('handleRequest')->with($request)->willReturnSelf();
$form->expects($this->once())->method('isSubmitted')->willReturn(true);
$form->expects($this->once())->method('isValid')->willReturn(true);
$createContextFactory = $this->createMock(BookingCreateContextFactory::class);
$createContextFactory->expects($this->never())->method('createWithParticipantPrices');
$controller = new TestableStep4Controller(
$bookingService,
$bookingSessionService,
$createContextFactory,
$apiClient,
$this->createMock(CacheInterface::class),
$this->createMock(NewsletterManager::class),
new NullLogger(),
$form,
);
$response = $controller->index($request);
$this->assertInstanceOf(RedirectResponse::class, $response);
$this->assertSame('app_booking_create_step_4', $response->getTargetUrl());
$this->assertSame(['warning', 'Die Zimmerpreise haben sich aktualisiert. Bitte überprüfe deine Buchung erneut.'], $controller->flashes[0]);
}
private function createBookingDto(): BookingDto
{
$travel = new Travel();
$travel->id = 1234;
$travel->hotelId = 77;
$room = new Room();
$room->id = 1;
$room->label = 'Doppelzimmer';
$room->available = 2;
$room->status = 'Frei';
$travel->rooms = [$room];
$bookingDto = new BookingDto($travel, 77);
$bookingDto->currentStep = 4;
$selection = new RoomSelectionDto();
$selection->id = 1;
$selection->quantity = 1;
$bookingDto->roomSelections = [$selection];
return $bookingDto;
}
}
final class TestableStep4Controller extends Step4Controller
{
/**
* @var array<int, array{string, mixed}>
*/
public array $flashes = [];
public function __construct(
BookingConfigurator $bookingService,
BookingSessionManager $bookingSessionService,
BookingCreateContextFactory $createContextFactory,
ApiClient $apiClient,
CacheInterface $cache,
NewsletterManager $doubleOptInService,
\Psr\Log\LoggerInterface $logger,
private readonly FormInterface $form,
) {
parent::__construct(
$bookingService,
$bookingSessionService,
$createContextFactory,
$apiClient,
$cache,
$doubleOptInService,
$logger,
);
}
protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface
{
return $this->form;
}
protected function addFlash(string $type, mixed $message): void
{
$this->flashes[] = [$type, $message];
}
protected function render(string $view, array $parameters = [], ?Response $response = null): Response
{
return new Response('rendered:'.$view);
}
protected function redirectToRoute(string $route, array $parameters = [], int $status = 302): RedirectResponse
{
return new RedirectResponse($route, $status);
}
protected function generateUrl(string $route, array $parameters = [], int $referenceType = 0): string
{
return $route;
}
protected function getUser(): ?\Symfony\Component\Security\Core\User\UserInterface
{
return null;
}
}
+161
View File
@@ -4,10 +4,12 @@ declare(strict_types=1);
namespace App\Tests\Form\Model;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Travel;
use App\BusProNet\Model\Address;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Model\RoomSelectionDto;
use PHPUnit\Framework\TestCase;
/**
@@ -89,6 +91,165 @@ class BookingDtoTest extends TestCase
$this->assertSame('43', $restored->participants[0]->shoeSize);
}
public function testSyncRoomSelectionsWithAvailableRoomsClearsMissingOrOverbookedRooms(): void
{
$travel = new Travel();
$travel->dateFrom = new \DateTimeImmutable('2025-06-01');
$travel->dateTo = new \DateTimeImmutable('2025-06-08');
$availableRoom = new Room();
$availableRoom->id = 1;
$availableRoom->label = 'Doppelzimmer';
$availableRoom->price = 100.0;
$availableRoom->available = 2;
$availableRoom->minPax = 2;
$availableRoom->status = 'Frei';
$limitedRoom = new Room();
$limitedRoom->id = 2;
$limitedRoom->label = 'Einzelzimmer';
$limitedRoom->price = 150.0;
$limitedRoom->available = 1;
$limitedRoom->minPax = 1;
$limitedRoom->status = 'Frei';
$travel->rooms = [$availableRoom, $limitedRoom];
$dto = new BookingDto($travel, 1);
$selection1 = new RoomSelectionDto();
$selection1->id = 1;
$selection1->quantity = 2;
$selection2 = new RoomSelectionDto();
$selection2->id = 2;
$selection2->quantity = 2;
$selection3 = new RoomSelectionDto();
$selection3->id = 3;
$selection3->quantity = 1;
$dto->roomSelections = [
1 => $selection1,
2 => $selection2,
3 => $selection3,
];
$changed = $dto->syncRoomSelectionsWithAvailableRooms();
$this->assertTrue($changed);
$this->assertCount(2, $dto->roomSelections);
$this->assertArrayHasKey(1, $dto->roomSelections);
$this->assertArrayHasKey(2, $dto->roomSelections);
$this->assertArrayNotHasKey(3, $dto->roomSelections);
$this->assertSame(1, $dto->roomSelections[1]->id);
$this->assertSame(2, $dto->roomSelections[1]->quantity);
$this->assertSame('Doppelzimmer', $dto->roomSelections[1]->label);
$this->assertSame(100.0, $dto->roomSelections[1]->price);
$this->assertSame(2, $dto->roomSelections[1]->maxQuantity);
$this->assertSame(2, $dto->roomSelections[1]->capacity);
$this->assertSame(2, $dto->roomSelections[2]->id);
$this->assertSame(0, $dto->roomSelections[2]->quantity);
}
public function testSyncRoomSelectionsWithAvailableRoomsSynchronizesZeroQuantityRoomMetadata(): void
{
$travel = new Travel();
$travel->dateFrom = new \DateTimeImmutable('2025-06-01');
$travel->dateTo = new \DateTimeImmutable('2025-06-08');
$room = new Room();
$room->id = 1;
$room->label = 'Aktualisiertes Zimmer';
$room->price = 125.0;
$room->available = 1;
$room->minPax = 2;
$room->status = 'Anfrage';
$travel->rooms = [$room];
$dto = new BookingDto($travel, 1);
$selection = new RoomSelectionDto();
$selection->id = 1;
$selection->label = 'Veraltetes Zimmer';
$selection->price = 100.0;
$selection->status = 'Frei';
$selection->maxQuantity = 5;
$selection->capacity = 1;
$selection->quantity = 0;
$dto->roomSelections = [1 => $selection];
$changed = $dto->syncRoomSelectionsWithAvailableRooms();
$this->assertFalse($changed);
$this->assertCount(1, $dto->roomSelections);
$this->assertArrayHasKey(1, $dto->roomSelections);
$this->assertSame('Aktualisiertes Zimmer', $dto->roomSelections[1]->label);
$this->assertSame(125.0, $dto->roomSelections[1]->price);
$this->assertSame('Anfrage', $dto->roomSelections[1]->status);
$this->assertSame(1, $dto->roomSelections[1]->maxQuantity);
$this->assertSame(2, $dto->roomSelections[1]->capacity);
$this->assertSame(0, $dto->roomSelections[1]->quantity);
}
public function testSyncRoomSelectionsWithAvailableRoomsAddsNewAvailableRooms(): void
{
$travel = new Travel();
$travel->dateFrom = new \DateTimeImmutable('2025-06-01');
$travel->dateTo = new \DateTimeImmutable('2025-06-08');
$room = new Room();
$room->id = 5;
$room->label = 'Familienzimmer';
$room->price = 250.0;
$room->available = 3;
$room->minPax = 4;
$room->status = 'Frei';
$travel->rooms = [$room];
$dto = new BookingDto($travel, 1);
$changed = $dto->syncRoomSelectionsWithAvailableRooms();
$this->assertFalse($changed);
$this->assertArrayHasKey(5, $dto->roomSelections);
$this->assertSame(5, $dto->roomSelections[5]->id);
$this->assertSame('Familienzimmer', $dto->roomSelections[5]->label);
$this->assertSame(250.0, $dto->roomSelections[5]->price);
$this->assertSame('Frei', $dto->roomSelections[5]->status);
$this->assertSame(0, $dto->roomSelections[5]->quantity);
$this->assertSame(3, $dto->roomSelections[5]->maxQuantity);
$this->assertSame(4, $dto->roomSelections[5]->capacity);
}
public function testCreateRoomSelectionSnapshotDetectsPriceOnlyChange(): void
{
$travel = new Travel();
$travel->dateFrom = new \DateTimeImmutable('2025-06-01');
$travel->dateTo = new \DateTimeImmutable('2025-06-08');
$dto = new BookingDto($travel, 1);
$selection = new RoomSelectionDto();
$selection->id = 1;
$selection->quantity = 2;
$selection->price = 100.0;
$dto->roomSelections = [$selection];
$before = $dto->createRoomSelectionSnapshot();
$selection->price = 120.0;
$after = $dto->createRoomSelectionSnapshot();
$this->assertSame([[1, 2, 100.0]], $before);
$this->assertSame([[1, 2, 120.0]], $after);
$this->assertNotSame($before, $after);
$this->assertTrue($dto->hasRoomSelectionChanged($before));
}
public function testUnserializeNormalizesLegacyParticipantStringsOnLoad(): void
{
$travel = new Travel();
@@ -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);
}
}