161 lines
6.0 KiB
PHP
161 lines
6.0 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Tests\Controller\Groups;
|
||
|
||
use App\BpnConnect\ContingentsClient;
|
||
use App\BpnConnect\Model\ContingentCalendarEntry;
|
||
use App\BpnConnect\Model\ContingentCalendarMeta;
|
||
use App\BpnConnect\Model\ContingentCalendarResponse;
|
||
use App\BpnConnect\Model\ContingentStatus;
|
||
use App\Controller\Groups\Step1Controller;
|
||
use App\Entity\Groups\AccommodationPrice;
|
||
use App\Repository\Groups\AccommodationPriceRepository;
|
||
use App\Service\AccommodationBookingService;
|
||
use App\Service\AccommodationPriceCoverage;
|
||
use App\Service\AccommodationSessionManager;
|
||
use App\Service\CalendarGridBuilder;
|
||
use App\Service\GroupsPriceCalculator;
|
||
use App\Service\PriceTimelineBuilder;
|
||
use PHPUnit\Framework\TestCase;
|
||
use Symfony\Contracts\Cache\CacheInterface;
|
||
|
||
class Step1CalendarDataTest extends TestCase
|
||
{
|
||
public function testDaysWithoutPriceAreBlockedEvenWhenContingentIsOk(): void
|
||
{
|
||
// Contingent says OK for the whole week, prices only cover 03.–05.
|
||
$price = new AccommodationPrice();
|
||
$price->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<string, ContingentStatus> $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<string, array{status: string, minNights: int}>
|
||
*/
|
||
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,
|
||
);
|
||
}
|
||
}
|