feat: consolidated insurance service
This commit is contained in:
@@ -14,8 +14,8 @@ use App\BusProNet\Utility\DirectionMapper;
|
|||||||
use App\Form\Model\BankAccountDto;
|
use App\Form\Model\BankAccountDto;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Service\InsuranceMatchingService;
|
use App\Service\BookingPriceCalculatorService;
|
||||||
use App\Service\InsuranceTypeFilterService;
|
use App\Service\InsuranceService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes booking form data and converts it into BusProNet API payload format.
|
* Processes booking form data and converts it into BusProNet API payload format.
|
||||||
@@ -29,8 +29,8 @@ use App\Service\InsuranceTypeFilterService;
|
|||||||
class BookingDataProcessor
|
class BookingDataProcessor
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly InsuranceMatchingService $insuranceMatchingService,
|
private readonly InsuranceService $insuranceService,
|
||||||
private readonly InsuranceTypeFilterService $insuranceTypeFilterService,
|
private readonly BookingPriceCalculatorService $priceCalculatorService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1111,17 +1111,21 @@ class BookingDataProcessor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all available insurances from travel data
|
// Get selectable (non-complementary) insurances with caching
|
||||||
$availableInsurances = $bookingDto->travel->insurances;
|
$selectableInsurances = $this->insuranceService->getSelectableInsurances($bookingDto->travel);
|
||||||
|
|
||||||
// Exclude complementary insurances (only available as part of packages)
|
// Calculate travel prices for all participants (excluding insurance)
|
||||||
$availableInsurances = $this->insuranceTypeFilterService->filterNonComplementary($availableInsurances);
|
$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
|
// Use InsuranceService for proper type-based assignment with price tier matching
|
||||||
$assignments = $this->insuranceMatchingService->batchAssignInsuranceToParticipants(
|
$assignments = $this->insuranceService->batchAssignInsuranceToParticipants(
|
||||||
$availableInsurances,
|
$selectableInsurances,
|
||||||
$applicant->insurance,
|
$applicant->insurance,
|
||||||
$bookingDto
|
$bookingDto,
|
||||||
|
$participantPrices
|
||||||
);
|
);
|
||||||
|
|
||||||
// Apply assignments to dependent participants (skip applicant at index 0)
|
// Apply assignments to dependent participants (skip applicant at index 0)
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ use App\BusProNet\Utility\DirectionMapper;
|
|||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
|
use App\Form\Service\Abstract\AbstractFieldOptionsProvider;
|
||||||
use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory;
|
use App\Form\Service\Factory\ParticipantRoomChoiceLoaderFactory;
|
||||||
use App\Service\InsuranceMatchingService;
|
use App\Service\BookingPriceCalculatorService;
|
||||||
use App\Service\InsuranceTypeFilterService;
|
use App\Service\InsuranceService;
|
||||||
use App\Service\ServiceAvailabilityCalculator;
|
use App\Service\ServiceAvailabilityCalculator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,13 +45,14 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
*
|
*
|
||||||
* @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders
|
* @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders
|
||||||
* @param ServiceAvailabilityCalculator $serviceAvailabilityCalculator Service for calculating dynamic availability
|
* @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(
|
public function __construct(
|
||||||
private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory,
|
private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory,
|
||||||
private readonly ServiceAvailabilityCalculator $serviceAvailabilityCalculator,
|
private readonly ServiceAvailabilityCalculator $serviceAvailabilityCalculator,
|
||||||
private readonly InsuranceMatchingService $insuranceMatchingService,
|
private readonly InsuranceService $insuranceService,
|
||||||
private readonly InsuranceTypeFilterService $insuranceTypeFilterService,
|
private readonly BookingPriceCalculatorService $priceCalculatorService,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@@ -756,16 +757,18 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider
|
|||||||
return [];
|
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)
|
// Calculate travel price for eligibility filtering
|
||||||
$availableInsurances = $this->insuranceTypeFilterService->filterNonComplementary($availableInsurances);
|
$travelPrice = $this->priceCalculatorService->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $participantIndex);
|
||||||
|
|
||||||
// Use insurance matching service to filter based on eligibility criteria
|
// Filter based on eligibility criteria for this participant
|
||||||
return $this->insuranceMatchingService->getEligibleInsurances(
|
return $this->insuranceService->getEligibleInsurances(
|
||||||
$availableInsurances,
|
$selectableInsurances,
|
||||||
$participant,
|
$participant,
|
||||||
$bookingDto
|
$bookingDto,
|
||||||
|
$travelPrice
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ namespace App\Form\Service;
|
|||||||
use App\BusProNet\Model\Insurance;
|
use App\BusProNet\Model\Insurance;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
|
||||||
use App\Service\InsuranceMatchingService;
|
use App\Service\BookingPriceCalculatorService;
|
||||||
use App\Service\InsuranceTypeFilterService;
|
use App\Service\InsuranceService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles processing of the insurance field for booking participants.
|
* Handles processing of the insurance field for booking participants.
|
||||||
@@ -35,8 +35,8 @@ use App\Service\InsuranceTypeFilterService;
|
|||||||
class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly InsuranceMatchingService $insuranceMatchingService,
|
private readonly InsuranceService $insuranceService,
|
||||||
private readonly InsuranceTypeFilterService $insuranceTypeFilterService,
|
private readonly BookingPriceCalculatorService $priceCalculatorService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,10 +132,12 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
|
|
||||||
$selectedInsuranceId = $this->getFieldValue($submittedData, $this->getFieldName());
|
$selectedInsuranceId = $this->getFieldValue($submittedData, $this->getFieldName());
|
||||||
$currentInsurance = $participant->insurance;
|
$currentInsurance = $participant->insurance;
|
||||||
$availableInsurances = $bookingDto->travel->insurances ?? [];
|
|
||||||
|
|
||||||
// Exclude complementary insurances (only available as part of packages)
|
// Get selectable (non-complementary) insurances with caching
|
||||||
$availableInsurances = $this->insuranceTypeFilterService->filterNonComplementary($availableInsurances);
|
$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
|
// Determine if this is a new user selection or just form resubmission
|
||||||
$isNewSelection = null !== $selectedInsuranceId
|
$isNewSelection = null !== $selectedInsuranceId
|
||||||
@@ -143,10 +145,10 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
|
|
||||||
// Handle explicit new insurance selection from user
|
// Handle explicit new insurance selection from user
|
||||||
if ($isNewSelection) {
|
if ($isNewSelection) {
|
||||||
$selectedInsurance = $this->findInsuranceById($availableInsurances, $selectedInsuranceId);
|
$selectedInsurance = $this->findInsuranceById($selectableInsurances, $selectedInsuranceId);
|
||||||
|
|
||||||
if (null !== $selectedInsurance) {
|
if (null !== $selectedInsurance) {
|
||||||
$eligibleInsurances = $this->insuranceMatchingService->getEligibleInsurances($availableInsurances, $participant, $bookingDto);
|
$eligibleInsurances = $this->insuranceService->getEligibleInsurances($selectableInsurances, $participant, $bookingDto, $travelPrice);
|
||||||
$isSelectedInsuranceEligible = $this->isInsuranceInList($selectedInsurance, $eligibleInsurances);
|
$isSelectedInsuranceEligible = $this->isInsuranceInList($selectedInsurance, $eligibleInsurances);
|
||||||
|
|
||||||
if ($isSelectedInsuranceEligible) {
|
if ($isSelectedInsuranceEligible) {
|
||||||
@@ -154,11 +156,12 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
$participant->insurance = $selectedInsurance;
|
$participant->insurance = $selectedInsurance;
|
||||||
} else {
|
} else {
|
||||||
// New selection is not eligible - try to find alternative in same type
|
// New selection is not eligible - try to find alternative in same type
|
||||||
$reassignedInsurance = $this->insuranceMatchingService->reassignInsuranceForPriceChange(
|
$reassignedInsurance = $this->insuranceService->reassignInsuranceForPriceChange(
|
||||||
$availableInsurances,
|
$selectableInsurances,
|
||||||
$selectedInsurance,
|
$selectedInsurance,
|
||||||
$participant,
|
$participant,
|
||||||
$bookingDto
|
$bookingDto,
|
||||||
|
$travelPrice
|
||||||
);
|
);
|
||||||
$participant->insurance = $reassignedInsurance;
|
$participant->insurance = $reassignedInsurance;
|
||||||
|
|
||||||
@@ -178,16 +181,17 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler
|
|||||||
// Handle form resubmission with existing insurance (automatic reassignment check)
|
// Handle form resubmission with existing insurance (automatic reassignment check)
|
||||||
if (null !== $currentInsurance && null !== $selectedInsuranceId) {
|
if (null !== $currentInsurance && null !== $selectedInsuranceId) {
|
||||||
// Check if current insurance is still eligible with updated participant data
|
// 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);
|
$isCurrentInsuranceStillEligible = $this->isInsuranceInList($currentInsurance, $eligibleInsurances);
|
||||||
|
|
||||||
if (!$isCurrentInsuranceStillEligible) {
|
if (!$isCurrentInsuranceStillEligible) {
|
||||||
// Current insurance no longer eligible - try to reassign to same type with new price tier
|
// Current insurance no longer eligible - try to reassign to same type with new price tier
|
||||||
$reassignedInsurance = $this->insuranceMatchingService->reassignInsuranceForPriceChange(
|
$reassignedInsurance = $this->insuranceService->reassignInsuranceForPriceChange(
|
||||||
$availableInsurances,
|
$selectableInsurances,
|
||||||
$currentInsurance,
|
$currentInsurance,
|
||||||
$participant,
|
$participant,
|
||||||
$bookingDto
|
$bookingDto,
|
||||||
|
$travelPrice
|
||||||
);
|
);
|
||||||
|
|
||||||
if (null !== $reassignedInsurance && $reassignedInsurance->id !== $currentInsurance->id) {
|
if (null !== $reassignedInsurance && $reassignedInsurance->id !== $currentInsurance->id) {
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ class BookingPriceCalculatorService
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ParticipantEligibilityService $participantEligibilityService,
|
private readonly ParticipantEligibilityService $participantEligibilityService,
|
||||||
private readonly InsuranceTypeFilterService $insuranceTypeFilterService,
|
private readonly InsuranceService $insuranceService,
|
||||||
private readonly InsuranceEligibilityService $insuranceEligibilityService,
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,22 +771,20 @@ class BookingPriceCalculatorService
|
|||||||
|
|
||||||
// For dependent participants: calculate price-tier-adjusted insurance
|
// For dependent participants: calculate price-tier-adjusted insurance
|
||||||
// This mirrors the logic in BookingDataProcessor::applyBulkInsuranceIfActive()
|
// This mirrors the logic in BookingDataProcessor::applyBulkInsuranceIfActive()
|
||||||
$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);
|
|
||||||
|
|
||||||
// Get insurances of the same type as applicant's selection
|
// Get insurances of the same type as applicant's selection
|
||||||
$sameTypeInsurances = $this->insuranceTypeFilterService->filterByType(
|
$sameTypeInsurances = $this->insuranceService->filterByType(
|
||||||
$availableInsurances,
|
$selectableInsurances,
|
||||||
$applicant->insurance
|
$applicant->insurance
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate participant's travel price (excluding insurance)
|
// Calculate travel price for eligibility checks
|
||||||
$travelPrice = $this->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $participant->index);
|
$travelPrice = $this->calculateIndividualParticipantPriceExcludingInsurance($bookingDto, $participant->index);
|
||||||
|
|
||||||
// Get eligible insurances for THIS participant (price tier adjusted)
|
// Get eligible insurances for THIS participant (price tier adjusted)
|
||||||
$eligibleInsurances = $this->insuranceEligibilityService->getEligibleInsurances(
|
$eligibleInsurances = $this->insuranceService->getEligibleInsurances(
|
||||||
$sameTypeInsurances,
|
$sameTypeInsurances,
|
||||||
$participant,
|
$participant,
|
||||||
$bookingDto,
|
$bookingDto,
|
||||||
|
|||||||
@@ -1,219 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Service;
|
|
||||||
|
|
||||||
use App\BusProNet\Model\Insurance;
|
|
||||||
use App\BusProNet\Traits\SortByPriceTrait;
|
|
||||||
use App\Form\Model\BookingDto;
|
|
||||||
use App\Form\Model\ParticipantDto;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for evaluating insurance eligibility without circular dependencies.
|
|
||||||
*
|
|
||||||
* This service contains the core eligibility logic that can be used by both
|
|
||||||
* InsuranceMatchingService and BookingPriceCalculatorService.
|
|
||||||
*/
|
|
||||||
class InsuranceEligibilityService
|
|
||||||
{
|
|
||||||
use SortByPriceTrait;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters insurances based on eligibility criteria.
|
|
||||||
*
|
|
||||||
* @param array<Insurance> $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<Insurance> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Service;
|
|
||||||
|
|
||||||
use App\BusProNet\Model\Insurance;
|
|
||||||
use App\BusProNet\Traits\SortByPriceTrait;
|
|
||||||
use App\Form\Model\BookingDto;
|
|
||||||
use App\Form\Model\ParticipantDto;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for matching insurances to participants based on eligibility criteria.
|
|
||||||
*
|
|
||||||
* Evaluates insurance constraints including age limits, travel dates, booking windows,
|
|
||||||
* travel price ranges, and duration limits to determine which insurances are available
|
|
||||||
* for specific participants and booking scenarios.
|
|
||||||
*/
|
|
||||||
class InsuranceMatchingService
|
|
||||||
{
|
|
||||||
use SortByPriceTrait;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly BookingPriceCalculatorService $priceCalculatorService,
|
|
||||||
private readonly InsuranceTypeFilterService $insuranceTypeFilterService,
|
|
||||||
private readonly InsuranceEligibilityService $insuranceEligibilityService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters insurances based on participant and booking criteria.
|
|
||||||
*
|
|
||||||
* @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
|
|
||||||
*
|
|
||||||
* @return array<Insurance> 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<Insurance> $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<Insurance> $availableInsurances All available insurances
|
|
||||||
* @param Insurance $selectedInsurance The insurance selected by the applicant
|
|
||||||
* @param BookingDto $booking The booking with all participants
|
|
||||||
*
|
|
||||||
* @return array<int, Insurance|null> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,353 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\BusProNet\Model\Insurance;
|
||||||
|
use App\BusProNet\Model\Travel;
|
||||||
|
use App\BusProNet\Traits\SortByPriceTrait;
|
||||||
|
use App\Form\Model\BookingDto;
|
||||||
|
use App\Form\Model\ParticipantDto;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Spatie\Blink\Blink;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consolidated service for all insurance-related operations.
|
||||||
|
*
|
||||||
|
* Handles insurance eligibility evaluation, filtering, caching, and assignment logic.
|
||||||
|
* Uses request-scoped caching via Blink to optimize performance for bookings with many participants.
|
||||||
|
* This service is stateless and has no dependencies to avoid circular dependency issues.
|
||||||
|
*/
|
||||||
|
class InsuranceService
|
||||||
|
{
|
||||||
|
use SortByPriceTrait;
|
||||||
|
|
||||||
|
private const CACHE_KEY_PREFIX = 'selectable_insurances_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns selectable (non-complementary) insurances for a travel with request-scoped caching.
|
||||||
|
*
|
||||||
|
* Complementary insurances are only available as part of packages and cannot
|
||||||
|
* be directly selected by users. This method filters them out and caches the
|
||||||
|
* result per travel for efficient repeated access.
|
||||||
|
*
|
||||||
|
* @param Travel $travel The travel to get selectable insurances for
|
||||||
|
*
|
||||||
|
* @return array<Insurance> 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<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 $travelPrice The participant's travel price (excluding insurance) for price tier matching
|
||||||
|
*
|
||||||
|
* @return array<Insurance> 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<Insurance> $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<Insurance> $availableInsurances All available insurances
|
||||||
|
* @param Insurance $selectedInsurance The insurance selected by the applicant
|
||||||
|
* @param BookingDto $booking The booking with all participants
|
||||||
|
* @param array<int, float> $participantPrices Map of participant index to travel price (excluding insurance)
|
||||||
|
*
|
||||||
|
* @return array<int, Insurance|null> 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<Insurance> $insurances All available insurances to filter
|
||||||
|
* @param Insurance $referenceInsurance The insurance to match against
|
||||||
|
*
|
||||||
|
* @return array<Insurance> 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<Insurance> $insurances Array of insurances to filter
|
||||||
|
*
|
||||||
|
* @return array<Insurance> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Service;
|
|
||||||
|
|
||||||
use App\BusProNet\Model\Insurance;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for filtering insurances by type without circular dependencies.
|
|
||||||
*/
|
|
||||||
class InsuranceTypeFilterService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 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<Insurance> $insurances Array of insurances to filter
|
|
||||||
*
|
|
||||||
* @return array<Insurance> 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<Insurance> $insurances All available insurances to filter
|
|
||||||
* @param Insurance $referenceInsurance The insurance to match against
|
|
||||||
*
|
|
||||||
* @return array<Insurance> 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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,8 +16,8 @@ use App\BusProNet\Model\Service;
|
|||||||
use App\BusProNet\Model\Travel;
|
use App\BusProNet\Model\Travel;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Service\InsuranceMatchingService;
|
use App\Service\BookingPriceCalculatorService;
|
||||||
use App\Service\InsuranceTypeFilterService;
|
use App\Service\InsuranceService;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,15 +32,19 @@ class BookingDataProcessorTest extends TestCase
|
|||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
// Create a mock InsuranceMatchingService
|
// Create a mock InsuranceService
|
||||||
$insuranceMatchingService = $this->createMock(InsuranceMatchingService::class);
|
$insuranceService = $this->createMock(InsuranceService::class);
|
||||||
|
$priceCalculatorService = $this->createMock(BookingPriceCalculatorService::class);
|
||||||
|
|
||||||
// Create a mock InsuranceTypeFilterService that simply returns the input
|
// Mock getSelectableInsurances to return empty array (not used in these tests)
|
||||||
$insuranceTypeFilterService = $this->createMock(InsuranceTypeFilterService::class);
|
$insuranceService->method('getSelectableInsurances')
|
||||||
$insuranceTypeFilterService->method('filterNonComplementary')
|
->willReturn([]);
|
||||||
->willReturnArgument(0);
|
|
||||||
|
|
||||||
$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
|
public function testCreateUpdateRequestPayloadWithCompleteData(): void
|
||||||
|
|||||||
@@ -9,24 +9,30 @@ use App\BusProNet\Model\Travel;
|
|||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Form\Service\ParticipantInsuranceFieldHandler;
|
use App\Form\Service\ParticipantInsuranceFieldHandler;
|
||||||
use App\Service\InsuranceMatchingService;
|
use App\Service\BookingPriceCalculatorService;
|
||||||
use App\Service\InsuranceTypeFilterService;
|
use App\Service\InsuranceService;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
class ParticipantInsuranceFieldHandlerTest extends TestCase
|
class ParticipantInsuranceFieldHandlerTest extends TestCase
|
||||||
{
|
{
|
||||||
private ParticipantInsuranceFieldHandler $handler;
|
private ParticipantInsuranceFieldHandler $handler;
|
||||||
private InsuranceMatchingService $insuranceMatchingService;
|
private InsuranceService $insuranceService;
|
||||||
|
private BookingPriceCalculatorService $priceCalculatorService;
|
||||||
|
|
||||||
protected function setUp(): void
|
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);
|
// Mock getSelectableInsurances to return input array
|
||||||
$insuranceTypeFilterService->method('filterNonComplementary')
|
$this->insuranceService->method('getSelectableInsurances')
|
||||||
->willReturnArgument(0);
|
->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
|
public function testGetFieldName(): void
|
||||||
@@ -85,8 +91,12 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase
|
|||||||
$participant = new ParticipantDto();
|
$participant = new ParticipantDto();
|
||||||
$participant->insurance = $this->createInsurance('123');
|
$participant->insurance = $this->createInsurance('123');
|
||||||
|
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->insurances = [];
|
||||||
|
|
||||||
$bookingDto = $this->createMockBookingDto();
|
$bookingDto = $this->createMockBookingDto();
|
||||||
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
||||||
|
$bookingDto->travel = $travel;
|
||||||
|
|
||||||
$this->handler->processField(['insurance' => null], $bookingDto, 0);
|
$this->handler->processField(['insurance' => null], $bookingDto, 0);
|
||||||
|
|
||||||
@@ -98,8 +108,12 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase
|
|||||||
$participant = new ParticipantDto();
|
$participant = new ParticipantDto();
|
||||||
$participant->insurance = $this->createInsurance('123');
|
$participant->insurance = $this->createInsurance('123');
|
||||||
|
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->insurances = [];
|
||||||
|
|
||||||
$bookingDto = $this->createMockBookingDto();
|
$bookingDto = $this->createMockBookingDto();
|
||||||
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
||||||
|
$bookingDto->travel = $travel;
|
||||||
|
|
||||||
$this->handler->processField(['insurance' => ''], $bookingDto, 0);
|
$this->handler->processField(['insurance' => ''], $bookingDto, 0);
|
||||||
|
|
||||||
@@ -111,8 +125,12 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase
|
|||||||
$participant = new ParticipantDto();
|
$participant = new ParticipantDto();
|
||||||
$participant->insurance = $this->createInsurance('123');
|
$participant->insurance = $this->createInsurance('123');
|
||||||
|
|
||||||
|
$travel = new Travel();
|
||||||
|
$travel->insurances = [];
|
||||||
|
|
||||||
$bookingDto = $this->createMockBookingDto();
|
$bookingDto = $this->createMockBookingDto();
|
||||||
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
||||||
|
$bookingDto->travel = $travel;
|
||||||
|
|
||||||
$this->handler->processField([], $bookingDto, 0);
|
$this->handler->processField([], $bookingDto, 0);
|
||||||
|
|
||||||
@@ -145,10 +163,10 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase
|
|||||||
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
||||||
$bookingDto->travel = $travel;
|
$bookingDto->travel = $travel;
|
||||||
|
|
||||||
$this->insuranceMatchingService
|
$this->insuranceService
|
||||||
->expects($this->once())
|
->expects($this->once())
|
||||||
->method('getEligibleInsurances')
|
->method('getEligibleInsurances')
|
||||||
->with([$insurance], $participant, $bookingDto)
|
->with([$insurance], $participant, $bookingDto, 500.0)
|
||||||
->willReturn([$insurance]);
|
->willReturn([$insurance]);
|
||||||
|
|
||||||
$this->handler->processField(['insurance' => '123'], $bookingDto, 0);
|
$this->handler->processField(['insurance' => '123'], $bookingDto, 0);
|
||||||
@@ -167,10 +185,10 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase
|
|||||||
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
||||||
$bookingDto->travel = $travel;
|
$bookingDto->travel = $travel;
|
||||||
|
|
||||||
$this->insuranceMatchingService
|
$this->insuranceService
|
||||||
->expects($this->once())
|
->expects($this->once())
|
||||||
->method('getEligibleInsurances')
|
->method('getEligibleInsurances')
|
||||||
->with([$insurance], $participant, $bookingDto)
|
->with([$insurance], $participant, $bookingDto, 500.0)
|
||||||
->willReturn([]); // Not eligible
|
->willReturn([]); // Not eligible
|
||||||
|
|
||||||
$this->handler->processField(['insurance' => '123'], $bookingDto, 0);
|
$this->handler->processField(['insurance' => '123'], $bookingDto, 0);
|
||||||
@@ -189,7 +207,7 @@ class ParticipantInsuranceFieldHandlerTest extends TestCase
|
|||||||
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
$bookingDto->method('getParticipant')->with(0)->willReturn($participant);
|
||||||
$bookingDto->travel = $travel;
|
$bookingDto->travel = $travel;
|
||||||
|
|
||||||
$this->insuranceMatchingService
|
$this->insuranceService
|
||||||
->expects($this->once())
|
->expects($this->once())
|
||||||
->method('getEligibleInsurances')
|
->method('getEligibleInsurances')
|
||||||
->willReturn([$insurance]);
|
->willReturn([$insurance]);
|
||||||
|
|||||||
@@ -23,13 +23,12 @@ class BookingPriceCalculatorServiceTest extends TestCase
|
|||||||
$participantEligibilityService = $this->createMock(ParticipantEligibilityService::class);
|
$participantEligibilityService = $this->createMock(ParticipantEligibilityService::class);
|
||||||
$participantEligibilityService->method('isParticipantEligible')->willReturn(true);
|
$participantEligibilityService->method('isParticipantEligible')->willReturn(true);
|
||||||
|
|
||||||
$insuranceTypeFilterService = new \App\Service\InsuranceTypeFilterService();
|
// Create a real InsuranceService (it will use a mocked price calculator internally when needed)
|
||||||
$insuranceEligibilityService = new \App\Service\InsuranceEligibilityService();
|
$insuranceService = new \App\Service\InsuranceService($this->service ?? $this->createMock(BookingPriceCalculatorService::class));
|
||||||
|
|
||||||
$this->service = new BookingPriceCalculatorService(
|
$this->service = new BookingPriceCalculatorService(
|
||||||
$participantEligibilityService,
|
$participantEligibilityService,
|
||||||
$insuranceTypeFilterService,
|
$insuranceService
|
||||||
$insuranceEligibilityService
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,13 +493,13 @@ class BookingPriceCalculatorServiceTest extends TestCase
|
|||||||
$participantEligibilityService->method('isParticipantEligible')
|
$participantEligibilityService->method('isParticipantEligible')
|
||||||
->willReturnCallback(fn ($booking, $index) => 0 === $index); // Only first participant eligible
|
->willReturnCallback(fn ($booking, $index) => 0 === $index); // Only first participant eligible
|
||||||
|
|
||||||
$insuranceTypeFilterService = new \App\Service\InsuranceTypeFilterService();
|
// Create a real InsuranceService with mocked price calculator
|
||||||
$insuranceEligibilityService = new \App\Service\InsuranceEligibilityService();
|
$mockPriceCalculator = $this->createMock(BookingPriceCalculatorService::class);
|
||||||
|
$insuranceService = new \App\Service\InsuranceService($mockPriceCalculator);
|
||||||
|
|
||||||
$this->service = new BookingPriceCalculatorService(
|
$this->service = new BookingPriceCalculatorService(
|
||||||
$participantEligibilityService,
|
$participantEligibilityService,
|
||||||
$insuranceTypeFilterService,
|
$insuranceService
|
||||||
$insuranceEligibilityService
|
|
||||||
);
|
);
|
||||||
|
|
||||||
$result = $this->service->calculateServicePricing($bookingDto);
|
$result = $this->service->calculateServicePricing($bookingDto);
|
||||||
|
|||||||
+219
-48
@@ -5,42 +5,21 @@ declare(strict_types=1);
|
|||||||
namespace App\Tests\Service;
|
namespace App\Tests\Service;
|
||||||
|
|
||||||
use App\BusProNet\Model\Insurance;
|
use App\BusProNet\Model\Insurance;
|
||||||
|
use App\BusProNet\Model\Travel;
|
||||||
use App\Form\Model\BookingDto;
|
use App\Form\Model\BookingDto;
|
||||||
use App\Form\Model\ParticipantDto;
|
use App\Form\Model\ParticipantDto;
|
||||||
use App\Service\BookingPriceCalculatorService;
|
use App\Service\InsuranceService;
|
||||||
use App\Service\InsuranceEligibilityService;
|
|
||||||
use App\Service\InsuranceMatchingService;
|
|
||||||
use App\Service\InsuranceTypeFilterService;
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
class InsuranceMatchingServiceTest extends TestCase
|
class InsuranceServiceTest extends TestCase
|
||||||
{
|
{
|
||||||
private InsuranceMatchingService $service;
|
private InsuranceService $service;
|
||||||
private BookingPriceCalculatorService $priceCalculator;
|
|
||||||
|
|
||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
// Mock the price calculator service
|
// InsuranceService is stateless and has no dependencies
|
||||||
$this->priceCalculator = $this->createMock(BookingPriceCalculatorService::class);
|
$this->service = new InsuranceService();
|
||||||
$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
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set a fixed test date for consistent test results
|
// Set a fixed test date for consistent test results
|
||||||
Carbon::setTestNow('2024-06-01 12:00:00');
|
Carbon::setTestNow('2024-06-01 12:00:00');
|
||||||
@@ -52,6 +31,36 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
Carbon::setTestNow();
|
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
|
public function testGetEligibleInsurancesWithMatchingCriteria(): void
|
||||||
{
|
{
|
||||||
$participant = $this->createParticipant('1990-06-15');
|
$participant = $this->createParticipant('1990-06-15');
|
||||||
@@ -70,7 +79,7 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
'travelDurationTo' => 14,
|
'travelDurationTo' => 14,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$result = $this->service->getEligibleInsurances([$insurance], $participant, $booking);
|
$result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0);
|
||||||
|
|
||||||
$this->assertCount(1, $result);
|
$this->assertCount(1, $result);
|
||||||
$this->assertContains($insurance, $result);
|
$this->assertContains($insurance, $result);
|
||||||
@@ -83,7 +92,7 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
|
|
||||||
$insurance = $this->createInsurance(['ageFrom' => 18, 'ageTo' => 65]);
|
$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);
|
$this->assertEmpty($result);
|
||||||
}
|
}
|
||||||
@@ -95,7 +104,7 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
|
|
||||||
$insurance = $this->createInsurance(['ageFrom' => 18, 'ageTo' => 65]);
|
$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);
|
$this->assertEmpty($result);
|
||||||
}
|
}
|
||||||
@@ -110,7 +119,7 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
'travelDateTo' => new \DateTimeImmutable('2024-12-31'),
|
'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);
|
$this->assertEmpty($result);
|
||||||
}
|
}
|
||||||
@@ -125,7 +134,7 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
'travelDateTo' => new \DateTimeImmutable('2024-07-31'),
|
'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);
|
$this->assertEmpty($result);
|
||||||
}
|
}
|
||||||
@@ -140,7 +149,7 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
'bookingDateTo' => new \DateTimeImmutable('2025-12-31'),
|
'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);
|
$this->assertEmpty($result);
|
||||||
}
|
}
|
||||||
@@ -155,7 +164,33 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
'bookingDateTo' => new \DateTimeImmutable('2023-12-31'),
|
'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);
|
$this->assertEmpty($result);
|
||||||
}
|
}
|
||||||
@@ -167,7 +202,7 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
|
|
||||||
$insurance = $this->createInsurance(['travelDurationFrom' => 7, 'travelDurationTo' => 30]);
|
$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);
|
$this->assertEmpty($result);
|
||||||
}
|
}
|
||||||
@@ -179,7 +214,7 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
|
|
||||||
$insurance = $this->createInsurance(['travelDurationFrom' => 1, 'travelDurationTo' => 14]);
|
$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);
|
$this->assertEmpty($result);
|
||||||
}
|
}
|
||||||
@@ -187,15 +222,15 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
public function testGetEligibleInsurancesHandlesParticipantWithoutBirthDate(): void
|
public function testGetEligibleInsurancesHandlesParticipantWithoutBirthDate(): void
|
||||||
{
|
{
|
||||||
$participant = new ParticipantDto();
|
$participant = new ParticipantDto();
|
||||||
$participant->index = 0; // Set participant index for price calculations
|
$participant->index = 0;
|
||||||
$participant->dateOfBirth = null;
|
$participant->dateOfBirth = null;
|
||||||
$booking = $this->createBooking('2024-08-01', '2024-08-08');
|
$booking = $this->createBooking('2024-08-01', '2024-08-08');
|
||||||
|
|
||||||
$insurance = $this->createInsurance(['ageFrom' => 18, 'ageTo' => 65]);
|
$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)
|
// (field visibility is controlled by field state conditions, not service logic)
|
||||||
$this->assertNotEmpty($result);
|
$this->assertNotEmpty($result);
|
||||||
$this->assertCount(1, $result);
|
$this->assertCount(1, $result);
|
||||||
@@ -210,7 +245,7 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
// Insurance with no constraints should match any criteria
|
// Insurance with no constraints should match any criteria
|
||||||
$insurance = $this->createInsurance([]);
|
$insurance = $this->createInsurance([]);
|
||||||
|
|
||||||
$result = $this->service->getEligibleInsurances([$insurance], $participant, $booking);
|
$result = $this->service->getEligibleInsurances([$insurance], $participant, $booking, 500.0);
|
||||||
|
|
||||||
$this->assertCount(1, $result);
|
$this->assertCount(1, $result);
|
||||||
$this->assertContains($insurance, $result);
|
$this->assertContains($insurance, $result);
|
||||||
@@ -226,7 +261,7 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
$ineligibleInsurance = $this->createInsurance(['ageFrom' => 60, 'ageTo' => 80]);
|
$ineligibleInsurance = $this->createInsurance(['ageFrom' => 60, 'ageTo' => 80]);
|
||||||
|
|
||||||
$insurances = [$eligibleInsurance1, $eligibleInsurance2, $ineligibleInsurance];
|
$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->assertCount(2, $result);
|
||||||
$this->assertContains($eligibleInsurance1, $result);
|
$this->assertContains($eligibleInsurance1, $result);
|
||||||
@@ -241,7 +276,7 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
|
|
||||||
$insurance = $this->createInsurance(['ageFrom' => 34, 'ageTo' => 34]);
|
$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->assertCount(1, $result);
|
||||||
$this->assertContains($insurance, $result);
|
$this->assertContains($insurance, $result);
|
||||||
@@ -254,16 +289,146 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
|
|
||||||
$insurance = $this->createInsurance(['travelDurationFrom' => 7, 'travelDurationTo' => 7]);
|
$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->assertCount(1, $result);
|
||||||
$this->assertContains($insurance, $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
|
private function createParticipant(string $dateOfBirth): ParticipantDto
|
||||||
{
|
{
|
||||||
$participant = new ParticipantDto();
|
$participant = new ParticipantDto();
|
||||||
$participant->index = 0; // Set participant index for price calculations
|
$participant->index = 0;
|
||||||
$participant->dateOfBirth = new \DateTimeImmutable($dateOfBirth);
|
$participant->dateOfBirth = new \DateTimeImmutable($dateOfBirth);
|
||||||
|
|
||||||
return $participant;
|
return $participant;
|
||||||
@@ -277,20 +442,26 @@ class InsuranceMatchingServiceTest extends TestCase
|
|||||||
return $booking;
|
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->dateFrom = new \DateTimeImmutable($dateFrom);
|
||||||
$travel->dateTo = new \DateTimeImmutable($dateTo);
|
$travel->dateTo = new \DateTimeImmutable($dateTo);
|
||||||
|
|
||||||
return $travel;
|
return $travel;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createInsurance(array $constraints = []): Insurance
|
private function createInsurance(array $properties = []): Insurance
|
||||||
{
|
{
|
||||||
$insurance = new 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;
|
$insurance->$property = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user