setDateFrom(new \DateTimeImmutable('2026-06-03')); $price->setDateTo(new \DateTimeImmutable('2026-06-05')); $price->setMinNights(2); $enriched = $this->buildEnrichedDayData( $this->calendarResponse([ '2026-06-01' => ContingentStatus::Ok, '2026-06-02' => ContingentStatus::Ok, '2026-06-03' => ContingentStatus::Ok, '2026-06-04' => ContingentStatus::Ok, '2026-06-05' => ContingentStatus::Ok, '2026-06-06' => ContingentStatus::Ok, '2026-06-07' => ContingentStatus::Ok, ]), [$price], '2026-06-01', '2026-06-07', ); self::assertSame('blocked', $enriched['2026-06-01']['status']); self::assertSame('blocked', $enriched['2026-06-02']['status']); // First priced night — arrival only self::assertSame('blocked-to-ok', $enriched['2026-06-03']['status']); self::assertSame('ok', $enriched['2026-06-04']['status']); self::assertSame('ok', $enriched['2026-06-05']['status']); // Day after the last priced night — still valid as checkout self::assertSame('checkout-only', $enriched['2026-06-06']['status']); self::assertSame('blocked', $enriched['2026-06-07']['status']); self::assertSame(2, $enriched['2026-06-04']['minNights']); self::assertSame(0, $enriched['2026-06-07']['minNights']); } public function testBlockedContingentWinsOverExistingPrice(): void { $price = new AccommodationPrice(); $price->setDateFrom(new \DateTimeImmutable('2026-06-01')); $price->setDateTo(new \DateTimeImmutable('2026-06-03')); $price->setMinNights(1); $enriched = $this->buildEnrichedDayData( $this->calendarResponse([ '2026-06-01' => ContingentStatus::Ok, '2026-06-02' => ContingentStatus::Blocked, '2026-06-03' => ContingentStatus::Ok, ]), [$price], '2026-06-01', '2026-06-03', ); self::assertSame('blocked-to-ok', $enriched['2026-06-01']['status']); self::assertSame('checkout-only', $enriched['2026-06-02']['status']); self::assertSame('blocked-to-ok', $enriched['2026-06-03']['status']); } public function testPriceCoverageStillAppliesWhenContingentApiIsUnavailable(): void { $price = new AccommodationPrice(); $price->setDateFrom(new \DateTimeImmutable('2026-06-01')); $price->setDateTo(new \DateTimeImmutable('2026-06-02')); $price->setMinNights(1); $enriched = $this->buildEnrichedDayData(null, [$price], '2026-06-01', '2026-06-03'); self::assertSame('blocked-to-ok', $enriched['2026-06-01']['status']); self::assertSame('ok', $enriched['2026-06-02']['status']); self::assertSame('checkout-only', $enriched['2026-06-03']['status']); } /** * @param array $statuses */ private function calendarResponse(array $statuses): ContingentCalendarResponse { $entries = []; foreach ($statuses as $date => $status) { $entries[] = new ContingentCalendarEntry(date: $date, status: $status); } return new ContingentCalendarResponse( new ContingentCalendarMeta('', '', 'days', 'HOTEL', 1, count($entries)), $entries, ); } /** * @param AccommodationPrice[] $prices * * @return array */ private function buildEnrichedDayData( ?ContingentCalendarResponse $calendar, array $prices, string $dateFrom, string $dateTo, ): array { $cache = $this->createMock(CacheInterface::class); if (null === $calendar) { $cache->method('get')->willThrowException(new \App\BpnConnect\Exception\BpnConnectException('down')); } else { $cache->method('get')->willReturn($calendar); } $priceRepository = $this->createMock(AccommodationPriceRepository::class); $priceRepository->method('findByHotelCodeAndDateRange')->willReturn($prices); $controller = new Step1Controller( $this->createMock(AccommodationBookingService::class), $this->createMock(AccommodationSessionManager::class), $this->createMock(ContingentsClient::class), $priceRepository, new PriceTimelineBuilder(), $cache, $this->createMock(CalendarGridBuilder::class), $this->createMock(GroupsPriceCalculator::class), new AccommodationPriceCoverage($priceRepository), ); $method = new \ReflectionMethod($controller, 'buildEnrichedDayData'); return $method->invoke( $controller, 'HOTEL', new \DateTimeImmutable($dateFrom), new \DateTimeImmutable($dateTo), $dateFrom, $dateTo, ); } }