feat: prefer discounted transportation services
addresses #869axbjbz
This commit is contained in:
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user