336 lines
11 KiB
PHP
336 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Service;
|
|
|
|
use App\BusProNet\Model\Insurance;
|
|
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\BookingPriceCalculator;
|
|
use App\Service\BookingPricingAssembler;
|
|
use App\Service\InsuranceManager;
|
|
use App\Service\ParticipantEligibilityChecker;
|
|
use App\Service\ParticipantPricingCalculator;
|
|
use App\Service\RoomPricingCalculator;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class BookingPriceCalculatorTest extends TestCase
|
|
{
|
|
private BookingPriceCalculator $service;
|
|
private RoomPricingCalculator $roomPricingCalculator;
|
|
private ParticipantPricingCalculator $participantPricingCalculator;
|
|
private BookingPricingAssembler $pricingAssembler;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$eligibilityChecker = $this->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);
|
|
}
|
|
}
|