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 = '