wip: insurance booking

This commit is contained in:
Björn Fromme
2025-09-29 19:54:20 +02:00
parent 0809f07d46
commit 90ae78262a
19 changed files with 807 additions and 128 deletions
+160 -43
View File
@@ -7,6 +7,9 @@ namespace App\Service;
use App\BusProNet\Model\Insurance;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\ParticipantDto;
use App\Model\InsuranceEligibilityCriteria;
use App\Service\BookingPriceCalculatorService;
use App\Service\InsuranceTypeResolver;
use Carbon\Carbon;
/**
@@ -18,6 +21,11 @@ use Carbon\Carbon;
*/
class InsuranceMatchingService
{
public function __construct(
private readonly BookingPriceCalculatorService $priceCalculatorService,
private readonly InsuranceTypeResolver $insuranceTypeResolver
) {
}
/**
* Filters insurances based on participant and booking criteria.
*
@@ -29,67 +37,151 @@ class InsuranceMatchingService
*/
public function getEligibleInsurances(array $insurances, ParticipantDto $participant, BookingCreateDto $booking): array
{
$travelStartDate = $booking->travel->dateFrom;
$travelEndDate = $booking->travel->dateTo;
$criteria = $this->createEligibilityCriteria($participant, $booking);
// Cannot match insurances without travel dates
if (null === $travelStartDate || null === $travelEndDate) {
return [];
if (null === $criteria) {
return []; // Cannot match insurances without travel dates
}
$bookingDate = Carbon::now()->toDateTimeImmutable();
$travelPrice = $this->calculateTravelPrice($booking);
$travelDurationDays = $this->calculateTravelDurationDays($travelStartDate, $travelEndDate);
return array_filter($insurances, function (Insurance $insurance) use ($participant, $travelStartDate, $travelEndDate, $bookingDate, $travelPrice, $travelDurationDays) {
return $this->isInsuranceEligible($insurance, $participant, $travelStartDate, $travelEndDate, $bookingDate, $travelPrice, $travelDurationDays);
});
return array_filter(
$insurances,
fn (Insurance $insurance) => $this->isInsuranceEligible($insurance, $criteria)
);
}
/**
* Checks if a specific insurance is eligible for a participant.
* Auto-reassigns an insurance to the same type with appropriate price tier.
*
* @param Insurance $insurance The insurance to check
* @param ParticipantDto $participant The participant
* @param \DateTimeImmutable $travelStartDate Travel start date
* @param \DateTimeImmutable $travelEndDate Travel end date
* @param \DateTimeImmutable $bookingDate Booking date
* @param float $travelPrice Total travel price
* @param int $travelDurationDays Travel duration in days
* This method is used when a participant's individual price changes and their
* current insurance is no longer eligible. It finds the same insurance type
* (subType + familyInsurance) with the correct price tier.
*
* @return bool True if insurance is eligible
* @param array<Insurance> $availableInsurances All available insurances
* @param Insurance $currentInsurance The currently selected insurance
* @param ParticipantDto $participant The participant to reassign for
* @param BookingCreateDto $booking The booking context
*
* @return Insurance|null The reassigned insurance or null if no suitable match found
*/
private function isInsuranceEligible(
Insurance $insurance,
ParticipantDto $participant,
\DateTimeImmutable $travelStartDate,
\DateTimeImmutable $travelEndDate,
\DateTimeImmutable $bookingDate,
float $travelPrice,
int $travelDurationDays,
): bool {
public function reassignInsuranceForPriceChange(array $availableInsurances, Insurance $currentInsurance, ParticipantDto $participant, BookingCreateDto $booking): ?Insurance
{
// Group insurances of the same type (subType + familyInsurance)
$sameTypeInsurances = $this->filterInsurancesByType($availableInsurances, $currentInsurance->subType, $currentInsurance->familyInsurance);
// Get eligible insurances for this participant
$eligibleInsurances = $this->getEligibleInsurances($sameTypeInsurances, $participant, $booking);
// Return the first eligible insurance (they should all be equivalent for the same type)
return !empty($eligibleInsurances) ? array_values($eligibleInsurances)[0] : null;
}
/**
* Batch-assigns insurances of the same type to all participants based on individual pricing.
*
* This method takes the applicant's insurance selection and assigns the same insurance type
* (subType + familyInsurance) to all participants, but selects the appropriate price tier
* based on each participant's individual travel price.
*
* @param array<Insurance> $availableInsurances All available insurances
* @param Insurance $selectedInsurance The insurance selected by the applicant
* @param BookingCreateDto $booking The booking with all participants
*
* @return array<int, Insurance|null> Array indexed by participant index with assigned insurances
*/
public function batchAssignInsuranceToParticipants(array $availableInsurances, Insurance $selectedInsurance, BookingCreateDto $booking): array
{
$assignments = [];
// Group insurances of the same type (subType + familyInsurance)
$sameTypeInsurances = $this->filterInsurancesByType($availableInsurances, $selectedInsurance->subType, $selectedInsurance->familyInsurance);
// Assign appropriate insurance to each participant
foreach ($booking->getParticipants() as $index => $participant) {
$eligibleInsurances = $this->getEligibleInsurances($sameTypeInsurances, $participant, $booking);
$assignments[$index] = !empty($eligibleInsurances) ? array_values($eligibleInsurances)[0] : null;
}
return $assignments;
}
/**
* Creates eligibility criteria from participant and booking data.
*/
private function createEligibilityCriteria(ParticipantDto $participant, BookingCreateDto $booking): ?InsuranceEligibilityCriteria
{
$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 (!$this->checkAgeConstraints($insurance, $participant, $travelStartDate)) {
if (false === $this->checkAgeConstraints($insurance, $criteria->participant, $criteria->travelStartDate)) {
return false;
}
// Travel date constraints
if (!$this->checkTravelDateConstraints($insurance, $travelStartDate, $travelEndDate)) {
if (false === $this->checkTravelDateConstraints($insurance, $criteria->travelStartDate, $criteria->travelEndDate)) {
return false;
}
// Booking date constraints
if (!$this->checkBookingDateConstraints($insurance, $bookingDate)) {
if (false === $this->checkBookingDateConstraints($insurance, $criteria->bookingDate)) {
return false;
}
// Travel price constraints
if (!$this->checkTravelPriceConstraints($insurance, $travelPrice)) {
if (false === $this->checkTravelPriceConstraints($insurance, $criteria->travelPrice)) {
return false;
}
// Travel duration constraints
if (!$this->checkTravelDurationConstraints($insurance, $travelDurationDays)) {
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, BookingCreateDto $booking): bool
{
$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;
}
@@ -103,8 +195,9 @@ class InsuranceMatchingService
{
$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 false; // Cannot determine age eligibility without birth date
return true;
}
// Check minimum age
@@ -197,17 +290,21 @@ class InsuranceMatchingService
}
/**
* Calculates the total travel price from booking data.
* Calculates the total travel price for a participant, excluding insurance prices.
*
* @param BookingCreateDto $booking The booking to calculate price for
* This method calculates the travel price used for insurance eligibility filtering.
* It excludes insurance prices to prevent circular dependency where insurance selection
* affects travel price which then affects insurance eligibility.
*
* @return float The total travel price
* @param BookingCreateDto $booking The booking to calculate price for
* @param int $participantIndex The participant index to calculate for
*
* @return float The total travel price for the participant excluding insurance
*/
private function calculateTravelPrice(BookingCreateDto $booking): float
private function calculateTravelPrice(BookingCreateDto $booking, int $participantIndex): float
{
// For now, return 0.0 as placeholder - this will be enhanced
// when we integrate with the existing pricing calculation system
return 0.0;
// Use the price calculator to get the participant's individual price excluding insurance
return $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($booking, $participantIndex);
}
/**
@@ -222,4 +319,24 @@ class InsuranceMatchingService
{
return $startDate->diff($endDate)->days;
}
/**
* Filters insurances by type (subType and familyInsurance combination).
*
* This method groups insurances of the same type together for reassignment or batch assignment.
* Insurance type is defined as the combination of subType (RRV, PAK, OHN) and familyInsurance flag.
*
* @param array<Insurance> $insurances All available insurances to filter
* @param string|null $subType The insurance subType to match (e.g., 'RRV', 'PAK')
* @param bool $familyInsurance Whether to match family or individual insurances
*
* @return array<Insurance> Filtered insurances of the same type
*/
private function filterInsurancesByType(array $insurances, ?string $subType, bool $familyInsurance): array
{
return array_filter(
$insurances,
fn (Insurance $insurance) => $insurance->subType === $subType && $insurance->familyInsurance === $familyInsurance
);
}
}