354 lines
14 KiB
PHP
354 lines
14 KiB
PHP
<?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;
|
|
}
|
|
|
|
}
|