Files
myep/tests/Service/BookingPriceCalculatorServiceTest.php
T

592 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Tests\Service;
use App\BusProNet\Model\Room;
use App\BusProNet\Model\Service;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Form\Model\RoomSelectionDto;
use App\Service\BookingPriceCalculatorService;
use App\Service\ParticipantEligibilityService;
use PHPUnit\Framework\TestCase;
class BookingPriceCalculatorServiceTest extends TestCase
{
private BookingPriceCalculatorService $service;
protected function setUp(): void
{
$participantEligibilityService = $this->createMock(ParticipantEligibilityService::class);
$participantEligibilityService->method('isParticipantEligible')->willReturn(true);
$insuranceTypeFilterService = new \App\Service\InsuranceTypeFilterService();
$insuranceEligibilityService = new \App\Service\InsuranceEligibilityService();
$this->service = new BookingPriceCalculatorService(
$participantEligibilityService,
$insuranceTypeFilterService,
$insuranceEligibilityService
);
}
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->roomId = 1;
$roomSelection->quantity = 2; // 2 rooms selected
$bookingDto = new BookingDto($travel, 1);
$bookingDto->roomSelections = [$roomSelection];
// Test the calculation
$result = $this->service->calculateRoomPricing($bookingDto);
// Expected: 2 rooms × 2 participants/room × €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']); // 4 participants total
$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->roomId = 2;
$roomSelection->quantity = 1; // 1 room selected
$bookingDto = new BookingDto($travel, 1);
$bookingDto->roomSelections = [$roomSelection];
// Test the calculation
$result = $this->service->calculateRoomPricing($bookingDto);
// Expected: 1 room × 3 participants/room × €150 = €450 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(3, $result[0]['participantCount']); // 3 participants total
$this->assertEquals(150.0, $result[0]['unitPrice']); // €150 per participant
$this->assertEquals(450.0, $result[0]['totalPrice']); // €450 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->roomId = 1;
$singleRoomSelection->quantity = 1; // 1 single room
$doubleRoomSelection = new RoomSelectionDto();
$doubleRoomSelection->roomId = 2;
$doubleRoomSelection->quantity = 2; // 2 double rooms
$bookingDto = new BookingDto($travel, 1);
$bookingDto->roomSelections = [$singleRoomSelection, $doubleRoomSelection];
// Test the calculation
$result = $this->service->calculateRoomPricing($bookingDto);
// Expected:
// - Single: 1 room × 1 participant × €80 = €80
// - Double: 2 rooms × 2 participants × €120 = €480
$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(4, $doubleResult['participantCount']);
$this->assertEquals(120.0, $doubleResult['unitPrice']);
$this->assertEquals(480.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->roomId = 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->roomId = 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 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
$insuranceTypeFilterService = new \App\Service\InsuranceTypeFilterService();
$insuranceEligibilityService = new \App\Service\InsuranceEligibilityService();
$this->service = new BookingPriceCalculatorService(
$participantEligibilityService,
$insuranceTypeFilterService,
$insuranceEligibilityService
);
$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;
}
}