diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index ef3d1b4..866595f 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -191,11 +191,18 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider $attributes = []; - // Make readonly and checked for mandatory services (pre-selection handled by service layer) + // Make mandatory services readonly only when actually selected by participant. + // Unselected mandatory services remain interactive so the user can select them + // after age changes (e.g. agency skeleton bookings where DOB is corrected). if (true === $service->mandatory) { - $attributes['checked'] = true; - $attributes['readonly'] = true; - $attributes['data-tooltip'] = 'Diese Leistung ist nicht abwählbar'; + $participant = $bookingDto->getParticipant($participantIndex); + $isSelected = null !== $participant + && $this->hasServiceById($participant->additionalServices, $service->id); + + if (true === $isSelected) { + $attributes['readonly'] = true; + $attributes['data-tooltip'] = 'Diese Leistung ist nicht abwählbar'; + } } // Add service description as data attribute for frontend use diff --git a/tests/Form/Service/ParticipantFieldOptionsProviderMandatoryServiceTest.php b/tests/Form/Service/ParticipantFieldOptionsProviderMandatoryServiceTest.php new file mode 100644 index 0000000..48e54b7 --- /dev/null +++ b/tests/Form/Service/ParticipantFieldOptionsProviderMandatoryServiceTest.php @@ -0,0 +1,151 @@ +createMock(ServiceAvailabilityCalculator::class); + $insuranceService = $this->createMock(InsuranceService::class); + $priceCalculatorService = $this->createMock(BookingPriceCalculatorService::class); + $translator = $this->createMock(TranslatorInterface::class); + + $this->provider = new ParticipantFieldOptionsProvider( + $serviceAvailabilityCalculator, + $insuranceService, + $priceCalculatorService, + $translator + ); + } + + public function testMandatoryServiceSelectedHasReadonlyAndTooltip(): void + { + $mandatoryService = $this->createMandatoryService(1, 'Ortstaxe'); + $travel = $this->createTravelWithAdditionalServices([$mandatoryService]); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->additionalServices = [$mandatoryService]; + $bookingDto->participants[0] = $participant; + + $options = $this->provider->getFieldOptions('additionalServices', $bookingDto, 0); + $choiceAttr = $options['choice_attr']; + $attributes = $choiceAttr($mandatoryService); + + $this->assertTrue($attributes['readonly']); + $this->assertSame('Diese Leistung ist nicht abwählbar', $attributes['data-tooltip']); + $this->assertArrayNotHasKey('checked', $attributes); + } + + public function testMandatoryServiceNotSelectedHasNoReadonlyOrChecked(): void + { + $mandatoryService = $this->createMandatoryService(1, 'Ortstaxe'); + $travel = $this->createTravelWithAdditionalServices([$mandatoryService]); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->additionalServices = []; + $bookingDto->participants[0] = $participant; + + $options = $this->provider->getFieldOptions('additionalServices', $bookingDto, 0); + $choiceAttr = $options['choice_attr']; + $attributes = $choiceAttr($mandatoryService); + + $this->assertArrayNotHasKey('readonly', $attributes); + $this->assertArrayNotHasKey('checked', $attributes); + $this->assertArrayNotHasKey('data-tooltip', $attributes); + } + + public function testNonMandatoryServiceUnaffected(): void + { + $nonMandatoryService = new Service(); + $nonMandatoryService->id = 2; + $nonMandatoryService->label = 'Keycard-Pfand'; + $nonMandatoryService->subType = Constants::TOKEN_ADDITIONAL; + $nonMandatoryService->available = 10; + $nonMandatoryService->price = 10.0; + $nonMandatoryService->mandatory = false; + + $travel = $this->createTravelWithAdditionalServices([$nonMandatoryService]); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->additionalServices = [$nonMandatoryService]; + $bookingDto->participants[0] = $participant; + + $options = $this->provider->getFieldOptions('additionalServices', $bookingDto, 0); + $choiceAttr = $options['choice_attr']; + $attributes = $choiceAttr($nonMandatoryService); + + $this->assertArrayNotHasKey('readonly', $attributes); + $this->assertArrayNotHasKey('checked', $attributes); + } + + public function testNullParticipantReturnsEmptyOptions(): void + { + $mandatoryService = $this->createMandatoryService(1, 'Ortstaxe'); + $travel = $this->createTravelWithAdditionalServices([$mandatoryService]); + $bookingDto = new BookingDto($travel, 1); + + // Do not set participant — age filtering cannot determine choices, returns empty + $options = $this->provider->getFieldOptions('additionalServices', $bookingDto, 0); + + $this->assertEmpty($options); + } + + private function createMandatoryService(int $id, string $label): Service + { + $service = new Service(); + $service->id = $id; + $service->label = $label; + $service->subType = Constants::TOKEN_ADDITIONAL; + $service->available = 10; + $service->price = 5.0; + $service->mandatory = true; + + return $service; + } + + private function createTravelWithAdditionalServices(array $services): Travel + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('+30 days'); + $travel->dateTo = new \DateTimeImmutable('+37 days'); + + $indexed = []; + foreach ($services as $service) { + $indexed[$service->id] = $service; + } + $travel->additionalServices = $indexed; + + return $travel; + } +}