fix: always filter ski passes by participant's age
This commit is contained in:
@@ -982,12 +982,10 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
/**
|
||||
* Filters ski pass choices for display.
|
||||
*
|
||||
* Unlike other services, ski passes are shown even when age-restricted
|
||||
* (marked as readonly instead of hidden) to avoid user confusion about
|
||||
* included ski passes not being visible.
|
||||
*
|
||||
* Baby filtering is preserved: babies only see services explicitly
|
||||
* including their age range.
|
||||
* Ski passes follow the same age evaluation rules as the rest of the booking flow.
|
||||
* Participants only see passes that match their age or birth year. In edit mode,
|
||||
* the currently booked ski pass is preserved even if it would otherwise be filtered
|
||||
* out, so existing bookings remain renderable and editable.
|
||||
*
|
||||
* @param Service[] $services Array of ski pass Service objects to filter
|
||||
* @param BookingDto $bookingDto The booking DTO containing participant data
|
||||
@@ -1004,17 +1002,18 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if participant is a baby
|
||||
$age = $participant->getAge($bookingDto->travel->dateFrom);
|
||||
$isBaby = null !== $age && $age <= Constants::BABY_MAX_AGE;
|
||||
$filteredServices = $this->filterServicesByAgeConstraints($services, $bookingDto, $participantIndex);
|
||||
|
||||
// For babies: keep existing filtering logic (only show services with explicit baby age ranges)
|
||||
if ($isBaby) {
|
||||
return $this->filterServicesByAgeConstraints($services, $bookingDto, $participantIndex);
|
||||
if (BookingDto::MODE_EDIT !== $bookingDto->getMode() || null === $participant->skiPass?->id) {
|
||||
return $filteredServices;
|
||||
}
|
||||
|
||||
// For non-babies: return all services (age-restricted ones will be marked readonly in choice_attr)
|
||||
return $services;
|
||||
$currentSkiPassId = $participant->skiPass->id;
|
||||
if (false === isset($filteredServices[$currentSkiPassId])) {
|
||||
$filteredServices[$currentSkiPassId] = $services[$currentSkiPassId] ?? $participant->skiPass;
|
||||
}
|
||||
|
||||
return $filteredServices;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,8 +5,10 @@ declare(strict_types=1);
|
||||
namespace App\Tests\Form\Service;
|
||||
|
||||
use App\BusProNet\Constants;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Service;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\XmlParser\TravelParser;
|
||||
use App\BusProNet\Utility\DirectionMapper;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
@@ -16,6 +18,7 @@ use App\Service\InsuranceManager;
|
||||
use App\Service\ServiceAvailabilityCalculator;
|
||||
use App\Service\ServiceLabelFormatter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ParticipantFieldOptionsProviderBabyTest extends TestCase
|
||||
@@ -30,6 +33,45 @@ class ParticipantFieldOptionsProviderBabyTest extends TestCase
|
||||
$priceCalculatorService = $this->createMock(BookingPriceCalculator::class);
|
||||
$serviceLabelFormatter = new ServiceLabelFormatter();
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$translator->method('trans')->willReturnCallback(
|
||||
function (string $message, array $parameters = []): string {
|
||||
return match ($message) {
|
||||
'service.age_constraint.prefix' => sprintf('Nur %s', $parameters['%constraint%'] ?? ''),
|
||||
'service.age_constraint.absolute_age.range' => sprintf(
|
||||
'Alter %s-%s',
|
||||
$parameters['%ageFrom%'] ?? '',
|
||||
$parameters['%ageTo%'] ?? ''
|
||||
),
|
||||
'service.age_constraint.absolute_age.min' => sprintf(
|
||||
'Alter ab %s',
|
||||
$parameters['%ageFrom%'] ?? ''
|
||||
),
|
||||
'service.age_constraint.absolute_age.max' => sprintf(
|
||||
'Alter bis %s',
|
||||
$parameters['%ageTo%'] ?? ''
|
||||
),
|
||||
'service.age_constraint.birth_year.single' => sprintf(
|
||||
'Jahrgang %s',
|
||||
$parameters['%year%'] ?? ''
|
||||
),
|
||||
'service.age_constraint.birth_year.range' => sprintf(
|
||||
'Jahrgang %s-%s',
|
||||
$parameters['%yearFrom%'] ?? '',
|
||||
$parameters['%yearTo%'] ?? ''
|
||||
),
|
||||
'service.age_constraint.birth_year.min' => sprintf(
|
||||
'Jahrgang ab %s',
|
||||
$parameters['%yearFrom%'] ?? ''
|
||||
),
|
||||
'service.age_constraint.birth_year.max' => sprintf(
|
||||
'Jahrgang bis %s',
|
||||
$parameters['%yearTo%'] ?? ''
|
||||
),
|
||||
'service.age_constraint.mixed.separator' => ' und ',
|
||||
default => $message,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
$this->provider = new ParticipantFieldOptionsProvider(
|
||||
$this->serviceAvailabilityCalculator,
|
||||
@@ -128,6 +170,75 @@ class ParticipantFieldOptionsProviderBabyTest extends TestCase
|
||||
$this->assertContains(3, $choiceIds, 'Service without age restrictions should be shown for adult');
|
||||
}
|
||||
|
||||
public function testSkiPassesAreFilteredByBirthYearForAdultParticipant(): void
|
||||
{
|
||||
$travel = $this->createTravelWithExportedSkiPasses([195992, 195984]);
|
||||
$bookingDto = new BookingDto($travel, 1);
|
||||
|
||||
$participant = new ParticipantDto();
|
||||
$participant->index = 0;
|
||||
$participant->dateOfBirth = new \DateTimeImmutable('1990-12-26');
|
||||
$bookingDto->participants[0] = $participant;
|
||||
|
||||
$options = $this->provider->getFieldOptions('skiPass', $bookingDto, 0);
|
||||
|
||||
$this->assertEmpty($options, 'Adult participant should not see birth-year-mismatched ski passes');
|
||||
}
|
||||
|
||||
public function testSkiPassesKeepMatchingBirthYearAndHideMismatchedPasses(): void
|
||||
{
|
||||
$travel = $this->createTravelWithExportedSkiPasses([195992, 195984]);
|
||||
$bookingDto = new BookingDto($travel, 1);
|
||||
|
||||
$participant = new ParticipantDto();
|
||||
$participant->index = 0;
|
||||
$participant->dateOfBirth = new \DateTimeImmutable('2011-12-26');
|
||||
$bookingDto->participants[0] = $participant;
|
||||
|
||||
$options = $this->provider->getFieldOptions('skiPass', $bookingDto, 0);
|
||||
|
||||
$choices = $options['choices'];
|
||||
$choiceIds = array_map(fn (Service $service) => $service->id, $choices);
|
||||
|
||||
$this->assertContains(195992, $choiceIds, 'Matching 2009-2012 ski pass should remain visible');
|
||||
$this->assertNotContains(195984, $choiceIds, '2013-2020 ski pass should be hidden for a 2011-born participant');
|
||||
}
|
||||
|
||||
public function testEditModePreservesBookedSkiPassEvenWhenBirthYearMismatched(): void
|
||||
{
|
||||
$travel = $this->createTravelWithExportedSkiPasses([195992, 195984]);
|
||||
$bookingDto = new BookingDto($travel, 1);
|
||||
|
||||
$bookedSkiPass = clone $travel->additionalServices[195992];
|
||||
$bookedSkiPass->mapping = [0];
|
||||
|
||||
$booking = new Booking();
|
||||
$booking->additionalServices = [
|
||||
195992 => $bookedSkiPass,
|
||||
];
|
||||
$bookingDto->booking = $booking;
|
||||
|
||||
$participant = new ParticipantDto();
|
||||
$participant->index = 0;
|
||||
$participant->dateOfBirth = new \DateTimeImmutable('1990-12-26');
|
||||
$participant->skiPass = $bookedSkiPass;
|
||||
$bookingDto->participants[0] = $participant;
|
||||
|
||||
$options = $this->provider->getFieldOptions('skiPass', $bookingDto, 0);
|
||||
|
||||
$choices = $options['choices'];
|
||||
$choiceIds = array_map(fn (Service $service) => $service->id, $choices);
|
||||
|
||||
$this->assertContains(195992, $choiceIds, 'Booked ski pass should remain visible in edit mode');
|
||||
|
||||
$choiceAttr = $options['choice_attr'];
|
||||
$attributes = $choiceAttr($choices[195992] ?? $choices[array_key_first($choices)]);
|
||||
|
||||
$this->assertTrue($attributes['readonly']);
|
||||
$this->assertArrayHasKey('data-tooltip', $attributes);
|
||||
$this->assertStringContainsString('2009', $attributes['data-tooltip']);
|
||||
}
|
||||
|
||||
public function testBabyGetRegularPkwInTransportation(): void
|
||||
{
|
||||
$travel = $this->createTravelWithTransportation();
|
||||
@@ -364,4 +475,22 @@ class ParticipantFieldOptionsProviderBabyTest extends TestCase
|
||||
|
||||
return $travel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $serviceIds
|
||||
*/
|
||||
private function createTravelWithExportedSkiPasses(array $serviceIds): Travel
|
||||
{
|
||||
$parser = new TravelParser();
|
||||
$crawler = new Crawler(file_get_contents('/var/www/html/var/xmlexport/Ziel_2456.xml'));
|
||||
$travelNode = $crawler->filterXPath('//reise/termin')->first();
|
||||
$travel = $parser->parse($travelNode);
|
||||
|
||||
$travel->additionalServices = array_intersect_key(
|
||||
$travel->additionalServices,
|
||||
array_flip($serviceIds)
|
||||
);
|
||||
|
||||
return $travel;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user