createStub(ServiceAvailabilityCalculator::class); // InsuranceManager is stateless - use the real implementation so actual // eligibility/price-tier filtering runs, not a stubbed-out mock. $insuranceService = new InsuranceManager(); $this->priceCalculatorService = $this->createStub(BookingPriceCalculator::class); $serviceLabelFormatter = new ServiceLabelFormatter(); $translator = $this->createStub(TranslatorInterface::class); $translator->method('trans')->willReturnCallback(fn (string $message) => $message); $this->provider = new ParticipantFieldOptionsProvider( $serviceAvailabilityCalculator, $insuranceService, $this->priceCalculatorService, $serviceLabelFormatter, $translator ); } public function testFamilyInsuranceCorrectTierAppearsInApplicantChoicesUsingTotalPrice(): void { $travel = new Travel(); $travel->dateFrom = new \DateTimeImmutable('2025-08-01'); $travel->dateTo = new \DateTimeImmutable('2025-08-08'); // Low tier only matches the applicant's individual price (300), not the total (450) $lowTierFamilyInsurance = new Insurance(); $lowTierFamilyInsurance->id = '1'; $lowTierFamilyInsurance->label = 'Reise-Rücktritt Familie'; $lowTierFamilyInsurance->familyInsurance = true; $lowTierFamilyInsurance->travelPriceFrom = 0.0; $lowTierFamilyInsurance->travelPriceTo = 400.0; // Correct tier matches the total booking price (450), not the applicant's individual price (300) $correctTierFamilyInsurance = new Insurance(); $correctTierFamilyInsurance->id = '2'; $correctTierFamilyInsurance->label = 'Reise-Rücktritt Familie'; $correctTierFamilyInsurance->familyInsurance = true; $correctTierFamilyInsurance->travelPriceFrom = 400.01; $correctTierFamilyInsurance->travelPriceTo = 600.0; $travel->insurances = [$lowTierFamilyInsurance, $correctTierFamilyInsurance]; $applicant = new ParticipantDto(); $applicant->index = 0; $applicant->dateOfBirth = new \DateTimeImmutable('1990-06-15'); $child = new ParticipantDto(); $child->index = 1; $child->dateOfBirth = new \DateTimeImmutable('2015-01-01'); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$applicant, $child]; $this->priceCalculatorService ->method('calculateIndividualParticipantPriceExcludingInsurance') ->willReturn(300.0); $this->priceCalculatorService ->method('calculateTotalBookingPriceExcludingInsurance') ->willReturn(450.0); $options = $this->provider->getFieldOptions('insurance', $bookingDto, 0); $choiceIds = array_map(fn (Insurance $insurance) => $insurance->id, $options['choices']); $this->assertContains('2', $choiceIds, 'Family insurance tier matching the total booking price must be selectable'); $this->assertNotContains('1', $choiceIds, 'Family insurance tier that only matches the individual price must not be selectable'); } public function testFamilyInsuranceNeverAppearsInDependentChoices(): void { $travel = new Travel(); $travel->dateFrom = new \DateTimeImmutable('2025-08-01'); $travel->dateTo = new \DateTimeImmutable('2025-08-08'); $familyInsurance = new Insurance(); $familyInsurance->id = '1'; $familyInsurance->label = 'Reise-Rücktritt Familie'; $familyInsurance->familyInsurance = true; $travel->insurances = [$familyInsurance]; $applicant = new ParticipantDto(); $applicant->index = 0; $applicant->dateOfBirth = new \DateTimeImmutable('1990-06-15'); $child = new ParticipantDto(); $child->index = 1; $child->dateOfBirth = new \DateTimeImmutable('2015-01-01'); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$applicant, $child]; $this->priceCalculatorService ->method('calculateIndividualParticipantPriceExcludingInsurance') ->willReturn(0.0); $this->priceCalculatorService ->method('calculateTotalBookingPriceExcludingInsurance') ->willReturn(450.0); $options = $this->provider->getFieldOptions('insurance', $bookingDto, 1); $choiceIds = array_map(fn (Insurance $insurance) => $insurance->id, $options['choices']); $this->assertNotContains('1', $choiceIds, 'Family insurance must never be selectable for a non-applicant participant'); } }