feat: move display-assembly out of BookingPriceCalculator
This commit is contained in:
@@ -4,14 +4,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Service;
|
||||
|
||||
use App\BusProNet\Model\Insurance;
|
||||
use App\BusProNet\Model\Room;
|
||||
use App\BusProNet\Model\Booking;
|
||||
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\BookingPriceCalculator;
|
||||
use App\Service\BookingPricingAssembler;
|
||||
use App\Service\InsuranceManager;
|
||||
use App\Service\ParticipantEligibilityChecker;
|
||||
use App\Service\ParticipantPricingCalculator;
|
||||
@@ -21,292 +22,33 @@ use PHPUnit\Framework\TestCase;
|
||||
class BookingPriceCalculatorTest extends TestCase
|
||||
{
|
||||
private BookingPriceCalculator $service;
|
||||
private ParticipantEligibilityChecker $participantEligibilityService;
|
||||
private RoomPricingCalculator $roomPricingCalculator;
|
||||
private ParticipantPricingCalculator $participantPricingCalculator;
|
||||
private BookingPricingAssembler $pricingAssembler;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->participantEligibilityService = $this->createMock(ParticipantEligibilityChecker::class);
|
||||
$this->participantEligibilityService->method('isParticipantEligible')->willReturn(true);
|
||||
$eligibilityChecker = $this->createMock(ParticipantEligibilityChecker::class);
|
||||
$eligibilityChecker->method('isParticipantEligible')->willReturn(true);
|
||||
|
||||
$this->roomPricingCalculator = new RoomPricingCalculator();
|
||||
$this->participantPricingCalculator = new ParticipantPricingCalculator($this->roomPricingCalculator);
|
||||
$this->pricingAssembler = new BookingPricingAssembler(
|
||||
$this->roomPricingCalculator,
|
||||
$eligibilityChecker,
|
||||
new InsuranceManager(),
|
||||
$this->participantPricingCalculator,
|
||||
);
|
||||
|
||||
$this->service = $this->createService($this->participantEligibilityService);
|
||||
}
|
||||
|
||||
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']);
|
||||
$this->service = new BookingPriceCalculator(
|
||||
$this->roomPricingCalculator,
|
||||
$this->pricingAssembler,
|
||||
$this->participantPricingCalculator,
|
||||
);
|
||||
}
|
||||
|
||||
public function testCalculateIndividualParticipantPriceWithRoomAndServices(): void
|
||||
{
|
||||
// Create test room
|
||||
$room = new Room();
|
||||
$room->id = 1;
|
||||
$room->price = 100.0;
|
||||
@@ -315,14 +57,12 @@ class BookingPriceCalculatorTest extends TestCase
|
||||
$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;
|
||||
@@ -333,7 +73,7 @@ class BookingPriceCalculatorTest extends TestCase
|
||||
|
||||
$result = $this->service->calculateIndividualParticipantPrice($bookingDto, 0);
|
||||
|
||||
// Expected: €100 (room) + €50 (ski pass) + €30 (course) = €180
|
||||
// €100 (room) + €50 (ski pass) + €30 (course) = €180
|
||||
$this->assertEquals(180.0, $result);
|
||||
}
|
||||
|
||||
@@ -341,13 +81,11 @@ class BookingPriceCalculatorTest extends TestCase
|
||||
{
|
||||
$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->assignedRoomId = null;
|
||||
$participant->skiPass = $skiPass;
|
||||
|
||||
$bookingDto = new BookingDto($travel, 1);
|
||||
@@ -355,7 +93,6 @@ class BookingPriceCalculatorTest extends TestCase
|
||||
|
||||
$result = $this->service->calculateIndividualParticipantPrice($bookingDto, 0);
|
||||
|
||||
// Expected: €0 (no room) + €50 (ski pass) = €50
|
||||
$this->assertEquals(50.0, $result);
|
||||
}
|
||||
|
||||
@@ -367,13 +104,11 @@ class BookingPriceCalculatorTest extends TestCase
|
||||
|
||||
$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;
|
||||
@@ -387,34 +122,31 @@ class BookingPriceCalculatorTest extends TestCase
|
||||
$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->assignedRoomId = 1;
|
||||
$participant1->skiPass = $skiPass;
|
||||
|
||||
$participant2 = new ParticipantDto();
|
||||
$participant2->assignedRoomId = 2; // Double room
|
||||
$participant2->assignedRoomId = 2;
|
||||
$participant2->courses = [$course];
|
||||
|
||||
$participant3 = new ParticipantDto();
|
||||
$participant3->assignedRoomId = null; // No room assigned
|
||||
$participant3->assignedRoomId = null;
|
||||
|
||||
$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
|
||||
// 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]);
|
||||
@@ -432,86 +164,60 @@ class BookingPriceCalculatorTest extends TestCase
|
||||
$this->assertEmpty($result);
|
||||
}
|
||||
|
||||
public function testServicePricingWithoutBulkInsurance(): void
|
||||
public function testCalculateGrandTotalCombinesRoomsAndServices(): 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;
|
||||
$room = new Room();
|
||||
$room->id = 1;
|
||||
$room->price = 100.0;
|
||||
$room->minPax = 1;
|
||||
|
||||
$travel = new Travel();
|
||||
$travel->insurances = [$insurance];
|
||||
$travel->dateFrom = new \DateTimeImmutable('2025-06-01');
|
||||
$travel->dateTo = new \DateTimeImmutable('2025-06-08');
|
||||
$travel->rooms = [$room];
|
||||
|
||||
// Applicant with bulk insurance enabled
|
||||
$participant1 = new ParticipantDto();
|
||||
$participant1->index = 0;
|
||||
$participant1->insurance = $insurance;
|
||||
$participant1->bulkInsuranceBooking = true;
|
||||
$skiPass = new Service();
|
||||
$skiPass->price = 40.0;
|
||||
|
||||
// Dependent with no insurance (will get price-tier-adjusted version)
|
||||
$participant2 = new ParticipantDto();
|
||||
$participant2->index = 1;
|
||||
$participant2->insurance = null;
|
||||
$participant = new ParticipantDto();
|
||||
$participant->assignedRoomId = 1;
|
||||
$participant->skiPass = $skiPass;
|
||||
|
||||
$participant3 = new ParticipantDto();
|
||||
$participant3->index = 2;
|
||||
$participant3->insurance = null;
|
||||
$roomSelection = new RoomSelectionDto();
|
||||
$roomSelection->id = 1;
|
||||
$roomSelection->quantity = 1;
|
||||
|
||||
$bookingDto = new BookingDto($travel, 3);
|
||||
$bookingDto->participants = [$participant1, $participant2, $participant3];
|
||||
$bookingDto = new BookingDto($travel, 1);
|
||||
$bookingDto->roomSelections = [$roomSelection];
|
||||
$bookingDto->participants = [$participant];
|
||||
|
||||
$result = $this->service->calculateServicePricing($bookingDto);
|
||||
$result = $this->service->calculateGrandTotal($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');
|
||||
// €100 (room) + €40 (ski pass) = €140
|
||||
$this->assertEquals(140.0, $result);
|
||||
}
|
||||
|
||||
public function testServicePricingWithBulkInsuranceShowsPriceTierAdjustment(): void
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
// 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');
|
||||
// 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;
|
||||
|
||||
$insuranceTier2 = $this->createInsurance(2, 'Reise-Rücktritt', 50.0, 'RRV');
|
||||
// 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;
|
||||
|
||||
@@ -520,188 +226,30 @@ class BookingPriceCalculatorTest extends TestCase
|
||||
$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;
|
||||
// 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;
|
||||
|
||||
// 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];
|
||||
$bookingDto->participants = [$applicant, $dependent];
|
||||
|
||||
$result = $this->service->calculateServicePricing($bookingDto);
|
||||
$serviceTotal = $this->service->calculateServiceTotal($bookingDto);
|
||||
$breakdownServiceTotal = array_sum(array_column(
|
||||
$this->pricingAssembler->calculateServicePricing($bookingDto),
|
||||
'groupTotal'
|
||||
));
|
||||
|
||||
// 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(ParticipantEligibilityChecker::class);
|
||||
$participantEligibilityService->method('isParticipantEligible')
|
||||
->willReturnCallback(fn ($booking, $index) => 0 === $index); // Only first participant eligible
|
||||
|
||||
$this->service = $this->createService($participantEligibilityService);
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
private function createService(ParticipantEligibilityChecker $participantEligibilityService): BookingPriceCalculator
|
||||
{
|
||||
return new BookingPriceCalculator(
|
||||
$this->roomPricingCalculator,
|
||||
$participantEligibilityService,
|
||||
new InsuranceManager(),
|
||||
$this->participantPricingCalculator,
|
||||
// The numeric total must match the display breakdown exactly.
|
||||
$this->assertEquals($breakdownServiceTotal, $serviceTotal,
|
||||
'calculateServiceTotal() must agree with the display breakdown on bulk-insurance bookings'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user