172 lines
8.6 KiB
PHP
172 lines
8.6 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Chatbot;
|
|
|
|
use App\Chatbot\TripCatalog;
|
|
use App\Entity\ContentDocument;
|
|
use App\Enum\ContentFetchState;
|
|
use App\Model\Chatbot\TripCriteria;
|
|
use App\Model\Export;
|
|
use App\Repository\ContentDocumentRepository;
|
|
use App\Service\ProductExportBuilder;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Psr\Log\NullLogger;
|
|
use Symfony\Component\Clock\MockClock;
|
|
|
|
class TripCatalogTest extends TestCase
|
|
{
|
|
public function testSearchReturnsUpcomingDatesWithTheLowestBookablePrice(): void
|
|
{
|
|
$result = $this->catalog([
|
|
$this->row('VTH', 1, '2026-09-01', 'HOT1', [$this->room(100)]),
|
|
$this->row('VTH', 2, '2026-12-05', 'HOT1', [$this->room(500), $this->room(450, status: 'Anfrage'), $this->room(99, status: 'Buchungsstop')]),
|
|
$this->row('VTH', 3, '2027-01-09', 'HOT1', [$this->room(520, status: 'Anfrage')]),
|
|
])->search(new TripCriteria());
|
|
|
|
self::assertCount(1, $result['results']);
|
|
$trip = $result['results'][0];
|
|
self::assertSame('VTH', $trip['code']);
|
|
self::assertSame('Val Thorens Apartments', $trip['name']);
|
|
self::assertSame('https://www.ep-reisen.de/val-thorens/', $trip['url']);
|
|
self::assertSame(['country' => 'Frankreich'], $trip['destination']);
|
|
self::assertSame(['Hotel Eins'], $trip['hotels']);
|
|
self::assertSame([
|
|
['dateFrom' => '2026-12-05', 'dateTo' => '2026-12-12', 'priceFrom' => 450.0, 'availability' => 'frei'],
|
|
['dateFrom' => '2027-01-09', 'dateTo' => '2027-01-16', 'priceFrom' => 520.0, 'availability' => 'auf Anfrage'],
|
|
], $trip['nextDates']);
|
|
self::assertSame('2026-10-08T06:00:00+00:00', $result['dataAsOf']);
|
|
}
|
|
|
|
public function testPersonsCountsPlacesAcrossBookableRoomsOnly(): void
|
|
{
|
|
$rows = [
|
|
// 2 rooms x 2 pax + 1 x 1 pax = 5 places; the stopped room's 10 do not count.
|
|
$this->row('VTH', 2, '2026-12-05', 'HOT1', [$this->room(400, available: 2, maxPax: 2), $this->room(300, available: 1, maxPax: 1), $this->room(200, available: 10, maxPax: 1, status: 'Buchungsstop')]),
|
|
];
|
|
|
|
self::assertCount(1, $this->catalog($rows)->search(new TripCriteria(persons: 5))['results']);
|
|
self::assertCount(0, $this->catalog($rows)->search(new TripCriteria(persons: 6))['results']);
|
|
}
|
|
|
|
public function testDatePeriodMatchesOnOverlapAndMaxPriceOnTheLowestPrice(): void
|
|
{
|
|
$rows = [$this->row('VTH', 2, '2026-12-05', 'HOT1', [$this->room(450), $this->room(700)])];
|
|
|
|
self::assertCount(1, $this->catalog($rows)->search(new TripCriteria(dateFrom: new \DateTimeImmutable('2026-12-12'), dateTo: new \DateTimeImmutable('2026-12-31')))['results']);
|
|
self::assertCount(0, $this->catalog($rows)->search(new TripCriteria(dateFrom: new \DateTimeImmutable('2026-12-13')))['results']);
|
|
self::assertCount(1, $this->catalog($rows)->search(new TripCriteria(maxPrice: 450))['results']);
|
|
self::assertCount(0, $this->catalog($rows)->search(new TripCriteria(maxPrice: 449.99))['results']);
|
|
}
|
|
|
|
public function testCountryAndConceptAreResolvedThroughTheContentRecords(): void
|
|
{
|
|
$rows = [$this->row('VTH', 2, '2026-12-05', 'HOT1', [$this->room(450)])];
|
|
|
|
self::assertCount(1, $this->catalog($rows)->search(new TripCriteria(country: 'frankreich', concept: 'ski'))['results']);
|
|
self::assertCount(0, $this->catalog($rows)->search(new TripCriteria(country: 'Österreich'))['results']);
|
|
self::assertCount(0, $this->catalog($rows)->search(new TripCriteria(concept: 'party'))['results']);
|
|
}
|
|
|
|
public function testGetJoinsHotelsOnTheirAliasesAndPreferTheOwner(): void
|
|
{
|
|
$trip = $this->catalog([
|
|
$this->row('VTH', 2, '2026-12-05', 'HOT1ALIAS', [$this->room(450)]),
|
|
$this->row('VTH', 2, '2026-12-05', 'HOT2', [$this->room(380, status: 'Buchungsstop')]),
|
|
])->get('vth', 'HOT1');
|
|
|
|
self::assertNotNull($trip);
|
|
self::assertSame(['Hotel Eins'], array_column($trip['hotels'], 'name'));
|
|
self::assertCount(1, $trip['dates']);
|
|
self::assertSame('Hotel Eins', $trip['dates'][0]['hotels'][0]['hotel']);
|
|
self::assertSame([['room' => 'Zimmer', 'board' => 'HP', 'nights' => 7, 'price' => 450, 'availability' => 'frei']], $trip['dates'][0]['hotels'][0]['rooms']);
|
|
self::assertSame([['city' => 'Frankfurt', 'place' => 'Flughafen', 'departure' => '2026-12-04 23:30', 'surcharge' => 5.9]], $trip['dates'][0]['pickups']);
|
|
self::assertArrayNotHasKey('guide', $trip['dates'][0]);
|
|
}
|
|
|
|
public function testProductsHiddenFromTheWebsiteSearchAreNotOfferedButCanBeNamed(): void
|
|
{
|
|
$rows = [$this->row('GRP', 2, '2026-12-05', 'HOT1', [$this->room(300)])];
|
|
|
|
self::assertSame([], $this->catalog($rows)->search(new TripCriteria())['results']);
|
|
self::assertSame('GRP', $this->catalog($rows)->get('GRP')['code'] ?? null);
|
|
}
|
|
|
|
public function testGetIsNullForACodeWithoutAPublishedProduct(): void
|
|
{
|
|
self::assertNull($this->catalog([$this->row('XYZ', 2, '2026-12-05', 'HOT1', [$this->room(1)])])->get('XYZ'));
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $rows
|
|
*/
|
|
private function catalog(array $rows): TripCatalog
|
|
{
|
|
$export = $this->createStub(ProductExportBuilder::class);
|
|
$export->method('build')->willReturn(new Export($rows, 0, 0, new \DateTimeImmutable('2026-10-08 09:00:00+00:00'), new \DateTimeImmutable('2026-10-08 06:00:00+00:00')));
|
|
|
|
$records = [];
|
|
foreach ([
|
|
$this->record('product', 1, 'Val Thorens Apartments', 'https://www.ep-reisen.de/val-thorens/', ['code' => 'VTH', 'countryUid' => 2, 'conceptUid' => 7, 'hotelUids' => [10], 'daytrip' => false, 'bookable' => true]),
|
|
$this->record('product', 2, 'Gruppenreise Uni X', 'https://www.ep-reisen.de/gruppe/', ['code' => 'GRP', 'countryUid' => 2, 'searchable' => false, 'bookable' => true]),
|
|
$this->record('hotel', 10, 'Hotel Eins', null, ['code' => 'HOT1', 'codes' => ['HOT1', 'HOT1ALIAS', 'SHARED'], 'facts' => ['wifi' => true, 'sauna' => null]]),
|
|
$this->record('hotel', 11, 'Hotel Zwei', null, ['code' => 'SHARED', 'codes' => ['HOT2']]),
|
|
$this->record('country', 2, 'Frankreich', null, ['code' => 'FR']),
|
|
$this->record('concept', 7, 'Skiurlaub ohne Programm', null, ['code' => 'ski']),
|
|
] as $record) {
|
|
$records[$record->getRecordRef()] = $record;
|
|
}
|
|
|
|
$repository = $this->createStub(ContentDocumentRepository::class);
|
|
$repository->method('findRecords')->willReturn($records);
|
|
$repository->method('matchRecords')->willReturn([]);
|
|
|
|
return new TripCatalog($export, $repository, new MockClock('2026-10-08 10:00:00'), new NullLogger());
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
*/
|
|
private function record(string $type, int $uid, string $title, ?string $url, array $data): ContentDocument
|
|
{
|
|
return (new ContentDocument($type, $uid, 'de', new \DateTimeImmutable()))
|
|
->setTitle($title)
|
|
->setUrl($url)
|
|
->setSite(null === $url ? null : 'ep-reisen')
|
|
->setData($data + ['uid' => $uid])
|
|
->setBodyMd('# '.$title)
|
|
->markFetched(ContentFetchState::Ok, new \DateTimeImmutable());
|
|
}
|
|
|
|
/**
|
|
* @param list<array<string, mixed>> $rooms
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function row(string $code, int $dateId, string $from, string $hotelCode, array $rooms): array
|
|
{
|
|
$dateFrom = new \DateTimeImmutable($from);
|
|
|
|
return [
|
|
'product' => ['code' => $code, 'busProId' => 1],
|
|
'date' => ['code' => $code.$dateId, 'busProId' => $dateId, 'from' => $from, 'to' => $dateFrom->modify('+7 days')->format('Y-m-d'), 'type' => 'F'],
|
|
'hotel' => ['code' => $hotelCode, 'busProId' => crc32($hotelCode)],
|
|
'rooms' => $rooms,
|
|
'pickups' => [
|
|
['busProId' => 4, 'code' => 'FRA', 'city' => 'Frankfurt', 'street' => 'Flughafen', 'price' => 5.9, 'datetime' => $dateFrom->modify('-1 day')->format('Y-m-d').'T23:30:00'],
|
|
['busProId' => 0, 'code' => null, 'city' => null, 'street' => null, 'price' => null, 'datetime' => null],
|
|
],
|
|
'guide' => ['name' => 'Anna', 'phone' => '0170'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function room(int $price, int $available = 5, int $maxPax = 2, string $status = 'Frei'): array
|
|
{
|
|
return ['code' => 'DZ', 'label' => 'Zimmer', 'minPax' => 1, 'maxPax' => $maxPax, 'nights' => 7, 'price' => $price, 'available' => $available, 'status' => $status, 'boardCode' => 'HP'];
|
|
}
|
|
}
|