diff --git a/src/Form/Service/CreateFieldStateProvider.php b/src/Form/Service/CreateFieldStateProvider.php index c3a2ec0..59fc895 100644 --- a/src/Form/Service/CreateFieldStateProvider.php +++ b/src/Form/Service/CreateFieldStateProvider.php @@ -6,6 +6,7 @@ namespace App\Form\Service; use App\BusProNet\Utility\DirectionMapper; use App\Form\Service\Abstract\AbstractFieldStateProvider; +use App\Form\Service\Condition\AgeRangeCondition; use App\Form\Service\Condition\ApplicantCondition; use App\Form\Service\Condition\AuthenticatedUserPersonalDataCondition; use App\Form\Service\Condition\BabyAgeCondition; @@ -250,11 +251,16 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider ), ]; - // Show parking only when outbound transportation is PKW (hidden by default) + // Show parking only when outbound transportation is PKW AND participant is 18+ (hidden by default) // Parking is offered at holiday destination for those arriving by car + // Participants under 18 cannot drive in Germany, so parking is not relevant for them + $minimumDrivingAgeCondition = new AgeRangeCondition(18); $this->fieldStateConditions['parking'] = [ - 'hidden' => CompositeCondition::not( - ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API) + 'hidden' => CompositeCondition::or( + CompositeCondition::not( + ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API) + ), + CompositeCondition::not($minimumDrivingAgeCondition) ), ]; diff --git a/src/Form/Service/EditFieldStateProvider.php b/src/Form/Service/EditFieldStateProvider.php index 7cf85dd..f9c1117 100644 --- a/src/Form/Service/EditFieldStateProvider.php +++ b/src/Form/Service/EditFieldStateProvider.php @@ -8,6 +8,7 @@ use App\BusProNet\Utility\DirectionMapper; use App\Form\Model\BookingDto; use App\Form\Service\Abstract\AbstractFieldStateProvider; use App\Form\Service\Condition\AdditionalServicesMutabilityCondition; +use App\Form\Service\Condition\AgeRangeCondition; use App\Form\Service\Condition\ApplicantCondition; use App\Form\Service\Condition\AuthenticatedUserPersonalDataCondition; use App\Form\Service\Condition\BookingModeCondition; @@ -188,10 +189,15 @@ class EditFieldStateProvider extends AbstractFieldStateProvider 'readonly' => $pickupsMutabilityCondition, ]; - // Parking - shown only when outbound transportation is PKW, readonly if transportation not mutable + // Parking - shown only when outbound transportation is PKW AND participant is 18+, readonly if transportation not mutable + // Participants under 18 cannot drive in Germany, so parking is not relevant for them + $minimumDrivingAgeCondition = new AgeRangeCondition(18); $this->fieldStateConditions['parking'] = [ - 'hidden' => CompositeCondition::not( - ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API) + 'hidden' => CompositeCondition::or( + CompositeCondition::not( + ServiceSubTypeCondition::equals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API) + ), + CompositeCondition::not($minimumDrivingAgeCondition) ), 'readonly' => $transportationServicesMutabilityCondition, ]; diff --git a/src/Form/Service/ParticipantParkingFieldHandler.php b/src/Form/Service/ParticipantParkingFieldHandler.php index 0699b94..84b3ce7 100644 --- a/src/Form/Service/ParticipantParkingFieldHandler.php +++ b/src/Form/Service/ParticipantParkingFieldHandler.php @@ -8,14 +8,15 @@ use App\BusProNet\Constants; use App\BusProNet\Model\Service; use App\BusProNet\Utility\DirectionMapper; use App\Form\Model\BookingDto; +use App\Form\Model\ParticipantDto; use App\Form\Service\Abstract\AbstractParticipantFieldHandler; /** * Handles parking service selection for self-organized transportation. * - * Parking is only available when outbound transportation is PKW (car). - * This is because parking is needed at the destination for those arriving by car. - * Automatically clears parking when outbound transportation is not PKW. + * Parking is only available when outbound transportation is PKW (car) AND + * participant is at least 18 years old (minimum driving age in Germany). + * Automatically clears parking when conditions are not met. */ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler { @@ -24,9 +25,11 @@ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler return 'parking'; } + private const MINIMUM_DRIVING_AGE = 18; + public function getDependencies(): array { - return ['transportationOutbound', 'transportationInbound']; + return ['transportationOutbound', 'transportationInbound', 'dateOfBirth']; } /** @@ -54,10 +57,10 @@ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler return; } - // Check if parking is applicable (outbound transportation is PKW) - if (!$this->isParkingApplicable($participant)) { - $participant->parking = false; // Clear parking when outbound is not PKW - $participant->parkingService = null; // Clear parking service object + // Check if parking is applicable (outbound transportation is PKW AND participant is 18+) + if (false === $this->isParkingApplicable($participant)) { + $participant->parking = false; + $participant->parkingService = null; return; } @@ -77,17 +80,30 @@ class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler } /** - * Checks if parking is applicable based on outbound transportation selection. + * Checks if parking is applicable based on outbound transportation and age. * - * Parking is only needed when arriving by car at the destination. + * Parking is only applicable when: + * 1. Outbound transportation is PKW (arriving by car at destination) + * 2. Participant is at least 18 years old (minimum driving age in Germany) * - * @param object $participant The participant DTO + * @param ParticipantDto $participant The participant DTO * * @return bool True if parking is applicable, false otherwise */ - private function isParkingApplicable(object $participant): bool + private function isParkingApplicable(ParticipantDto $participant): bool { - return DirectionMapper::SUBTYPE_CAR_API === $participant->transportationOutbound?->subType; + // Must have PKW outbound transportation + if (DirectionMapper::SUBTYPE_CAR_API !== $participant->transportationOutbound?->subType) { + return false; + } + + // Must be at least minimum driving age + $age = $participant->getAge(); + if (null === $age || $age < self::MINIMUM_DRIVING_AGE) { + return false; + } + + return true; } /** diff --git a/tests/Form/Service/ParticipantParkingFieldHandlerTest.php b/tests/Form/Service/ParticipantParkingFieldHandlerTest.php new file mode 100644 index 0000000..ae26588 --- /dev/null +++ b/tests/Form/Service/ParticipantParkingFieldHandlerTest.php @@ -0,0 +1,253 @@ +handler = new ParticipantParkingFieldHandler(); + } + + public function testGetFieldName(): void + { + $this->assertSame('parking', $this->handler->getFieldName()); + } + + public function testGetDependencies(): void + { + $this->assertSame(['transportationOutbound', 'transportationInbound', 'dateOfBirth'], $this->handler->getDependencies()); + } + + public function testShouldProcessAlwaysReturnsTrue(): void + { + $this->assertTrue($this->handler->shouldProcess([], BookingDto::MODE_CREATE, 0)); + $this->assertTrue($this->handler->shouldProcess(['some' => 'data'], BookingDto::MODE_EDIT, 5)); + } + + public function testProcessFieldWithoutParticipant(): void + { + $travel = new Travel(); + $bookingDto = new BookingDto($travel, 1); + $submittedData = ['parking' => true]; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->expectNotToPerformAssertions(); + } + + public function testProcessFieldClearsParkingWhenTransportationIsNotPkw(): void + { + $busService = $this->createTransportationService(DirectionMapper::SUBTYPE_BUS_API); + + $participant = new ParticipantDto(); + $participant->dateOfBirth = new \DateTimeImmutable('-25 years'); + $participant->transportationOutbound = $busService; + $participant->parking = true; + + $travel = new Travel(); + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['parking' => true]; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertFalse($participant->parking); + $this->assertNull($participant->parkingService); + } + + public function testProcessFieldClearsParkingWhenParticipantIsUnder18(): void + { + $pkwService = $this->createTransportationService(DirectionMapper::SUBTYPE_CAR_API); + + // 17-year-old participant + $participant = new ParticipantDto(); + $participant->dateOfBirth = new \DateTimeImmutable('-17 years'); + $participant->transportationOutbound = $pkwService; + $participant->parking = true; + + $travel = new Travel(); + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['parking' => true]; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertFalse($participant->parking); + $this->assertNull($participant->parkingService); + } + + public function testProcessFieldClearsParkingWhenParticipantIsExactly17(): void + { + $pkwService = $this->createTransportationService(DirectionMapper::SUBTYPE_CAR_API); + + // Participant who is exactly 17 (just before 18th birthday) + $participant = new ParticipantDto(); + $participant->dateOfBirth = new \DateTimeImmutable('2007-06-15'); + $participant->transportationOutbound = $pkwService; + $participant->parking = true; + + $travel = new Travel(); + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant]; + + // Assume today is 2024-06-14 (one day before 17th birthday would make them 16) + // But since we use current date, we need to be careful with this test + // Let's use a fixed approach - participant born 17 years ago today is exactly 17 + $participant->dateOfBirth = (new \DateTimeImmutable())->modify('-17 years'); + + $submittedData = ['parking' => true]; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertFalse($participant->parking); + } + + public function testProcessFieldAllowsParkingForAdultWithPkw(): void + { + $pkwService = $this->createTransportationService(DirectionMapper::SUBTYPE_CAR_API); + + // 25-year-old participant + $participant = new ParticipantDto(); + $participant->dateOfBirth = new \DateTimeImmutable('-25 years'); + $participant->transportationOutbound = $pkwService; + + $travel = new Travel(); + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['parking' => '1']; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertTrue($participant->parking); + } + + public function testProcessFieldAllowsParkingForExactly18YearOld(): void + { + $pkwService = $this->createTransportationService(DirectionMapper::SUBTYPE_CAR_API); + + // Exactly 18 years old + $participant = new ParticipantDto(); + $participant->dateOfBirth = (new \DateTimeImmutable())->modify('-18 years'); + $participant->transportationOutbound = $pkwService; + + $travel = new Travel(); + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['parking' => true]; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertTrue($participant->parking); + } + + public function testProcessFieldClearsParkingWhenNoDateOfBirth(): void + { + $pkwService = $this->createTransportationService(DirectionMapper::SUBTYPE_CAR_API); + + $participant = new ParticipantDto(); + $participant->dateOfBirth = null; // No date of birth + $participant->transportationOutbound = $pkwService; + $participant->parking = true; + + $travel = new Travel(); + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['parking' => true]; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertFalse($participant->parking); + $this->assertNull($participant->parkingService); + } + + public function testProcessFieldClearsParkingWhenBabyParticipant(): void + { + $pkwService = $this->createTransportationService(DirectionMapper::SUBTYPE_CAR_API); + + // Baby participant (1 year old) + $participant = new ParticipantDto(); + $participant->dateOfBirth = new \DateTimeImmutable('-1 year'); + $participant->transportationOutbound = $pkwService; + $participant->parking = true; + + $travel = new Travel(); + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['parking' => true]; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertFalse($participant->parking); + $this->assertNull($participant->parkingService); + } + + public function testProcessFieldClearsParkingForChildParticipant(): void + { + $pkwService = $this->createTransportationService(DirectionMapper::SUBTYPE_CAR_API); + + // 10-year-old child + $participant = new ParticipantDto(); + $participant->dateOfBirth = new \DateTimeImmutable('-10 years'); + $participant->transportationOutbound = $pkwService; + $participant->parking = true; + + $travel = new Travel(); + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['parking' => true]; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertFalse($participant->parking); + } + + public function testProcessFieldSetsParkingToFalseWhenNotSelected(): void + { + $pkwService = $this->createTransportationService(DirectionMapper::SUBTYPE_CAR_API); + + $participant = new ParticipantDto(); + $participant->dateOfBirth = new \DateTimeImmutable('-25 years'); + $participant->transportationOutbound = $pkwService; + + $travel = new Travel(); + $bookingDto = new BookingDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['parking' => false]; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertFalse($participant->parking); + $this->assertNull($participant->parkingService); + } + + private function createTransportationService(string $subType): Service + { + $service = new Service(); + $service->id = 100; + $service->label = 'Test Transportation'; + $service->subType = $subType; + + return $service; + } +}