fix: revert pickup pricing to match current BusPro behavior

Reverts split pricing calculation to use outbound-only pricing,
matching BusPro's current behavior where pickup is only charged when
outbound transportation is BUS.

- Use pickup->price directly instead of calculateEffectivePrice()
- PKW+BUS scenario now correctly charges €0 (matches BusPro loophole)
- priceOutbound/priceInbound remain populated for future activation
- Updated documentation with current behavior and future plan
This commit is contained in:
Björn Fromme
2026-01-21 17:18:09 +01:00
parent 2aeed762fd
commit 9471abce94
6 changed files with 262 additions and 11 deletions
@@ -233,10 +233,20 @@ class BookingDataProcessor
$participant->transportationInbound = $travel->transportationServices[$participant->transportationInbound->id];
}
// Enrich pickup (only outbound - API limitation: pickups are only supported when
// outbound transportation is bus; inbound-only bus bookings cannot have pickups)
// Enrich pickup with data from travel (includes both outbound and inbound prices)
// The outbound pickups already have inbound prices merged in from TravelParser
if (null !== $participant->pickup && isset($travel->pickupsOutbound[$participant->pickup->id])) {
$participant->pickup = $travel->pickupsOutbound[$participant->pickup->id];
$enrichedPickup = $travel->pickupsOutbound[$participant->pickup->id];
// Copy all properties from enriched pickup to participant's pickup
$participant->pickup->price = $enrichedPickup->price;
$participant->pickup->priceOutbound = $enrichedPickup->priceOutbound;
$participant->pickup->priceInbound = $enrichedPickup->priceInbound;
$participant->pickup->time = $enrichedPickup->time;
$participant->pickup->city = $enrichedPickup->city;
$participant->pickup->street = $enrichedPickup->street;
$participant->pickup->postalCode = $enrichedPickup->postalCode;
$participant->pickup->code = $enrichedPickup->code;
}
// Enrich insurance
+77
View File
@@ -39,6 +39,12 @@ class Pickup
#[Groups(['api:single'])]
public ?float $price = null;
#[Groups(['api:single'])]
public ?float $priceOutbound = null;
#[Groups(['api:single'])]
public ?float $priceInbound = null;
#[Groups(['api:booking'])]
public array $mapping = [];
@@ -90,4 +96,75 @@ class Pickup
return sprintf('%s (€%s)', $label, number_format($this->price, 2, ',', '.'));
}
/**
* Gets the formatted label with context-aware pricing based on transportation directions.
*
* Calculates the effective price based on which directions use bus transportation:
* - Both directions BUS: shows combined price (outbound + inbound)
* - Only outbound BUS: shows outbound price only
* - Only inbound BUS: shows inbound price only
* - Falls back to legacy price property when split pricing data is unavailable
*
* @param bool $hasOutboundBus Whether outbound transportation is bus
* @param bool $hasInboundBus Whether inbound transportation is bus
*
* @return string The formatted pickup label with contextual pricing
*/
public function getLabelWithContextualPrice(bool $hasOutboundBus, bool $hasInboundBus): string
{
$label = $this->getLabel();
$effectivePrice = $this->calculateEffectivePrice($hasOutboundBus, $hasInboundBus);
if (null === $effectivePrice) {
return $label;
}
if (0.0 === $effectivePrice) {
return sprintf('%s (inkl.)', $label);
}
if ($effectivePrice < 0) {
return sprintf('%s (-%s€ Rabatt)', $label, number_format(abs($effectivePrice), 2, ',', '.'));
}
return sprintf('%s (€%s)', $label, number_format($effectivePrice, 2, ',', '.'));
}
/**
* Calculates the effective pickup price based on transportation directions.
*
* Uses split pricing (priceOutbound/priceInbound) when available, falling back
* to the legacy price property for backward compatibility with older data.
*
* @param bool $hasOutboundBus Whether outbound transportation is bus
* @param bool $hasInboundBus Whether inbound transportation is bus
*
* @return float|null The calculated effective price or null if no applicable price
*/
public function calculateEffectivePrice(bool $hasOutboundBus, bool $hasInboundBus): ?float
{
// Neither direction is bus - no pickup price applies
if (false === $hasOutboundBus && false === $hasInboundBus) {
return null;
}
$effectivePrice = 0.0;
$hasSplitPricing = null !== $this->priceOutbound || null !== $this->priceInbound;
if ($hasOutboundBus) {
if ($hasSplitPricing) {
$effectivePrice += $this->priceOutbound ?? 0.0;
} else {
// Fallback to legacy price for backward compatibility
$effectivePrice += $this->price ?? 0.0;
}
}
if ($hasInboundBus && null !== $this->priceInbound) {
$effectivePrice += $this->priceInbound;
}
return $effectivePrice;
}
}
+36 -4
View File
@@ -76,8 +76,12 @@ class TravelParser extends AbstractParser
$travel->transportationServices = $this
->getTransportationServices($node->filterXPath('//lei_befoerderung/leistung'));
$travel->rooms = $this->getRooms($hotelNode);
$travel->pickupsOutbound = $this->getPickups($node->filterXPath('//zustiege/zustieg'), $dateFrom);
$travel->pickupsInbound = $this->getPickups($node->filterXPath('//zustiege_rueck/zustieg_rueck'), $dateTo);
$travel->pickupsOutbound = $this->getPickups($node->filterXPath('//zustiege/zustieg'), $dateFrom, true);
$travel->pickupsInbound = $this->getPickups($node->filterXPath('//zustiege_rueck/zustieg_rueck'), $dateTo, false);
// Merge inbound prices into outbound pickups for split pricing support
$this->mergeInboundPricesIntoOutboundPickups($travel->pickupsOutbound, $travel->pickupsInbound);
$travel->guide = $this->getGuide($node);
return $travel;
@@ -256,14 +260,15 @@ class TravelParser extends AbstractParser
*
* @param Crawler $node The XML node containing pickup data
* @param \DateTimeImmutable|null $defaultDate Default date for time parsing (outbound only)
* @param bool $isOutbound Whether these are outbound pickups (for split pricing)
*
* @return array<int, Pickup> Array of pickup locations indexed by ID
*/
public function getPickups(Crawler $node, ?\DateTimeImmutable $defaultDate = null): array
public function getPickups(Crawler $node, ?\DateTimeImmutable $defaultDate = null, bool $isOutbound = true): array
{
$pickups = [];
$node->each(function (Crawler $pickupNode) use (&$pickups, $defaultDate) {
$node->each(function (Crawler $pickupNode) use (&$pickups, $defaultDate, $isOutbound) {
$pickupId = (int) $pickupNode->attr('idbuspro');
$pickup = new Pickup();
@@ -271,6 +276,13 @@ class TravelParser extends AbstractParser
$pickup->price = $pickupNode->attr('preis') ?
$this->stringToFloat($pickupNode->attr('preis')) : null;
// Set direction-specific price for split pricing support
if ($isOutbound) {
$pickup->priceOutbound = $pickup->price;
} else {
$pickup->priceInbound = $pickup->price;
}
// parse date and time only for direction 'to' indicated by provided default date
if (null !== $defaultDate) {
$pickup->time = $this->stringToDateTimeFuzzy($pickupNode->attr('zeit'), $defaultDate);
@@ -408,4 +420,24 @@ class TravelParser extends AbstractParser
$service->rawAgeConstraintData = $constraintData;
}
}
/**
* Merges inbound pickup prices into outbound pickups for split pricing support.
*
* For each outbound pickup, finds the matching inbound pickup by ID and copies
* the inbound price to the outbound pickup's priceInbound property. This enables
* split pricing calculations where pickup costs are distributed between outbound
* and inbound transportation.
*
* @param array<int, Pickup> $pickupsOutbound Outbound pickups indexed by ID (modified in place)
* @param array<int, Pickup> $pickupsInbound Inbound pickups indexed by ID
*/
private function mergeInboundPricesIntoOutboundPickups(array &$pickupsOutbound, array $pickupsInbound): void
{
foreach ($pickupsOutbound as $pickupId => $outboundPickup) {
if (isset($pickupsInbound[$pickupId])) {
$outboundPickup->priceInbound = $pickupsInbound[$pickupId]->priceInbound;
}
}
}
}
+3 -2
View File
@@ -230,8 +230,9 @@ class ParticipantPricingCalculator
}
}
// Pickup pricing - only charged when outbound transportation is bus
// BusProNet API ignores pickup surcharges for self-organized (PKW) outbound
// Pickup pricing - only charged when outbound transportation is BUS
// This matches current BusPro behavior where pickup price is only applied for outbound bus
// Note: priceOutbound/priceInbound are populated for future split pricing support when BusPro is updated
$hasOutboundBus = null !== $participant->transportationOutbound
&& 'BUS' === $participant->transportationOutbound->subType;
+3 -2
View File
@@ -129,8 +129,9 @@ class ServicePricingCalculator
}
}
// Pickup pricing - only charged when outbound transportation is bus
// BusProNet API ignores pickup surcharges for self-organized (PKW) outbound
// Pickup pricing - only charged when outbound transportation is BUS
// This matches current BusPro behavior where pickup price is only applied for outbound bus
// Note: priceOutbound/priceInbound are populated for future split pricing support when BusPro is updated
$hasOutboundBus = null !== $participant->transportationOutbound
&& 'BUS' === $participant->transportationOutbound->subType;