From 6d2db1b258d820f32b3eb1d0a6e31aeb94430691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Thu, 15 Oct 2026 17:26:12 +0200 Subject: [PATCH] feat: add the product code to the travel export --- docs/api-consumer-guide.md | 6 ++++++ src/Service/TravelExportBuilder.php | 5 +++++ tests/Service/TravelExportBuilderTest.php | 16 ++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/docs/api-consumer-guide.md b/docs/api-consumer-guide.md index 9145f6d6..128e29ab 100644 --- a/docs/api-consumer-guide.md +++ b/docs/api-consumer-guide.md @@ -172,6 +172,7 @@ an error: it returns `200` with an empty `data` array and the usual `meta`. { "busProId": 12794, "code": "PCJSER261226", + "productCode": "PCJSER", "product": "Semesterende - Portes du Soleil - Joliment", "dateFrom": "2026-12-26", "dateTo": "2026-12-30", @@ -212,6 +213,11 @@ an error: it returns `200` with an empty `data` array and the usual `meta`. is always an int and never null; `hotel` is always an object and never null; `pickups` is always an array, possibly empty, never null. +**`productCode`** is the code of the travel (product) the date belongs to, e.g. `DKIWE`. Key on it to +recognise dates of the same product. Do not derive it from the date `code` by cutting off the date: some +products run dates under a different code (`DWWMP` runs `DWWMW…`), and group travels carry each group's own +code on the date. It is `null` for the few travels that have no code upstream. + **`meta` describes the whole export, not the page.** `total`, `totalPages`, `skipped`, `pickupsWithoutMasterData`, `sourceGeneratedAt` and `generatedAt` are identical on every page, so you can read them once on page 1 and decide whether to walk the rest. `total` is the row count **after** skips, and `totalPages` is `ceil(total / perPage)`. diff --git a/src/Service/TravelExportBuilder.php b/src/Service/TravelExportBuilder.php index ad4f05e5..fe370388 100644 --- a/src/Service/TravelExportBuilder.php +++ b/src/Service/TravelExportBuilder.php @@ -157,9 +157,14 @@ class TravelExportBuilder $pickupsWithoutMasterData, ); + // The product a date belongs to, from the enclosing . Not derivable from the + // date code: DWWMP runs dates coded DWWMW…, and group travels carry each group's code. + $productNode = $node->getNode(0)?->parentNode; + $travel = [ 'busProId' => $travelId, 'code' => $node->attr('code'), + 'productCode' => $productNode instanceof \DOMElement ? $this->trimOrNull($productNode->getAttribute('code')) : null, 'product' => $this->trimOrNull($this->getStringOrNullValue($node->filterXPath('.//text'))), 'dateFrom' => $dateFrom->format('Y-m-d'), 'dateTo' => $dateTo->format('Y-m-d'), diff --git a/tests/Service/TravelExportBuilderTest.php b/tests/Service/TravelExportBuilderTest.php index aca3818e..759b42af 100644 --- a/tests/Service/TravelExportBuilderTest.php +++ b/tests/Service/TravelExportBuilderTest.php @@ -42,6 +42,7 @@ class TravelExportBuilderTest extends TestCase self::assertSame(900, $row['busProId']); self::assertIsInt($row['busProId']); self::assertSame('SBW261226', $row['code']); + self::assertSame('SBW', $row['productCode']); self::assertSame('Semesterende - Portes du Soleil', $row['product'], 'the product label is trimmed'); self::assertSame('2026-12-26', $row['dateFrom']); self::assertSame('2026-12-30', $row['dateTo']); @@ -56,6 +57,21 @@ class TravelExportBuilderTest extends TestCase ], $row['hotel']); } + public function testCarriesTheProductCodeOfTheEnclosingTravelRatherThanTheDateCode(): void + { + $rows = $this->build('')->rows; + + self::assertSame('DWWMP', $rows[0]['productCode'], 'taken from , trimmed'); + self::assertSame('DWWMW010127', $rows[0]['code']); + } + + public function testTheProductCodeIsNullWhenTheTravelCarriesNone(): void + { + $rows = $this->build('')->rows; + + self::assertNull($rows[0]['productCode']); + } + public function testResolvesBothZeitShapesAndRollsAnOvernightPickupOntoTheNextDay(): void { $pickups = array_column($this->rowFor('900-1')['pickups'], null, 'busProId');