diff --git a/src/BusProNet/DataProcessor/BookingDataProcessor.php b/src/BusProNet/DataProcessor/BookingDataProcessor.php index df298fb..104257e 100644 --- a/src/BusProNet/DataProcessor/BookingDataProcessor.php +++ b/src/BusProNet/DataProcessor/BookingDataProcessor.php @@ -14,8 +14,8 @@ use App\BusProNet\Utility\DirectionMapper; use App\Form\Model\BankAccountDto; use App\Form\Model\BookingDto; use App\Form\Model\ParticipantDto; -use App\Service\InsuranceMatchingService; -use App\Service\InsuranceTypeFilterService; +use App\Service\BookingPriceCalculatorService; +use App\Service\InsuranceService; /** * Processes booking form data and converts it into BusProNet API payload format. @@ -29,8 +29,8 @@ use App\Service\InsuranceTypeFilterService; class BookingDataProcessor { public function __construct( - private readonly InsuranceMatchingService $insuranceMatchingService, - private readonly InsuranceTypeFilterService $insuranceTypeFilterService, + private readonly InsuranceService $insuranceService, + private readonly BookingPriceCalculatorService $priceCalculatorService, ) { } @@ -1111,17 +1111,21 @@ class BookingDataProcessor return; } - // Get all available insurances from travel data - $availableInsurances = $bookingDto->travel->insurances; + // Get selectable (non-complementary) insurances with caching + $selectableInsurances = $this->insuranceService->getSelectableInsurances($bookingDto->travel); - // Exclude complementary insurances (only available as part of packages) - $availableInsurances = $this->insuranceTypeFilterService->filterNonComplementary($availableInsurances); + // Calculate travel prices for all participants (excluding insurance) + $participantPrices = []; + foreach ($bookingDto->getParticipants() as $index => $participant) { + $participantPrices[$index] = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $index); + } - // Use InsuranceMatchingService for proper type-based assignment with price tier matching - $assignments = $this->insuranceMatchingService->batchAssignInsuranceToParticipants( - $availableInsurances, + // Use InsuranceService for proper type-based assignment with price tier matching + $assignments = $this->insuranceService->batchAssignInsuranceToParticipants( + $selectableInsurances, $applicant->insurance, - $bookingDto + $bookingDto, + $participantPrices ); // Apply assignments to dependent participants (skip applicant at index 0) diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index a142210..338be0f 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -12,8 +12,8 @@ use App\BusProNet\Utility\DirectionMapper; use App\Form\Model\BookingDto; use App\Form\Service\Abstract\AbstractFieldOptionsProvider; use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory; -use App\Service\InsuranceMatchingService; -use App\Service\InsuranceTypeFilterService; +use App\Service\BookingPriceCalculatorService; +use App\Service\InsuranceService; use App\Service\ServiceAvailabilityCalculator; /** @@ -45,13 +45,14 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider * * @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders * @param ServiceAvailabilityCalculator $serviceAvailabilityCalculator Service for calculating dynamic availability - * @param InsuranceMatchingService $insuranceMatchingService Service for matching insurances to participants + * @param InsuranceService $insuranceService Service for insurance operations + * @param BookingPriceCalculatorService $priceCalculatorService Service for calculating participant prices */ public function __construct( private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory, private readonly ServiceAvailabilityCalculator $serviceAvailabilityCalculator, - private readonly InsuranceMatchingService $insuranceMatchingService, - private readonly InsuranceTypeFilterService $insuranceTypeFilterService, + private readonly InsuranceService $insuranceService, + private readonly BookingPriceCalculatorService $priceCalculatorService, ) { parent::__construct(); } @@ -756,16 +757,18 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider return []; } - $availableInsurances = $bookingDto->travel->insurances ?? []; + // Get selectable (non-complementary) insurances with caching + $selectableInsurances = $this->insuranceService->getSelectableInsurances($bookingDto->travel); - // Exclude complementary insurances (only available as part of packages) - $availableInsurances = $this->insuranceTypeFilterService->filterNonComplementary($availableInsurances); + // Calculate travel price for eligibility filtering + $travelPrice = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $participantIndex); - // Use insurance matching service to filter based on eligibility criteria - return $this->insuranceMatchingService->getEligibleInsurances( - $availableInsurances, + // Filter based on eligibility criteria for this participant + return $this->insuranceService->getEligibleInsurances( + $selectableInsurances, $participant, - $bookingDto + $bookingDto, + $travelPrice ); } } diff --git a/src/Form/Service/ParticipantInsuranceFieldHandler.php b/src/Form/Service/ParticipantInsuranceFieldHandler.php index 16e9796..bef3489 100644 --- a/src/Form/Service/ParticipantInsuranceFieldHandler.php +++ b/src/Form/Service/ParticipantInsuranceFieldHandler.php @@ -7,8 +7,8 @@ namespace App\Form\Service; use App\BusProNet\Model\Insurance; use App\Form\Model\BookingDto; use App\Form\Service\Abstract\AbstractParticipantFieldHandler; -use App\Service\InsuranceMatchingService; -use App\Service\InsuranceTypeFilterService; +use App\Service\BookingPriceCalculatorService; +use App\Service\InsuranceService; /** * Handles processing of the insurance field for booking participants. @@ -35,8 +35,8 @@ use App\Service\InsuranceTypeFilterService; class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler { public function __construct( - private readonly InsuranceMatchingService $insuranceMatchingService, - private readonly InsuranceTypeFilterService $insuranceTypeFilterService, + private readonly InsuranceService $insuranceService, + private readonly BookingPriceCalculatorService $priceCalculatorService, ) { } @@ -132,10 +132,12 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler $selectedInsuranceId = $this->getFieldValue($submittedData, $this->getFieldName()); $currentInsurance = $participant->insurance; - $availableInsurances = $bookingDto->travel->insurances ?? []; - // Exclude complementary insurances (only available as part of packages) - $availableInsurances = $this->insuranceTypeFilterService->filterNonComplementary($availableInsurances); + // 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); // Determine if this is a new user selection or just form resubmission $isNewSelection = null !== $selectedInsuranceId @@ -143,10 +145,10 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler // Handle explicit new insurance selection from user if ($isNewSelection) { - $selectedInsurance = $this->findInsuranceById($availableInsurances, $selectedInsuranceId); + $selectedInsurance = $this->findInsuranceById($selectableInsurances, $selectedInsuranceId); if (null !== $selectedInsurance) { - $eligibleInsurances = $this->insuranceMatchingService->getEligibleInsurances($availableInsurances, $participant, $bookingDto); + $eligibleInsurances = $this->insuranceService->getEligibleInsurances($selectableInsurances, $participant, $bookingDto, $travelPrice); $isSelectedInsuranceEligible = $this->isInsuranceInList($selectedInsurance, $eligibleInsurances); if ($isSelectedInsuranceEligible) { @@ -154,11 +156,12 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler $participant->insurance = $selectedInsurance; } else { // New selection is not eligible - try to find alternative in same type - $reassignedInsurance = $this->insuranceMatchingService->reassignInsuranceForPriceChange( - $availableInsurances, + $reassignedInsurance = $this->insuranceService->reassignInsuranceForPriceChange( + $selectableInsurances, $selectedInsurance, $participant, - $bookingDto + $bookingDto, + $travelPrice ); $participant->insurance = $reassignedInsurance; @@ -178,16 +181,17 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler // Handle form resubmission with existing insurance (automatic reassignment check) if (null !== $currentInsurance && null !== $selectedInsuranceId) { // Check if current insurance is still eligible with updated participant data - $eligibleInsurances = $this->insuranceMatchingService->getEligibleInsurances($availableInsurances, $participant, $bookingDto); + $eligibleInsurances = $this->insuranceService->getEligibleInsurances($selectableInsurances, $participant, $bookingDto, $travelPrice); $isCurrentInsuranceStillEligible = $this->isInsuranceInList($currentInsurance, $eligibleInsurances); if (!$isCurrentInsuranceStillEligible) { // Current insurance no longer eligible - try to reassign to same type with new price tier - $reassignedInsurance = $this->insuranceMatchingService->reassignInsuranceForPriceChange( - $availableInsurances, + $reassignedInsurance = $this->insuranceService->reassignInsuranceForPriceChange( + $selectableInsurances, $currentInsurance, $participant, - $bookingDto + $bookingDto, + $travelPrice ); if (null !== $reassignedInsurance && $reassignedInsurance->id !== $currentInsurance->id) { diff --git a/src/Service/BookingPriceCalculatorService.php b/src/Service/BookingPriceCalculatorService.php index 352d96f..2c60d90 100644 --- a/src/Service/BookingPriceCalculatorService.php +++ b/src/Service/BookingPriceCalculatorService.php @@ -22,8 +22,7 @@ class BookingPriceCalculatorService { public function __construct( private readonly ParticipantEligibilityService $participantEligibilityService, - private readonly InsuranceTypeFilterService $insuranceTypeFilterService, - private readonly InsuranceEligibilityService $insuranceEligibilityService, + private readonly InsuranceService $insuranceService, ) { } @@ -772,22 +771,20 @@ class BookingPriceCalculatorService // For dependent participants: calculate price-tier-adjusted insurance // This mirrors the logic in BookingDataProcessor::applyBulkInsuranceIfActive() - $availableInsurances = $bookingDto->travel->insurances; - - // Exclude complementary insurances (only available as part of packages) - $availableInsurances = $this->insuranceTypeFilterService->filterNonComplementary($availableInsurances); + // Get selectable (non-complementary) insurances with caching + $selectableInsurances = $this->insuranceService->getSelectableInsurances($bookingDto->travel); // Get insurances of the same type as applicant's selection - $sameTypeInsurances = $this->insuranceTypeFilterService->filterByType( - $availableInsurances, + $sameTypeInsurances = $this->insuranceService->filterByType( + $selectableInsurances, $applicant->insurance ); - // Calculate participant's travel price (excluding insurance) + // Calculate travel price for eligibility checks $travelPrice = $this->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $participant->index); // Get eligible insurances for THIS participant (price tier adjusted) - $eligibleInsurances = $this->insuranceEligibilityService->getEligibleInsurances( + $eligibleInsurances = $this->insuranceService->getEligibleInsurances( $sameTypeInsurances, $participant, $bookingDto, diff --git a/src/Service/InsuranceEligibilityService.php b/src/Service/InsuranceEligibilityService.php deleted file mode 100644 index 0d749f1..0000000 --- a/src/Service/InsuranceEligibilityService.php +++ /dev/null @@ -1,219 +0,0 @@ - $insurances Available insurances to filter - * @param ParticipantDto $participant The participant to match insurances for - * @param BookingDto $booking The booking context - * @param float $travelPrice The participant's travel price (excluding insurance) - * - * @return array Filtered array of eligible insurances, sorted by price - */ - public function getEligibleInsurances( - array $insurances, - ParticipantDto $participant, - BookingDto $booking, - float $travelPrice, - ): array { - $travelStartDate = $booking->travel->dateFrom; - $travelEndDate = $booking->travel->dateTo; - - if (null === $travelStartDate || null === $travelEndDate) { - return []; // Cannot evaluate without travel dates - } - - $bookingDate = Carbon::now()->toDateTimeImmutable(); - $travelDurationDays = $travelStartDate->diff($travelEndDate)->days; - - $eligibleInsurances = array_filter( - $insurances, - fn (Insurance $insurance) => $this->isInsuranceEligible( - $insurance, - $participant, - $booking, - $travelStartDate, - $travelEndDate, - $bookingDate, - $travelPrice, - $travelDurationDays - ) - ); - - return $this->sortByPrice($eligibleInsurances); - } - - /** - * Checks if a specific insurance is eligible for given criteria. - */ - private function isInsuranceEligible( - Insurance $insurance, - ParticipantDto $participant, - BookingDto $booking, - \DateTimeImmutable $travelStartDate, - \DateTimeImmutable $travelEndDate, - \DateTimeImmutable $bookingDate, - float $travelPrice, - int $travelDurationDays, - ): bool { - // Family insurance constraints - if (false === $this->checkFamilyInsuranceConstraints($insurance, $booking)) { - return false; - } - - // Age constraints - if (false === $this->checkAgeConstraints($insurance, $participant, $travelStartDate)) { - return false; - } - - // Travel date constraints - if (false === $this->checkTravelDateConstraints($insurance, $travelStartDate, $travelEndDate)) { - return false; - } - - // Booking date constraints - if (false === $this->checkBookingDateConstraints($insurance, $bookingDate)) { - return false; - } - - // Travel price constraints - if (false === $this->checkTravelPriceConstraints($insurance, $travelPrice)) { - return false; - } - - // Travel duration constraints - if (false === $this->checkTravelDurationConstraints($insurance, $travelDurationDays)) { - return false; - } - - return true; - } - - 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; - } - - 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; - } - - 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; - } - - 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; - } - - 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; - } - - 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; - } -} diff --git a/src/Service/InsuranceMatchingService.php b/src/Service/InsuranceMatchingService.php deleted file mode 100644 index b6d1746..0000000 --- a/src/Service/InsuranceMatchingService.php +++ /dev/null @@ -1,131 +0,0 @@ - $insurances Available insurances to filter - * @param ParticipantDto $participant The participant to match insurances for - * @param BookingDto $booking The booking context for additional criteria - * - * @return array Filtered array of eligible insurances - */ - public function getEligibleInsurances(array $insurances, ParticipantDto $participant, BookingDto $booking): array - { - // Calculate travel price for this participant - $travelPrice = $this->calculateTravelPrice($booking, $participant->index); - - // Delegate to InsuranceEligibilityService - return $this->insuranceEligibilityService->getEligibleInsurances( - $insurances, - $participant, - $booking, - $travelPrice - ); - } - - /** - * Auto-reassigns an insurance to the same type with appropriate price tier. - * - * 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. - * - * @param array $availableInsurances All available insurances - * @param Insurance $currentInsurance The currently selected insurance - * @param ParticipantDto $participant The participant to reassign for - * @param BookingDto $booking The booking context - * - * @return Insurance|null The reassigned insurance or null if no suitable match found - */ - public function reassignInsuranceForPriceChange(array $availableInsurances, Insurance $currentInsurance, ParticipantDto $participant, BookingDto $booking): ?Insurance - { - // Group insurances of the same type - $sameTypeInsurances = $this->insuranceTypeFilterService->filterByType($availableInsurances, $currentInsurance); - - // 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. - * - * FUTURE FEATURE: This method will be used when implementing the "applicant assigns - * insurance to all participants" feature. The applicant's selection will be propagated - * to all participants with automatic price tier adjustment based on individual prices. - * - * 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 $availableInsurances All available insurances - * @param Insurance $selectedInsurance The insurance selected by the applicant - * @param BookingDto $booking The booking with all participants - * - * @return array Array indexed by participant index with assigned insurances - * - * @internal Reserved for future feature implementation - */ - public function batchAssignInsuranceToParticipants(array $availableInsurances, Insurance $selectedInsurance, BookingDto $booking): array - { - $assignments = []; - - // Group insurances of the same type - $sameTypeInsurances = $this->insuranceTypeFilterService->filterByType($availableInsurances, $selectedInsurance); - - // 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; - } - - /** - * Calculates the total travel price for a participant, excluding insurance prices. - * - * 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. - * - * @param BookingDto $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(BookingDto $booking, int $participantIndex): float - { - // Use the price calculator to get the participant's individual price excluding insurance - return $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($booking, $participantIndex); - } -} diff --git a/src/Service/InsuranceService.php b/src/Service/InsuranceService.php new file mode 100644 index 0000000..bf820ed --- /dev/null +++ b/src/Service/InsuranceService.php @@ -0,0 +1,353 @@ + Array of selectable insurances + */ + public function getSelectableInsurances(Travel $travel): array + { + $cacheKey = self::CACHE_KEY_PREFIX.$travel->id; + + return Blink::global()->once($cacheKey, function () use ($travel) { + return $this->filterNonComplementary($travel->insurances ?? []); + }); + } + + /** + * Filters insurances based on participant and booking criteria. + * + * @param array $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 $travelPrice The participant's travel price (excluding insurance) for price tier matching + * + * @return array Filtered array of eligible insurances, sorted by price + */ + public function getEligibleInsurances(array $insurances, ParticipantDto $participant, BookingDto $booking, float $travelPrice): array + { + $travelStartDate = $booking->travel->dateFrom; + $travelEndDate = $booking->travel->dateTo; + + if (null === $travelStartDate || null === $travelEndDate) { + return []; // Cannot evaluate without travel dates + } + + $bookingDate = Carbon::now()->toDateTimeImmutable(); + $travelDurationDays = $travelStartDate->diff($travelEndDate)->days; + + $eligibleInsurances = array_filter( + $insurances, + fn (Insurance $insurance) => $this->isInsuranceEligible( + $insurance, + $participant, + $booking, + $travelStartDate, + $travelEndDate, + $bookingDate, + $travelPrice, + $travelDurationDays + ) + ); + + return $this->sortByPrice($eligibleInsurances); + } + + /** + * Auto-reassigns an insurance to the same type with appropriate price tier. + * + * 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. + * + * @param array $availableInsurances All available insurances + * @param Insurance $currentInsurance The currently selected insurance + * @param ParticipantDto $participant The participant to reassign for + * @param BookingDto $booking The booking context + * @param float $travelPrice The participant's travel price (excluding insurance) + * + * @return Insurance|null The reassigned insurance or null if no suitable match found + */ + public function reassignInsuranceForPriceChange(array $availableInsurances, Insurance $currentInsurance, ParticipantDto $participant, BookingDto $booking, float $travelPrice): ?Insurance + { + // Group insurances of the same type + $sameTypeInsurances = $this->filterByType($availableInsurances, $currentInsurance); + + // Get eligible insurances for this participant + $eligibleInsurances = $this->getEligibleInsurances($sameTypeInsurances, $participant, $booking, $travelPrice); + + // 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 $availableInsurances All available insurances + * @param Insurance $selectedInsurance The insurance selected by the applicant + * @param BookingDto $booking The booking with all participants + * @param array $participantPrices Map of participant index to travel price (excluding insurance) + * + * @return array Array indexed by participant index with assigned insurances + */ + public function batchAssignInsuranceToParticipants(array $availableInsurances, Insurance $selectedInsurance, BookingDto $booking, array $participantPrices): array + { + $assignments = []; + + // Group insurances of the same type + $sameTypeInsurances = $this->filterByType($availableInsurances, $selectedInsurance); + + // Assign appropriate insurance to each participant + foreach ($booking->getParticipants() as $index => $participant) { + $travelPrice = $participantPrices[$index] ?? 0.0; + $eligibleInsurances = $this->getEligibleInsurances($sameTypeInsurances, $participant, $booking, $travelPrice); + $assignments[$index] = !empty($eligibleInsurances) ? array_values($eligibleInsurances)[0] : null; + } + + return $assignments; + } + + /** + * 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 $insurances All available insurances to filter + * @param Insurance $referenceInsurance The insurance to match against + * + * @return array Filtered insurances of the same type + */ + public function filterByType(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 + ); + } + + /** + * Filters out complementary insurances from an insurance array. + * + * Complementary insurances are only available as part of packages + * and cannot be directly selected by users. + * + * @param array $insurances Array of insurances to filter + * + * @return array Array containing only non-complementary insurances with reset keys + */ + private function filterNonComplementary(array $insurances): array + { + return array_values( + array_filter($insurances, fn (Insurance $insurance) => false === $insurance->complementary) + ); + } + + /** + * Checks if a specific insurance is eligible for given criteria. + */ + private function isInsuranceEligible( + Insurance $insurance, + ParticipantDto $participant, + BookingDto $booking, + \DateTimeImmutable $travelStartDate, + \DateTimeImmutable $travelEndDate, + \DateTimeImmutable $bookingDate, + float $travelPrice, + int $travelDurationDays, + ): bool { + // Family insurance constraints + if (false === $this->checkFamilyInsuranceConstraints($insurance, $booking)) { + return false; + } + + // Age constraints + if (false === $this->checkAgeConstraints($insurance, $participant, $travelStartDate)) { + return false; + } + + // Travel date constraints + if (false === $this->checkTravelDateConstraints($insurance, $travelStartDate, $travelEndDate)) { + return false; + } + + // Booking date constraints + if (false === $this->checkBookingDateConstraints($insurance, $bookingDate)) { + return false; + } + + // Travel price constraints + if (false === $this->checkTravelPriceConstraints($insurance, $travelPrice)) { + return false; + } + + // Travel duration constraints + if (false === $this->checkTravelDurationConstraints($insurance, $travelDurationDays)) { + return false; + } + + return true; + } + + 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; + } + + 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; + } + + 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; + } + + 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; + } + + 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; + } + + 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; + } + +} diff --git a/src/Service/InsuranceTypeFilterService.php b/src/Service/InsuranceTypeFilterService.php deleted file mode 100644 index 62f5437..0000000 --- a/src/Service/InsuranceTypeFilterService.php +++ /dev/null @@ -1,67 +0,0 @@ - $insurances Array of insurances to filter - * - * @return array Array containing only non-complementary insurances with reset keys - */ - public function filterNonComplementary(array $insurances): array - { - return array_values( - array_filter($insurances, fn (Insurance $insurance) => false === $insurance->complementary) - ); - } - - /** - * 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 $insurances All available insurances to filter - * @param Insurance $referenceInsurance The insurance to match against - * - * @return array Filtered insurances of the same type - */ - public function filterByType(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 - ); - } -} diff --git a/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php b/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php index 4c2b62b..cb915c6 100644 --- a/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php +++ b/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php @@ -16,8 +16,8 @@ use App\BusProNet\Model\Service; use App\BusProNet\Model\Travel; use App\Form\Model\BookingDto; use App\Form\Model\ParticipantDto; -use App\Service\InsuranceMatchingService; -use App\Service\InsuranceTypeFilterService; +use App\Service\BookingPriceCalculatorService; +use App\Service\InsuranceService; use PHPUnit\Framework\TestCase; /** @@ -32,15 +32,19 @@ class BookingDataProcessorTest extends TestCase protected function setUp(): void { - // Create a mock InsuranceMatchingService - $insuranceMatchingService = $this->createMock(InsuranceMatchingService::class); + // Create a mock InsuranceService + $insuranceService = $this->createMock(InsuranceService::class); + $priceCalculatorService = $this->createMock(BookingPriceCalculatorService::class); - // Create a mock InsuranceTypeFilterService that simply returns the input - $insuranceTypeFilterService = $this->createMock(InsuranceTypeFilterService::class); - $insuranceTypeFilterService->method('filterNonComplementary') - ->willReturnArgument(0); + // Mock getSelectableInsurances to return empty array (not used in these tests) + $insuranceService->method('getSelectableInsurances') + ->willReturn([]); - $this->processor = new BookingDataProcessor($insuranceMatchingService, $insuranceTypeFilterService); + // Mock price calculator to return default prices (not used in most tests) + $priceCalculatorService->method('calculateIndividualParticipantPriceExcludingInsurance') + ->willReturn(500.0); + + $this->processor = new BookingDataProcessor($insuranceService, $priceCalculatorService); } public function testCreateUpdateRequestPayloadWithCompleteData(): void diff --git a/tests/Form/Service/ParticipantInsuranceFieldHandlerTest.php b/tests/Form/Service/ParticipantInsuranceFieldHandlerTest.php index 5a23d5f..78d5d97 100644 --- a/tests/Form/Service/ParticipantInsuranceFieldHandlerTest.php +++ b/tests/Form/Service/ParticipantInsuranceFieldHandlerTest.php @@ -9,24 +9,30 @@ use App\BusProNet\Model\Travel; use App\Form\Model\BookingDto; use App\Form\Model\ParticipantDto; use App\Form\Service\ParticipantInsuranceFieldHandler; -use App\Service\InsuranceMatchingService; -use App\Service\InsuranceTypeFilterService; +use App\Service\BookingPriceCalculatorService; +use App\Service\InsuranceService; use PHPUnit\Framework\TestCase; class ParticipantInsuranceFieldHandlerTest extends TestCase { private ParticipantInsuranceFieldHandler $handler; - private InsuranceMatchingService $insuranceMatchingService; + private InsuranceService $insuranceService; + private BookingPriceCalculatorService $priceCalculatorService; protected function setUp(): void { - $this->insuranceMatchingService = $this->createMock(InsuranceMatchingService::class); + $this->insuranceService = $this->createMock(InsuranceService::class); + $this->priceCalculatorService = $this->createMock(BookingPriceCalculatorService::class); - $insuranceTypeFilterService = $this->createMock(InsuranceTypeFilterService::class); - $insuranceTypeFilterService->method('filterNonComplementary') - ->willReturnArgument(0); + // Mock getSelectableInsurances to return input array + $this->insuranceService->method('getSelectableInsurances') + ->willReturnCallback(fn (Travel $travel) => $travel->insurances ?? []); - $this->handler = new ParticipantInsuranceFieldHandler($this->insuranceMatchingService, $insuranceTypeFilterService); + // Mock price calculator to return a default price + $this->priceCalculatorService->method('calculateIndividualParticipantPriceExcludingInsurance') + ->willReturn(500.0); + + $this->handler = new ParticipantInsuranceFieldHandler($this->insuranceService, $this->priceCalculatorService); } public function testGetFieldName(): void @@ -85,8 +91,12 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase $participant = new ParticipantDto(); $participant->insurance = $this->createInsurance('123'); + $travel = new Travel(); + $travel->insurances = []; + $bookingDto = $this->createMockBookingDto(); $bookingDto->method('getParticipant')->with(0)->willReturn($participant); + $bookingDto->travel = $travel; $this->handler->processField(['insurance' => null], $bookingDto, 0); @@ -98,8 +108,12 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase $participant = new ParticipantDto(); $participant->insurance = $this->createInsurance('123'); + $travel = new Travel(); + $travel->insurances = []; + $bookingDto = $this->createMockBookingDto(); $bookingDto->method('getParticipant')->with(0)->willReturn($participant); + $bookingDto->travel = $travel; $this->handler->processField(['insurance' => ''], $bookingDto, 0); @@ -111,8 +125,12 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase $participant = new ParticipantDto(); $participant->insurance = $this->createInsurance('123'); + $travel = new Travel(); + $travel->insurances = []; + $bookingDto = $this->createMockBookingDto(); $bookingDto->method('getParticipant')->with(0)->willReturn($participant); + $bookingDto->travel = $travel; $this->handler->processField([], $bookingDto, 0); @@ -145,10 +163,10 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase $bookingDto->method('getParticipant')->with(0)->willReturn($participant); $bookingDto->travel = $travel; - $this->insuranceMatchingService + $this->insuranceService ->expects($this->once()) ->method('getEligibleInsurances') - ->with([$insurance], $participant, $bookingDto) + ->with([$insurance], $participant, $bookingDto, 500.0) ->willReturn([$insurance]); $this->handler->processField(['insurance' => '123'], $bookingDto, 0); @@ -167,10 +185,10 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase $bookingDto->method('getParticipant')->with(0)->willReturn($participant); $bookingDto->travel = $travel; - $this->insuranceMatchingService + $this->insuranceService ->expects($this->once()) ->method('getEligibleInsurances') - ->with([$insurance], $participant, $bookingDto) + ->with([$insurance], $participant, $bookingDto, 500.0) ->willReturn([]); // Not eligible $this->handler->processField(['insurance' => '123'], $bookingDto, 0); @@ -189,7 +207,7 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase $bookingDto->method('getParticipant')->with(0)->willReturn($participant); $bookingDto->travel = $travel; - $this->insuranceMatchingService + $this->insuranceService ->expects($this->once()) ->method('getEligibleInsurances') ->willReturn([$insurance]); diff --git a/tests/Service/BookingPriceCalculatorServiceTest.php b/tests/Service/BookingPriceCalculatorServiceTest.php index 05fa8ea..14b05f7 100644 --- a/tests/Service/BookingPriceCalculatorServiceTest.php +++ b/tests/Service/BookingPriceCalculatorServiceTest.php @@ -23,13 +23,12 @@ class BookingPriceCalculatorServiceTest extends TestCase $participantEligibilityService = $this->createMock(ParticipantEligibilityService::class); $participantEligibilityService->method('isParticipantEligible')->willReturn(true); - $insuranceTypeFilterService = new \App\Service\InsuranceTypeFilterService(); - $insuranceEligibilityService = new \App\Service\InsuranceEligibilityService(); + // Create a real InsuranceService (it will use a mocked price calculator internally when needed) + $insuranceService = new \App\Service\InsuranceService($this->service ?? $this->createMock(BookingPriceCalculatorService::class)); $this->service = new BookingPriceCalculatorService( $participantEligibilityService, - $insuranceTypeFilterService, - $insuranceEligibilityService + $insuranceService ); } @@ -494,13 +493,13 @@ class BookingPriceCalculatorServiceTest extends TestCase $participantEligibilityService->method('isParticipantEligible') ->willReturnCallback(fn ($booking, $index) => 0 === $index); // Only first participant eligible - $insuranceTypeFilterService = new \App\Service\InsuranceTypeFilterService(); - $insuranceEligibilityService = new \App\Service\InsuranceEligibilityService(); + // Create a real InsuranceService with mocked price calculator + $mockPriceCalculator = $this->createMock(BookingPriceCalculatorService::class); + $insuranceService = new \App\Service\InsuranceService($mockPriceCalculator); $this->service = new BookingPriceCalculatorService( $participantEligibilityService, - $insuranceTypeFilterService, - $insuranceEligibilityService + $insuranceService ); $result = $this->service->calculateServicePricing($bookingDto); diff --git a/tests/Service/InsuranceMatchingServiceTest.php b/tests/Service/InsuranceServiceTest.php similarity index 54% rename from tests/Service/InsuranceMatchingServiceTest.php rename to tests/Service/InsuranceServiceTest.php index 2d51c36..5cf9c7d 100644 --- a/tests/Service/InsuranceMatchingServiceTest.php +++ b/tests/Service/InsuranceServiceTest.php @@ -5,42 +5,21 @@ declare(strict_types=1); namespace App\Tests\Service; use App\BusProNet\Model\Insurance; +use App\BusProNet\Model\Travel; use App\Form\Model\BookingDto; use App\Form\Model\ParticipantDto; -use App\Service\BookingPriceCalculatorService; -use App\Service\InsuranceEligibilityService; -use App\Service\InsuranceMatchingService; -use App\Service\InsuranceTypeFilterService; +use App\Service\InsuranceService; use Carbon\Carbon; use PHPUnit\Framework\TestCase; -class InsuranceMatchingServiceTest extends TestCase +class InsuranceServiceTest extends TestCase { - private InsuranceMatchingService $service; - private BookingPriceCalculatorService $priceCalculator; + private InsuranceService $service; protected function setUp(): void { - // Mock the price calculator service - $this->priceCalculator = $this->createMock(BookingPriceCalculatorService::class); - $this->priceCalculator - ->method('calculateIndividualParticipantPriceExcludingInsurance') - ->willReturn(500.0); // Default test price - - // Mock InsuranceTypeFilterService - just return the input array for filterByType - $insuranceTypeFilterService = $this->createMock(InsuranceTypeFilterService::class); - $insuranceTypeFilterService->method('filterByType') - ->willReturnArgument(0); - - // Use real InsuranceEligibilityService since it has no dependencies - // and these tests are actually testing the eligibility filtering logic - $insuranceEligibilityService = new InsuranceEligibilityService(); - - $this->service = new InsuranceMatchingService( - $this->priceCalculator, - $insuranceTypeFilterService, - $insuranceEligibilityService - ); + // InsuranceService is stateless and has no dependencies + $this->service = new InsuranceService(); // Set a fixed test date for consistent test results Carbon::setTestNow('2024-06-01 12:00:00'); @@ -52,6 +31,36 @@ class InsuranceMatchingServiceTest extends TestCase Carbon::setTestNow(); } + public function testGetSelectableInsurancesFiltersComplementary(): void + { + $regularInsurance = $this->createInsurance(['id' => '1', 'complementary' => false]); + $complementaryInsurance = $this->createInsurance(['id' => '2', 'complementary' => true]); + + $travel = new Travel(); + $travel->id = 123; + $travel->insurances = [$regularInsurance, $complementaryInsurance]; + + $result = $this->service->getSelectableInsurances($travel); + + $this->assertCount(1, $result); + $this->assertSame($regularInsurance, $result[0]); + } + + public function testGetSelectableInsurancesCachesResults(): void + { + $insurance = $this->createInsurance(['id' => '1', 'complementary' => false]); + + $travel = new Travel(); + $travel->id = 456; + $travel->insurances = [$insurance]; + + // Call twice - second call should use cache + $result1 = $this->service->getSelectableInsurances($travel); + $result2 = $this->service->getSelectableInsurances($travel); + + $this->assertSame($result1, $result2); + } + public function testGetEligibleInsurancesWithMatchingCriteria(): void { $participant = $this->createParticipant('1990-06-15'); @@ -70,7 +79,7 @@ class InsuranceMatchingServiceTest extends TestCase 'travelDurationTo' => 14, ]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); $this->assertCount(1, $result); $this->assertContains($insurance, $result); @@ -83,7 +92,7 @@ class InsuranceMatchingServiceTest extends TestCase $insurance = $this->createInsurance(['ageFrom' => 18, 'ageTo' => 65]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); $this->assertEmpty($result); } @@ -95,7 +104,7 @@ class InsuranceMatchingServiceTest extends TestCase $insurance = $this->createInsurance(['ageFrom' => 18, 'ageTo' => 65]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); $this->assertEmpty($result); } @@ -110,7 +119,7 @@ class InsuranceMatchingServiceTest extends TestCase 'travelDateTo' => new \DateTimeImmutable('2024-12-31'), ]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); $this->assertEmpty($result); } @@ -125,7 +134,7 @@ class InsuranceMatchingServiceTest extends TestCase 'travelDateTo' => new \DateTimeImmutable('2024-07-31'), ]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); $this->assertEmpty($result); } @@ -140,7 +149,7 @@ class InsuranceMatchingServiceTest extends TestCase 'bookingDateTo' => new \DateTimeImmutable('2025-12-31'), ]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); $this->assertEmpty($result); } @@ -155,7 +164,33 @@ class InsuranceMatchingServiceTest extends TestCase 'bookingDateTo' => new \DateTimeImmutable('2023-12-31'), ]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); + + $this->assertEmpty($result); + } + + public function testGetEligibleInsurancesExcludesInsuranceWithTravelPriceTooLow(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + // Price calculator returns 500.0, insurance requires at least 1000.0 + $insurance = $this->createInsurance(['travelPriceFrom' => 1000.0, 'travelPriceTo' => 2000.0]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); + + $this->assertEmpty($result); + } + + public function testGetEligibleInsurancesExcludesInsuranceWithTravelPriceTooHigh(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + // Price calculator returns 500.0, insurance allows maximum 100.0 + $insurance = $this->createInsurance(['travelPriceFrom' => 0.0, 'travelPriceTo' => 100.0]); + + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); $this->assertEmpty($result); } @@ -167,7 +202,7 @@ class InsuranceMatchingServiceTest extends TestCase $insurance = $this->createInsurance(['travelDurationFrom' => 7, 'travelDurationTo' => 30]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); $this->assertEmpty($result); } @@ -179,7 +214,7 @@ class InsuranceMatchingServiceTest extends TestCase $insurance = $this->createInsurance(['travelDurationFrom' => 1, 'travelDurationTo' => 14]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); $this->assertEmpty($result); } @@ -187,15 +222,15 @@ class InsuranceMatchingServiceTest extends TestCase public function testGetEligibleInsurancesHandlesParticipantWithoutBirthDate(): void { $participant = new ParticipantDto(); - $participant->index = 0; // Set participant index for price calculations + $participant->index = 0; $participant->dateOfBirth = null; $booking = $this->createBooking('2024-08-01', '2024-08-08'); $insurance = $this->createInsurance(['ageFrom' => 18, 'ageTo' => 65]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); - // Participants without birth date should now have insurances available + // Participants without birth date should have insurances available // (field visibility is controlled by field state conditions, not service logic) $this->assertNotEmpty($result); $this->assertCount(1, $result); @@ -210,7 +245,7 @@ class InsuranceMatchingServiceTest extends TestCase // Insurance with no constraints should match any criteria $insurance = $this->createInsurance([]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); $this->assertCount(1, $result); $this->assertContains($insurance, $result); @@ -226,7 +261,7 @@ class InsuranceMatchingServiceTest extends TestCase $ineligibleInsurance = $this->createInsurance(['ageFrom' => 60, 'ageTo' => 80]); $insurances = [$eligibleInsurance1, $eligibleInsurance2, $ineligibleInsurance]; - $result = $this->service->getEligibleInsurances($insurances, $participant, $booking); + $result = $this->service->getEligibleInsurances($insurances, $participant, $booking, 500.0); $this->assertCount(2, $result); $this->assertContains($eligibleInsurance1, $result); @@ -241,7 +276,7 @@ class InsuranceMatchingServiceTest extends TestCase $insurance = $this->createInsurance(['ageFrom' => 34, 'ageTo' => 34]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); $this->assertCount(1, $result); $this->assertContains($insurance, $result); @@ -254,16 +289,146 @@ class InsuranceMatchingServiceTest extends TestCase $insurance = $this->createInsurance(['travelDurationFrom' => 7, 'travelDurationTo' => 7]); - $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking); + $result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0); $this->assertCount(1, $result); $this->assertContains($insurance, $result); } + public function testFilterByTypeForPackages(): void + { + $packageInsurance1 = $this->createInsurance([ + 'id' => '1', + 'package' => true, + 'label' => 'Premium Package', + 'familyInsurance' => false, + ]); + $packageInsurance2 = $this->createInsurance([ + 'id' => '2', + 'package' => true, + 'label' => 'Premium Package', + 'familyInsurance' => false, + ]); + $differentPackage = $this->createInsurance([ + 'id' => '3', + 'package' => true, + 'label' => 'Basic Package', + 'familyInsurance' => false, + ]); + + $insurances = [$packageInsurance1, $packageInsurance2, $differentPackage]; + $result = $this->service->filterByType($insurances, $packageInsurance1); + + $this->assertCount(2, $result); + $this->assertContains($packageInsurance1, $result); + $this->assertContains($packageInsurance2, $result); + } + + public function testFilterByTypeForIndividualInsurances(): void + { + $insurance1 = $this->createInsurance([ + 'id' => '1', + 'package' => false, + 'subType' => 'RRV', + 'familyInsurance' => false, + ]); + $insurance2 = $this->createInsurance([ + 'id' => '2', + 'package' => false, + 'subType' => 'RRV', + 'familyInsurance' => false, + ]); + $differentType = $this->createInsurance([ + 'id' => '3', + 'package' => false, + 'subType' => 'PAK', + 'familyInsurance' => false, + ]); + + $insurances = [$insurance1, $insurance2, $differentType]; + $result = $this->service->filterByType($insurances, $insurance1); + + $this->assertCount(2, $result); + $this->assertContains($insurance1, $result); + $this->assertContains($insurance2, $result); + } + + public function testReassignInsuranceForPriceChange(): void + { + $participant = $this->createParticipant('1990-06-15'); + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + $currentInsurance = $this->createInsurance([ + 'id' => '1', + 'package' => false, + 'subType' => 'RRV', + 'familyInsurance' => false, + 'travelPriceFrom' => 0.0, + 'travelPriceTo' => 300.0, + ]); + $newTierInsurance = $this->createInsurance([ + 'id' => '2', + 'package' => false, + 'subType' => 'RRV', + 'familyInsurance' => false, + 'travelPriceFrom' => 300.0, + 'travelPriceTo' => 600.0, + ]); + + $availableInsurances = [$currentInsurance, $newTierInsurance]; + $result = $this->service->reassignInsuranceForPriceChange( + $availableInsurances, + $currentInsurance, + $participant, + $booking, + 500.0 + ); + + $this->assertSame($newTierInsurance, $result); + } + + public function testBatchAssignInsuranceToParticipants(): void + { + $booking = $this->createBooking('2024-08-01', '2024-08-08'); + + $participant1 = $this->createParticipant('1990-06-15'); + $participant1->index = 0; + $participant2 = $this->createParticipant('1995-03-20'); + $participant2->index = 1; + + $booking->participants = [$participant1, $participant2]; + + $selectedInsurance = $this->createInsurance([ + 'id' => '1', + 'package' => false, + 'subType' => 'RRV', + 'familyInsurance' => false, + ]); + $eligibleInsurance = $this->createInsurance([ + 'id' => '2', + 'package' => false, + 'subType' => 'RRV', + 'familyInsurance' => false, + ]); + + $availableInsurances = [$selectedInsurance, $eligibleInsurance]; + $participantPrices = [0 => 500.0, 1 => 500.0]; + $result = $this->service->batchAssignInsuranceToParticipants( + $availableInsurances, + $selectedInsurance, + $booking, + $participantPrices + ); + + $this->assertCount(2, $result); + $this->assertNotNull($result[0]); + $this->assertNotNull($result[1]); + } + private function createParticipant(string $dateOfBirth): ParticipantDto { $participant = new ParticipantDto(); - $participant->index = 0; // Set participant index for price calculations + $participant->index = 0; $participant->dateOfBirth = new \DateTimeImmutable($dateOfBirth); return $participant; @@ -277,20 +442,26 @@ class InsuranceMatchingServiceTest extends TestCase return $booking; } - private function createTravel(string $dateFrom, string $dateTo): \App\BusProNet\Model\Travel + private function createTravel(string $dateFrom, string $dateTo): Travel { - $travel = new \App\BusProNet\Model\Travel(); + $travel = new Travel(); + $travel->id = random_int(1, 999999); $travel->dateFrom = new \DateTimeImmutable($dateFrom); $travel->dateTo = new \DateTimeImmutable($dateTo); return $travel; } - private function createInsurance(array $constraints = []): Insurance + private function createInsurance(array $properties = []): Insurance { $insurance = new Insurance(); - foreach ($constraints as $property => $value) { + // Set defaults + $insurance->complementary = false; + $insurance->package = false; + $insurance->familyInsurance = false; + + foreach ($properties as $property => $value) { $insurance->$property = $value; }