503 lines
21 KiB
PHP
503 lines
21 KiB
PHP
<?php
|
|
|
|
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\Utility\DirectionMapper;
|
|
use App\BusProNet\XmlCrawlerFactory;
|
|
use App\BusProNet\XmlParser\TravelParser;
|
|
use App\Form\Model\BookingDto;
|
|
use App\Form\Model\ParticipantDto;
|
|
use App\Form\Service\ParticipantFieldOptionsProvider;
|
|
use App\Service\BookingPriceCalculator;
|
|
use App\Service\InsuranceManager;
|
|
use App\Service\ServiceAvailabilityCalculator;
|
|
use App\Service\ServiceLabelFormatter;
|
|
use PHPUnit\Framework\TestCase;
|
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
|
|
|
class ParticipantFieldOptionsProviderBabyTest extends TestCase
|
|
{
|
|
private ParticipantFieldOptionsProvider $provider;
|
|
private ServiceAvailabilityCalculator $serviceAvailabilityCalculator;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->serviceAvailabilityCalculator = $this->createMock(ServiceAvailabilityCalculator::class);
|
|
// These tests target age filtering, so availability filtering passes everything through
|
|
// unless a test stubs it explicitly
|
|
$this->serviceAvailabilityCalculator
|
|
->method('filterAvailableServices')
|
|
->willReturnArgument(0)
|
|
;
|
|
$insuranceService = $this->createMock(InsuranceManager::class);
|
|
$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,
|
|
$insuranceService,
|
|
$priceCalculatorService,
|
|
$serviceLabelFormatter,
|
|
$translator
|
|
);
|
|
}
|
|
|
|
public function testBabyOnlySeesServicesWithExplicitAgeRange(): void
|
|
{
|
|
$travel = $this->createTravelWithMixedServices();
|
|
$bookingDto = new BookingDto($travel, 1);
|
|
|
|
// Create a baby participant (1 year old at travel date)
|
|
$participant = new ParticipantDto();
|
|
$participant->index = 0;
|
|
$participant->dateOfBirth = $travel->dateFrom->modify('-1 year');
|
|
$bookingDto->participants[0] = $participant;
|
|
|
|
$options = $this->provider->getFieldOptions('additionalServices', $bookingDto, 0);
|
|
|
|
$choices = $options['choices'];
|
|
$choiceIds = array_map(fn (Service $service) => $service->id, $choices);
|
|
|
|
// Baby should see only service with age range 0-14 that includes them
|
|
$this->assertContains(1, $choiceIds, 'Service with age range 0-14 should be shown for baby');
|
|
$this->assertNotContains(2, $choiceIds, 'Service with age range 6-14 should be hidden for baby (age < 6)');
|
|
$this->assertNotContains(3, $choiceIds, 'Service without age restrictions should be hidden for baby');
|
|
}
|
|
|
|
public function testTwoYearOldOnlySeesServicesWithExplicitAgeRange(): void
|
|
{
|
|
$travel = $this->createTravelWithMixedServices();
|
|
$bookingDto = new BookingDto($travel, 1);
|
|
|
|
// Create a participant exactly at BABY_MAX_AGE (2 years old at travel date)
|
|
$participant = new ParticipantDto();
|
|
$participant->index = 0;
|
|
$participant->dateOfBirth = $travel->dateFrom->modify('-2 years');
|
|
$bookingDto->participants[0] = $participant;
|
|
|
|
$options = $this->provider->getFieldOptions('additionalServices', $bookingDto, 0);
|
|
|
|
$choices = $options['choices'];
|
|
$choiceIds = array_map(fn (Service $service) => $service->id, $choices);
|
|
|
|
// 2-year-old should see only service with age range that includes them
|
|
$this->assertContains(1, $choiceIds, 'Service with age range 0-14 should be shown for 2-year-old');
|
|
$this->assertNotContains(2, $choiceIds, 'Service with age range 6-14 should be hidden for 2-year-old');
|
|
$this->assertNotContains(3, $choiceIds, 'Service without age restrictions should be hidden for 2-year-old');
|
|
}
|
|
|
|
public function testThreeYearOldSeesAllApplicableServices(): void
|
|
{
|
|
$travel = $this->createTravelWithMixedServices();
|
|
$bookingDto = new BookingDto($travel, 1);
|
|
|
|
// Create a participant just over BABY_MAX_AGE (3 years old at travel date)
|
|
$participant = new ParticipantDto();
|
|
$participant->index = 0;
|
|
$participant->dateOfBirth = $travel->dateFrom->modify('-3 years');
|
|
$bookingDto->participants[0] = $participant;
|
|
|
|
$options = $this->provider->getFieldOptions('additionalServices', $bookingDto, 0);
|
|
|
|
$choices = $options['choices'];
|
|
$choiceIds = array_map(fn (Service $service) => $service->id, $choices);
|
|
|
|
// 3-year-old is not a baby, should see service with age range that includes them AND no-restriction services
|
|
$this->assertContains(1, $choiceIds, 'Service with age range 0-14 should be shown for 3-year-old');
|
|
$this->assertNotContains(2, $choiceIds, 'Service with age range 6-14 should be hidden for 3-year-old (age < 6)');
|
|
$this->assertContains(3, $choiceIds, 'Service without age restrictions should be shown for non-baby');
|
|
}
|
|
|
|
public function testAdultSeesAllApplicableServices(): void
|
|
{
|
|
$travel = $this->createTravelWithMixedServices();
|
|
$bookingDto = new BookingDto($travel, 1);
|
|
|
|
// Create an adult participant (25 years old at travel date)
|
|
$participant = new ParticipantDto();
|
|
$participant->index = 0;
|
|
$participant->dateOfBirth = $travel->dateFrom->modify('-25 years');
|
|
$bookingDto->participants[0] = $participant;
|
|
|
|
$options = $this->provider->getFieldOptions('additionalServices', $bookingDto, 0);
|
|
|
|
$choices = $options['choices'];
|
|
$choiceIds = array_map(fn (Service $service) => $service->id, $choices);
|
|
|
|
// Adult should see service without age restrictions but not children services
|
|
$this->assertNotContains(1, $choiceIds, 'Service with age range 0-14 should be hidden for adult (age > 14)');
|
|
$this->assertNotContains(2, $choiceIds, 'Service with age range 6-14 should be hidden for adult (age > 14)');
|
|
$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();
|
|
$bookingDto = new BookingDto($travel, 1);
|
|
|
|
// Create a baby participant (1 year old at travel date)
|
|
$participant = new ParticipantDto();
|
|
$participant->index = 0;
|
|
$participant->dateOfBirth = $travel->dateFrom->modify('-1 year');
|
|
$bookingDto->participants[0] = $participant;
|
|
|
|
$options = $this->provider->getFieldOptions('transportationOutbound', $bookingDto, 0);
|
|
|
|
$choices = $options['choices'];
|
|
$choiceIds = array_map(fn (Service $service) => $service->id, $choices);
|
|
|
|
// Baby should see BUS and regular PKW, but NOT discounted PKW
|
|
$this->assertContains(1, $choiceIds, 'BUS should be shown for baby');
|
|
$this->assertContains(3, $choiceIds, 'Regular PKW should be shown for baby');
|
|
$this->assertNotContains(2, $choiceIds, 'Discounted PKW should be hidden for baby');
|
|
}
|
|
|
|
public function testTwoYearOldGetRegularPkwInTransportation(): void
|
|
{
|
|
$travel = $this->createTravelWithTransportation();
|
|
$bookingDto = new BookingDto($travel, 1);
|
|
|
|
// Create a participant exactly at BABY_MAX_AGE (2 years old at travel date)
|
|
$participant = new ParticipantDto();
|
|
$participant->index = 0;
|
|
$participant->dateOfBirth = $travel->dateFrom->modify('-2 years');
|
|
$bookingDto->participants[0] = $participant;
|
|
|
|
$options = $this->provider->getFieldOptions('transportationOutbound', $bookingDto, 0);
|
|
|
|
$choices = $options['choices'];
|
|
$choiceIds = array_map(fn (Service $service) => $service->id, $choices);
|
|
|
|
// 2-year-old (at BABY_MAX_AGE) should see BUS and regular PKW, but NOT discounted PKW
|
|
$this->assertContains(1, $choiceIds, 'BUS should be shown for 2-year-old');
|
|
$this->assertContains(3, $choiceIds, 'Regular PKW should be shown for 2-year-old');
|
|
$this->assertNotContains(2, $choiceIds, 'Discounted PKW should be hidden for 2-year-old');
|
|
}
|
|
|
|
public function testThreeYearOldGetSmartPkwFiltering(): void
|
|
{
|
|
$travel = $this->createTravelWithTransportation();
|
|
$bookingDto = new BookingDto($travel, 1);
|
|
|
|
// Create a participant just over BABY_MAX_AGE (3 years old at travel date)
|
|
$participant = new ParticipantDto();
|
|
$participant->index = 0;
|
|
$participant->dateOfBirth = $travel->dateFrom->modify('-3 years');
|
|
$bookingDto->participants[0] = $participant;
|
|
|
|
// Mock availability: discounted PKW is available
|
|
$this->serviceAvailabilityCalculator
|
|
->method('isServiceUnavailable')
|
|
->willReturn(false);
|
|
|
|
$options = $this->provider->getFieldOptions('transportationOutbound', $bookingDto, 0);
|
|
|
|
$choices = $options['choices'];
|
|
$choiceIds = array_map(fn (Service $service) => $service->id, $choices);
|
|
|
|
// 3-year-old is not a baby, should get smart filtering (discounted when available)
|
|
$this->assertContains(1, $choiceIds, 'BUS should be shown for 3-year-old');
|
|
$this->assertContains(2, $choiceIds, 'Discounted PKW should be shown for 3-year-old when available');
|
|
$this->assertNotContains(3, $choiceIds, 'Regular PKW should be hidden when discounted is available');
|
|
}
|
|
|
|
public function testBabyFilteringAppliesToMultipleServiceTypes(): void
|
|
{
|
|
$travel = $this->createTravelWithMultipleServiceTypes();
|
|
$bookingDto = new BookingDto($travel, 1);
|
|
|
|
// Create a baby participant
|
|
$participant = new ParticipantDto();
|
|
$participant->index = 0;
|
|
$participant->dateOfBirth = $travel->dateFrom->modify('-1 year');
|
|
$bookingDto->participants[0] = $participant;
|
|
|
|
// Test board services
|
|
$boardOptions = $this->provider->getFieldOptions('board', $bookingDto, 0);
|
|
$boardChoiceIds = array_map(fn (Service $service) => $service->id, $boardOptions['choices']);
|
|
|
|
$this->assertContains(10, $boardChoiceIds, 'Board with age range 0-5 should be shown for baby');
|
|
$this->assertNotContains(11, $boardChoiceIds, 'Board without age range should be hidden for baby');
|
|
|
|
// Test courses services
|
|
$coursesOptions = $this->provider->getFieldOptions('courses', $bookingDto, 0);
|
|
$coursesChoiceIds = array_map(fn (Service $service) => $service->id, $coursesOptions['choices']);
|
|
|
|
$this->assertContains(20, $coursesChoiceIds, 'Course with age range 0-3 should be shown for baby');
|
|
$this->assertNotContains(21, $coursesChoiceIds, 'Course without age range should be hidden for baby');
|
|
}
|
|
|
|
private function createTravelWithMixedServices(): Travel
|
|
{
|
|
$travel = new Travel();
|
|
$travel->dateFrom = new \DateTimeImmutable('+30 days');
|
|
$travel->dateTo = new \DateTimeImmutable('+37 days');
|
|
|
|
// Service with explicit age range including babies (0-14)
|
|
$serviceWithBabyAge = new Service();
|
|
$serviceWithBabyAge->id = 1;
|
|
$serviceWithBabyAge->label = 'Ortstaxe Kinder';
|
|
$serviceWithBabyAge->subType = Constants::TOKEN_ADDITIONAL;
|
|
$serviceWithBabyAge->available = 10;
|
|
$serviceWithBabyAge->price = 5.0;
|
|
$serviceWithBabyAge->ageConstraintType = 'absolute_age';
|
|
$serviceWithBabyAge->ageFrom = 0;
|
|
$serviceWithBabyAge->ageTo = 14;
|
|
|
|
// Service with explicit age range NOT including babies (6-14)
|
|
$serviceWithOlderAge = new Service();
|
|
$serviceWithOlderAge->id = 2;
|
|
$serviceWithOlderAge->label = 'Mobilitätsabgabe Kinder';
|
|
$serviceWithOlderAge->subType = Constants::TOKEN_ADDITIONAL;
|
|
$serviceWithOlderAge->available = 10;
|
|
$serviceWithOlderAge->price = 3.0;
|
|
$serviceWithOlderAge->ageConstraintType = 'absolute_age';
|
|
$serviceWithOlderAge->ageFrom = 6;
|
|
$serviceWithOlderAge->ageTo = 14;
|
|
|
|
// Service without age restrictions (generic service)
|
|
$serviceNoAge = new Service();
|
|
$serviceNoAge->id = 3;
|
|
$serviceNoAge->label = 'Keycard-Pfand';
|
|
$serviceNoAge->subType = Constants::TOKEN_ADDITIONAL;
|
|
$serviceNoAge->available = 10;
|
|
$serviceNoAge->price = 10.0;
|
|
$serviceNoAge->ageConstraintType = null;
|
|
|
|
$travel->additionalServices = [
|
|
1 => $serviceWithBabyAge,
|
|
2 => $serviceWithOlderAge,
|
|
3 => $serviceNoAge,
|
|
];
|
|
|
|
return $travel;
|
|
}
|
|
|
|
private function createTravelWithTransportation(): Travel
|
|
{
|
|
$travel = new Travel();
|
|
$travel->dateFrom = new \DateTimeImmutable('+30 days');
|
|
$travel->dateTo = new \DateTimeImmutable('+37 days');
|
|
|
|
// BUS service
|
|
$busService = new Service();
|
|
$busService->id = 1;
|
|
$busService->label = 'Bus';
|
|
$busService->subType = DirectionMapper::SUBTYPE_BUS_API;
|
|
$busService->direction = DirectionMapper::OUTBOUND_TRAVEL;
|
|
$busService->available = 10;
|
|
$busService->price = 0.0;
|
|
|
|
// Discounted PKW (negative price)
|
|
$discountedPkw = new Service();
|
|
$discountedPkw->id = 2;
|
|
$discountedPkw->label = 'Selbstorganisiert (-15€ Rabatt)';
|
|
$discountedPkw->subType = DirectionMapper::SUBTYPE_CAR_API;
|
|
$discountedPkw->direction = DirectionMapper::OUTBOUND_TRAVEL;
|
|
$discountedPkw->available = 5;
|
|
$discountedPkw->price = -15.0;
|
|
|
|
// Regular PKW
|
|
$regularPkw = new Service();
|
|
$regularPkw->id = 3;
|
|
$regularPkw->label = 'Selbstorganisiert';
|
|
$regularPkw->subType = DirectionMapper::SUBTYPE_CAR_API;
|
|
$regularPkw->direction = DirectionMapper::OUTBOUND_TRAVEL;
|
|
$regularPkw->available = 100;
|
|
$regularPkw->price = 0.0;
|
|
|
|
$travel->transportationServices = [$busService, $discountedPkw, $regularPkw];
|
|
|
|
return $travel;
|
|
}
|
|
|
|
private function createTravelWithMultipleServiceTypes(): Travel
|
|
{
|
|
$travel = new Travel();
|
|
$travel->dateFrom = new \DateTimeImmutable('+30 days');
|
|
$travel->dateTo = new \DateTimeImmutable('+37 days');
|
|
|
|
// Board with baby age range
|
|
$boardWithBabyAge = new Service();
|
|
$boardWithBabyAge->id = 10;
|
|
$boardWithBabyAge->label = 'Baby Verpflegung';
|
|
$boardWithBabyAge->subType = Constants::TOKEN_BOARD;
|
|
$boardWithBabyAge->available = 10;
|
|
$boardWithBabyAge->price = 0.0;
|
|
$boardWithBabyAge->ageConstraintType = 'absolute_age';
|
|
$boardWithBabyAge->ageFrom = 0;
|
|
$boardWithBabyAge->ageTo = 5;
|
|
|
|
// Board without age range
|
|
$boardNoAge = new Service();
|
|
$boardNoAge->id = 11;
|
|
$boardNoAge->label = 'Verpflegung Standard';
|
|
$boardNoAge->subType = Constants::TOKEN_BOARD;
|
|
$boardNoAge->available = 10;
|
|
$boardNoAge->price = 50.0;
|
|
$boardNoAge->ageConstraintType = null;
|
|
|
|
// Course with baby age range
|
|
$courseWithBabyAge = new Service();
|
|
$courseWithBabyAge->id = 20;
|
|
$courseWithBabyAge->label = 'Baby Kurs';
|
|
$courseWithBabyAge->subType = Constants::TOKEN_COURSES;
|
|
$courseWithBabyAge->available = 10;
|
|
$courseWithBabyAge->price = 20.0;
|
|
$courseWithBabyAge->ageConstraintType = 'absolute_age';
|
|
$courseWithBabyAge->ageFrom = 0;
|
|
$courseWithBabyAge->ageTo = 3;
|
|
|
|
// Course without age range
|
|
$courseNoAge = new Service();
|
|
$courseNoAge->id = 21;
|
|
$courseNoAge->label = 'Standard Kurs';
|
|
$courseNoAge->subType = Constants::TOKEN_COURSES;
|
|
$courseNoAge->available = 10;
|
|
$courseNoAge->price = 100.0;
|
|
$courseNoAge->ageConstraintType = null;
|
|
|
|
$travel->additionalServices = [
|
|
10 => $boardWithBabyAge,
|
|
11 => $boardNoAge,
|
|
20 => $courseWithBabyAge,
|
|
21 => $courseNoAge,
|
|
];
|
|
|
|
return $travel;
|
|
}
|
|
|
|
/**
|
|
* @param list<int> $serviceIds
|
|
*/
|
|
private function createTravelWithExportedSkiPasses(array $serviceIds): Travel
|
|
{
|
|
$parser = new TravelParser();
|
|
$crawler = XmlCrawlerFactory::create(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;
|
|
}
|
|
}
|