Compare commits
2
Commits
0c6126010f
...
be0e15b3d4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be0e15b3d4 | ||
|
|
f5e6d2105d |
No files matched your search
@@ -92,6 +92,15 @@ APP_TRAVEL_PREFER_REMOTE=false
|
||||
APP_TRAVEL_ENABLE_FALLBACK=true
|
||||
APP_TRAVEL_SNAPSHOT_RETENTION_BUFFER_DAYS=14
|
||||
|
||||
# Compensates a BusPro export defect: a <zustieg_rueck> can omit its preis although the booking
|
||||
# engine still bills the return-leg Abfahrtsortzuschlag, so we undercharge by exactly one leg.
|
||||
# A boarding point costs the same in both directions, so the stated price is applied to both.
|
||||
# Off = report the export verbatim. Provisional until the vendor fixes the export; see
|
||||
# docs/buspro-database-reference.md section A7.
|
||||
# Flipping this needs `app:bpn:xml-cache-invalidate` and `app:bpn:refresh-travel-snapshot`,
|
||||
# because the cached travels and TravelSnapshot rows carry the parsed price.
|
||||
APP_BPN_LEVEL_BOARDING_POINT_PRICES=true
|
||||
|
||||
# Emails
|
||||
APP_DEFAULT_EMAIL_FROM=[email protected]
|
||||
APP_DEFAULT_EMAIL_TO=[email protected]
|
||||
|
||||
Generated
+61
-55
@@ -3372,16 +3372,16 @@
|
||||
},
|
||||
{
|
||||
"name": "nesbot/carbon",
|
||||
"version": "3.14.0",
|
||||
"version": "3.14.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/CarbonPHP/carbon.git",
|
||||
"reference": "0023eaa2c9110e47446dd512a263c69c40cd41f2"
|
||||
"reference": "34e9109535165bf1d7ff17a610492372ae73a896"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/0023eaa2c9110e47446dd512a263c69c40cd41f2",
|
||||
"reference": "0023eaa2c9110e47446dd512a263c69c40cd41f2",
|
||||
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/34e9109535165bf1d7ff17a610492372ae73a896",
|
||||
"reference": "34e9109535165bf1d7ff17a610492372ae73a896",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3473,7 +3473,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-09-12T20:45:19+00:00"
|
||||
"time": "2026-09-27T12:07:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nyholm/psr7",
|
||||
@@ -3674,20 +3674,25 @@
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-common",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
|
||||
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
|
||||
"reference": "ee15a5a2022c8383906b225055e9496a6a5e6e3b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
|
||||
"reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
|
||||
"url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/ee15a5a2022c8383906b225055e9496a6a5e6e3b",
|
||||
"reference": "ee15a5a2022c8383906b225055e9496a6a5e6e3b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^1.8",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"vimeo/psalm": "^4.25"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
@@ -3711,7 +3716,7 @@
|
||||
}
|
||||
],
|
||||
"description": "Common reflection classes used by phpdocumentor to reflect the code structure",
|
||||
"homepage": "http://www.phpdoc.org",
|
||||
"homepage": "https://www.phpdoc.org",
|
||||
"keywords": [
|
||||
"FQSEN",
|
||||
"phpDocumentor",
|
||||
@@ -3721,9 +3726,9 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
|
||||
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
|
||||
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.2.1"
|
||||
},
|
||||
"time": "2020-06-27T09:03:43+00:00"
|
||||
"time": "2025-11-10T21:21:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpdocumentor/reflection-docblock",
|
||||
@@ -4070,16 +4075,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpdoc-parser",
|
||||
"version": "2.3.5",
|
||||
"version": "2.3.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/phpstan/phpdoc-parser.git",
|
||||
"reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9"
|
||||
"reference": "1427af4647235b4a73b13940b1f1258d3584f36e"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/148cefffaf0233e4c08cc13db8a195a56dd6dfe9",
|
||||
"reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9",
|
||||
"url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1427af4647235b4a73b13940b1f1258d3584f36e",
|
||||
"reference": "1427af4647235b4a73b13940b1f1258d3584f36e",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4111,9 +4116,9 @@
|
||||
"description": "PHPDoc parser with support for nullable, intersection and generic types",
|
||||
"support": {
|
||||
"issues": "https://github.com/phpstan/phpdoc-parser/issues",
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.5"
|
||||
"source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.6"
|
||||
},
|
||||
"time": "2026-08-31T16:05:28+00:00"
|
||||
"time": "2026-09-27T20:07:45+00:00"
|
||||
},
|
||||
{
|
||||
"name": "psr/cache",
|
||||
@@ -11350,16 +11355,16 @@
|
||||
},
|
||||
{
|
||||
"name": "twig/intl-extra",
|
||||
"version": "v3.29.0",
|
||||
"version": "v3.30.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/intl-extra.git",
|
||||
"reference": "51c52470aca59f3a9715c88f6fe505133b88c752"
|
||||
"reference": "211c603b3f7d5a8f6c6543a02fe9f82c09b18025"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/51c52470aca59f3a9715c88f6fe505133b88c752",
|
||||
"reference": "51c52470aca59f3a9715c88f6fe505133b88c752",
|
||||
"url": "https://api.github.com/repos/twigphp/intl-extra/zipball/211c603b3f7d5a8f6c6543a02fe9f82c09b18025",
|
||||
"reference": "211c603b3f7d5a8f6c6543a02fe9f82c09b18025",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11398,7 +11403,7 @@
|
||||
"twig"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/twigphp/intl-extra/tree/v3.29.0"
|
||||
"source": "https://github.com/twigphp/intl-extra/tree/v3.30.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -11410,7 +11415,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-09-06T20:35:36+00:00"
|
||||
"time": "2026-09-25T05:50:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "twig/string-extra",
|
||||
@@ -11481,16 +11486,16 @@
|
||||
},
|
||||
{
|
||||
"name": "twig/twig",
|
||||
"version": "v3.29.0",
|
||||
"version": "v3.30.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twigphp/Twig.git",
|
||||
"reference": "45a3c6e9224c3377a39c7b150bb29d5d97d2c75d"
|
||||
"reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/45a3c6e9224c3377a39c7b150bb29d5d97d2c75d",
|
||||
"reference": "45a3c6e9224c3377a39c7b150bb29d5d97d2c75d",
|
||||
"url": "https://api.github.com/repos/twigphp/Twig/zipball/8c737079b726af72ff8ef3c595be9f6a810ea1ef",
|
||||
"reference": "8c737079b726af72ff8ef3c595be9f6a810ea1ef",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11545,7 +11550,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/twigphp/Twig/issues",
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.29.0"
|
||||
"source": "https://github.com/twigphp/Twig/tree/v3.30.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -11557,7 +11562,7 @@
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-09-18T09:10:14+00:00"
|
||||
"time": "2026-09-25T13:20:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "webmozart/assert",
|
||||
@@ -11776,16 +11781,16 @@
|
||||
},
|
||||
{
|
||||
"name": "composer/semver",
|
||||
"version": "3.4.4",
|
||||
"version": "3.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/composer/semver.git",
|
||||
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
|
||||
"reference": "f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
|
||||
"reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
|
||||
"url": "https://api.github.com/repos/composer/semver/zipball/f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5",
|
||||
"reference": "f7a296f4c4cf8cb8bb83e35d6951a406bb11afa5",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11827,7 +11832,7 @@
|
||||
"homepage": "http://robbast.nl"
|
||||
}
|
||||
],
|
||||
"description": "Semver library that offers utilities, version constraint parsing and validation.",
|
||||
"description": "Version comparison library that offers utilities, version constraint parsing and validation.",
|
||||
"keywords": [
|
||||
"semantic",
|
||||
"semver",
|
||||
@@ -11837,7 +11842,7 @@
|
||||
"support": {
|
||||
"irc": "ircs://irc.libera.chat:6697/composer",
|
||||
"issues": "https://github.com/composer/semver/issues",
|
||||
"source": "https://github.com/composer/semver/tree/3.4.4"
|
||||
"source": "https://github.com/composer/semver/tree/3.5.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -11849,7 +11854,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-20T19:15:30+00:00"
|
||||
"time": "2026-09-24T14:38:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/xdebug-handler",
|
||||
@@ -12089,23 +12094,23 @@
|
||||
},
|
||||
{
|
||||
"name": "fidry/cpu-core-counter",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/theofidry/cpu-core-counter.git",
|
||||
"reference": "db9508f7b1474469d9d3c53b86f817e344732678"
|
||||
"reference": "7cbeb03e286fa81b2bbe0e6bd6b8a27c71a3144d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678",
|
||||
"reference": "db9508f7b1474469d9d3c53b86f817e344732678",
|
||||
"url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/7cbeb03e286fa81b2bbe0e6bd6b8a27c71a3144d",
|
||||
"reference": "7cbeb03e286fa81b2bbe0e6bd6b8a27c71a3144d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.2 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"fidry/makefile": "^0.2.0",
|
||||
"fidry/makefile": "^0.2.0 || ^1.1.2",
|
||||
"fidry/php-cs-fixer-config": "^1.1.2",
|
||||
"phpstan/extension-installer": "^1.2.0",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
@@ -12113,6 +12118,7 @@
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpstan/phpstan-strict-rules": "^2.0",
|
||||
"phpunit/phpunit": "^8.5.31 || ^9.5.26",
|
||||
"symfony/polyfill-mbstring": "^1.33",
|
||||
"webmozarts/strict-phpunit": "^7.5"
|
||||
},
|
||||
"type": "library",
|
||||
@@ -12138,7 +12144,7 @@
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/theofidry/cpu-core-counter/issues",
|
||||
"source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0"
|
||||
"source": "https://github.com/theofidry/cpu-core-counter/tree/1.4.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -12146,7 +12152,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-14T07:29:31+00:00"
|
||||
"time": "2026-09-27T17:40:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "friendsofphp/php-cs-fixer",
|
||||
@@ -12489,11 +12495,11 @@
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.2.15",
|
||||
"version": "2.2.16",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/b158556ffd26825cf615a1c1f72fb5157a2301b7",
|
||||
"reference": "b158556ffd26825cf615a1c1f72fb5157a2301b7",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/46a6d9060e5a7763adfcc21ebcb8b504ebdbcb92",
|
||||
"reference": "46a6d9060e5a7763adfcc21ebcb8b504ebdbcb92",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -12549,7 +12555,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-09-23T12:23:07+00:00"
|
||||
"time": "2026-09-25T09:31:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpstan/phpstan-doctrine",
|
||||
@@ -13049,16 +13055,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/phpunit",
|
||||
"version": "12.5.35",
|
||||
"version": "12.5.36",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/phpunit.git",
|
||||
"reference": "345ac22e42cb14cdc7aa8428655d8bb8c82a4aab"
|
||||
"reference": "ddb775588a0ea447d1c9b190c0f29c174ce81d65"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/345ac22e42cb14cdc7aa8428655d8bb8c82a4aab",
|
||||
"reference": "345ac22e42cb14cdc7aa8428655d8bb8c82a4aab",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ddb775588a0ea447d1c9b190c0f29c174ce81d65",
|
||||
"reference": "ddb775588a0ea447d1c9b190c0f29c174ce81d65",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -13127,7 +13133,7 @@
|
||||
"support": {
|
||||
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
|
||||
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.35"
|
||||
"source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.36"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@@ -13135,7 +13141,7 @@
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2026-09-09T04:48:50+00:00"
|
||||
"time": "2026-09-25T08:47:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "react/cache",
|
||||
|
||||
@@ -185,6 +185,10 @@ services:
|
||||
arguments:
|
||||
$retentionBufferDays: '%travel_snapshot_retention_buffer_days%'
|
||||
|
||||
App\BusProNet\XmlParser\TravelParser:
|
||||
arguments:
|
||||
$levelBoardingPointPrices: '%env(bool:APP_BPN_LEVEL_BOARDING_POINT_PRICES)%'
|
||||
|
||||
App\Service\TravelDataProvider:
|
||||
arguments:
|
||||
$logger: '@monolog.logger.core'
|
||||
|
||||
@@ -225,6 +225,17 @@ JOIN dbo.ZustiegPreis zp ON zp.IDArtZustiegPreis_FS=lb.IDArtZustiegPreis_FS
|
||||
WHERE l.IDLeistung IN (@OutboundServiceId,@ReturnServiceId);
|
||||
```
|
||||
|
||||
**V 2026-09-28 — the XML export can omit the return-leg price.** Since one price row is
|
||||
surfaced twice, an asymmetric pair is an export defect: on products 701 and 2435 a
|
||||
`<zustieg_rueck>` carries no `preis` although the engine still bills the return
|
||||
`Abfahrtsortzuschlag`, so reading the export verbatim undercharges by one leg. Measured over
|
||||
all exports, a stated return price never differs from its outbound counterpart (510 pairs,
|
||||
zero counter-examples). `TravelParser` therefore levels a boarding point's price across both
|
||||
directions, switchable through `APP_BPN_LEVEL_BOARDING_POINT_PRICES` while the defect is open
|
||||
with the vendor. Flipping the flag needs `app:bpn:xml-cache-invalidate` and
|
||||
`app:bpn:refresh-travel-snapshot`. A point appearing **only** in `zustiege_rueck` without a
|
||||
price cannot be priced from our side — that is a catalog job.
|
||||
|
||||
**N — the departure surcharge is computed.** Booking responses carry a position
|
||||
`art="SON" unterart="TRA"` (*Abfahrtsortzuschlag*) with no matching row type: `ArtZuschlag`
|
||||
— the lookup behind `BuchungTNZuschlag` — holds only discounts and fees
|
||||
|
||||
@@ -8,6 +8,11 @@ use App\BusProNet\XmlCrawlerFactory;
|
||||
|
||||
class ApiResponseParser extends AbstractParser
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TravelParser $travelParser,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ResponseParserException
|
||||
*/
|
||||
@@ -81,7 +86,7 @@ class ApiResponseParser extends AbstractParser
|
||||
throw new ResponseParserException('No travel data node found in product data response');
|
||||
}
|
||||
|
||||
return (new TravelParser())->parse($travelNode->first(), ...$additionalArgs);
|
||||
return $this->travelParser->parse($travelNode->first(), ...$additionalArgs);
|
||||
case ApiClient::TYPE_AGENCIES:
|
||||
return (new AgencyParser())->parse($resultNode);
|
||||
case ApiClient::TYPE_PURCHASE_VOUCHER:
|
||||
|
||||
@@ -29,8 +29,20 @@ class TravelParser extends AbstractParser
|
||||
Constants::PSEUDO_PRICE_ROOM_CODE,
|
||||
];
|
||||
|
||||
/**
|
||||
* `$levelBoardingPointPrices` carries no default on purpose: a hand-built parser would
|
||||
* otherwise silently ignore APP_BPN_LEVEL_BOARDING_POINT_PRICES and price one travel tier
|
||||
* differently from the others. The registry gave up its own default along with it, so that
|
||||
* there is one way to build this class — through the container, or explicitly in a test.
|
||||
*
|
||||
* @param bool $levelBoardingPointPrices Whether to compensate the BusPro export defect that
|
||||
* omits a boarding point's return-leg price. The
|
||||
* container wires it from
|
||||
* APP_BPN_LEVEL_BOARDING_POINT_PRICES.
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly AgeConstraintParserRegistry $ageConstraintRegistry = new AgeConstraintParserRegistry(),
|
||||
private readonly AgeConstraintParserRegistry $ageConstraintRegistry,
|
||||
private readonly bool $levelBoardingPointPrices,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -88,7 +100,9 @@ class TravelParser extends AbstractParser
|
||||
$travel->dropOffs = $this->getPickups($node->filterXPath('.//zustiege_rueck/zustieg_rueck'), $dateTo, false);
|
||||
|
||||
// A boarding point costs the same in both directions
|
||||
$this->levelBoardingPointPrices($travel->pickups, $travel->dropOffs);
|
||||
if ($this->levelBoardingPointPrices) {
|
||||
$this->applyBoardingPointPriceLevelling($travel->pickups, $travel->dropOffs);
|
||||
}
|
||||
|
||||
$travel->guide = $this->getGuide($node);
|
||||
|
||||
@@ -465,10 +479,15 @@ class TravelParser extends AbstractParser
|
||||
* Where both directions state a price they agree in practice; the outbound one wins
|
||||
* to keep the outcome deterministic. Points without a counterpart are left as they are.
|
||||
*
|
||||
* Compensating the defect locally is provisional, so it is switchable: with
|
||||
* `$levelBoardingPointPrices` off the export is reported verbatim and the affected
|
||||
* travels undercharge the return leg again. See
|
||||
* `docs/buspro-database-reference.md` section A7.
|
||||
*
|
||||
* @param array<int, Pickup> $pickups Pickups indexed by ID (modified in place)
|
||||
* @param array<int, Pickup> $dropOffs Drop-offs indexed by ID (modified in place)
|
||||
*/
|
||||
private function levelBoardingPointPrices(array &$pickups, array $dropOffs): void
|
||||
private function applyBoardingPointPriceLevelling(array $pickups, array $dropOffs): void
|
||||
{
|
||||
foreach ($dropOffs as $pickupId => $dropOff) {
|
||||
if (false === isset($pickups[$pickupId])) {
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Tests\BusProNet\XmlParser;
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ResponseParserException;
|
||||
use App\BusProNet\XmlParser\ApiResponseParser;
|
||||
use App\BusProNet\XmlParser\TravelParser;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
@@ -22,7 +23,7 @@ class ApiResponseParserFailureTest extends TestCase
|
||||
$this->expectException(ResponseParserException::class);
|
||||
$this->expectExceptionMessage('empty response (0 bytes)');
|
||||
|
||||
(new ApiResponseParser())->parseXmlString(ApiClient::TYPE_NOTIFICATION, '');
|
||||
$this->createParser()->parseXmlString(ApiClient::TYPE_NOTIFICATION, '');
|
||||
}
|
||||
|
||||
public function testTruncatedResponseNamesTheLibxmlReason(): void
|
||||
@@ -30,7 +31,7 @@ class ApiResponseParserFailureTest extends TestCase
|
||||
$xml = '<?xml version="1.0" encoding="utf-8"?><ergebnis><satz typ="HINWEIS"></satz><hinweis';
|
||||
|
||||
try {
|
||||
(new ApiResponseParser())->parseXmlString(ApiClient::TYPE_NOTIFICATION, $xml);
|
||||
$this->createParser()->parseXmlString(ApiClient::TYPE_NOTIFICATION, $xml);
|
||||
$this->fail('Expected a ResponseParserException');
|
||||
} catch (ResponseParserException $e) {
|
||||
self::assertStringContainsString('('.strlen($xml).' bytes)', $e->getMessage());
|
||||
@@ -45,7 +46,7 @@ class ApiResponseParserFailureTest extends TestCase
|
||||
$this->expectException(ResponseParserException::class);
|
||||
$this->expectExceptionMessage('Unrecognised BusProNet response type "IRGENDWAS"');
|
||||
|
||||
(new ApiResponseParser())->parseXmlString(ApiClient::TYPE_NOTIFICATION, $xml);
|
||||
$this->createParser()->parseXmlString(ApiClient::TYPE_NOTIFICATION, $xml);
|
||||
}
|
||||
|
||||
public function testUnknownCustomerDataSubtype(): void
|
||||
@@ -55,7 +56,7 @@ class ApiResponseParserFailureTest extends TestCase
|
||||
$this->expectException(ResponseParserException::class);
|
||||
$this->expectExceptionMessage('Unrecognised customer data subtype "Irgendwas"');
|
||||
|
||||
(new ApiResponseParser())->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $xml);
|
||||
$this->createParser()->parseXmlString(ApiClient::TYPE_CUSTOMER_DATA, $xml);
|
||||
}
|
||||
|
||||
public function testDiagnosingDoesNotLeakLibxmlErrorState(): void
|
||||
@@ -64,7 +65,7 @@ class ApiResponseParserFailureTest extends TestCase
|
||||
|
||||
try {
|
||||
try {
|
||||
(new ApiResponseParser())->parseXmlString(ApiClient::TYPE_NOTIFICATION, '<ergebnis');
|
||||
$this->createParser()->parseXmlString(ApiClient::TYPE_NOTIFICATION, '<ergebnis');
|
||||
} catch (ResponseParserException) {
|
||||
}
|
||||
|
||||
@@ -74,4 +75,12 @@ class ApiResponseParserFailureTest extends TestCase
|
||||
libxml_use_internal_errors($previous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* None of these responses reach the travel parser; a stub keeps the dependency out of view.
|
||||
*/
|
||||
private function createParser(): ApiResponseParser
|
||||
{
|
||||
return new ApiResponseParser($this->createStub(TravelParser::class));
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace App\Tests\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\XmlCrawlerFactory;
|
||||
use App\BusProNet\XmlParser\AgeConstraintParserRegistry;
|
||||
use App\BusProNet\XmlParser\TravelParser;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -16,7 +17,7 @@ class TravelParserTest extends TestCase
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->parser = new TravelParser();
|
||||
$this->parser = $this->createParser();
|
||||
}
|
||||
|
||||
public function testParseServiceDescriptions(): void
|
||||
@@ -411,7 +412,27 @@ class TravelParserTest extends TestCase
|
||||
$this->assertSame(5.9, $travel->dropOffs[99]->price);
|
||||
}
|
||||
|
||||
private function parseTravelWithBoardingPoints(string $pickups, string $dropOffs): Travel
|
||||
public function testBoardingPricesAreReportedVerbatimWhenLevellingIsDisabled(): void
|
||||
{
|
||||
// The levelling compensates a vendor export defect and is therefore switchable.
|
||||
// With it off the product 701 shape undercharges the return leg again.
|
||||
$travel = $this->parseTravelWithBoardingPoints(
|
||||
'<zustieg idbuspro="18" zeit="12.12.2026 21:45:00" preis="17,90" />',
|
||||
'<zustieg_rueck idbuspro="18" />',
|
||||
$this->createParser(levelBoardingPointPrices: false)
|
||||
);
|
||||
|
||||
$this->assertSame(17.9, $travel->pickups[18]->price);
|
||||
$this->assertNull($travel->pickups[18]->priceInbound);
|
||||
$this->assertNull($travel->dropOffs[18]->price);
|
||||
}
|
||||
|
||||
private function createParser(bool $levelBoardingPointPrices = true): TravelParser
|
||||
{
|
||||
return new TravelParser(new AgeConstraintParserRegistry(), $levelBoardingPointPrices);
|
||||
}
|
||||
|
||||
private function parseTravelWithBoardingPoints(string $pickups, string $dropOffs, ?TravelParser $parser = null): Travel
|
||||
{
|
||||
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
|
||||
<reisen>
|
||||
@@ -426,7 +447,7 @@ class TravelParserTest extends TestCase
|
||||
|
||||
$crawler = XmlCrawlerFactory::create($xmlContent);
|
||||
|
||||
return $this->parser->parse($crawler->filterXPath('//reise/termin')->first());
|
||||
return ($parser ?? $this->parser)->parse($crawler->filterXPath('//reise/termin')->first());
|
||||
}
|
||||
|
||||
public function testGetPickupsAdjustsOvernightTimes(): void
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\BusProNet\XmlCrawlerFactory;
|
||||
use App\BusProNet\XmlParser\AgeConstraintParserRegistry;
|
||||
use App\BusProNet\XmlParser\TravelParser;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
@@ -487,7 +488,7 @@ class ParticipantFieldOptionsProviderBabyTest extends TestCase
|
||||
*/
|
||||
private function createTravelWithExportedSkiPasses(array $serviceIds): Travel
|
||||
{
|
||||
$parser = new TravelParser();
|
||||
$parser = new TravelParser(new AgeConstraintParserRegistry(), true);
|
||||
$crawler = XmlCrawlerFactory::create(file_get_contents('/var/www/html/var/xmlexport/Ziel_2456.xml'));
|
||||
$travelNode = $crawler->filterXPath('//reise/termin')->first();
|
||||
$travel = $parser->parse($travelNode);
|
||||
|
||||
Reference in new issue
Block a user