diff --git a/src/BusProNet/XmlLoader/TravelLoader.php b/src/BusProNet/XmlLoader/TravelLoader.php index ab4945dd..729555b3 100644 --- a/src/BusProNet/XmlLoader/TravelLoader.php +++ b/src/BusProNet/XmlLoader/TravelLoader.php @@ -307,10 +307,10 @@ class TravelLoader extends AbstractLoader $availability = $serviceAvailabilities[$service->id]; $service->available = $availability->available; - // The live status is the only signal that a contingent has run out: BusPro moves a - // Leistung to 'Anfrage' without necessarily reporting frei="0". Only overwrite when the - // response actually carries one, so a response that omits the attribute does not wipe - // the value parsed from the XML export. Mirrors TravelSnapshotManager::applyExtendedToService(). + // The live status is fresher than the nightly export's. The parser has already mapped it + // onto the export vocabulary. Only overwrite when the response actually carries one, so a + // response that omits the attribute does not wipe the value parsed from the XML export. + // Mirrors TravelSnapshotManager::applyExtendedToService(). if (null !== $availability->status && '' !== trim($availability->status)) { $service->status = $availability->status; } diff --git a/src/BusProNet/XmlParser/AbstractParser.php b/src/BusProNet/XmlParser/AbstractParser.php index 56f52c41..ae0614e1 100644 --- a/src/BusProNet/XmlParser/AbstractParser.php +++ b/src/BusProNet/XmlParser/AbstractParser.php @@ -2,6 +2,7 @@ namespace App\BusProNet\XmlParser; +use App\BusProNet\Constants; use App\BusProNet\Traits\TypeConversionTrait; use Symfony\Component\DomCrawler\Crawler; @@ -9,6 +10,19 @@ abstract class AbstractParser { use TypeConversionTrait; + /** + * Live availability wording for a Leistung, mapped onto the XML export's. + * + * The availability endpoints answer "buchbar" or "ausgebucht", while every availability rule + * compares against the export's "Frei" / "Anfrage" / "Buchungsstop". "ausgebucht" also covers + * a Leistung that would still take an inquiry (anfragemoeglich="J"); that is not offered + * either, so it maps to the booking stop. + */ + private const LIVE_SERVICE_STATUS = [ + 'buchbar' => Constants::STATUS_AVAILABLE, + 'ausgebucht' => Constants::STATUS_BLOCKED, + ]; + protected function getStringOrNullValue(Crawler $node): ?string { return 0 < $node->count() ? $node->text() : null; @@ -87,4 +101,16 @@ abstract class AbstractParser return $value; } + + /** + * Translates a live Leistung status into the export vocabulary; anything else passes through. + */ + protected function toServiceStatus(?string $liveStatus): ?string + { + if (null === $liveStatus) { + return null; + } + + return self::LIVE_SERVICE_STATUS[strtolower(trim($liveStatus))] ?? $liveStatus; + } } diff --git a/src/BusProNet/XmlParser/AvailabilitiesParser.php b/src/BusProNet/XmlParser/AvailabilitiesParser.php index 55b4102d..83707812 100644 --- a/src/BusProNet/XmlParser/AvailabilitiesParser.php +++ b/src/BusProNet/XmlParser/AvailabilitiesParser.php @@ -19,7 +19,7 @@ class AvailabilitiesParser extends AbstractParser ->each(function (Crawler $node) use (&$availabilities) { $availability = new Availability(); $availability->serviceId = (int) $node->attr('id'); - $availability->status = $node->attr('status'); + $availability->status = $this->toServiceStatus($node->attr('status')); $availability->available = (int) $node->attr('frei'); $availability->price = $this->stringToFloat($node->attr('preis')); diff --git a/src/BusProNet/XmlParser/ExtendedAvailabilitiesParser.php b/src/BusProNet/XmlParser/ExtendedAvailabilitiesParser.php index 174b6839..573f2ba0 100644 --- a/src/BusProNet/XmlParser/ExtendedAvailabilitiesParser.php +++ b/src/BusProNet/XmlParser/ExtendedAvailabilitiesParser.php @@ -30,7 +30,7 @@ class ExtendedAvailabilitiesParser extends AbstractParser $availability = new ExtendedAvailability(); $availability->serviceId = $serviceId; - $availability->status = $node->attr('status'); + $availability->status = $this->toServiceStatus($node->attr('status')); $availability->available = (int) $node->attr('frei'); $availability->price = $this->stringToFloat($node->attr('preis')); $availability->dateFrom = $this->stringToDate($node->attr('termin')); diff --git a/tests/BusProNet/XmlLoader/TravelLoaderAvailabilityTest.php b/tests/BusProNet/XmlLoader/TravelLoaderAvailabilityTest.php index a7d17ea3..e691990e 100644 --- a/tests/BusProNet/XmlLoader/TravelLoaderAvailabilityTest.php +++ b/tests/BusProNet/XmlLoader/TravelLoaderAvailabilityTest.php @@ -19,9 +19,9 @@ use Symfony\Contracts\Cache\CacheInterface; /** * Tests that patchAvailabilities() carries the live service status, not just the contingent. * - * When a Leistung's contingent runs out BusPro moves it to 'Anfrage' without necessarily - * reporting frei="0", so the status is the only reliable signal. Dropping it meant every - * availability rule read the status from the nightly XML export instead. + * Dropping the status meant every availability rule read it from the nightly XML export instead. + * The statuses here are already in export vocabulary; the parsers translate BusPro's live + * "buchbar" / "ausgebucht" (see AvailabilitiesParserTest). */ class TravelLoaderAvailabilityTest extends TestCase { @@ -45,10 +45,10 @@ class TravelLoaderAvailabilityTest extends TestCase $this->loader->patchAvailabilities( $travel, - $this->createResponse([$this->createAvailability(60, Constants::STATUS_ON_REQUEST, 3)]) + $this->createResponse([$this->createAvailability(60, Constants::STATUS_BLOCKED, 3)]) ); - $this->assertSame(Constants::STATUS_ON_REQUEST, $service->status); + $this->assertSame(Constants::STATUS_BLOCKED, $service->status); $this->assertSame(3, $service->available); } diff --git a/tests/BusProNet/XmlParser/AvailabilitiesParserTest.php b/tests/BusProNet/XmlParser/AvailabilitiesParserTest.php new file mode 100644 index 00000000..0af78486 --- /dev/null +++ b/tests/BusProNet/XmlParser/AvailabilitiesParserTest.php @@ -0,0 +1,75 @@ +parser = new AvailabilitiesParser(); + } + + public function testMapsLiveServiceStatusOntoExportVocabulary(): void + { + $xml = ' + + + + + + + +'; + + $services = $this->parser->parseServices(XmlCrawlerFactory::create($xml))->getServices(); + + $this->assertSame(Constants::STATUS_AVAILABLE, $services[201200]->status); + $this->assertSame(3, $services[201200]->available); + $this->assertSame(Constants::STATUS_BLOCKED, $services[201590]->status); + $this->assertSame(0, $services[201590]->available); + } + + public function testPassesExportVocabularyAndUnknownStatusThrough(): void + { + $xml = ' + + + + + + +'; + + $services = $this->parser->parseServices(XmlCrawlerFactory::create($xml))->getServices(); + + $this->assertSame(Constants::STATUS_ON_REQUEST, $services[1]->status); + $this->assertSame('unbekannt', $services[2]->status); + $this->assertNull($services[3]->status); + } + + public function testLeavesTravelLevelStatusUntouched(): void + { + $xml = ' + + + +'; + + $response = $this->parser->parseServices(XmlCrawlerFactory::create($xml)); + + $this->assertSame('frei', $response->travelStatus); + $this->assertSame(['F', 'O', 'A'], $response->allowedBookingStatus); + } +} diff --git a/tests/BusProNet/XmlParser/ExtendedAvailabilitiesParserTest.php b/tests/BusProNet/XmlParser/ExtendedAvailabilitiesParserTest.php index 68ada31b..59e89e02 100644 --- a/tests/BusProNet/XmlParser/ExtendedAvailabilitiesParserTest.php +++ b/tests/BusProNet/XmlParser/ExtendedAvailabilitiesParserTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Tests\BusProNet\XmlParser; +use App\BusProNet\Constants; use App\BusProNet\XmlCrawlerFactory; use App\BusProNet\XmlParser\ExtendedAvailabilitiesParser; use PHPUnit\Framework\TestCase; @@ -48,6 +49,22 @@ class ExtendedAvailabilitiesParserTest extends TestCase $this->assertTrue($availability->mandatory); } + public function testMapsLiveServiceStatusOntoExportVocabulary(): void + { + $xml = ' + + + + + +'; + + $services = $this->parser->parseServices(XmlCrawlerFactory::create($xml))->getServices(); + + $this->assertSame(Constants::STATUS_AVAILABLE, $services[103]->status); + $this->assertSame(Constants::STATUS_BLOCKED, $services[104]->status); + } + public function testIgnoresEmptyAgeAndMandatoryAttributes(): void { $xml = '