feat: prefer discounted transportation services

addresses #869axbjbz
This commit is contained in:
Björn Fromme
2026-03-16 11:59:11 +01:00
parent d0c8302d21
commit af163d597e
4 changed files with 261 additions and 3 deletions
+6 -2
View File
@@ -48,7 +48,7 @@ class Travel
/**
* Travel booking status from BusProNet API.
* Possible values: 'Frei', 'Anfrage', 'Buchungsstop'
* Possible values: 'Frei', 'Anfrage', 'Buchungsstop'.
*/
#[Groups(['api:single'])]
public ?string $status = null;
@@ -153,8 +153,12 @@ class Travel
* Filters transportation services based on travel direction and optionally
* by availability. Services are sorted by subtype.
*
* Note: PKW/CAR filtering based on booking context is NOT applied here.
* That filtering happens dynamically in ParticipantFieldOptionsProvider using
* per-booking availability calculations from ServiceAvailabilityCalculator.
*
* @param string $direction The travel direction to filter by
* @param bool $filterAvailable Whether to include only available services
* @param bool $filterAvailable Whether to include only available services (API availability)
*
* @return array<int, Service> The filtered and sorted transportation services
*/
@@ -349,7 +349,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
// Outbound Transportation
$this->fieldOptionProviders['transportationOutbound'] = fn (BookingDto $bookingDto, int $participantIndex, array $options = []) => [
'label' => 'Hinfahrt',
'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL),
'choices' => $this->filterTransportationChoices(
$bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL),
$bookingDto,
$participantIndex
),
'choice_label' => fn (Service $service) => $this->formatTransportationServiceLabel($service),
'choice_value' => 'id',
'expanded' => true,
@@ -767,4 +771,81 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
$travelPrice
);
}
/**
* Filters transportation choices to show only one PKW option based on per-booking availability.
*
* When both discounted and regular self-organized (PKW/CAR) options exist:
* - If discounted option has remaining availability within this booking, show only discounted
* - If discounted option is sold out within this booking, show only regular
* - This creates dynamic fallback: as participants select/deselect, availability updates in real-time
*
* Business logic: Prefer showing discounted options while they have availability, but automatically
* fall back to regular options when discounts are exhausted. This allows participants who initially
* got the discount to change their mind (e.g., select bus instead), making the discount available
* for others in the same booking session.
*
* @param array $services Transportation services from Travel model
* @param BookingDto $bookingDto Current booking DTO with participant selections
* @param int $participantIndex Current participant being processed
*
* @return array Filtered transportation choices with smart PKW option selection
*/
private function filterTransportationChoices(array $services, BookingDto $bookingDto, int $participantIndex): array
{
// Only apply filtering in create mode
if (BookingDto::MODE_CREATE !== $bookingDto->getMode()) {
return $services;
}
// Separate PKW/CAR from other services (BUS, etc.)
$pkwServices = [];
$otherServices = [];
foreach ($services as $service) {
if (in_array($service->subType, ['PKW', 'CAR'], true)) {
$pkwServices[] = $service;
} else {
$otherServices[] = $service;
}
}
// If only one or no PKW service, no filtering needed
if (count($pkwServices) <= 1) {
return [...$otherServices, ...$pkwServices];
}
// Find discounted (negative price) and regular (zero/positive price) PKW options
$discountedPkw = null;
$regularPkw = null;
foreach ($pkwServices as $pkw) {
if (null !== $pkw->price && $pkw->price < 0) {
$discountedPkw = $pkw;
} else {
$regularPkw = $pkw;
}
}
// If we have both discounted and regular options, apply smart filtering
if (null !== $discountedPkw && null !== $regularPkw) {
// Check if discounted option is unavailable for this participant (per-booking availability)
$discountedIsUnavailable = $this->serviceAvailabilityCalculator->isServiceUnavailable(
$discountedPkw->id,
$bookingDto,
$participantIndex
);
if ($discountedIsUnavailable) {
// Discounted is sold out within this booking, show only regular
return [...$otherServices, $regularPkw];
} else {
// Discounted has availability, show only discounted (hide regular)
return [...$otherServices, $discountedPkw];
}
}
// Fallback: return all services if we don't have the expected discount/regular pair
return [...$otherServices, ...$pkwServices];
}
}