feat: unfiltered product export for the website's catalogue import
This commit is contained in:
1 parent
272c811f4f
commit
1d42565a84
11 files changed
+970
-33
No files matched your search
@@ -5,8 +5,9 @@ declare(strict_types=1);
|
||||
namespace App\Tests\Controller\Api;
|
||||
|
||||
use App\Controller\Api\ExportController;
|
||||
use App\Model\TravelExport;
|
||||
use App\Model\TravelExportQuery;
|
||||
use App\Model\Export;
|
||||
use App\Model\ExportQuery;
|
||||
use App\Service\ProductExportBuilder;
|
||||
use App\Service\TravelExportBuilder;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\Container;
|
||||
@@ -16,7 +17,7 @@ class ExportControllerTest extends TestCase
|
||||
{
|
||||
public function testPaginatesAndReportsWholeExportFigures(): void
|
||||
{
|
||||
$meta = $this->call(new TravelExportQuery(1, 5))['meta'];
|
||||
$meta = $this->call(new ExportQuery(1, 5))['meta'];
|
||||
|
||||
self::assertSame(1, $meta['page']);
|
||||
self::assertSame(5, $meta['perPage']);
|
||||
@@ -27,8 +28,8 @@ class ExportControllerTest extends TestCase
|
||||
public function testCountersAreIdenticalOnEveryPage(): void
|
||||
{
|
||||
// Whole-export figures repeated per page, so a consumer can abort after page 1.
|
||||
$first = $this->call(new TravelExportQuery(1, 5))['meta'];
|
||||
$last = $this->call(new TravelExportQuery(3, 5))['meta'];
|
||||
$first = $this->call(new ExportQuery(1, 5))['meta'];
|
||||
$last = $this->call(new ExportQuery(3, 5))['meta'];
|
||||
|
||||
foreach (['total', 'totalPages', 'skipped', 'pickupsWithoutMasterData', 'sourceGeneratedAt', 'generatedAt'] as $key) {
|
||||
self::assertSame($first[$key], $last[$key], $key.' must not vary by page');
|
||||
@@ -37,8 +38,8 @@ class ExportControllerTest extends TestCase
|
||||
|
||||
public function testConsecutivePagesDoNotOverlap(): void
|
||||
{
|
||||
$first = array_column($this->call(new TravelExportQuery(1, 5))['data'], 'busProId');
|
||||
$second = array_column($this->call(new TravelExportQuery(2, 5))['data'], 'busProId');
|
||||
$first = array_column($this->call(new ExportQuery(1, 5))['data'], 'busProId');
|
||||
$second = array_column($this->call(new ExportQuery(2, 5))['data'], 'busProId');
|
||||
|
||||
self::assertSame([1, 2, 3, 4, 5], $first);
|
||||
self::assertSame([6, 7, 8, 9, 10], $second);
|
||||
@@ -47,7 +48,7 @@ class ExportControllerTest extends TestCase
|
||||
|
||||
public function testTheLastPageIsShort(): void
|
||||
{
|
||||
$data = $this->call(new TravelExportQuery(3, 5))['data'];
|
||||
$data = $this->call(new ExportQuery(3, 5))['data'];
|
||||
|
||||
self::assertCount(2, $data);
|
||||
self::assertSame([11, 12], array_column($data, 'busProId'));
|
||||
@@ -55,7 +56,7 @@ class ExportControllerTest extends TestCase
|
||||
|
||||
public function testAPageBeyondTheEndIsEmptyRatherThanAnError(): void
|
||||
{
|
||||
$body = $this->call(new TravelExportQuery(99, 5));
|
||||
$body = $this->call(new ExportQuery(99, 5));
|
||||
|
||||
self::assertSame([], $body['data']);
|
||||
self::assertSame(12, $body['meta']['total']);
|
||||
@@ -84,9 +85,9 @@ class ExportControllerTest extends TestCase
|
||||
public function testAnUnknownSourceExportIsNullRatherThanOmitted(): void
|
||||
{
|
||||
$builder = $this->createStub(TravelExportBuilder::class);
|
||||
$builder->method('build')->willReturn(new TravelExport([], 0, 0, new \DateTimeImmutable()));
|
||||
$builder->method('build')->willReturn(new Export([], 0, 0, new \DateTimeImmutable()));
|
||||
|
||||
$meta = json_decode((string) $this->controller($builder)->travels(new TravelExportQuery())->getContent(), true)['meta'];
|
||||
$meta = json_decode((string) $this->controller($builder)->travels(new ExportQuery())->getContent(), true)['meta'];
|
||||
|
||||
self::assertArrayHasKey('sourceGeneratedAt', $meta);
|
||||
self::assertNull($meta['sourceGeneratedAt']);
|
||||
@@ -103,13 +104,57 @@ class ExportControllerTest extends TestCase
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
|
||||
$this->controller($builder)->travels(new TravelExportQuery());
|
||||
$this->controller($builder)->travels(new ExportQuery());
|
||||
}
|
||||
|
||||
public function testProductsUseTheSameEnvelopeAndPaging(): void
|
||||
{
|
||||
$rows = [];
|
||||
for ($i = 1; $i <= 3; ++$i) {
|
||||
$rows[] = ['date' => ['busProId' => $i], 'hotel' => ['busProId' => 1]];
|
||||
}
|
||||
|
||||
$productBuilder = $this->createStub(ProductExportBuilder::class);
|
||||
$productBuilder->method('build')->willReturn(new Export(
|
||||
$rows,
|
||||
2,
|
||||
5,
|
||||
new \DateTimeImmutable('2026-09-29 03:20:00', new \DateTimeZone('Europe/Berlin')),
|
||||
new \DateTimeImmutable('2026-09-28 23:10:15', new \DateTimeZone('Europe/Berlin')),
|
||||
));
|
||||
|
||||
$response = $this->controller($this->createStub(TravelExportBuilder::class), $productBuilder)
|
||||
->products(new ExportQuery(2, 2));
|
||||
$body = json_decode((string) $response->getContent(), true);
|
||||
|
||||
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
self::assertSame([3], array_column(array_column($body['data'], 'date'), 'busProId'));
|
||||
self::assertSame([
|
||||
'page' => 2,
|
||||
'perPage' => 2,
|
||||
'total' => 3,
|
||||
'totalPages' => 2,
|
||||
'skipped' => 2,
|
||||
'pickupsWithoutMasterData' => 5,
|
||||
'sourceGeneratedAt' => '2026-09-28T23:10:15+02:00',
|
||||
'generatedAt' => '2026-09-29T03:20:00+02:00',
|
||||
], $body['meta']);
|
||||
}
|
||||
|
||||
public function testAProductBuilderFailureIsNotDowngradedToATwoHundred(): void
|
||||
{
|
||||
$productBuilder = $this->createStub(ProductExportBuilder::class);
|
||||
$productBuilder->method('build')->willThrowException(new \RuntimeException('cache exploded'));
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
|
||||
$this->controller($this->createStub(TravelExportBuilder::class), $productBuilder)->products(new ExportQuery());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function call(?TravelExportQuery $query = null, int $skipped = 0, int $withoutMasterData = 0): array
|
||||
private function call(?ExportQuery $query = null, int $skipped = 0, int $withoutMasterData = 0): array
|
||||
{
|
||||
$rows = [];
|
||||
for ($i = 1; $i <= 12; ++$i) {
|
||||
@@ -117,7 +162,7 @@ class ExportControllerTest extends TestCase
|
||||
}
|
||||
|
||||
$builder = $this->createStub(TravelExportBuilder::class);
|
||||
$builder->method('build')->willReturn(new TravelExport(
|
||||
$builder->method('build')->willReturn(new Export(
|
||||
$rows,
|
||||
$skipped,
|
||||
$withoutMasterData,
|
||||
@@ -125,16 +170,16 @@ class ExportControllerTest extends TestCase
|
||||
new \DateTimeImmutable('2026-09-28 23:10:15', new \DateTimeZone('Europe/Berlin')),
|
||||
));
|
||||
|
||||
$response = $this->controller($builder)->travels($query ?? new TravelExportQuery());
|
||||
$response = $this->controller($builder)->travels($query ?? new ExportQuery());
|
||||
|
||||
self::assertSame(Response::HTTP_OK, $response->getStatusCode());
|
||||
|
||||
return json_decode((string) $response->getContent(), true);
|
||||
}
|
||||
|
||||
private function controller(TravelExportBuilder $builder): ExportController
|
||||
private function controller(TravelExportBuilder $builder, ?ProductExportBuilder $productBuilder = null): ExportController
|
||||
{
|
||||
$controller = new ExportController($builder);
|
||||
$controller = new ExportController($builder, $productBuilder ?? $this->createStub(ProductExportBuilder::class));
|
||||
|
||||
// No 'serializer' service registered, so AbstractController::json() falls back to
|
||||
// JsonResponse -- which is what this endpoint produces in production anyway.
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<reisen>
|
||||
<reise id="1" idbuspro="1127" code="DKIWE">
|
||||
<!-- Two hotels on one date: one row per pair, date-level data shared, hotel-level data not. -->
|
||||
<termin id="1" idbuspro="900" idprodukt="1127" termin="04.12.2026" bis="06.12.2026" code="DKIWE041226" reiseart="F">
|
||||
<selektiongruppe id="1" idbuspro="1" bezeichnung="Reisezeitraum">
|
||||
<selektion id="1" idbuspro="863" bezeichnung="Wintersportreise" />
|
||||
</selektiongruppe>
|
||||
<selektiongruppe id="2" idbuspro="7" bezeichnung="Leistungen Skipass">
|
||||
<selektion id="1" idbuspro="92" bezeichnung="3 Tage Skipass" />
|
||||
<selektion id="2" idbuspro="93" bezeichnung="Skipass Region" />
|
||||
</selektiongruppe>
|
||||
<selektiongruppe id="3" idbuspro="99" bezeichnung="Leistungen Personal" />
|
||||
<text>Davos Klosters</text>
|
||||
<lei_befoerderung>
|
||||
<leistung id="1" idbuspro="193804" unterart="PKW">
|
||||
<text>Eigene Anreise mit Rabatt</text>
|
||||
<preis>-50,00</preis>
|
||||
<status>Frei</status>
|
||||
<richtung>HIN</richtung>
|
||||
</leistung>
|
||||
<!-- A return-leg bus with a guide: the guide must not be taken from it. -->
|
||||
<leistung id="2" idbuspro="193811" unterart="BUS">
|
||||
<text>Bus Rückfahrt</text>
|
||||
<preis>0,00</preis>
|
||||
<status>Buchungsstop</status>
|
||||
<richtung>RUECK</richtung>
|
||||
<zustiegsplanung>
|
||||
<reiseleiter><name>Rück Leiter</name><telefon>000</telefon></reiseleiter>
|
||||
</zustiegsplanung>
|
||||
</leistung>
|
||||
<leistung id="3" idbuspro="193802" unterart="BUS">
|
||||
<text>Bus Hinfahrt</text>
|
||||
<preis>0,00</preis>
|
||||
<status>Frei</status>
|
||||
<richtung>HIN</richtung>
|
||||
<zustiegsplanung>
|
||||
<reiseleiter><name>Hin Leiterin</name><telefon>0171 123</telefon></reiseleiter>
|
||||
</zustiegsplanung>
|
||||
</leistung>
|
||||
<leistung id="4" idbuspro="193820" unterart="BAH">
|
||||
<text>Zug</text>
|
||||
<preis>1.234,50</preis>
|
||||
<status>Anfrage</status>
|
||||
<richtung>HIN</richtung>
|
||||
</leistung>
|
||||
</lei_befoerderung>
|
||||
<lei_sonstiges>
|
||||
<leistung id="1" idbuspro="193805" unterart="SON">
|
||||
<text>Keycard-Pfand (Pflichtabgabe)</text>
|
||||
<preis>5,40</preis>
|
||||
<status>Frei</status>
|
||||
</leistung>
|
||||
<leistung id="2" idbuspro="193830" unterart="EIN">
|
||||
<text>Einzelzimmerzuschlag</text>
|
||||
<status>Frei</status>
|
||||
</leistung>
|
||||
</lei_sonstiges>
|
||||
<hotel id="1" code="DKIWEKT" idbuspro="222931">
|
||||
<!-- Same per-list id as the date-level Keycard entry, different service. -->
|
||||
<lei_sonstiges>
|
||||
<leistung id="1" idbuspro="193840" unterart="SPA">
|
||||
<text>Skipass Hotel</text>
|
||||
<preis>60,00</preis>
|
||||
<status>Frei</status>
|
||||
</leistung>
|
||||
</lei_sonstiges>
|
||||
<zimmer>
|
||||
<preis zimmercode="MBZET " idbuspro_zimmer="74" zimmertext="Bett im Mehrbettzimmer" MinPax="1" MaxPax="1" naechte="2" kat="GR" vpcode="Lt. Auss." idbuspro_vp="2" preis="229,50" status="Frei" verfuegbar="24" />
|
||||
<preis zimmercode="PDGS" idbuspro_zimmer="999" zimmertext="Pseudo" MinPax="1" MaxPax="1" naechte="2" preis="299,00" status="Frei" verfuegbar="0" />
|
||||
<preis zimmercode="3erET" idbuspro_zimmer="98" zimmertext="3er Zimmer" MinPax="3" MaxPax="3" naechte="2" kat="GR" vpcode="HP" idbuspro_vp="3" preis="259,00" status="Buchungsstop" verfuegbar="0" />
|
||||
</zimmer>
|
||||
</hotel>
|
||||
<hotel id="2" code="DKIWEK2" idbuspro="222932">
|
||||
<zimmer>
|
||||
<preis zimmercode="DZ" idbuspro_zimmer="11" zimmertext="Doppelzimmer" MinPax="2" MaxPax="2" naechte="2" kat="DZ" vpcode="HP" idbuspro_vp="3" preis="" status="Anfrage" verfuegbar="" />
|
||||
</zimmer>
|
||||
</hotel>
|
||||
<zustiege>
|
||||
<zustieg id="1" idbuspro="4" zeit="03.12.2026 23:30:00" preis="5,90" />
|
||||
<!-- Bare time after the 23:30 above: rolls onto the next calendar day. -->
|
||||
<zustieg id="2" idbuspro="27" zeit="01:45" />
|
||||
<!-- Referenced but absent from zustiege.xml. -->
|
||||
<zustieg id="3" idbuspro="55" zeit="03.12.2026 21:00:00" preis="0,00" />
|
||||
<!-- No id: the consumer drops it, not the export. -->
|
||||
<zustieg id="4" idbuspro="0" zeit="03.12.2026 20:00:00" />
|
||||
</zustiege>
|
||||
<zustiege_rueck>
|
||||
<!-- Priced on the return leg only: levelling would copy 3,00 onto the outbound entry. -->
|
||||
<zustieg_rueck id="1" idbuspro="27" preis="3,00" />
|
||||
</zustiege_rueck>
|
||||
</termin>
|
||||
<!-- Unparseable date: its hotel row is dropped and counted. -->
|
||||
<termin id="2" idbuspro="901" termin="kaputt" bis="06.12.2026" code="DKIWE000000">
|
||||
<hotel id="1" code="DKIWEKT" idbuspro="222931" />
|
||||
</termin>
|
||||
</reise>
|
||||
</reisen>
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
use App\BusProNet\XmlParser\AgeConstraintParserRegistry;
|
||||
use App\BusProNet\XmlParser\ProductExportParser;
|
||||
use App\BusProNet\XmlParser\TravelParser;
|
||||
use App\Model\Export;
|
||||
use App\Service\ProductExportBuilder;
|
||||
use League\Flysystem\DirectoryListing;
|
||||
use League\Flysystem\FileAttributes;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\NullLogger;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\Cache\Adapter\TagAwareAdapter;
|
||||
|
||||
class ProductExportBuilderTest extends TestCase
|
||||
{
|
||||
public function testEmitsOneRowPerTravelDateAndHotelPair(): void
|
||||
{
|
||||
$export = $this->build();
|
||||
|
||||
$keys = array_map(
|
||||
static fn (array $row): string => $row['date']['busProId'].'-'.$row['hotel']['busProId'],
|
||||
$export->rows,
|
||||
);
|
||||
|
||||
self::assertSame(['900-222931', '900-222932'], $keys);
|
||||
}
|
||||
|
||||
public function testCarriesIdentityFromTheTravelXmlAlone(): void
|
||||
{
|
||||
$row = $this->rowFor('900-222931');
|
||||
|
||||
self::assertSame(['code' => 'DKIWE', 'busProId' => 1127], $row['product']);
|
||||
self::assertSame([
|
||||
'code' => 'DKIWE041226',
|
||||
'busProId' => 900,
|
||||
'from' => '2026-12-04',
|
||||
'to' => '2026-12-06',
|
||||
'type' => 'F',
|
||||
], $row['date']);
|
||||
self::assertSame(['code' => 'DKIWEKT', 'busProId' => 222931], $row['hotel']);
|
||||
}
|
||||
|
||||
public function testDropsAndCountsRowsOfAnUnparseableDate(): void
|
||||
{
|
||||
self::assertSame(1, $this->build()->skipped);
|
||||
}
|
||||
|
||||
public function testEmitsEveryRoomButThePseudoRoomVerbatim(): void
|
||||
{
|
||||
$rooms = $this->rowFor('900-222931')['rooms'];
|
||||
|
||||
self::assertSame(['MBZET', '3erET'], array_column($rooms, 'code'), 'PDGS is not a room; the code is trimmed');
|
||||
self::assertSame([
|
||||
'busProId' => 74,
|
||||
'code' => 'MBZET',
|
||||
'label' => 'Bett im Mehrbettzimmer',
|
||||
'category' => 'GR',
|
||||
'minPax' => 1,
|
||||
'maxPax' => 1,
|
||||
'nights' => 2,
|
||||
'price' => 229.5,
|
||||
'available' => 24,
|
||||
'status' => 'Frei',
|
||||
'boardCode' => 'Lt. Auss.',
|
||||
'boardBusProId' => 2,
|
||||
], $rooms[0]);
|
||||
|
||||
// Buchungsstop rooms reach the consumer: "ausgebucht" is its call.
|
||||
self::assertSame('Buchungsstop', $rooms[1]['status']);
|
||||
self::assertSame(3, $rooms[1]['maxPax'], 'MaxPax is read despite its casing');
|
||||
}
|
||||
|
||||
public function testAMissingRoomPriceOrAvailabilityIsNullNotZero(): void
|
||||
{
|
||||
$room = $this->rowFor('900-222932')['rooms'][0];
|
||||
|
||||
self::assertNull($room['price']);
|
||||
self::assertNull($room['available']);
|
||||
}
|
||||
|
||||
public function testThePseudoRoomBecomesThePseudoPriceOfItsHotelOnly(): void
|
||||
{
|
||||
self::assertSame(299.0, $this->rowFor('900-222931')['pseudoPrice']);
|
||||
self::assertNull($this->rowFor('900-222932')['pseudoPrice']);
|
||||
}
|
||||
|
||||
public function testKeepsTheThreeServiceListsApartAndUnfiltered(): void
|
||||
{
|
||||
$first = $this->rowFor('900-222931');
|
||||
$second = $this->rowFor('900-222932');
|
||||
|
||||
self::assertSame([193804, 193811, 193802, 193820], array_column($first['transportServices'], 'busProId'), 'RUECK and Buchungsstop are kept');
|
||||
self::assertSame([193805, 193830], array_column($first['dateServices'], 'busProId'), 'EIN is kept; hotel services do not leak in');
|
||||
self::assertSame([193840], array_column($first['hotelServices'], 'busProId'));
|
||||
self::assertSame([], $second['hotelServices'], 'a hotel without its own list gets an empty one, not the date-level one');
|
||||
self::assertSame($first['dateServices'], $second['dateServices']);
|
||||
}
|
||||
|
||||
public function testServicesAreNumericAndNullWhereAbsent(): void
|
||||
{
|
||||
$row = $this->rowFor('900-222931');
|
||||
|
||||
self::assertSame([
|
||||
'busProId' => 193804,
|
||||
'subType' => 'PKW',
|
||||
'label' => 'Eigene Anreise mit Rabatt',
|
||||
'price' => -50.0,
|
||||
'status' => 'Frei',
|
||||
'direction' => 'HIN',
|
||||
], $row['transportServices'][0]);
|
||||
self::assertSame(1234.5, $row['transportServices'][3]['price'], 'thousands separator');
|
||||
|
||||
self::assertNull($row['dateServices'][0]['direction'], 'general services have no direction');
|
||||
self::assertNull($row['dateServices'][1]['price'], 'a missing price is null, not 0');
|
||||
}
|
||||
|
||||
public function testEmitsEverySelectionGroupInSourceOrder(): void
|
||||
{
|
||||
$groups = $this->rowFor('900-222931')['selectionGroups'];
|
||||
|
||||
self::assertSame([1, 7, 99], array_column($groups, 'busProId'));
|
||||
self::assertSame('Leistungen Skipass', $groups[1]['label']);
|
||||
self::assertSame([
|
||||
['busProId' => 92, 'label' => '3 Tage Skipass'],
|
||||
['busProId' => 93, 'label' => 'Skipass Region'],
|
||||
], $groups[1]['selections']);
|
||||
self::assertSame([], $groups[2]['selections']);
|
||||
}
|
||||
|
||||
public function testTakesTheGuideFromTheOutboundBus(): void
|
||||
{
|
||||
self::assertSame(['name' => 'Hin Leiterin', 'phone' => '0171 123'], $this->rowFor('900-222931')['guide']);
|
||||
}
|
||||
|
||||
public function testTheGuideIsNullWithoutAnOutboundOne(): void
|
||||
{
|
||||
$xml = str_replace('<name>Hin Leiterin</name>', '', (string) file_get_contents(__DIR__.'/../Resources/product_export_data.xml'));
|
||||
$xml = preg_replace('#<leistung id="3" idbuspro="193802".*?</leistung>#s', '', $xml);
|
||||
|
||||
self::assertNull($this->build((string) $xml)->rows[0]['guide']);
|
||||
}
|
||||
|
||||
public function testEmitsPickupsVerbatimWithResolvedDatetimes(): void
|
||||
{
|
||||
$pickups = $this->rowFor('900-222931')['pickups'];
|
||||
|
||||
self::assertSame([4, 27, 55, 0], array_column($pickups, 'busProId'), 'source order, id 0 kept');
|
||||
self::assertSame([
|
||||
'busProId' => 4,
|
||||
'code' => 'Z4',
|
||||
'city' => 'Stadt 4',
|
||||
'street' => 'Strasse 4',
|
||||
'price' => 5.9,
|
||||
'datetime' => '2026-12-03T23:30:00+01:00',
|
||||
], $pickups[0]);
|
||||
self::assertNull($pickups[1]['price'], 'an absent price is null');
|
||||
self::assertSame('2026-12-04T01:45:00+01:00', $pickups[1]['datetime']);
|
||||
self::assertSame(0.0, $pickups[2]['price'], 'a stated 0,00 is kept as 0');
|
||||
self::assertNull($pickups[2]['code']);
|
||||
}
|
||||
|
||||
public function testDoesNotLevelBoardingPointPrices(): void
|
||||
{
|
||||
// Point 27 is priced on the return leg only. The booking path levels that onto the
|
||||
// outbound entry; the website publishes the outbound price as exported.
|
||||
self::assertNull($this->rowFor('900-222931')['pickups'][1]['price']);
|
||||
}
|
||||
|
||||
public function testCountsPickupsWithoutMasterDataButNotTheIdlessOne(): void
|
||||
{
|
||||
self::assertSame(1, $this->build()->pickupsWithoutMasterData);
|
||||
}
|
||||
|
||||
public function testCarriesTheSourceExportTimestamp(): void
|
||||
{
|
||||
$export = $this->build(null, "22.09.2026 11:10:15\r\nLetzte \xDCbertragung\r\nAnzahl \xDCbertragene Dateien: 177\r\nVollexport\r\n");
|
||||
|
||||
self::assertSame('2026-09-22T11:10:15+02:00', $export->sourceGeneratedAt?->format('c'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function rowFor(string $key): array
|
||||
{
|
||||
foreach ($this->build()->rows as $row) {
|
||||
if ($row['date']['busProId'].'-'.$row['hotel']['busProId'] === $key) {
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
|
||||
self::fail('No row for '.$key);
|
||||
}
|
||||
|
||||
private function build(?string $xml = null, ?string $marker = null): Export
|
||||
{
|
||||
$xml ??= (string) file_get_contents(__DIR__.'/../Resources/product_export_data.xml');
|
||||
|
||||
$filesystem = $this->createStub(FilesystemOperator::class);
|
||||
$filesystem->method('listContents')->willReturn(new DirectoryListing([new FileAttributes('Ziel_1127.xml')]));
|
||||
$filesystem->method('read')->willReturnCallback(
|
||||
static fn (string $path): string => 'uebertragung.info' === $path ? (string) $marker : $xml
|
||||
);
|
||||
$filesystem->method('fileExists')->willReturnCallback(
|
||||
static fn (string $path): bool => 'uebertragung.info' === $path && null !== $marker
|
||||
);
|
||||
|
||||
$pickupLoader = $this->createStub(PickupLoader::class);
|
||||
$pickupLoader->method('loadAll')->willReturn([
|
||||
4 => $this->pickup(4),
|
||||
27 => $this->pickup(27),
|
||||
]);
|
||||
|
||||
return (new ProductExportBuilder(
|
||||
$pickupLoader,
|
||||
// Levelling on, to prove the export does not go through it.
|
||||
new TravelParser(new AgeConstraintParserRegistry([]), true),
|
||||
new ProductExportParser(),
|
||||
$filesystem,
|
||||
new TagAwareAdapter(new ArrayAdapter()),
|
||||
new NullLogger(),
|
||||
))->build();
|
||||
}
|
||||
|
||||
private function pickup(int $id): Pickup
|
||||
{
|
||||
$pickup = new Pickup();
|
||||
$pickup->id = $id;
|
||||
$pickup->code = 'Z'.$id;
|
||||
$pickup->city = 'Stadt '.$id;
|
||||
$pickup->street = 'Strasse '.$id;
|
||||
|
||||
return $pickup;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ use App\BusProNet\XmlLoader\HotelLoader;
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
use App\BusProNet\XmlParser\AgeConstraintParserRegistry;
|
||||
use App\BusProNet\XmlParser\TravelParser;
|
||||
use App\Model\TravelExport;
|
||||
use App\Model\Export;
|
||||
use App\Service\TravelExportBuilder;
|
||||
use League\Flysystem\DirectoryListing;
|
||||
use League\Flysystem\FileAttributes;
|
||||
@@ -182,7 +182,7 @@ class TravelExportBuilderTest extends TestCase
|
||||
/**
|
||||
* @param list<string> $files
|
||||
*/
|
||||
private function build(?string $xml = null, array $files = ['Ziel_1.xml'], ?string $marker = null): TravelExport
|
||||
private function build(?string $xml = null, array $files = ['Ziel_1.xml'], ?string $marker = null): Export
|
||||
{
|
||||
$contents = [
|
||||
'Ziel_1.xml' => $xml ?? (string) file_get_contents(__DIR__.'/../Resources/travel_export_data.xml'),
|
||||
|
||||
Reference in new issue
Block a user