createStub(ParticipantEligibilityChecker::class); $eligibilityChecker->method('isParticipantEligible')->willReturn(true); $this->roomPricingCalculator = new RoomPricingCalculator(); $this->participantPricingCalculator = new ParticipantPricingCalculator($this->roomPricingCalculator); $this->pricingAssembler = new BookingPricingAssembler( $this->roomPricingCalculator, $eligibilityChecker, ); $this->service = new BookingPriceCalculator( $this->roomPricingCalculator, $this->pricingAssembler, $this->participantPricingCalculator, ); } public function testCalculateIndividualParticipantPriceWithRoomAndServices(): void { $room = new Room(); $room->id = 1; $room->price = 100.0; $room->minPax = 2; $travel = new Travel(); $travel->rooms = [$room]; $skiPass = new Service(); $skiPass->price = 50.0; $course = new Service(); $course->price = 30.0; $participant = new ParticipantDto(); $participant->assignedRoomId = 1; $participant->skiPass = $skiPass; $participant->courses = [$course]; $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant]; $result = $this->service->calculateIndividualParticipantPrice($bookingDto, 0); // €100 (room) + €50 (ski pass) + €30 (course) = €180 $this->assertEquals(180.0, $result); } public function testCalculateIndividualParticipantPriceWithoutRoomAssignment(): void { $travel = new Travel(); $skiPass = new Service(); $skiPass->price = 50.0; $participant = new ParticipantDto(); $participant->assignedRoomId = null; $participant->skiPass = $skiPass; $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant]; $result = $this->service->calculateIndividualParticipantPrice($bookingDto, 0); $this->assertEquals(50.0, $result); } public function testCalculateIndividualParticipantPriceForNonExistentParticipant(): void { $travel = new Travel(); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = []; $result = $this->service->calculateIndividualParticipantPrice($bookingDto, 0); $this->assertEquals(0.0, $result); } public function testCalculateAllParticipantIndividualPricesWithMultipleParticipants(): void { $singleRoom = new Room(); $singleRoom->id = 1; $singleRoom->price = 80.0; $singleRoom->minPax = 1; $doubleRoom = new Room(); $doubleRoom->id = 2; $doubleRoom->price = 120.0; $doubleRoom->minPax = 2; $travel = new Travel(); $travel->rooms = [$singleRoom, $doubleRoom]; $skiPass = new Service(); $skiPass->price = 50.0; $course = new Service(); $course->price = 30.0; $participant1 = new ParticipantDto(); $participant1->assignedRoomId = 1; $participant1->skiPass = $skiPass; $participant2 = new ParticipantDto(); $participant2->assignedRoomId = 2; $participant2->courses = [$course]; $participant3 = new ParticipantDto(); $participant3->assignedRoomId = null; $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant1, $participant2, $participant3]; $result = $this->service->calculateAllParticipantIndividualPrices($bookingDto); // Participant 0: €80 + €50 = €130 // Participant 1: €120 + €30 = €150 // Participant 2: €0 $this->assertCount(3, $result); $this->assertEquals(130.0, $result[0]); $this->assertEquals(150.0, $result[1]); $this->assertEquals(0.0, $result[2]); } public function testCalculateAllParticipantIndividualPricesWithEmptyBooking(): void { $travel = new Travel(); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = []; $result = $this->service->calculateAllParticipantIndividualPrices($bookingDto); $this->assertEmpty($result); } public function testCalculateGrandTotalCombinesRoomsAndServices(): void { $room = new Room(); $room->id = 1; $room->price = 100.0; $room->minPax = 1; $travel = new Travel(); $travel->rooms = [$room]; $skiPass = new Service(); $skiPass->price = 40.0; $participant = new ParticipantDto(); $participant->assignedRoomId = 1; $participant->skiPass = $skiPass; $roomSelection = new RoomSelectionDto(); $roomSelection->id = 1; $roomSelection->quantity = 1; $bookingDto = new BookingDto($travel, 1); $bookingDto->roomSelections = [$roomSelection]; $bookingDto->participants = [$participant]; $result = $this->service->calculateGrandTotal($bookingDto); // €100 (room) + €40 (ski pass) = €140 $this->assertEquals(140.0, $result); } /** * Regression: calculateServiceTotal() must use the same insurance-tier resolution * as BookingPricingAssembler. In a bulk-insurance booking, dependents may qualify * for a cheaper tier than the applicant, so copying the applicant's insurance object * directly produces a higher total than what the display breakdown shows. */ public function testCalculateServiceTotalMatchesDisplayBreakdownForBulkInsurance(): void { // Tier 1 (cheap): covers travel prices €0–€500 at €30/person $insuranceTier1 = new Insurance(); $insuranceTier1->id = '1'; $insuranceTier1->label = 'Reise-Rücktritt'; $insuranceTier1->price = 30.0; $insuranceTier1->subType = 'RRV'; $insuranceTier1->travelPriceFrom = 0.0; $insuranceTier1->travelPriceTo = 500.0; // Tier 2 (expensive): covers travel prices €501–€1000 at €100/person $insuranceTier2 = new Insurance(); $insuranceTier2->id = '2'; $insuranceTier2->label = 'Reise-Rücktritt'; $insuranceTier2->price = 100.0; $insuranceTier2->subType = 'RRV'; $insuranceTier2->travelPriceFrom = 501.0; $insuranceTier2->travelPriceTo = 1000.0; $travel = new Travel(); $travel->insurances = [$insuranceTier1, $insuranceTier2]; $travel->dateFrom = new \DateTimeImmutable('2025-06-01'); $travel->dateTo = new \DateTimeImmutable('2025-06-08'); // Applicant: tier 2 insurance (high travel price), enables bulk insurance $applicant = new ParticipantDto(); $applicant->index = 0; $applicant->insurance = $insuranceTier2; $applicant->bulkInsuranceBooking = true; // No room assigned — service-total-only scenario // Dependent: no insurance assigned; eligible for tier 1 (lower travel price) $dependent = new ParticipantDto(); $dependent->index = 1; $dependent->insurance = null; $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$applicant, $dependent]; $serviceTotal = $this->service->calculateServiceTotal($bookingDto); $breakdownServiceTotal = array_sum(array_column( $this->pricingAssembler->calculateServicePricing($bookingDto), 'groupTotal' )); // The numeric total must match the display breakdown exactly. $this->assertEquals($breakdownServiceTotal, $serviceTotal, 'calculateServiceTotal() must agree with the display breakdown on bulk-insurance bookings' ); } public function testResolveInsuranceTravelPriceUsesIndividualPriceForNonFamilyInsurance(): void { $skiPass = new Service(); $skiPass->price = 50.0; $applicant = new ParticipantDto(); $applicant->index = 0; $applicant->skiPass = $skiPass; $dependent = new ParticipantDto(); $dependent->index = 1; $travel = new Travel(); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$applicant, $dependent]; $nonFamilyInsurance = new Insurance(); $nonFamilyInsurance->familyInsurance = false; $result = $this->service->resolveInsuranceTravelPrice($bookingDto, 0, $nonFamilyInsurance); $this->assertEquals(50.0, $result); } public function testResolveInsuranceTravelPriceUsesTotalPriceForFamilyInsuranceOnApplicant(): void { $applicantSkiPass = new Service(); $applicantSkiPass->price = 50.0; $applicant = new ParticipantDto(); $applicant->index = 0; $applicant->skiPass = $applicantSkiPass; $dependentSkiPass = new Service(); $dependentSkiPass->price = 30.0; $dependent = new ParticipantDto(); $dependent->index = 1; $dependent->skiPass = $dependentSkiPass; $travel = new Travel(); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$applicant, $dependent]; $familyInsurance = new Insurance(); $familyInsurance->familyInsurance = true; $result = $this->service->resolveInsuranceTravelPrice($bookingDto, 0, $familyInsurance); $this->assertEquals(80.0, $result); } public function testResolveInsuranceTravelPriceUsesTotalPriceForFamilyInsuranceRegardlessOfParticipantIndex(): void { // Family insurances are restricted to the applicant by InsuranceManager's eligibility // constraints, not by this method - it always resolves the total price for them. $applicantSkiPass = new Service(); $applicantSkiPass->price = 50.0; $applicant = new ParticipantDto(); $applicant->index = 0; $applicant->skiPass = $applicantSkiPass; $dependentSkiPass = new Service(); $dependentSkiPass->price = 30.0; $dependent = new ParticipantDto(); $dependent->index = 1; $dependent->skiPass = $dependentSkiPass; $travel = new Travel(); $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$applicant, $dependent]; $familyInsurance = new Insurance(); $familyInsurance->familyInsurance = true; $result = $this->service->resolveInsuranceTravelPrice($bookingDto, 1, $familyInsurance); $this->assertEquals(80.0, $result); } }