feat: chatbot trip search finds trips by trip name, hotel name and code

This commit is contained in:
fromme committed 2026-10-13 09:29:27 +02:00
1 parent 26d4140c18
commit fe40e1807e
8 files changed
+448 -52

No files matched your search

+105 -4
View File
@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Tests\Chatbot;
use App\BusProNet\Model\Hotel;
use App\BusProNet\XmlLoader\HotelLoader;
use App\Chatbot\TripCatalog;
use App\Entity\ContentDocument;
use App\Enum\ContentFetchState;
@@ -35,6 +37,7 @@ class TripCatalogTest extends TestCase
'code' => 'VTH',
'dateCode' => 'VTH2',
'hotel' => 'Hotel Zwei',
'hotelCode' => 'HOT2',
'accommodation' => null,
'destination' => ['country' => 'Frankreich'],
'concept' => 'Skiurlaub ohne Programm',
@@ -115,6 +118,91 @@ class TripCatalogTest extends TestCase
self::assertCount(0, $this->catalog($rows)->search(new TripCriteria(concept: 'party'))['results']);
}
public function testANameInTheQueryReturnsWhatItNamesAndNotWhatMentionsIt(): void
{
$rows = [
$this->row('VTH', 2, '2026-12-05', 'HOT1', [$this->room(500)]),
$this->row('VTH', 2, '2026-12-05', 'HOT2', [$this->room(480)]),
$this->row('VTH', 3, '2027-01-09', 'HOT1', [$this->room(520)]),
];
// Every page mentions every word: the texts must not be asked when a name answers.
$catalog = $this->catalog($rows, static fn (): array => [['type' => 'product', 'uid' => 1]]);
$search = static fn (string $query): array => array_map(
static fn (array $result): string => $result['dateCode'].'/'.$result['hotelCode'],
$catalog->search(new TripCriteria(query: $query))['results'],
);
self::assertSame(['VTH2/HOT2'], $search('Hotel Zwei'), 'a hotel stands for itself, not for its trip');
self::assertSame(['VTH2/HOT2'], $search('zwei'));
self::assertSame(['VTH2/HOT2'], $search('Gibt es das Hotel Zwei in Frankreich?'), 'spelled out among other words, the hotel beats the country');
self::assertSame(['VTH2/HOT2', 'VTH2/HOT1', 'VTH3/HOT1'], $search('Hotel'));
self::assertSame(['VTH2/HOT2', 'VTH2/HOT1', 'VTH3/HOT1'], $search('val thorens'));
self::assertSame(['VTH2/HOT2', 'VTH2/HOT1', 'VTH3/HOT1'], $search('Urlaub in Frankreich'));
self::assertSame([], $search('Gruppenreise Uni X'), 'a hidden trip is not offered, and nothing else in its place');
}
public function testACodeInTheQueryFindsItsTripHotelOrDate(): void
{
$catalog = $this->catalog([
$this->row('VTH', 2, '2026-12-05', 'HOT1', [$this->room(500)]),
$this->row('VTH', 2, '2026-12-05', 'HOT2', [$this->room(480)]),
$this->row('VTH', 3, '2027-01-09', 'HOT1', [$this->room(520)]),
]);
$search = static fn (string $query): array => array_map(
static fn (array $result): string => $result['dateCode'].'/'.$result['hotelCode'],
$catalog->search(new TripCriteria(query: $query))['results'],
);
self::assertSame(['VTH2/HOT2', 'VTH2/HOT1', 'VTH3/HOT1'], $search('vth'));
self::assertSame(['VTH2/HOT1', 'VTH3/HOT1'], $search('HOT1ALIAS'));
self::assertSame(['VTH3/HOT1'], $search('vth-3'));
}
public function testHotelsAreAlsoFoundByTheirBusProNameAndShownByItWithoutAWebsiteRecord(): void
{
$catalog = $this->catalog([
$this->row('VTH', 2, '2026-12-05', 'HOT1', [$this->room(500)]),
$this->row('VTH', 2, '2026-12-05', 'HOT2', [$this->room(480)]),
$this->row('VTH', 2, '2026-12-05', 'HOT3', [$this->room(520)]),
]);
$search = static fn (string $query): array => array_map(
static fn (array $result): string => $result['hotelCode'].'/'.$result['hotel'],
$catalog->search(new TripCriteria(query: $query))['results'],
);
self::assertSame(['HOT3/Oceanpeak'], $search('oceanpeak'), 'no website record: found and shown by BusPro\'s name');
self::assertSame(['HOT3/Oceanpeak'], $search('hot3'));
self::assertSame(['HOT3/Oceanpeak'], $search('Hotel Oceanpeak in Frankreich'));
self::assertSame(['HOT1/Hotel Eins'], $search('Residence Eins'), 'BusPro\'s wording finds the hotel, the website\'s names it');
self::assertSame('Oceanpeak', $catalog->get('VTH', 'HOT3')['dates'][0]['hotels'][0]['hotel'] ?? null);
self::assertSame([], $catalog->get('VTH', 'HOT3')['hotels'] ?? null, 'not the trip\'s other hotels');
}
public function testOtherWordsAreLookedUpInThePageTextsEveryTermBeforeAnyTerm(): void
{
$rows = [
$this->row('VTH', 2, '2026-12-05', 'HOT1', [$this->room(500)]),
$this->row('VTH', 2, '2026-12-05', 'HOT2', [$this->room(480)]),
];
$asked = [];
$hits = static function (string $booleanQuery) use (&$asked): array {
$asked[] = $booleanQuery;
return match ($booleanQuery) {
'+sauna* +whirlpool*' => [['type' => 'hotel', 'uid' => 11]],
'kamin* whirlpool*' => [['type' => 'country', 'uid' => 2]],
default => [],
};
};
$search = fn (string $query): array => array_column($this->catalog($rows, $hits)->search(new TripCriteria(query: $query))['results'], 'hotelCode');
self::assertSame(['HOT2'], $search('Sauna Whirlpool'));
self::assertSame(['+sauna* +whirlpool*'], $asked);
self::assertSame(['HOT2', 'HOT1'], $search('Kamin Whirlpool'), 'no page has both, so any of them');
self::assertSame([], $search('Kamin'));
self::assertSame(['HOT2', 'HOT1'], $search('was ist das'), 'nothing searchable restricts nothing');
}
public function testGetJoinsHotelsOnTheirAliasesAndPreferTheOwner(): void
{
$trip = $this->catalog([
@@ -192,9 +280,10 @@ class TripCatalogTest extends TestCase
}
/**
* @param list<array<string, mixed>> $rows
* @param list<array<string, mixed>> $rows
* @param (\Closure(string): list<array{type: string, uid: int}>)|null $textHits the full-text index, by boolean query
*/
private function catalog(array $rows): TripCatalog
private function catalog(array $rows, ?\Closure $textHits = null): 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')));
@@ -213,9 +302,21 @@ class TripCatalogTest extends TestCase
$repository = $this->createStub(ContentDocumentRepository::class);
$repository->method('findRecords')->willReturn($records);
$repository->method('matchRecords')->willReturn([]);
$repository->method('matchRecords')->willReturnCallback($textHits ?? static fn (): array => []);
return new TripCatalog($export, $repository, new MockClock('2026-10-08 10:00:00'), new NullLogger());
$hotels = [];
foreach (['HOT1' => "Agence X\nRésidence Eins Wochenreisen", 'HOT3' => 'Oceanpeak'] as $code => $name) {
$hotel = new Hotel();
$hotel->id = crc32($code);
$hotel->code = $code;
$hotel->name = $name;
$hotels[$hotel->id] = $hotel;
}
$hotelLoader = $this->createStub(HotelLoader::class);
$hotelLoader->method('loadAll')->willReturn($hotels);
return new TripCatalog($export, $repository, $hotelLoader, new MockClock('2026-10-08 10:00:00'), new NullLogger());
}
/**