Request-scoped cache for participant prices */ private array $participantPriceCache = []; public function __construct( private readonly RoomPricingCalculator $roomPricingCalculator, ) { } /** * Calculates the total price for an individual participant. * * This method calculates the complete price breakdown for a single participant, * including their room allocation (full room price) and all selected services. * * @param BookingDto $bookingDto The booking data containing all participants * @param int $participantIndex The index of the participant to calculate for * * @return float The total price for the specified participant */ public function calculateIndividualParticipantPrice(BookingDto $bookingDto, int $participantIndex): float { $participant = $bookingDto->getParticipant($participantIndex); if (null === $participant) { return 0.0; } $totalPrice = 0.0; // Add room price if participant is assigned to a room if (null !== $participant->assignedRoomId) { $room = $this->roomPricingCalculator->getRoomById($bookingDto, $participant->assignedRoomId); if (null !== $room && null !== $room->price) { // Each participant pays the full room price $totalPrice += $room->price; } } // Add service prices for this participant (with booking context for bulk insurance) $totalPrice += $this->calculateParticipantServiceTotal($participant, true); return $totalPrice; } /** * Calculates individual prices for all participants in a booking. * * @param BookingDto $bookingDto The booking data containing all participants * * @return array Array indexed by participant index containing individual prices */ public function calculateAllParticipantIndividualPrices(BookingDto $bookingDto): array { $participantPrices = []; $participants = $bookingDto->getParticipants(); foreach ($participants as $index => $participant) { $participantPrices[$index] = $this->calculateIndividualParticipantPrice($bookingDto, $index); } return $participantPrices; } /** * Calculates the total price for an individual participant excluding insurance. * * This method is used for insurance eligibility filtering to avoid circular dependency * where insurance selection affects travel price which affects insurance eligibility. * * Only includes services where versicherungsberechnung='J' in the XML. Services with * versicherungsberechnung='N' (like CO2 compensation) are excluded from the calculation * as per BPN API requirements for insurance tier determination. * * @param BookingDto $bookingDto The booking data containing all participants * @param int $participantIndex The index of the participant to calculate for * * @return float The total price for the specified participant excluding insurance and non-calculated services */ public function calculateIndividualParticipantPriceExcludingInsurance( BookingDto $bookingDto, int $participantIndex, ): float { $participant = $bookingDto->getParticipant($participantIndex); if (null === $participant) { return 0.0; } // Generate cache key based on participant state that affects pricing $stateComponents = [ 'room' => $participant->assignedRoomId ?? 'none', 'skiPass' => $participant->skiPass->id ?? 'none', 'rentalInsurance' => $participant->rentalInsurance->id ?? 'none', 'transportationOut' => $participant->transportationOutbound->id ?? 'none', 'transportationIn' => $participant->transportationInbound->id ?? 'none', 'pickup' => $participant->pickup->id ?? 'none', 'dropOff' => $participant->dropOff->id ?? 'none', 'parking' => $participant->parkingService->id ?? 'none', 'courses' => implode('_', array_map(fn ($s) => $s->id, $participant->courses)), 'additionalServices' => implode('_', array_map(fn ($s) => $s->id, $participant->additionalServices)), 'board' => implode('_', array_map(fn ($s) => $s->id, $participant->board)), 'rentals' => implode('_', array_map(fn ($s) => $s->id, $participant->rentals)), ]; $cacheKey = sprintf( 'participant_price_%d_%s', $participantIndex, md5(json_encode($stateComponents)) ); return $this->participantPriceCache[$cacheKey] ??= (function () use ($bookingDto, $participant) { $totalPrice = 0.0; // Add room price if participant is assigned to a room if (null !== $participant->assignedRoomId) { $room = $this->roomPricingCalculator->getRoomById($bookingDto, $participant->assignedRoomId); if (null !== $room && null !== $room->price) { // Each participant pays the full room price $totalPrice += $room->price; } } // Add service prices for this participant (excluding insurance) // For insurance eligibility calculation, only include services marked with versicherungsberechnung='J' $totalPrice += $this->calculateParticipantServiceTotal($participant, false, true); return $totalPrice; })(); } /** * Calculates the total service cost for a single participant. * * @param ParticipantDto $participant The participant to calculate services for * @param bool $includeInsurance Whether to include insurance pricing (default: true) * @param bool $onlyInsuranceCalculationServices Only include services with includeInInsuranceCalculation=true (default: false) * * @return float The total service cost for this participant */ public function calculateParticipantServiceTotal( ParticipantDto $participant, bool $includeInsurance = true, bool $onlyInsuranceCalculationServices = false, ): float { $serviceTotal = 0.0; // Single service selections if (null !== $participant->skiPass && null !== $participant->skiPass->price) { if (false === $onlyInsuranceCalculationServices || true === $participant->skiPass->includeInInsuranceCalculation) { $serviceTotal += $participant->skiPass->price; } } if (null !== $participant->rentalInsurance && null !== $participant->rentalInsurance->price) { if (false === $onlyInsuranceCalculationServices || true === $participant->rentalInsurance->includeInInsuranceCalculation) { $serviceTotal += $participant->rentalInsurance->price; } } // Get effective insurance (considering bulk insurance for dependent participants) // ApplicantInsuranceCascade materialises bulk and family assignments into the DTO as // they happen, so the participant's own field is already the effective insurance. $effectiveInsurance = $participant->insurance; if ($includeInsurance && null !== $effectiveInsurance && null !== $effectiveInsurance->price) { $serviceTotal += $effectiveInsurance->price; } // Transportation services if (null !== $participant->transportationOutbound && null !== $participant->transportationOutbound->price) { if (false === $onlyInsuranceCalculationServices || true === $participant->transportationOutbound->includeInInsuranceCalculation) { $serviceTotal += $participant->transportationOutbound->price; } } if (null !== $participant->transportationInbound && null !== $participant->transportationInbound->price) { if (false === $onlyInsuranceCalculationServices || true === $participant->transportationInbound->includeInInsuranceCalculation) { $serviceTotal += $participant->transportationInbound->price; } } // Boarding surcharges are charged per leg, at the same rate in both directions $hasOutboundBus = null !== $participant->transportationOutbound && DirectionMapper::isBus($participant->transportationOutbound->subType); $hasInboundBus = null !== $participant->transportationInbound && DirectionMapper::isBus($participant->transportationInbound->subType); if ($hasOutboundBus && null !== $participant->pickup && null !== $participant->pickup->price) { $serviceTotal += $participant->pickup->price; } if ($hasInboundBus && null !== $participant->dropOff && null !== $participant->dropOff->price) { $serviceTotal += $participant->dropOff->price; } if (null !== $participant->parkingService && null !== $participant->parkingService->price) { if (false === $onlyInsuranceCalculationServices || true === $participant->parkingService->includeInInsuranceCalculation) { $serviceTotal += $participant->parkingService->price; } } // Multiple service selections $multipleServiceArrays = [ 'courses' => $participant->courses, 'additionalServices' => $participant->additionalServices, 'board' => $participant->board, 'rentals' => $participant->rentals, ]; foreach ($multipleServiceArrays as $serviceArray) { foreach ($serviceArray as $service) { if (null !== $service->price) { if (false === $onlyInsuranceCalculationServices || true === $service->includeInInsuranceCalculation) { $serviceTotal += $service->price; } } } } return $serviceTotal; } }