diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php
index 597702c..e26aa40 100644
--- a/src/Form/Service/ParticipantFieldOptionsProvider.php
+++ b/src/Form/Service/ParticipantFieldOptionsProvider.php
@@ -754,12 +754,26 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
return [];
}
- return array_filter($services, function (Service $service) use ($bookingDto, $participantIndex, $ageEvaluator) {
- // No age constraints = available to all
- if (false === $ageEvaluator->canEvaluate($service)) {
+ // Check if participant is a baby
+ $age = $participant->getAge($bookingDto->travel->dateFrom);
+ $isBaby = null !== $age && $age <= Constants::BABY_MAX_AGE;
+
+ return array_filter($services, function (Service $service) use ($bookingDto, $participantIndex, $ageEvaluator, $isBaby) {
+ // Check if service has age constraints
+ $hasAgeConstraints = $ageEvaluator->canEvaluate($service);
+
+ // For babies: ONLY show services with explicit age ranges that include them
+ // Services without age restrictions are hidden for babies
+ if ($isBaby && false === $hasAgeConstraints) {
+ return false;
+ }
+
+ // No age constraints and not a baby = available to all
+ if (false === $hasAgeConstraints) {
return true;
}
+ // Has age constraints = check if participant's age is within range
return $ageEvaluator->isServiceAvailableForParticipant($service, $bookingDto, $participantIndex);
});
}
@@ -887,8 +901,15 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
// If we have both discounted and regular options, apply smart filtering
if (null !== $discountedPkw && null !== $regularPkw) {
- // Get participant to check inbound transportation
$participant = $bookingDto->getParticipant($participantIndex);
+
+ // Baby participants always get regular PKW (no discounts)
+ $age = $participant?->getAge($bookingDto->travel->dateFrom);
+ if (null !== $age && $age <= Constants::BABY_MAX_AGE) {
+ return [...$otherServices, $regularPkw];
+ }
+
+ // Check if inbound is BUS
$inboundIsBus = null !== $participant?->transportationInbound
&& DirectionMapper::SUBTYPE_BUS_API === $participant->transportationInbound->subType;
diff --git a/src/Service/BookingService.php b/src/Service/BookingService.php
index 5d2e6bf..c5b49a9 100644
--- a/src/Service/BookingService.php
+++ b/src/Service/BookingService.php
@@ -442,6 +442,12 @@ class BookingService
continue; // Skip participants without age information
}
+ // Skip babies - they only get services with explicit age ranges (handled by field filtering)
+ $age = $participant->getAge($bookingDto->travel->dateFrom);
+ if (null !== $age && $age <= Constants::BABY_MAX_AGE) {
+ continue;
+ }
+
// Skip ineligible participants (no skipasses available for their age)
if (false === $this->participantEligibilityService->isParticipantEligible($bookingDto, $participantIndex)) {
continue;
diff --git a/templates/booking/_form_theme.html.twig b/templates/booking/_form_theme.html.twig
index a45f86a..11238ff 100644
--- a/templates/booking/_form_theme.html.twig
+++ b/templates/booking/_form_theme.html.twig
@@ -1,3 +1,13 @@
+{%- block form_errors -%}
+ {%- if errors|length > 0 -%}
+
|
- {{ roomPricing.quantity }}x {{ roomPricing.label }}
- {% if summaryData.assignmentCounts[roomPricing.roomId] is defined %}
- {{ summaryData.assignmentCounts[roomPricing.roomId] }}x Erwachsener
- {% endif %}
+ {{ roomPricing.quantity }}x {{ roomPricing.label }}
|
{{ roomPricing.totalPrice | format_currency('EUR') }}
diff --git a/tests/Form/Service/ParticipantFieldOptionsProviderBabyTest.php b/tests/Form/Service/ParticipantFieldOptionsProviderBabyTest.php
new file mode 100644
index 0000000..e9f6577
--- /dev/null
+++ b/tests/Form/Service/ParticipantFieldOptionsProviderBabyTest.php
@@ -0,0 +1,364 @@
+createMock(ParticipantRoomChoiceLoaderFactory::class);
+ $this->serviceAvailabilityCalculator = $this->createMock(ServiceAvailabilityCalculator::class);
+ $insuranceService = $this->createMock(InsuranceService::class);
+ $priceCalculatorService = $this->createMock(BookingPriceCalculatorService::class);
+
+ $this->provider = new ParticipantFieldOptionsProvider(
+ $roomChoiceLoaderFactory,
+ $this->serviceAvailabilityCalculator,
+ $insuranceService,
+ $priceCalculatorService
+ );
+ }
+
+ 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 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;
+ }
+}
diff --git a/tests/Service/BookingServiceBabyTest.php b/tests/Service/BookingServiceBabyTest.php
new file mode 100644
index 0000000..853dbf4
--- /dev/null
+++ b/tests/Service/BookingServiceBabyTest.php
@@ -0,0 +1,213 @@
+createMock(TravelDataService::class);
+ $priceCalculator = $this->createMock(BookingPriceCalculatorService::class);
+ $this->participantEligibilityService = $this->createMock(ParticipantEligibilityService::class);
+
+ $this->bookingService = new BookingService(
+ $travelDataService,
+ $priceCalculator,
+ $this->participantEligibilityService
+ );
+ }
+
+ public function testPreselectMandatoryServicesSkipsBabyParticipant(): void
+ {
+ $travel = $this->createTravelWithMandatoryServices();
+ $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');
+ $participant->additionalServices = [];
+ $bookingDto->participants[0] = $participant;
+
+ $this->bookingService->preselectMandatoryServices($bookingDto);
+
+ $this->assertEmpty($participant->additionalServices, 'Mandatory services should not be preselected for baby participants');
+ }
+
+ public function testPreselectMandatoryServicesSkipsTwoYearOldParticipant(): void
+ {
+ $travel = $this->createTravelWithMandatoryServices();
+ $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');
+ $participant->additionalServices = [];
+ $bookingDto->participants[0] = $participant;
+
+ $this->bookingService->preselectMandatoryServices($bookingDto);
+
+ $this->assertEmpty($participant->additionalServices, 'Mandatory services should not be preselected for participants at BABY_MAX_AGE');
+ }
+
+ public function testPreselectMandatoryServicesIncludesThreeYearOldParticipant(): void
+ {
+ $travel = $this->createTravelWithMandatoryServices();
+ $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');
+ $participant->additionalServices = [];
+ $bookingDto->participants[0] = $participant;
+
+ // Mock eligibility check - 3-year-old is eligible
+ $this->participantEligibilityService
+ ->method('isParticipantEligible')
+ ->willReturn(true);
+
+ $this->bookingService->preselectMandatoryServices($bookingDto);
+
+ $this->assertNotEmpty($participant->additionalServices, 'Mandatory services should be preselected for non-baby participants');
+ $this->assertCount(1, $participant->additionalServices);
+ $this->assertSame('Mandatory Service', $participant->additionalServices[0]->label);
+ }
+
+ public function testPreselectMandatoryServicesIncludesAdultParticipant(): void
+ {
+ $travel = $this->createTravelWithMandatoryServices();
+ $bookingDto = new BookingDto($travel, 1);
+
+ // Create an adult participant
+ $participant = new ParticipantDto();
+ $participant->index = 0;
+ $participant->dateOfBirth = $travel->dateFrom->modify('-25 years');
+ $participant->additionalServices = [];
+ $bookingDto->participants[0] = $participant;
+
+ // Mock eligibility check - adult is eligible
+ $this->participantEligibilityService
+ ->method('isParticipantEligible')
+ ->willReturn(true);
+
+ $this->bookingService->preselectMandatoryServices($bookingDto);
+
+ $this->assertNotEmpty($participant->additionalServices, 'Mandatory services should be preselected for adult participants');
+ $this->assertCount(1, $participant->additionalServices);
+ }
+
+ public function testPreselectMandatoryServicesHandlesMixedParticipants(): void
+ {
+ $travel = $this->createTravelWithMandatoryServices();
+ $bookingDto = new BookingDto($travel, 1);
+
+ // Create a baby participant (index 0)
+ $babyParticipant = new ParticipantDto();
+ $babyParticipant->index = 0;
+ $babyParticipant->dateOfBirth = $travel->dateFrom->modify('-1 year');
+ $babyParticipant->additionalServices = [];
+ $bookingDto->participants[0] = $babyParticipant;
+
+ // Create an adult participant (index 1)
+ $adultParticipant = new ParticipantDto();
+ $adultParticipant->index = 1;
+ $adultParticipant->dateOfBirth = $travel->dateFrom->modify('-25 years');
+ $adultParticipant->additionalServices = [];
+ $bookingDto->participants[1] = $adultParticipant;
+
+ // Mock eligibility check - adult is eligible (baby won't be checked)
+ $this->participantEligibilityService
+ ->method('isParticipantEligible')
+ ->with($bookingDto, 1) // Only called for adult
+ ->willReturn(true);
+
+ $this->bookingService->preselectMandatoryServices($bookingDto);
+
+ $this->assertEmpty($babyParticipant->additionalServices, 'Baby should not have mandatory services preselected');
+ $this->assertNotEmpty($adultParticipant->additionalServices, 'Adult should have mandatory services preselected');
+ }
+
+ public function testPreselectMandatoryServicesSkipsNewbornBaby(): void
+ {
+ $travel = $this->createTravelWithMandatoryServices();
+ $bookingDto = new BookingDto($travel, 1);
+
+ // Create a newborn (born on travel date)
+ $participant = new ParticipantDto();
+ $participant->index = 0;
+ $participant->dateOfBirth = $travel->dateFrom;
+ $participant->additionalServices = [];
+ $bookingDto->participants[0] = $participant;
+
+ $this->bookingService->preselectMandatoryServices($bookingDto);
+
+ $this->assertEmpty($participant->additionalServices, 'Mandatory services should not be preselected for newborn babies');
+ }
+
+ public function testBabyAgeCalculatedAtTravelDate(): void
+ {
+ $travel = new Travel();
+ $travel->dateFrom = new \DateTimeImmutable('2025-06-01');
+ $travel->dateTo = new \DateTimeImmutable('2025-06-08');
+ $travel->additionalServices = $this->createMandatoryAdditionalServices();
+
+ $bookingDto = new BookingDto($travel, 1);
+
+ // Create a participant who will be 2 at travel date
+ // Born 2023-06-02 (turns 2 on 2025-06-02), travel date 2025-06-01 = 1 year old at travel start
+ $participant = new ParticipantDto();
+ $participant->index = 0;
+ $participant->dateOfBirth = new \DateTimeImmutable('2023-06-02');
+ $participant->additionalServices = [];
+ $bookingDto->participants[0] = $participant;
+
+ $this->bookingService->preselectMandatoryServices($bookingDto);
+
+ // At age 1, participant is a baby and should be skipped
+ $this->assertEmpty($participant->additionalServices, 'Age should be calculated at travel date, not current date');
+ }
+
+ private function createTravelWithMandatoryServices(): Travel
+ {
+ $travel = new Travel();
+ $travel->dateFrom = new \DateTimeImmutable('+30 days');
+ $travel->dateTo = new \DateTimeImmutable('+37 days');
+ $travel->additionalServices = $this->createMandatoryAdditionalServices();
+
+ return $travel;
+ }
+
+ /**
+ * @return array
+ */
+ private function createMandatoryAdditionalServices(): array
+ {
+ $mandatoryService = new Service();
+ $mandatoryService->id = 1;
+ $mandatoryService->label = 'Mandatory Service';
+ $mandatoryService->subType = Constants::TOKEN_ADDITIONAL;
+ $mandatoryService->mandatory = true;
+ $mandatoryService->available = 10;
+ $mandatoryService->price = 10.0;
+
+ return [1 => $mandatoryService];
+ }
+}
|