participantEligibilityService = $this->createMock(ParticipantEligibilityService::class); $this->participantEligibilityService->method('isParticipantEligible')->willReturn(true); // Build the calculator chain $roomPricingCalculator = new RoomPricingCalculator(); $participantPricingCalculator = new ParticipantPricingCalculator($roomPricingCalculator); // Create InsuranceService with a mock price calculator (to avoid circular reference in tests) $mockPriceCalculator = $this->createMock(BookingPriceCalculatorService::class); $insuranceService = new InsuranceService($mockPriceCalculator); $servicePricingCalculator = new ServicePricingCalculator( $this->participantEligibilityService, $insuranceService, $participantPricingCalculator ); $this->service = new BookingPriceCalculatorService( $roomPricingCalculator, $servicePricingCalculator, $participantPricingCalculator ); } public function testCalculateRoomPricingWithParticipantBasedCalculation(): void { // Create test data: 2 double rooms at €100 each, minPax=2 $room = new Room(); $room->id = 1; $room->price = 100.0; $room->minPax = 2; $room->label = 'Double Room'; $travel = new Travel(); $travel->rooms = [$room]; $roomSelection = new RoomSelectionDto(); $roomSelection->id = 1; $roomSelection->quantity = 2; // 2 rooms selected $bookingDto = new BookingDto($travel, 1); $bookingDto->roomSelections = [$roomSelection]; $bookingDto->participants = [ $this->createParticipantWithAssignedRoom(1), $this->createParticipantWithAssignedRoom(1), $this->createParticipantWithAssignedRoom(1), $this->createParticipantWithAssignedRoom(1), ]; // Test the calculation $result = $this->service->calculateRoomPricing($bookingDto); // Expected: 4 assigned participants × €100 = €400 total $this->assertCount(1, $result); $this->assertEquals(1, $result[0]['roomId']); $this->assertEquals('Double Room', $result[0]['label']); $this->assertEquals(2, $result[0]['quantity']); // 2 rooms $this->assertEquals(4, $result[0]['participantCount']); // billed units follow assigned participants $this->assertEquals(100.0, $result[0]['unitPrice']); // €100 per participant $this->assertEquals(400.0, $result[0]['totalPrice']); // €400 total } public function testCalculateRoomPricingWithSingleRoomSelection(): void { // Create test data: 1 triple room at €150, minPax=3 $room = new Room(); $room->id = 2; $room->price = 150.0; $room->minPax = 3; $room->label = 'Triple Room'; $travel = new Travel(); $travel->rooms = [$room]; $roomSelection = new RoomSelectionDto(); $roomSelection->id = 2; $roomSelection->quantity = 1; // 1 room selected $bookingDto = new BookingDto($travel, 1); $bookingDto->roomSelections = [$roomSelection]; $bookingDto->participants = [ $this->createParticipantWithAssignedRoom(2), $this->createParticipantWithAssignedRoom(2), ]; // Test the calculation $result = $this->service->calculateRoomPricing($bookingDto); // Expected: 2 assigned participants × €150 = €300 total $this->assertCount(1, $result); $this->assertEquals(2, $result[0]['roomId']); $this->assertEquals('Triple Room', $result[0]['label']); $this->assertEquals(1, $result[0]['quantity']); // 1 room $this->assertEquals(2, $result[0]['participantCount']); // billed units follow assigned participants $this->assertEquals(150.0, $result[0]['unitPrice']); // €150 per participant $this->assertEquals(300.0, $result[0]['totalPrice']); // €300 total } public function testCalculateRoomPricingWithMultipleRoomTypes(): void { // Create test data: Multiple room types $singleRoom = new Room(); $singleRoom->id = 1; $singleRoom->price = 80.0; $singleRoom->minPax = 1; $singleRoom->label = 'Single Room'; $doubleRoom = new Room(); $doubleRoom->id = 2; $doubleRoom->price = 120.0; $doubleRoom->minPax = 2; $doubleRoom->label = 'Double Room'; $travel = new Travel(); $travel->rooms = [$singleRoom, $doubleRoom]; $singleRoomSelection = new RoomSelectionDto(); $singleRoomSelection->id = 1; $singleRoomSelection->quantity = 1; // 1 single room $doubleRoomSelection = new RoomSelectionDto(); $doubleRoomSelection->id = 2; $doubleRoomSelection->quantity = 2; // 2 double rooms $bookingDto = new BookingDto($travel, 1); $bookingDto->roomSelections = [$singleRoomSelection, $doubleRoomSelection]; $bookingDto->participants = [ $this->createParticipantWithAssignedRoom(1), $this->createParticipantWithAssignedRoom(2), $this->createParticipantWithAssignedRoom(2), $this->createParticipantWithAssignedRoom(2), ]; // Test the calculation $result = $this->service->calculateRoomPricing($bookingDto); // Expected: // - Single: 1 assigned participant × €80 = €80 // - Double: 3 assigned participants × €120 = €360 $this->assertCount(2, $result); // Single room result $singleResult = $result[0]; $this->assertEquals(1, $singleResult['roomId']); $this->assertEquals(1, $singleResult['quantity']); $this->assertEquals(1, $singleResult['participantCount']); $this->assertEquals(80.0, $singleResult['unitPrice']); $this->assertEquals(80.0, $singleResult['totalPrice']); // Double room result $doubleResult = $result[1]; $this->assertEquals(2, $doubleResult['roomId']); $this->assertEquals(2, $doubleResult['quantity']); $this->assertEquals(3, $doubleResult['participantCount']); $this->assertEquals(120.0, $doubleResult['unitPrice']); $this->assertEquals(360.0, $doubleResult['totalPrice']); } public function testCalculateRoomPricingSkipsRoomsWithNullPrice(): void { $room = new Room(); $room->id = 1; $room->price = null; // No price set $room->minPax = 2; $room->label = 'Free Room'; $travel = new Travel(); $travel->rooms = [$room]; $roomSelection = new RoomSelectionDto(); $roomSelection->id = 1; $roomSelection->quantity = 1; $bookingDto = new BookingDto($travel, 1); $bookingDto->roomSelections = [$roomSelection]; $result = $this->service->calculateRoomPricing($bookingDto); // Should skip rooms with null price $this->assertEmpty($result); } public function testCalculateRoomPricingWithZeroQuantityRooms(): void { $room = new Room(); $room->id = 1; $room->price = 100.0; $room->minPax = 2; $room->label = 'Double Room'; $travel = new Travel(); $travel->rooms = [$room]; $roomSelection = new RoomSelectionDto(); $roomSelection->id = 1; $roomSelection->quantity = 0; // Zero quantity - not selected $bookingDto = new BookingDto($travel, 1); $bookingDto->roomSelections = [$roomSelection]; $result = $this->service->calculateRoomPricing($bookingDto); // Should skip rooms with zero quantity $this->assertEmpty($result); } public function testCalculateRoomPricingShowsSelectedRoomWithZeroAssignments(): void { $room = new Room(); $room->id = 1; $room->price = 100.0; $room->label = 'Double Room'; $travel = new Travel(); $travel->rooms = [$room]; $roomSelection = new RoomSelectionDto(); $roomSelection->id = 1; $roomSelection->quantity = 1; $bookingDto = new BookingDto($travel, 1); $bookingDto->roomSelections = [$roomSelection]; $bookingDto->participants = []; // nothing assigned yet $result = $this->service->calculateRoomPricing($bookingDto); $this->assertCount(1, $result); $this->assertEquals(1, $result[0]['roomId']); $this->assertEquals(1, $result[0]['quantity']); $this->assertEquals(0, $result[0]['participantCount']); $this->assertEquals(0.0, $result[0]['totalPrice']); } public function testCalculateRoomPricingUsesSelectionModeForStepOneSummary(): void { $room = new Room(); $room->id = 3; $room->price = 229.0; $room->minPax = 2; $room->label = 'Doppelzimmer Dusche/WC'; $travel = new Travel(); $travel->rooms = [$room]; $roomSelection = new RoomSelectionDto(); $roomSelection->id = 3; $roomSelection->quantity = 1; $bookingDto = new BookingDto($travel, 1); $bookingDto->roomSelections = [$roomSelection]; $bookingDto->participants = [ $this->createParticipantWithAssignedRoom(3), $this->createParticipantWithAssignedRoom(3), ]; $result = $this->service->calculateRoomPricing($bookingDto, RoomPricingCalculator::PRICING_MODE_SELECTION); $this->assertCount(1, $result); $this->assertEquals(2, $result[0]['participantCount']); $this->assertEquals(458.0, $result[0]['totalPrice']); } public function testCalculateRoomPricingInEditModeUsesStoredIndividualPrices(): void { $travel = new Travel(); $bookingDto = new BookingDto($travel, 2); $participantA = new ParticipantDto(); $participantA->assignedRoomId = 10; $participantB = new ParticipantDto(); $participantB->assignedRoomId = 10; $bookingDto->participants = [$participantA, $participantB]; $booking = new Booking(); $room = new Room(); $room->id = 10; $room->label = 'Stored Room'; $room->mapping = [0, 1]; $room->individualPrice = [0 => 200.0, 1 => 220.0]; $room->totalCount = 1; $booking->rooms = [$room]; $bookingDto->booking = $booking; $result = $this->service->calculateRoomPricing($bookingDto); $this->assertCount(1, $result); $this->assertEquals(10, $result[0]['roomId']); $this->assertEquals(2, $result[0]['participantCount']); $this->assertEquals(210.0, $result[0]['unitPrice']); $this->assertEquals(420.0, $result[0]['totalPrice']); } public function testCalculateIndividualParticipantPriceWithRoomAndServices(): void { // Create test room $room = new Room(); $room->id = 1; $room->price = 100.0; $room->minPax = 2; $travel = new Travel(); $travel->rooms = [$room]; // Create test services $skiPass = new Service(); $skiPass->price = 50.0; $course = new Service(); $course->price = 30.0; // Create participant with room assignment and services $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); // Expected: €100 (room) + €50 (ski pass) + €30 (course) = €180 $this->assertEquals(180.0, $result); } public function testCalculateIndividualParticipantPriceWithoutRoomAssignment(): void { $travel = new Travel(); // Create test services $skiPass = new Service(); $skiPass->price = 50.0; // Create participant without room assignment $participant = new ParticipantDto(); $participant->assignedRoomId = null; // No room assigned $participant->skiPass = $skiPass; $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant]; $result = $this->service->calculateIndividualParticipantPrice($bookingDto, 0); // Expected: €0 (no room) + €50 (ski pass) = €50 $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); // Expected: €0 for non-existent participant $this->assertEquals(0.0, $result); } public function testCalculateAllParticipantIndividualPricesWithMultipleParticipants(): void { // Create test rooms $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]; // Create test services $skiPass = new Service(); $skiPass->price = 50.0; $course = new Service(); $course->price = 30.0; // Create participants $participant1 = new ParticipantDto(); $participant1->assignedRoomId = 1; // Single room $participant1->skiPass = $skiPass; $participant2 = new ParticipantDto(); $participant2->assignedRoomId = 2; // Double room $participant2->courses = [$course]; $participant3 = new ParticipantDto(); $participant3->assignedRoomId = null; // No room assigned $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant1, $participant2, $participant3]; $result = $this->service->calculateAllParticipantIndividualPrices($bookingDto); // Expected results: // Participant 0: €80 (single room) + €50 (ski pass) = €130 // Participant 1: €120 (double room) + €30 (course) = €150 // Participant 2: €0 (no room) + €0 (no services) = €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 testServicePricingWithoutBulkInsurance(): void { // Test that aggregation works normally when bulk insurance is not enabled $travel = new Travel(); $travel->insurances = []; // Create participants with their own insurances $insurance1 = $this->createInsurance(1, 'Insurance A', 50.0); $insurance2 = $this->createInsurance(2, 'Insurance B', 75.0); $participant1 = new ParticipantDto(); $participant1->index = 0; $participant1->insurance = $insurance1; $participant1->bulkInsuranceBooking = false; $participant2 = new ParticipantDto(); $participant2->index = 1; $participant2->insurance = $insurance2; $bookingDto = new BookingDto($travel, 2); $bookingDto->participants = [$participant1, $participant2]; $result = $this->service->calculateServicePricing($bookingDto); // Expected: Both insurances should be counted separately $this->assertNotEmpty($result); $insuranceGroup = $this->findServiceGroup($result, 'Reiseversicherungen'); $this->assertNotNull($insuranceGroup, 'Insurance group should exist'); $this->assertCount(2, $insuranceGroup['services'], 'Should have 2 different insurance line items'); } public function testServicePricingWithBulkInsuranceSamePriceTier(): void { // Test that bulk insurance counts all participants when enabled with same price tier $insurance = $this->createInsurance(1, 'Reise-Rücktritt', 50.0, 'RRV'); $insurance->travelPriceFrom = 0.0; // Accepts all prices $insurance->travelPriceTo = 10000.0; $travel = new Travel(); $travel->insurances = [$insurance]; $travel->dateFrom = new \DateTimeImmutable('2025-06-01'); $travel->dateTo = new \DateTimeImmutable('2025-06-08'); // Applicant with bulk insurance enabled $participant1 = new ParticipantDto(); $participant1->index = 0; $participant1->insurance = $insurance; $participant1->bulkInsuranceBooking = true; // Dependent with no insurance (will get price-tier-adjusted version) $participant2 = new ParticipantDto(); $participant2->index = 1; $participant2->insurance = null; $participant3 = new ParticipantDto(); $participant3->index = 2; $participant3->insurance = null; $bookingDto = new BookingDto($travel, 3); $bookingDto->participants = [$participant1, $participant2, $participant3]; $result = $this->service->calculateServicePricing($bookingDto); // Expected: 3x same insurance should be aggregated into one line item $insuranceGroup = $this->findServiceGroup($result, 'Reiseversicherungen'); $this->assertNotNull($insuranceGroup, 'Insurance group should exist'); $this->assertCount(1, $insuranceGroup['services'], 'Should have 1 insurance line item'); $this->assertEquals(3, $insuranceGroup['services'][0]['participantCount'], 'Should count all 3 participants'); $this->assertEquals(150.0, $insuranceGroup['services'][0]['totalPrice'], 'Total should be 3 × €50'); } public function testServicePricingWithBulkInsuranceShowsPriceTierAdjustment(): void { // Test that bulk insurance shows correct price tiers based on individual travel prices // Price tiers: Tier 1 (€0-€500): €30, Tier 2 (€501-€1000): €50 $insuranceTier1 = $this->createInsurance(1, 'Reise-Rücktritt', 30.0, 'RRV'); $insuranceTier1->travelPriceFrom = 0.0; $insuranceTier1->travelPriceTo = 500.0; $insuranceTier2 = $this->createInsurance(2, 'Reise-Rücktritt', 50.0, '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 in Tier 2 (travel price €600) $participant1 = new ParticipantDto(); $participant1->index = 0; $participant1->insurance = $insuranceTier2; $participant1->bulkInsuranceBooking = true; // This test will aggregate based on what insurance is resolved // Without room/service assignments, we can't test real price calculation // So this test verifies the logic structure is correct $bookingDto = new BookingDto($travel, 1); $bookingDto->participants = [$participant1]; $result = $this->service->calculateServicePricing($bookingDto); // Expected: Applicant's insurance is counted $insuranceGroup = $this->findServiceGroup($result, 'Reiseversicherungen'); $this->assertNotNull($insuranceGroup, 'Insurance group should exist'); $this->assertEquals(1, $insuranceGroup['services'][0]['participantCount'], 'Should count applicant'); } public function testServicePricingWithBulkInsuranceWhenNoBulkEnabled(): void { // Test that dependents are not counted when bulk insurance checkbox is not enabled $insurance = $this->createInsurance(1, 'Reise-Rücktritt', 50.0, 'RRV'); $travel = new Travel(); $travel->insurances = [$insurance]; // Applicant WITHOUT bulk insurance enabled $participant1 = new ParticipantDto(); $participant1->index = 0; $participant1->insurance = $insurance; $participant1->bulkInsuranceBooking = false; // NOT enabled // Dependent with no insurance $participant2 = new ParticipantDto(); $participant2->index = 1; $participant2->insurance = null; // No insurance assigned $bookingDto = new BookingDto($travel, 2); $bookingDto->participants = [$participant1, $participant2]; $result = $this->service->calculateServicePricing($bookingDto); // Expected: Only applicant's insurance counted $insuranceGroup = $this->findServiceGroup($result, 'Reiseversicherungen'); $this->assertNotNull($insuranceGroup, 'Insurance group should exist'); $this->assertCount(1, $insuranceGroup['services'], 'Should have 1 insurance line item'); $this->assertEquals(1, $insuranceGroup['services'][0]['participantCount'], 'Should count only applicant'); $this->assertEquals(50.0, $insuranceGroup['services'][0]['totalPrice'], 'Total should be 1 × €50'); } public function testServicePricingWithIneligibleParticipant(): void { // Test that ineligible participants are skipped (not counted at all) $insurance = $this->createInsurance(1, 'Reise-Rücktritt', 50.0, 'RRV'); $travel = new Travel(); $travel->insurances = [$insurance]; // Applicant with insurance $participant1 = new ParticipantDto(); $participant1->index = 0; $participant1->insurance = $insurance; $participant1->bulkInsuranceBooking = false; // Dependent (will be marked as ineligible by the eligibility service) $participant2 = new ParticipantDto(); $participant2->index = 1; $participant2->insurance = null; $bookingDto = new BookingDto($travel, 2); $bookingDto->participants = [$participant1, $participant2]; // Mock participant eligibility to mark second participant as ineligible $participantEligibilityService = $this->createMock(ParticipantEligibilityService::class); $participantEligibilityService->method('isParticipantEligible') ->willReturnCallback(fn ($booking, $index) => 0 === $index); // Only first participant eligible // Build the calculator chain with the custom eligibility service $roomPricingCalculator = new RoomPricingCalculator(); $participantPricingCalculator = new ParticipantPricingCalculator($roomPricingCalculator); $mockPriceCalculator = $this->createMock(BookingPriceCalculatorService::class); $insuranceService = new InsuranceService($mockPriceCalculator); $servicePricingCalculator = new ServicePricingCalculator( $participantEligibilityService, $insuranceService, $participantPricingCalculator ); $this->service = new BookingPriceCalculatorService( $roomPricingCalculator, $servicePricingCalculator, $participantPricingCalculator ); $result = $this->service->calculateServicePricing($bookingDto); // Expected: Only applicant's insurance counted (dependent is ineligible) $insuranceGroup = $this->findServiceGroup($result, 'Reiseversicherungen'); $this->assertNotNull($insuranceGroup, 'Insurance group should exist'); $this->assertCount(1, $insuranceGroup['services'], 'Should have 1 insurance line item'); $this->assertEquals(1, $insuranceGroup['services'][0]['participantCount'], 'Should count only applicant'); } public function testServicePricingWithBulkInsuranceCountsAllParticipants(): void { // Test that bulk insurance counts all participants (price tier adjusted per participant) $insuranceA = $this->createInsurance(1, 'Reise-Rücktritt', 50.0, 'RRV'); $travel = new Travel(); $travel->insurances = [$insuranceA]; $travel->dateFrom = new \DateTimeImmutable('2025-06-01'); $travel->dateTo = new \DateTimeImmutable('2025-06-08'); // Applicant with bulk insurance enabled $participant1 = new ParticipantDto(); $participant1->index = 0; $participant1->insurance = $insuranceA; $participant1->bulkInsuranceBooking = true; // Dependent 1 - no insurance (will get price-tier-adjusted version of A) $participant2 = new ParticipantDto(); $participant2->index = 1; $participant2->insurance = null; // Dependent 2 - no insurance (will get price-tier-adjusted version of A) $participant3 = new ParticipantDto(); $participant3->index = 2; $participant3->insurance = null; $bookingDto = new BookingDto($travel, 3); $bookingDto->participants = [$participant1, $participant2, $participant3]; $result = $this->service->calculateServicePricing($bookingDto); // Expected: All 3 participants counted (price tier may vary per participant based on travel price) $insuranceGroup = $this->findServiceGroup($result, 'Reiseversicherungen'); $this->assertNotNull($insuranceGroup, 'Insurance group should exist'); // Count total participants across all insurance line items $totalParticipants = array_sum(array_column($insuranceGroup['services'], 'participantCount')); $this->assertEquals(3, $totalParticipants, 'Should count all 3 participants across all tiers'); } // Helper methods private function createInsurance(int $id, string $label, float $price, string $subType = 'RRV', bool $package = false, bool $complementary = false): \App\BusProNet\Model\Insurance { $insurance = new \App\BusProNet\Model\Insurance(); $insurance->id = (string) $id; $insurance->label = $label; $insurance->price = $price; $insurance->subType = $subType; $insurance->package = $package; $insurance->complementary = $complementary; return $insurance; } private function findServiceGroup(array $groups, string $groupName): ?array { foreach ($groups as $group) { if ($group['groupName'] === $groupName) { return $group; } } return null; } private function findServiceItem(array $items, string $label): ?array { foreach ($items as $item) { if ($item['label'] === $label) { return $item; } } return null; } private function createParticipantWithAssignedRoom(int $roomId): ParticipantDto { $participant = new ParticipantDto(); $participant->assignedRoomId = $roomId; return $participant; } }