setDateFrom(new \DateTimeImmutable('2026-06-03')); $price->setDateTo(new \DateTimeImmutable('2026-06-05')); $price->setMinNights(2); $enriched = $this->buildEnrichedDayData( [ '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( [ '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 testPriceCoverageStillAppliesWhenThereIsNoUsableSnapshot(): 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|null $statuses null = no usable snapshot * @param AccommodationPrice[] $prices * * @return array */ private function buildEnrichedDayData( ?array $statuses, array $prices, string $dateFrom, string $dateTo, ): array { $snapshotReader = $this->createMock(ContingentSnapshotReader::class); $snapshotReader->method('statusesFor')->willReturn($statuses); $priceRepository = $this->createMock(AccommodationPriceRepository::class); $priceRepository->method('findByHotelCodeAndDateRange')->willReturn($prices); $controller = new Step1Controller( $this->createMock(AccommodationBookingService::class), $this->createMock(AccommodationSessionManager::class), $priceRepository, new PriceTimelineBuilder(), $snapshotReader, $this->createMock(CalendarGridBuilder::class), $this->createMock(GroupsPriceCalculator::class), new AccommodationPriceCoverage($priceRepository), ); $method = new \ReflectionMethod($controller, 'buildEnrichedDayData'); return $method->invoke( $controller, (new Accommodation())->setCalendarCode('HOTEL'), 'HOTEL', new \DateTimeImmutable($dateFrom), new \DateTimeImmutable($dateTo), ); } }