feat: derive calendar date range from price configurations
This commit is contained in:
@@ -14,6 +14,7 @@ use App\Repository\Groups\AccommodationRepository;
|
||||
use App\Service\AccommodationPriceCoverage;
|
||||
use App\Service\ContingentSnapshotReader;
|
||||
use App\Service\PriceTimelineBuilder;
|
||||
use Carbon\CarbonImmutable;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\Container;
|
||||
@@ -21,6 +22,11 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ContingentControllerTest extends TestCase
|
||||
{
|
||||
protected function tearDown(): void
|
||||
{
|
||||
CarbonImmutable::setTestNow();
|
||||
}
|
||||
|
||||
public function testEnrichEntryIncludesPricingMetadata(): void
|
||||
{
|
||||
$controller = $this->createController();
|
||||
@@ -84,20 +90,145 @@ class ContingentControllerTest extends TestCase
|
||||
self::assertSame(['BLOCKED', 'BLOCKED', 'BLOCKED'], array_column($data, 'status'));
|
||||
}
|
||||
|
||||
public function testCalendarWithoutARangeCoversTheWholePricedSpan(): void
|
||||
{
|
||||
CarbonImmutable::setTestNow('2026-06-01 09:00:00');
|
||||
|
||||
$data = $this->callFullSpanCalendar(
|
||||
['from' => new \DateTimeImmutable('2026-07-01'), 'to' => new \DateTimeImmutable('2026-08-31')],
|
||||
[$this->createPrice(), $this->createPrice('2026-08-10', '2026-08-31')],
|
||||
);
|
||||
|
||||
// Every day between the bounds, gaps included: consumers key off status, so a day that is
|
||||
// simply absent has no status at all and cannot be told apart from one we never mentioned.
|
||||
self::assertSame('2026-07-01', $data[0]['date']);
|
||||
self::assertSame('2026-08-31', $data[array_key_last($data)]['date']);
|
||||
self::assertCount(62, $data);
|
||||
}
|
||||
|
||||
public function testCalendarWithoutARangeIsContiguous(): void
|
||||
{
|
||||
CarbonImmutable::setTestNow('2026-06-01 09:00:00');
|
||||
|
||||
$dates = array_column($this->callFullSpanCalendar(
|
||||
['from' => new \DateTimeImmutable('2026-07-01'), 'to' => new \DateTimeImmutable('2026-08-31')],
|
||||
[$this->createPrice(), $this->createPrice('2026-08-10', '2026-08-31')],
|
||||
), 'date');
|
||||
|
||||
$expected = [];
|
||||
for ($day = new \DateTimeImmutable('2026-07-01'); $day <= new \DateTimeImmutable('2026-08-31'); $day = $day->modify('+1 day')) {
|
||||
$expected[] = $day->format('Y-m-d');
|
||||
}
|
||||
|
||||
self::assertSame($expected, $dates);
|
||||
}
|
||||
|
||||
public function testCalendarWithoutARangeBlocksDaysNoPriceCovers(): void
|
||||
{
|
||||
CarbonImmutable::setTestNow('2026-06-01 09:00:00');
|
||||
|
||||
$data = $this->callFullSpanCalendar(
|
||||
['from' => new \DateTimeImmutable('2026-07-01'), 'to' => new \DateTimeImmutable('2026-08-31')],
|
||||
[$this->createPrice(), $this->createPrice('2026-08-10', '2026-08-31')],
|
||||
['2026-07-31' => ContingentStatus::Ok, '2026-08-01' => ContingentStatus::Ok],
|
||||
);
|
||||
|
||||
$byDate = array_column($data, null, 'date');
|
||||
|
||||
// The closure between the two periods keeps a status, and it is BLOCKED even though the
|
||||
// snapshot reports the day as free: no price means not sold.
|
||||
self::assertSame('OK', $byDate['2026-07-31']['status']);
|
||||
self::assertSame('BLOCKED', $byDate['2026-08-01']['status']);
|
||||
self::assertNull($byDate['2026-08-01']['pricePerNight']);
|
||||
self::assertSame('BLOCKED', $byDate['2026-08-09']['status']);
|
||||
}
|
||||
|
||||
public function testCalendarWithoutARangeNeverStartsBeforeToday(): void
|
||||
{
|
||||
CarbonImmutable::setTestNow('2026-07-15 09:00:00');
|
||||
|
||||
$data = $this->callFullSpanCalendar(
|
||||
['from' => new \DateTimeImmutable('2026-07-01'), 'to' => new \DateTimeImmutable('2026-07-31')],
|
||||
[$this->createPrice()],
|
||||
);
|
||||
|
||||
self::assertSame('2026-07-15', $data[0]['date']);
|
||||
self::assertCount(17, $data);
|
||||
}
|
||||
|
||||
public function testCalendarWithoutARangeIsEmptyWhenTheHotelHasNoPrices(): void
|
||||
{
|
||||
self::assertSame([], $this->callFullSpanCalendar(null, []));
|
||||
}
|
||||
|
||||
public function testCalendarWithoutARangeIsEmptyWhenEveryPricedPeriodHasPassed(): void
|
||||
{
|
||||
CarbonImmutable::setTestNow('2026-09-01 09:00:00');
|
||||
|
||||
$data = $this->callFullSpanCalendar(
|
||||
['from' => new \DateTimeImmutable('2026-07-01'), 'to' => new \DateTimeImmutable('2026-07-31')],
|
||||
[$this->createPrice()],
|
||||
);
|
||||
|
||||
self::assertSame([], $data);
|
||||
}
|
||||
|
||||
public function testExplicitRangeNeverConsultsThePricedBounds(): void
|
||||
{
|
||||
$priceRepository = $this->createMock(AccommodationPriceRepository::class);
|
||||
$priceRepository->expects(self::never())->method('findPricedDateBoundsByHotelCode');
|
||||
|
||||
$this->callCalendar([], null, null, $priceRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{from: \DateTimeImmutable, to: \DateTimeImmutable}|null $bounds
|
||||
* @param AccommodationPrice[] $prices
|
||||
* @param array<string, ContingentStatus> $statuses
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
private function callFullSpanCalendar(?array $bounds, array $prices, array $statuses = []): array
|
||||
{
|
||||
$priceRepository = $this->createMock(AccommodationPriceRepository::class);
|
||||
$priceRepository->method('findPricedDateBoundsByHotelCode')->willReturn($bounds);
|
||||
$priceRepository->method('findByHotelCodeAndDateRange')->willReturn($prices);
|
||||
|
||||
$response = $this->callCalendar(
|
||||
$statuses,
|
||||
new ContingentCalendarQuery('HOTEL1'),
|
||||
$prices,
|
||||
$priceRepository,
|
||||
);
|
||||
|
||||
return json_decode((string) $response->getContent(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, ContingentStatus>|null $statuses
|
||||
* @param AccommodationPrice[]|null $prices
|
||||
*/
|
||||
private function callCalendar(?array $statuses): Response
|
||||
{
|
||||
private function callCalendar(
|
||||
?array $statuses,
|
||||
?ContingentCalendarQuery $query = null,
|
||||
?array $prices = null,
|
||||
?AccommodationPriceRepository $priceRepository = null,
|
||||
): Response {
|
||||
$accommodationRepository = $this->createMock(AccommodationRepository::class);
|
||||
$accommodationRepository->method('findOneBy')->willReturn((new Accommodation())->setCalendarCode('HOTEL1')->setCurrency('EUR'));
|
||||
|
||||
$snapshotReader = $this->createMock(ContingentSnapshotReader::class);
|
||||
$snapshotReader->method('statusesFor')->willReturn($statuses);
|
||||
|
||||
$priceRepository ??= $this->createMock(AccommodationPriceRepository::class);
|
||||
|
||||
if (null !== $prices) {
|
||||
$priceRepository->method('findByHotelCodeAndDateRange')->willReturn($prices);
|
||||
}
|
||||
|
||||
$controller = new ContingentController(
|
||||
$accommodationRepository,
|
||||
$this->createMock(AccommodationPriceRepository::class),
|
||||
$priceRepository,
|
||||
$snapshotReader,
|
||||
new PriceTimelineBuilder(),
|
||||
new AccommodationPriceCoverage($this->createMock(AccommodationPriceRepository::class)),
|
||||
@@ -108,7 +239,7 @@ class ContingentControllerTest extends TestCase
|
||||
// JsonResponse — which is what this endpoint produces in production anyway.
|
||||
$controller->setContainer(new Container());
|
||||
|
||||
return $controller->calendar(new ContingentCalendarQuery('HOTEL1', '2026-09-01', '2026-09-03'));
|
||||
return $controller->calendar($query ?? new ContingentCalendarQuery('HOTEL1', '2026-09-01', '2026-09-03'));
|
||||
}
|
||||
|
||||
private function createController(): ContingentController
|
||||
@@ -123,11 +254,11 @@ class ContingentControllerTest extends TestCase
|
||||
);
|
||||
}
|
||||
|
||||
private function createPrice(): AccommodationPrice
|
||||
private function createPrice(string $dateFrom = '2026-07-01', string $dateTo = '2026-07-31'): AccommodationPrice
|
||||
{
|
||||
$price = new AccommodationPrice();
|
||||
$price->setDateFrom(new \DateTimeImmutable('2026-07-01'));
|
||||
$price->setDateTo(new \DateTimeImmutable('2026-07-31'));
|
||||
$price->setDateFrom(new \DateTimeImmutable($dateFrom));
|
||||
$price->setDateTo(new \DateTimeImmutable($dateTo));
|
||||
$price->setIncludedPax(4);
|
||||
$price->setPricePerNight(12345);
|
||||
$price->setPriceAdditionalPerson(1500);
|
||||
|
||||
Reference in New Issue
Block a user