fix: incorrect rules around family insurances

addresses #869dr80qj
This commit is contained in:
Björn Fromme
2026-08-05 15:11:37 +02:00
parent 736128476b
commit 8a46908856
15 changed files with 859 additions and 50 deletions
+35
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Service;
use App\BusProNet\Model\Insurance;
use App\Form\Model\BookingDto;
/**
@@ -76,6 +77,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.
*
@@ -95,4 +112,22 @@ class BookingPriceCalculator
$participantIndex
);
}
/**
* Resolves the travel price to use for insurance eligibility checks.
*
* Family insurances are always evaluated against the total booking price (all
* participants combined); everything else uses the individual participant's price.
* Non-applicant participants can never hold a family insurance (InsuranceManager
* restricts family insurances to the applicant), so this does not need to know
* whether $participantIndex is the applicant.
*/
public function resolveInsuranceTravelPrice(BookingDto $bookingDto, int $participantIndex, Insurance $insurance): float
{
if (true === $insurance->familyInsurance) {
return $this->calculateTotalBookingPriceExcludingInsurance($bookingDto);
}
return $this->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $participantIndex);
}
}
+64 -20
View File
@@ -107,6 +107,42 @@ class InsuranceManager
})();
}
/**
* Returns eligible insurances for a participant, pricing family and non-family
* insurances on the correct basis.
*
* Family insurances must be priced by the total booking price, not the individual
* participant's price - this splits the given insurances by type and evaluates
* each group against the appropriate price before merging the results back together.
*
* @param array<Insurance> $insurances Available insurances to filter
* @param ParticipantDto $participant The participant to match insurances for
* @param BookingDto $booking The booking context for additional criteria
* @param float $individualPrice The participant's individual travel price (excluding insurance)
* @param float $totalBookingPrice The total booking price across all participants (excluding insurance)
*
* @return array<Insurance> Filtered array of eligible insurances, sorted by price
*/
public function getEligibleInsurancesForParticipant(
array $insurances,
ParticipantDto $participant,
BookingDto $booking,
float $individualPrice,
float $totalBookingPrice,
): array {
$nonFamilyInsurances = array_values(array_filter($insurances, static fn (Insurance $i) => false === $i->familyInsurance));
$familyInsurances = array_values(array_filter($insurances, static fn (Insurance $i) => true === $i->familyInsurance));
$eligibleInsurances = $this->getEligibleInsurances($nonFamilyInsurances, $participant, $booking, $individualPrice);
if (!empty($familyInsurances)) {
$eligibleFamilyInsurances = $this->getEligibleInsurances($familyInsurances, $participant, $booking, $totalBookingPrice);
$eligibleInsurances = $this->sortByPrice(array_merge($eligibleInsurances, $eligibleFamilyInsurances));
}
return $eligibleInsurances;
}
/**
* Auto-reassigns an insurance to the same type with appropriate price tier.
*
@@ -159,12 +195,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 +294,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 +326,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;