insuranceService = $this->createStub(InsuranceManager::class); $this->priceCalculatorService = $this->createStub(BookingPriceCalculator::class); $this->insuranceService->method('getSelectableInsurances') ->willReturnCallback(fn (Travel $travel) => $travel->insurances ?? []); $this->priceCalculatorService->method('calculateTotalBookingPriceExcludingInsurance') ->willReturn(500.0); $this->checker = new FamilyInsuranceAvailabilityChecker($this->insuranceService, $this->priceCalculatorService); } public function testUpgradeIsAvailableWhenFamilyBecameEligibleAfterTheApplicantChose(): void { $bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance(); $this->allowAllInsurancesAsEligible(); $this->assertTrue($this->checker->isFamilyInsuranceUpgradeAvailable($bookingDto)); } public function testUpgradeIsNotAvailableWhenTheApplicantChoseWhileFamilyWasAlreadyEligible(): void { $bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance(); $bookingDto->applicantInsuranceChosenWhileFamilyIneligible = false; $this->allowAllInsurancesAsEligible(); $this->assertFalse( $this->checker->isFamilyInsuranceUpgradeAvailable($bookingDto), 'An applicant who was offered family insurance and picked something else must not be nagged' ); } public function testUpgradeIsNotAvailableWhenApplicantHasNoInsuranceYet(): void { $bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance(); $bookingDto->getParticipant(0)->insurance = null; $this->allowAllInsurancesAsEligible(); $this->assertFalse($this->checker->isFamilyInsuranceUpgradeAvailable($bookingDto)); } public function testUpgradeIsNotAvailableWhenApplicantAlreadyHasFamilyInsurance(): void { $bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance(); $familyInsurance = $this->createInsurance('2'); $familyInsurance->familyInsurance = true; $bookingDto->getParticipant(0)->insurance = $familyInsurance; $this->allowAllInsurancesAsEligible(); $this->assertFalse($this->checker->isFamilyInsuranceUpgradeAvailable($bookingDto)); } public function testUpgradeIsNotAvailableWhenNoFamilyInsuranceProductExists(): void { $bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance(); $bookingDto->travel->insurances = array_values(array_filter( $bookingDto->travel->insurances, static fn (Insurance $i) => false === $i->familyInsurance )); $this->allowAllInsurancesAsEligible(); $this->assertFalse($this->checker->isFamilyInsuranceUpgradeAvailable($bookingDto)); } public function testUpgradeIsNotAvailableWhenNoFamilyPriceTierMatches(): void { $bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance(); // Family composition qualifies, but no family insurance matches the total price $this->insuranceService->method('getEligibleInsurances')->willReturn([]); $this->assertFalse($this->checker->isFamilyInsuranceUpgradeAvailable($bookingDto)); } public function testUpgradeIsNotAvailableWhenTheBookingIsNotAFamilyBooking(): void { $bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance(); // Dependent is an adult - no children left, so the constellation no longer qualifies $bookingDto->getParticipant(1)->dateOfBirth = new \DateTimeImmutable('1992-01-01'); $this->allowAllInsurancesAsEligible(); $this->assertFalse($this->checker->isFamilyInsuranceUpgradeAvailable($bookingDto)); } public function testHasEligibleFamilyInsuranceIgnoresTheSelectionTimeRecording(): void { // The raw availability predicate is what the field handler records against, so it // must not itself depend on the flag it is used to compute $bookingDto = $this->createFamilyBookingDtoWithNonFamilyApplicantInsurance(); $bookingDto->applicantInsuranceChosenWhileFamilyIneligible = false; $this->allowAllInsurancesAsEligible(); $this->assertTrue($this->checker->hasEligibleFamilyInsurance($bookingDto)); } private function allowAllInsurancesAsEligible(): void { $this->insuranceService->method('getEligibleInsurances')->willReturnCallback( fn (array $insurances) => $insurances ); } private function createFamilyBookingDtoWithNonFamilyApplicantInsurance(): BookingDto { $nonFamilyInsurance = $this->createInsurance('1'); $nonFamilyInsurance->familyInsurance = false; $familyInsurance = $this->createInsurance('2', 'Reise-Rücktritt Familie'); $familyInsurance->familyInsurance = true; $travel = new Travel(); $travel->dateFrom = new \DateTimeImmutable('2025-08-01'); $travel->dateTo = new \DateTimeImmutable('2025-08-08'); $travel->insurances = [$nonFamilyInsurance, $familyInsurance]; $applicant = new ParticipantDto(); $applicant->index = 0; $applicant->dateOfBirth = new \DateTimeImmutable('1990-06-15'); $applicant->insurance = $nonFamilyInsurance; $child = new ParticipantDto(); $child->index = 1; $child->dateOfBirth = new \DateTimeImmutable('2015-01-01'); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$applicant, $child]; $bookingDto->applicantInsuranceChosenWhileFamilyIneligible = true; return $bookingDto; } private function createInsurance(string|int $id, string $label = 'Test Insurance'): Insurance { $insurance = new Insurance(); $insurance->id = (string) $id; $insurance->label = $label; return $insurance; } }