wip: bulk insurance booking fix

This commit is contained in:
Björn Fromme
2025-10-18 13:07:27 +02:00
parent b5dfe17b73
commit 08210b8e52
6 changed files with 1132 additions and 263 deletions
+11 -257
View File
@@ -8,8 +8,6 @@ use App\BusProNet\Model\Insurance;
use App\BusProNet\Traits\SortByPriceTrait;
use App\Form\Model\BookingDto;
use App\Form\Model\ParticipantDto;
use App\Model\InsuranceEligibilityCriteria;
use Carbon\Carbon;
/**
* Service for matching insurances to participants based on eligibility criteria.
@@ -24,6 +22,8 @@ class InsuranceMatchingService
public function __construct(
private readonly BookingPriceCalculatorService $priceCalculatorService,
private readonly InsuranceTypeFilterService $insuranceTypeFilterService,
private readonly InsuranceEligibilityService $insuranceEligibilityService,
) {
}
@@ -38,18 +38,16 @@ class InsuranceMatchingService
*/
public function getEligibleInsurances(array $insurances, ParticipantDto $participant, BookingDto $booking): array
{
$criteria = $this->createEligibilityCriteria($participant, $booking);
// Calculate travel price for this participant
$travelPrice = $this->calculateTravelPrice($booking, $participant->index);
if (null === $criteria) {
return []; // Cannot match insurances without travel dates
}
$eligibleInsurances = array_filter(
// Delegate to InsuranceEligibilityService
return $this->insuranceEligibilityService->getEligibleInsurances(
$insurances,
fn (Insurance $insurance) => $this->isInsuranceEligible($insurance, $criteria)
$participant,
$booking,
$travelPrice
);
return $this->sortByPrice($eligibleInsurances);
}
/**
@@ -69,7 +67,7 @@ class InsuranceMatchingService
public function reassignInsuranceForPriceChange(array $availableInsurances, Insurance $currentInsurance, ParticipantDto $participant, BookingDto $booking): ?Insurance
{
// Group insurances of the same type
$sameTypeInsurances = $this->filterInsurancesByType($availableInsurances, $currentInsurance);
$sameTypeInsurances = $this->insuranceTypeFilterService->filterByType($availableInsurances, $currentInsurance);
// Get eligible insurances for this participant
$eligibleInsurances = $this->getEligibleInsurances($sameTypeInsurances, $participant, $booking);
@@ -102,7 +100,7 @@ class InsuranceMatchingService
$assignments = [];
// Group insurances of the same type
$sameTypeInsurances = $this->filterInsurancesByType($availableInsurances, $selectedInsurance);
$sameTypeInsurances = $this->insuranceTypeFilterService->filterByType($availableInsurances, $selectedInsurance);
// Assign appropriate insurance to each participant
foreach ($booking->getParticipants() as $index => $participant) {
@@ -113,200 +111,6 @@ class InsuranceMatchingService
return $assignments;
}
/**
* Creates eligibility criteria from participant and booking data.
*
* This method performs early validation before creating the criteria object
* to avoid unnecessary object instantiation when criteria cannot be satisfied.
*/
private function createEligibilityCriteria(ParticipantDto $participant, BookingDto $booking): ?InsuranceEligibilityCriteria
{
// Early return if travel dates are missing - cannot evaluate any criteria
$travelStartDate = $booking->travel->dateFrom;
$travelEndDate = $booking->travel->dateTo;
if (null === $travelStartDate || null === $travelEndDate) {
return null; // Cannot create criteria without travel dates
}
return new InsuranceEligibilityCriteria(
participant: $participant,
travelStartDate: $travelStartDate,
travelEndDate: $travelEndDate,
bookingDate: Carbon::now()->toDateTimeImmutable(),
travelPrice: $this->calculateTravelPrice($booking, $participant->index),
travelDurationDays: $this->calculateTravelDurationDays($travelStartDate, $travelEndDate),
booking: $booking,
);
}
/**
* Checks if a specific insurance is eligible for given criteria.
*/
private function isInsuranceEligible(Insurance $insurance, InsuranceEligibilityCriteria $criteria): bool
{
// Family insurance constraints
if (false === $this->checkFamilyInsuranceConstraints($insurance, $criteria->booking)) {
return false;
}
// Age constraints
if (false === $this->checkAgeConstraints($insurance, $criteria->participant, $criteria->travelStartDate)) {
return false;
}
// Travel date constraints
if (false === $this->checkTravelDateConstraints($insurance, $criteria->travelStartDate, $criteria->travelEndDate)) {
return false;
}
// Booking date constraints
if (false === $this->checkBookingDateConstraints($insurance, $criteria->bookingDate)) {
return false;
}
// Travel price constraints
if (false === $this->checkTravelPriceConstraints($insurance, $criteria->travelPrice)) {
return false;
}
// Travel duration constraints
if (false === $this->checkTravelDurationConstraints($insurance, $criteria->travelDurationDays)) {
return false;
}
return true;
}
/**
* Checks if family insurance constraints are met.
*
* Family insurances should only be available for family bookings,
* and individual insurances should only be available for non-family bookings.
*/
private function checkFamilyInsuranceConstraints(Insurance $insurance, 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;
}
return true;
}
/**
* Checks if participant age meets insurance age constraints.
*/
private function checkAgeConstraints(Insurance $insurance, ParticipantDto $participant, \DateTimeImmutable $travelStartDate): bool
{
$participantAge = $participant->getAge($travelStartDate);
// If no birth date is provided, skip age constraints (field will be hidden via field state conditions)
if (null === $participantAge) {
return true;
}
// Check minimum age
if (null !== $insurance->ageFrom && $participantAge < $insurance->ageFrom) {
return false;
}
// Check maximum age
if (null !== $insurance->ageTo && $participantAge > $insurance->ageTo) {
return false;
}
return true;
}
/**
* Checks if travel dates fall within insurance validity period.
*/
private function checkTravelDateConstraints(Insurance $insurance, \DateTimeImmutable $travelStartDate, \DateTimeImmutable $travelEndDate): bool
{
// Check travel start date
if (null !== $insurance->travelDateFrom && $travelStartDate < $insurance->travelDateFrom) {
return false;
}
if (null !== $insurance->travelDateTo && $travelStartDate > $insurance->travelDateTo) {
return false;
}
// Check travel end date
if (null !== $insurance->travelDateTo && $travelEndDate > $insurance->travelDateTo) {
return false;
}
return true;
}
/**
* Checks if booking date falls within insurance booking window.
*/
private function checkBookingDateConstraints(Insurance $insurance, \DateTimeImmutable $bookingDate): bool
{
// Check booking window start
if (null !== $insurance->bookingDateFrom && $bookingDate < $insurance->bookingDateFrom) {
return false;
}
// Check booking window end
if (null !== $insurance->bookingDateTo && $bookingDate > $insurance->bookingDateTo) {
return false;
}
return true;
}
/**
* Checks if travel price falls within insurance price range.
*/
private function checkTravelPriceConstraints(Insurance $insurance, float $travelPrice): bool
{
// Check minimum price
if (null !== $insurance->travelPriceFrom && $travelPrice < $insurance->travelPriceFrom) {
return false;
}
// Check maximum price
if (null !== $insurance->travelPriceTo && $travelPrice > $insurance->travelPriceTo) {
return false;
}
return true;
}
/**
* Checks if travel duration falls within insurance duration limits.
*/
private function checkTravelDurationConstraints(Insurance $insurance, int $travelDurationDays): bool
{
// Check minimum duration
if (null !== $insurance->travelDurationFrom && $travelDurationDays < $insurance->travelDurationFrom) {
return false;
}
// Check maximum duration
if (null !== $insurance->travelDurationTo && $travelDurationDays > $insurance->travelDurationTo) {
return false;
}
return true;
}
/**
* Calculates the total travel price for a participant, excluding insurance prices.
*
@@ -324,54 +128,4 @@ class InsuranceMatchingService
// Use the price calculator to get the participant's individual price excluding insurance
return $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($booking, $participantIndex);
}
/**
* Calculates travel duration in days.
*
* @param \DateTimeImmutable $startDate Travel start date
* @param \DateTimeImmutable $endDate Travel end date
*
* @return int Duration in days
*/
private function calculateTravelDurationDays(\DateTimeImmutable $startDate, \DateTimeImmutable $endDate): int
{
return $startDate->diff($endDate)->days;
}
/**
* Filters insurances by type based on label (for packages) or subType (for individual insurances).
*
* This method groups insurances of the same type together for reassignment or batch assignment.
* Insurance type matching strategy:
* - **Packages**: Match by label + familyInsurance (packages with same label are different price tiers)
* - **Individual insurances**: Match by subType + familyInsurance
*
* Example: "Reise-Rücktritt + Selbstbehaltübernahme" at €10, €14, €26 are the same type,
* but different from "Reiseschutz Platin Auto/Bahn/Bus (Europa) + Selbstbehaltübernahme".
*
* @param array<Insurance> $insurances All available insurances to filter
* @param Insurance $referenceInsurance The insurance to match against
*
* @return array<Insurance> Filtered insurances of the same type
*/
private function filterInsurancesByType(array $insurances, Insurance $referenceInsurance): array
{
// For packages, match by label (packages with same label are different price tiers of same type)
if (true === $referenceInsurance->package) {
return array_filter(
$insurances,
fn (Insurance $insurance) => true === $insurance->package
&& $insurance->label === $referenceInsurance->label
&& $insurance->familyInsurance === $referenceInsurance->familyInsurance
);
}
// For individual insurances, match by subType
return array_filter(
$insurances,
fn (Insurance $insurance) => false === $insurance->package
&& $insurance->subType === $referenceInsurance->subType
&& $insurance->familyInsurance === $referenceInsurance->familyInsurance
);
}
}