wip: incorrect rules around family insurances
addresses #869dr80qj
This commit is contained in:
1 parent
a73e8a593f
commit
75e4d46996
5 files changed
+166
-25
No files matched your search
@@ -409,10 +409,11 @@ class BookingDataProcessor
|
||||
$participantPrices
|
||||
);
|
||||
|
||||
// Apply assignments to dependent participants (skip applicant at index 0)
|
||||
// Apply assignments to all participants; for non-family insurance skip the applicant
|
||||
// (they already hold the insurance they selected themselves)
|
||||
foreach ($assignments as $index => $insurance) {
|
||||
if (0 === $index) {
|
||||
continue; // Skip applicant
|
||||
if (0 === $index && false === $assignments[0]?->familyInsurance) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$participant = $bookingDto->getParticipant($index);
|
||||
|
||||
@@ -136,8 +136,9 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
// Get selectable (non-complementary) insurances with caching
|
||||
$selectableInsurances = $this->insuranceService->getSelectableInsurances($bookingDto->travel);
|
||||
|
||||
// Calculate travel price for eligibility checks
|
||||
$travelPrice = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $participantIndex);
|
||||
// Calculate individual travel price; family insurance on the applicant uses total booking price instead
|
||||
$isApplicant = 0 === $participantIndex;
|
||||
$individualPrice = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $participantIndex);
|
||||
|
||||
// Determine if this is a new user selection or just form resubmission
|
||||
$isNewSelection = null !== $selectedInsuranceId
|
||||
@@ -161,6 +162,8 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
$selectedInsurance = $this->findInsuranceById($selectableInsurances, $selectedInsuranceId);
|
||||
|
||||
if (null !== $selectedInsurance) {
|
||||
$travelPrice = $this->resolveTravelPrice($isApplicant, $selectedInsurance, $individualPrice, $bookingDto);
|
||||
|
||||
$eligibleInsurances = $this->insuranceService->getEligibleInsurances($selectableInsurances, $participant, $bookingDto, $travelPrice);
|
||||
$isSelectedInsuranceEligible = $this->isInsuranceInList($selectedInsurance, $eligibleInsurances);
|
||||
|
||||
@@ -198,6 +201,8 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
return;
|
||||
}
|
||||
|
||||
$travelPrice = $this->resolveTravelPrice($isApplicant, $currentInsurance, $individualPrice, $bookingDto);
|
||||
|
||||
// Check if current insurance is still eligible with updated participant data
|
||||
$eligibleInsurances = $this->insuranceService->getEligibleInsurances($selectableInsurances, $participant, $bookingDto, $travelPrice);
|
||||
$isCurrentInsuranceStillEligible = $this->isInsuranceInList($currentInsurance, $eligibleInsurances);
|
||||
@@ -259,6 +264,16 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
private function resolveTravelPrice(bool $isApplicant, Insurance $insurance, float $individualPrice, BookingDto $bookingDto): float
|
||||
{
|
||||
if ($isApplicant && true === $insurance->familyInsurance) {
|
||||
return $this->priceCalculatorService->calculateTotalBookingPriceExcludingInsurance($bookingDto);
|
||||
}
|
||||
|
||||
return $individualPrice;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an insurance by ID from the available insurances array.
|
||||
*
|
||||
|
||||
@@ -76,6 +76,22 @@ class BookingPriceCalculator
|
||||
return $this->participantPricingCalculator->calculateAllParticipantIndividualPrices($bookingDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates total booking price across all participants, excluding insurance.
|
||||
*
|
||||
* Used for family insurance price-tier determination, where the tier is based on
|
||||
* the combined trip cost rather than any single participant's price.
|
||||
*/
|
||||
public function calculateTotalBookingPriceExcludingInsurance(BookingDto $bookingDto): float
|
||||
{
|
||||
$total = 0.0;
|
||||
foreach ($bookingDto->getParticipants() as $index => $participant) {
|
||||
$total += $this->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $index);
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the total price for an individual participant excluding insurance.
|
||||
*
|
||||
|
||||
@@ -159,12 +159,28 @@ class InsuranceManager
|
||||
BookingDto $booking,
|
||||
array $participantPrices,
|
||||
): array {
|
||||
$assignments = [];
|
||||
|
||||
// Group insurances of the same type
|
||||
$sameTypeInsurances = $this->filterByType($availableInsurances, $selectedInsurance);
|
||||
|
||||
// Assign appropriate insurance to each participant
|
||||
// Family insurance: assign to applicant only, priced by total booking price
|
||||
if (true === $selectedInsurance->familyInsurance) {
|
||||
$totalPrice = array_sum($participantPrices);
|
||||
$applicant = $booking->getParticipant(0);
|
||||
$eligibleInsurances = null !== $applicant
|
||||
? $this->getEligibleInsurances($sameTypeInsurances, $applicant, $booking, $totalPrice)
|
||||
: [];
|
||||
$tieredInsurance = !empty($eligibleInsurances) ? array_values($eligibleInsurances)[0] : null;
|
||||
|
||||
$assignments = [];
|
||||
foreach ($booking->getParticipants() as $index => $participant) {
|
||||
$assignments[$index] = 0 === $index ? $tieredInsurance : null;
|
||||
}
|
||||
|
||||
return $assignments;
|
||||
}
|
||||
|
||||
// Non-family insurance: assign to each participant with their individual price tier
|
||||
$assignments = [];
|
||||
foreach ($booking->getParticipants() as $index => $participant) {
|
||||
$travelPrice = $participantPrices[$index] ?? 0.0;
|
||||
$eligibleInsurances = $this->getEligibleInsurances($sameTypeInsurances, $participant, $booking, $travelPrice);
|
||||
@@ -242,7 +258,7 @@ class InsuranceManager
|
||||
int $travelDurationDays,
|
||||
): bool {
|
||||
// Family insurance constraints
|
||||
if (false === $this->checkFamilyInsuranceConstraints($insurance, $booking)) {
|
||||
if (false === $this->checkFamilyInsuranceConstraints($insurance, $participant, $booking)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -274,23 +290,15 @@ class InsuranceManager
|
||||
return true;
|
||||
}
|
||||
|
||||
private function checkFamilyInsuranceConstraints(Insurance $insurance, BookingDto $booking): bool
|
||||
private function checkFamilyInsuranceConstraints(Insurance $insurance, ParticipantDto $participant, BookingDto $booking): bool
|
||||
{
|
||||
// Family booking detection only available in create mode
|
||||
if (BookingDto::MODE_EDIT === $booking->getMode()) {
|
||||
return true; // Skip family constraints for edit mode
|
||||
}
|
||||
|
||||
$isFamilyBooking = $booking->isFamilyBooking();
|
||||
|
||||
// If it's a family insurance, it should only be available for family bookings
|
||||
if (true === $insurance->familyInsurance && false === $isFamilyBooking) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If it's not a family insurance, it should only be available for non-family bookings
|
||||
if (false === $insurance->familyInsurance && true === $isFamilyBooking) {
|
||||
return false;
|
||||
if (true === $insurance->familyInsurance) {
|
||||
if (false === $booking->isFamilyBooking()) {
|
||||
return false; // Family insurance only available for family bookings
|
||||
}
|
||||
if (0 !== $participant->index) {
|
||||
return false; // Family insurance only assignable to the applicant
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -425,6 +425,93 @@ class InsuranceManagerTest extends TestCase
|
||||
$this->assertNotNull($result[1]);
|
||||
}
|
||||
|
||||
public function testBatchAssignFamilyInsuranceToApplicantOnly(): void
|
||||
{
|
||||
$booking = $this->createFamilyBooking('2024-08-01', '2024-08-08');
|
||||
|
||||
// Tier matched by total price (300 + 0 = 300, fits 250–400 tier)
|
||||
$lowTierInsurance = $this->createInsurance([
|
||||
'id' => '1',
|
||||
'package' => false,
|
||||
'subType' => 'FAM',
|
||||
'familyInsurance' => true,
|
||||
'travelPriceFrom' => 0.0,
|
||||
'travelPriceTo' => 249.99,
|
||||
]);
|
||||
$correctTierInsurance = $this->createInsurance([
|
||||
'id' => '2',
|
||||
'package' => false,
|
||||
'subType' => 'FAM',
|
||||
'familyInsurance' => true,
|
||||
'travelPriceFrom' => 250.0,
|
||||
'travelPriceTo' => 400.0,
|
||||
]);
|
||||
|
||||
$availableInsurances = [$lowTierInsurance, $correctTierInsurance];
|
||||
// Individual prices: applicant 300, child 0 → total 300
|
||||
$participantPrices = [0 => 300.0, 1 => 0.0];
|
||||
|
||||
$result = $this->service->batchAssignInsuranceToParticipants(
|
||||
$availableInsurances,
|
||||
$correctTierInsurance,
|
||||
$booking,
|
||||
$participantPrices
|
||||
);
|
||||
|
||||
$this->assertCount(2, $result);
|
||||
$this->assertSame($correctTierInsurance, $result[0], 'Applicant should get tier matching total price');
|
||||
$this->assertNull($result[1], 'Non-applicant participants must be cleared for family insurance');
|
||||
}
|
||||
|
||||
public function testFamilyInsuranceIneligibleForNonApplicant(): void
|
||||
{
|
||||
$booking = $this->createFamilyBooking('2024-08-01', '2024-08-08');
|
||||
|
||||
$nonApplicant = $this->createParticipant('2015-01-01'); // child
|
||||
$nonApplicant->index = 1;
|
||||
|
||||
$familyInsurance = $this->createInsurance(['familyInsurance' => true]);
|
||||
|
||||
$result = $this->service->getEligibleInsurances([$familyInsurance], $nonApplicant, $booking, 500.0);
|
||||
|
||||
$this->assertEmpty($result, 'Family insurance must not be eligible for non-applicant participants');
|
||||
}
|
||||
|
||||
public function testNonFamilyInsuranceEligibleForFamilyBooking(): void
|
||||
{
|
||||
$booking = $this->createFamilyBooking('2024-08-01', '2024-08-08');
|
||||
|
||||
$adult = $this->createParticipant('1990-06-15');
|
||||
$adult->index = 0;
|
||||
$child = $this->createParticipant('2015-01-01');
|
||||
$child->index = 1;
|
||||
|
||||
$nonFamilyInsurance = $this->createInsurance(['familyInsurance' => false]);
|
||||
|
||||
$adultResult = $this->service->getEligibleInsurances([$nonFamilyInsurance], $adult, $booking, 500.0);
|
||||
$childResult = $this->service->getEligibleInsurances([$nonFamilyInsurance], $child, $booking, 500.0);
|
||||
|
||||
$this->assertNotEmpty($adultResult, 'Non-family insurance must be available in family bookings for adults');
|
||||
$this->assertNotEmpty($childResult, 'Non-family insurance must be available in family bookings for children');
|
||||
}
|
||||
|
||||
public function testFamilyInsuranceIneligibleForNonFamilyBooking(): void
|
||||
{
|
||||
// Two adults, no children — not a family booking
|
||||
$booking = $this->createBooking('2024-08-01', '2024-08-08');
|
||||
$adult1 = $this->createParticipant('1990-06-15');
|
||||
$adult1->index = 0;
|
||||
$adult2 = $this->createParticipant('1985-03-10');
|
||||
$adult2->index = 1;
|
||||
$booking->participants = [$adult1, $adult2];
|
||||
|
||||
$familyInsurance = $this->createInsurance(['familyInsurance' => true]);
|
||||
|
||||
$result = $this->service->getEligibleInsurances([$familyInsurance], $adult1, $booking, 500.0);
|
||||
|
||||
$this->assertEmpty($result, 'Family insurance must not be eligible when booking is not a family booking');
|
||||
}
|
||||
|
||||
private function createParticipant(string $dateOfBirth): ParticipantDto
|
||||
{
|
||||
$participant = new ParticipantDto();
|
||||
@@ -442,6 +529,20 @@ class InsuranceManagerTest extends TestCase
|
||||
return $booking;
|
||||
}
|
||||
|
||||
private function createFamilyBooking(string $travelDateFrom, string $travelDateTo): BookingDto
|
||||
{
|
||||
$booking = $this->createBooking($travelDateFrom, $travelDateTo);
|
||||
|
||||
$adult = $this->createParticipant('1990-06-15'); // 34 years old
|
||||
$adult->index = 0;
|
||||
$child = $this->createParticipant('2015-01-01'); // 9 years old
|
||||
$child->index = 1;
|
||||
|
||||
$booking->participants = [$adult, $child];
|
||||
|
||||
return $booking;
|
||||
}
|
||||
|
||||
private function createTravel(string $dateFrom, string $dateTo): Travel
|
||||
{
|
||||
$travel = new Travel();
|
||||
|
||||
Reference in new issue
Block a user