service = new ParticipantEligibilityChecker( new ServiceAgeEvaluator(), new ServiceAvailabilityCalculator() ); } public function testBabyParticipantIsAlwaysEligible(): void { $travel = $this->createTravelWithNoSkiPasses(); $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; $result = $this->service->isParticipantEligible($bookingDto, 0); $this->assertTrue($result, 'Baby participant (1 year old) should be eligible regardless of ski pass availability'); } public function testTwoYearOldParticipantIsEligibleAsBaby(): void { $travel = $this->createTravelWithNoSkiPasses(); $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; $result = $this->service->isParticipantEligible($bookingDto, 0); $this->assertTrue($result, 'Participant at BABY_MAX_AGE (2 years) should be eligible'); } public function testThreeYearOldParticipantIsNotExemptAsBaby(): void { $travel = $this->createTravelWithSkiPasses(ageFrom: 6); $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; $this->assertTrue( $this->service->isSkiPassRequired($bookingDto, 0), 'Participant over BABY_MAX_AGE (3 years) should still need a ski pass' ); $this->assertFalse( $this->service->isParticipantEligible($bookingDto, 0), 'Participant over BABY_MAX_AGE (3 years) should not be eligible when no ski pass fits their age' ); } public function testAdultParticipantWithSkiPassIsEligible(): void { $travel = $this->createTravelWithSkiPasses(); $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; $result = $this->service->isParticipantEligible($bookingDto, 0); $this->assertTrue($result, 'Adult participant with available ski passes should be eligible'); } public function testParticipantWithoutDateOfBirthIsNotEligible(): void { $travel = $this->createTravelWithSkiPasses(); $bookingDto = new BookingDto($travel, 1); // Create a participant without date of birth $participant = new ParticipantDto(); $participant->index = 0; $participant->dateOfBirth = null; $bookingDto->participants[0] = $participant; $result = $this->service->isParticipantEligible($bookingDto, 0); $this->assertFalse($result, 'Participant without date of birth should not be eligible'); } public function testNewbornBabyIsEligible(): void { $travel = $this->createTravelWithNoSkiPasses(); $bookingDto = new BookingDto($travel, 1); // Create a newborn (born on travel date) $participant = new ParticipantDto(); $participant->index = 0; $participant->dateOfBirth = $travel->dateFrom; $bookingDto->participants[0] = $participant; $result = $this->service->isParticipantEligible($bookingDto, 0); $this->assertTrue($result, 'Newborn baby (0 years old) should be eligible'); } public function testBabyAgeIsCalculatedAtTravelDate(): void { $travel = new Travel(); $travel->dateFrom = new \DateTimeImmutable('2025-06-01'); $travel->dateTo = new \DateTimeImmutable('2025-06-08'); $travel->additionalServices = []; $bookingDto = new BookingDto($travel, 1); // Create a participant who will be 2 at travel date but 3 now // Born 2023-06-02, travel date 2025-06-01 = 1 year 364 days = 1 year old $participant = new ParticipantDto(); $participant->index = 0; $participant->dateOfBirth = new \DateTimeImmutable('2023-06-02'); $bookingDto->participants[0] = $participant; $result = $this->service->isParticipantEligible($bookingDto, 0); $this->assertTrue($result, 'Age should be calculated at travel date, not current date'); } public function testAdultIsEligibleOnTravelWithoutSkiPasses(): void { $travel = $this->createTravelWithNoSkiPasses(); $bookingDto = new BookingDto($travel, 1); $participant = new ParticipantDto(); $participant->index = 0; $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); $bookingDto->participants[0] = $participant; $this->assertFalse( $this->service->isSkiPassRequired($bookingDto, 0), 'A travel that offers no ski passes cannot require one' ); $this->assertTrue( $this->service->isParticipantEligible($bookingDto, 0), 'Adult should be eligible on a travel that offers no ski passes at all' ); } public function testTeenagerIsEligibleOnTravelWithoutSkiPasses(): void { $travel = $this->createTravelWithNoSkiPasses(); $bookingDto = new BookingDto($travel, 1); $participant = new ParticipantDto(); $participant->index = 0; $participant->dateOfBirth = $travel->dateFrom->modify('-14 years'); $bookingDto->participants[0] = $participant; $this->assertFalse($this->service->isSkiPassRequired($bookingDto, 0)); $this->assertTrue( $this->service->isParticipantEligible($bookingDto, 0), 'Age must not be blamed when there is no ski pass to match against' ); } public function testParticipantIsNotEligibleWhenNoSkiPassMatchesTheirAge(): void { $travel = $this->createTravelWithSkiPasses(ageFrom: 18); $bookingDto = new BookingDto($travel, 1); $participant = new ParticipantDto(); $participant->index = 0; $participant->dateOfBirth = $travel->dateFrom->modify('-14 years'); $bookingDto->participants[0] = $participant; $this->assertTrue($this->service->isSkiPassRequired($bookingDto, 0)); $this->assertFalse( $this->service->isParticipantEligible($bookingDto, 0), 'Ski passes exist but none fits the age - the participant stays ineligible' ); } public function testParticipantIsNotEligibleWhenSkiPassIsSoldOut(): void { $travel = $this->createTravelWithSkiPasses(available: 0); $bookingDto = new BookingDto($travel, 1); $participant = new ParticipantDto(); $participant->index = 0; $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); $bookingDto->participants[0] = $participant; $this->assertTrue( $this->service->isSkiPassRequired($bookingDto, 0), 'A sold out ski pass must not turn the requirement off' ); $this->assertFalse($this->service->isParticipantEligible($bookingDto, 0)); } public function testParticipantIsNotEligibleWhenSkiPassIsOnBookingStop(): void { $travel = $this->createTravelWithSkiPasses(status: Constants::STATUS_BLOCKED); $bookingDto = new BookingDto($travel, 1); $participant = new ParticipantDto(); $participant->index = 0; $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); $bookingDto->participants[0] = $participant; $this->assertTrue($this->service->isSkiPassRequired($bookingDto, 0)); $this->assertFalse($this->service->isParticipantEligible($bookingDto, 0)); } public function testParticipantIsNotEligibleWhenLastSkiPassIsTakenByAnotherParticipant(): void { $travel = $this->createTravelWithSkiPasses(available: 1); $bookingDto = new BookingDto($travel, 2); $firstParticipant = new ParticipantDto(); $firstParticipant->index = 0; $firstParticipant->dateOfBirth = $travel->dateFrom->modify('-25 years'); $firstParticipant->skiPass = $travel->additionalServices[1]; $bookingDto->participants[0] = $firstParticipant; $secondParticipant = new ParticipantDto(); $secondParticipant->index = 1; $secondParticipant->dateOfBirth = $travel->dateFrom->modify('-25 years'); $bookingDto->participants[1] = $secondParticipant; $this->assertTrue($this->service->isParticipantEligible($bookingDto, 0)); $this->assertFalse( $this->service->isParticipantEligible($bookingDto, 1), 'The only ski pass is consumed by the first participant' ); } public function testBabyNeedsNoSkiPassEvenWhenTravelOffersThem(): void { $travel = $this->createTravelWithSkiPasses(); $bookingDto = new BookingDto($travel, 1); $participant = new ParticipantDto(); $participant->index = 0; $participant->dateOfBirth = $travel->dateFrom->modify('-1 year'); $bookingDto->participants[0] = $participant; $this->assertFalse($this->service->isSkiPassRequired($bookingDto, 0)); $this->assertTrue($this->service->isParticipantEligible($bookingDto, 0)); } private function createTravelWithNoSkiPasses(): Travel { $travel = new Travel(); $travel->dateFrom = new \DateTimeImmutable('+30 days'); $travel->dateTo = new \DateTimeImmutable('+37 days'); $travel->additionalServices = []; return $travel; } private function createTravelWithSkiPasses(?int $ageFrom = null, ?int $available = 10, ?string $status = null): Travel { $travel = new Travel(); $travel->dateFrom = new \DateTimeImmutable('+30 days'); $travel->dateTo = new \DateTimeImmutable('+37 days'); // Create a ski pass service, by default available for all ages $skiPass = new Service(); $skiPass->id = 1; $skiPass->label = 'Test Ski Pass'; $skiPass->subType = Constants::TOKEN_SKI_PASS; $skiPass->available = $available; $skiPass->status = $status; $skiPass->price = 100.0; $skiPass->ageConstraintType = null === $ageFrom ? null : 'absolute_age'; $skiPass->ageFrom = $ageFrom; $skiPass->dateFrom = $travel->dateFrom; $skiPass->dateTo = $travel->dateTo; $travel->additionalServices = [1 => $skiPass]; return $travel; } }