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, $bookingDto); 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', '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, null, true); return $totalPrice; })(); } /** * Gets the effective insurance for a participant, considering bulk insurance assignment. * * When bulk insurance is active and the participant is a dependent (index > 0), * returns the applicant's insurance. Otherwise returns the participant's own insurance. * * This method is used for pricing calculations to show correct prices when bulk * insurance is enabled, even though the actual assignment happens in the processor. * * @param ParticipantDto $participant The participant to get insurance for * @param BookingDto|null $bookingDto Optional booking DTO for bulk insurance check * * @return Insurance|null The effective insurance for pricing purposes */ public function getEffectiveInsurance(ParticipantDto $participant, ?BookingDto $bookingDto): ?Insurance { // If no booking context, use participant's own insurance if (null === $bookingDto) { return $participant->insurance; } // Applicant always uses their own insurance if (0 === $participant->index) { return $participant->insurance; } // Check if bulk insurance is active $applicant = $bookingDto->getParticipant(0); if (null === $applicant || false === $applicant->bulkInsuranceBooking) { return $participant->insurance; } // Bulk insurance is active - use applicant's insurance for dependent participants return $applicant->insurance; } /** * 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 BookingDto|null $bookingDto Optional booking DTO for bulk insurance resolution * @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, ?BookingDto $bookingDto = null, 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) $effectiveInsurance = $this->getEffectiveInsurance($participant, $bookingDto); 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; } } if (null !== $participant->pickup && null !== $participant->pickup->price) { $serviceTotal += $participant->pickup->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) { if (true === is_array($serviceArray)) { foreach ($serviceArray as $service) { if ($service instanceof Service && null !== $service->price) { if (false === $onlyInsuranceCalculationServices || true === $service->includeInInsuranceCalculation) { $serviceTotal += $service->price; } } } } } return $serviceTotal; } }