157 lines
5.8 KiB
PHP
157 lines
5.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Controller\Api;
|
|
|
|
use App\BpnConnect\Model\ContingentStatus;
|
|
use App\Controller\Api\ContingentController;
|
|
use App\Entity\Groups\Accommodation;
|
|
use App\Entity\Groups\AccommodationPrice;
|
|
use App\Model\ContingentCalendarQuery;
|
|
use App\Repository\Groups\AccommodationPriceRepository;
|
|
use App\Repository\Groups\AccommodationRepository;
|
|
use App\Service\AccommodationPriceCoverage;
|
|
use App\Service\ContingentSnapshotReader;
|
|
use App\Service\PriceTimelineBuilder;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\LoggerInterface;
|
|
use Symfony\Component\DependencyInjection\Container;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class ContingentControllerTest extends TestCase
|
|
{
|
|
public function testEnrichEntryIncludesPricingMetadata(): void
|
|
{
|
|
$controller = $this->createController();
|
|
$price = $this->createPrice();
|
|
|
|
$result = $this->enrichEntry($controller, '2026-07-06', 'available', [$price], ['2026-07-06' => true]);
|
|
|
|
self::assertSame('available', $result['status']);
|
|
self::assertSame(4, $result['includedPax']);
|
|
self::assertSame(3, $result['minNights']);
|
|
self::assertSame(123.45, $result['pricePerNight']);
|
|
self::assertSame('EUR', $result['currency']);
|
|
}
|
|
|
|
public function testEnrichEntryBlocksDayWithoutPrice(): void
|
|
{
|
|
$controller = $this->createController();
|
|
$price = $this->createPrice();
|
|
|
|
$result = $this->enrichEntry($controller, '2026-08-01', 'OK', [$price], ['2026-07-06' => true]);
|
|
|
|
self::assertSame('BLOCKED', $result['status']);
|
|
self::assertNull($result['pricePerNight']);
|
|
self::assertNull($result['includedPax']);
|
|
self::assertNull($result['minNights']);
|
|
}
|
|
|
|
public function testCalendarFailsLoudlyWhenThereIsNoUsableSnapshot(): void
|
|
{
|
|
$response = $this->callCalendar(null);
|
|
|
|
self::assertSame(Response::HTTP_BAD_GATEWAY, $response->getStatusCode());
|
|
self::assertSame('{"error":"Failed to fetch contingent data."}', $response->getContent());
|
|
}
|
|
|
|
public function testCalendarServesTheSnapshotStatuses(): void
|
|
{
|
|
$response = $this->callCalendar([
|
|
'2026-09-01' => ContingentStatus::Ok,
|
|
'2026-09-02' => ContingentStatus::OnRequest,
|
|
]);
|
|
|
|
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
|
|
|
|
$data = json_decode((string) $response->getContent(), true);
|
|
|
|
// One entry per requested day, and the format is unchanged from the upstream-backed version.
|
|
self::assertCount(3, $data);
|
|
self::assertSame(
|
|
['date', 'status', 'type', 'pricePerNight', 'defaultPricePerNight', 'priceAdditionalPerson', 'defaultPriceAdditionalPerson', 'currency', 'includedPax', 'minNights'],
|
|
array_keys($data[0]),
|
|
);
|
|
// No price covers these days, so the "not sold without a price" rule blocks them all.
|
|
self::assertSame(['BLOCKED', 'BLOCKED', 'BLOCKED'], array_column($data, 'status'));
|
|
}
|
|
|
|
public function testCalendarBlocksDaysMissingFromTheSnapshot(): void
|
|
{
|
|
$data = json_decode((string) $this->callCalendar([])->getContent(), true);
|
|
|
|
self::assertSame(['BLOCKED', 'BLOCKED', 'BLOCKED'], array_column($data, 'status'));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, ContingentStatus>|null $statuses
|
|
*/
|
|
private function callCalendar(?array $statuses): 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);
|
|
|
|
$controller = new ContingentController(
|
|
$accommodationRepository,
|
|
$this->createMock(AccommodationPriceRepository::class),
|
|
$snapshotReader,
|
|
new PriceTimelineBuilder(),
|
|
new AccommodationPriceCoverage($this->createMock(AccommodationPriceRepository::class)),
|
|
$this->createMock(LoggerInterface::class),
|
|
);
|
|
|
|
// No 'serializer' service registered, so AbstractController::json() falls back to
|
|
// 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'));
|
|
}
|
|
|
|
private function createController(): ContingentController
|
|
{
|
|
return new ContingentController(
|
|
$this->createMock(AccommodationRepository::class),
|
|
$this->createMock(AccommodationPriceRepository::class),
|
|
$this->createMock(ContingentSnapshotReader::class),
|
|
new PriceTimelineBuilder(),
|
|
new AccommodationPriceCoverage($this->createMock(AccommodationPriceRepository::class)),
|
|
$this->createMock(LoggerInterface::class),
|
|
);
|
|
}
|
|
|
|
private function createPrice(): AccommodationPrice
|
|
{
|
|
$price = new AccommodationPrice();
|
|
$price->setDateFrom(new \DateTimeImmutable('2026-07-01'));
|
|
$price->setDateTo(new \DateTimeImmutable('2026-07-31'));
|
|
$price->setIncludedPax(4);
|
|
$price->setPricePerNight(12345);
|
|
$price->setPriceAdditionalPerson(1500);
|
|
$price->setMinNights(3);
|
|
|
|
return $price;
|
|
}
|
|
|
|
/**
|
|
* @param AccommodationPrice[] $prices
|
|
* @param array<string, true> $covered
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function enrichEntry(
|
|
ContingentController $controller,
|
|
string $date,
|
|
string $status,
|
|
array $prices,
|
|
array $covered,
|
|
): array {
|
|
$method = new \ReflectionMethod($controller, 'enrichEntry');
|
|
|
|
return $method->invoke($controller, $date, $status, $prices, $covered, 'EUR');
|
|
}
|
|
}
|