>, services: array>, grandTotal: float, surcharges?: array>} */ public function getPricingBreakdown( BookingDto $bookingDto, string $roomPricingMode = RoomPricingCalculator::PRICING_MODE_ASSIGNMENT, ): array { $roomPricing = $this->roomPricingCalculator->calculateRoomPricing($bookingDto, $roomPricingMode); $servicePricing = $this->calculateServicePricing($bookingDto); $grandTotal = array_sum(array_column($roomPricing, 'totalPrice')) + array_sum(array_column($servicePricing, 'groupTotal')); $result = [ 'rooms' => $roomPricing, 'services' => $servicePricing, 'grandTotal' => $grandTotal, ]; if (null !== $bookingDto->booking && [] !== $bookingDto->booking->surcharges) { $surchargePricing = $this->calculateSurchargePricing($bookingDto->booking); if ([] !== $surchargePricing) { $result['surcharges'] = $surchargePricing; $result['grandTotal'] += array_sum(array_column($surchargePricing, 'totalPrice')); } } return $result; } /** * Calculates pricing for all selected services across all participants, grouped by subtype. * * Only includes services from eligible participants (those with available skipasses for their age). * * @return array> */ public function calculateServicePricing(BookingDto $bookingDto): array { $participants = $bookingDto->getParticipants(); if ([] === $participants) { return []; } $serviceAggregation = []; foreach ($participants as $participantIndex => $participant) { if (false === $this->participantEligibilityChecker->isParticipantEligible($bookingDto, $participantIndex)) { continue; } $this->aggregateParticipantServices($participant, $serviceAggregation, $bookingDto); } foreach ($this->aggregateTransportationServices($bookingDto) as $key => $transportationItem) { $serviceAggregation[$key] = $transportationItem; } return $this->groupServicesBySubtype($serviceAggregation); } /** * Calculates surcharge pricing grouped by label (edit mode only). * * @return array> */ public function calculateSurchargePricing(Booking $bookingData): array { $surchargePricing = []; foreach ($bookingData->surcharges as $surcharge) { $surchargePricing[] = [ 'label' => $surcharge->label ?? 'unbekannt', 'participantCount' => count($surcharge->mapping), 'totalPrice' => $surcharge->totalPrice ?? 0.0, ]; } return $surchargePricing; } /** * Aggregates all transportation-related services and pricing into separate line items. * * Creates separate entries for: * - Beförderung: Sum of all positive pickup prices and base transportation costs * - Beförderung - Rabatt: Sum of all negative transportation prices (discounts) * - Parkplatz: Sum of all parking service prices * * Only includes transportation costs from eligible participants. * * @return array> */ private function aggregateTransportationServices(BookingDto $bookingDto): array { $participants = $bookingDto->getParticipants(); $transportationPositiveTotal = 0.0; $transportationDiscountTotal = 0.0; $parkingTotal = 0.0; $transportationParticipants = 0; $discountParticipants = 0; $parkingParticipants = 0; foreach ($participants as $participantIndex => $participant) { if (false === $this->participantEligibilityChecker->isParticipantEligible($bookingDto, $participantIndex)) { continue; } $participantTransportationPositiveCost = 0.0; $participantTransportationDiscountCost = 0.0; $participantParkingCost = 0.0; if (null !== $participant->transportationOutbound && null !== $participant->transportationOutbound->price) { if ($participant->transportationOutbound->price < 0) { $participantTransportationDiscountCost += $participant->transportationOutbound->price; } else { $participantTransportationPositiveCost += $participant->transportationOutbound->price; } } if (null !== $participant->transportationInbound && null !== $participant->transportationInbound->price) { if ($participant->transportationInbound->price < 0) { $participantTransportationDiscountCost += $participant->transportationInbound->price; } else { $participantTransportationPositiveCost += $participant->transportationInbound->price; } } $hasOutboundBus = null !== $participant->transportationOutbound && 'BUS' === $participant->transportationOutbound->subType; if ($hasOutboundBus && null !== $participant->pickup && null !== $participant->pickup->price) { if ($participant->pickup->price < 0) { $participantTransportationDiscountCost += $participant->pickup->price; } else { $participantTransportationPositiveCost += $participant->pickup->price; } } if (null !== $participant->dropOff && null !== $participant->dropOff->price) { if ($participant->dropOff->price < 0) { $participantTransportationDiscountCost += $participant->dropOff->price; } else { $participantTransportationPositiveCost += $participant->dropOff->price; } } if (null !== $participant->parkingService && null !== $participant->parkingService->price) { $participantParkingCost += $participant->parkingService->price; } if ($participantTransportationPositiveCost > 0) { $transportationPositiveTotal += $participantTransportationPositiveCost; ++$transportationParticipants; } if ($participantTransportationDiscountCost < 0) { $transportationDiscountTotal += $participantTransportationDiscountCost; ++$discountParticipants; } if ($participantParkingCost > 0) { $parkingTotal += $participantParkingCost; ++$parkingParticipants; } } $transportationItems = []; if ($transportationPositiveTotal > 0) { $transportationItems['transportation_positive'] = [ 'serviceId' => 'transportation_positive', 'label' => 'Beförderung', 'unitPrice' => null, 'participantCount' => $transportationParticipants, 'totalPrice' => $transportationPositiveTotal, 'subType' => Constants::GROUP_TRANSPORTATION, ]; } if ($transportationDiscountTotal < 0) { $transportationItems['transportation_discount'] = [ 'serviceId' => 'transportation_discount', 'label' => 'Beförderung - Rabatt', 'unitPrice' => null, 'participantCount' => $discountParticipants, 'totalPrice' => $transportationDiscountTotal, 'subType' => Constants::GROUP_TRANSPORTATION.'_discount', ]; } if ($parkingTotal > 0) { $transportationItems['transportation_parking'] = [ 'serviceId' => 'transportation_parking', 'label' => Constants::SERVICE_LABELS[Constants::TOKEN_PARKING], 'unitPrice' => null, 'participantCount' => $parkingParticipants, 'totalPrice' => $parkingTotal, 'subType' => Constants::GROUP_TRANSPORTATION, ]; } return $transportationItems; } /** * Groups services by their subtypes for display, separating positive costs and discounts. * * @param array> $serviceAggregation * * @return array> */ private function groupServicesBySubtype(array $serviceAggregation): array { $groupedServices = []; $servicesBySubtypeAndSign = []; foreach ($serviceAggregation as $serviceData) { $subType = $serviceData['subType'] ?? 'other'; if ('' === $subType) { $subType = 'other'; } if (true === in_array($subType, Constants::TOKEN_RENTALS, true)) { $subType = Constants::GROUP_RENTALS; } if (true === in_array($subType, Constants::TOKEN_INSURANCES, true)) { $subType = Constants::GROUP_INSURANCE; } $isDiscount = $serviceData['totalPrice'] < 0; $bucketKey = $subType.($isDiscount ? '_discount' : '_regular'); if (false === isset($servicesBySubtypeAndSign[$bucketKey])) { $servicesBySubtypeAndSign[$bucketKey] = [ 'subType' => $subType, 'isDiscount' => $isDiscount, 'services' => [], 'total' => 0.0, 'participantCount' => 0, ]; } $servicesBySubtypeAndSign[$bucketKey]['services'][] = $serviceData; $servicesBySubtypeAndSign[$bucketKey]['total'] += $serviceData['totalPrice']; $servicesBySubtypeAndSign[$bucketKey]['participantCount'] += $serviceData['participantCount']; } foreach ($servicesBySubtypeAndSign as $bucketData) { $baseGroupName = $this->getGroupNameForSubtype($bucketData['subType']); $groupName = $bucketData['isDiscount'] ? $baseGroupName.' - Rabatt' : $baseGroupName; $services = array_map( fn (array $serviceData): array => $this->createServicePricingLineItem($serviceData), $bucketData['services'] ); $groupedServices[] = [ 'groupName' => $groupName, 'services' => $services, 'groupTotal' => $bucketData['total'], ]; } return $groupedServices; } private function getGroupNameForSubtype(string $subType): string { if (true === in_array($subType, Constants::TOKEN_RENTALS, true)) { return Constants::SERVICE_LABELS[Constants::GROUP_RENTALS]; } return Constants::SERVICE_LABELS[$subType] ?? 'Sonstige Leistungen'; } /** @param array> $serviceAggregation */ private function aggregateParticipantServices( ParticipantDto $participant, array &$serviceAggregation, BookingDto $bookingDto, ): void { if (null !== $participant->skiPass && null !== $participant->skiPass->price) { $this->addToServiceAggregation($serviceAggregation, $participant->skiPass); } if (null !== $participant->veg && null !== $participant->veg->price) { $this->addToServiceAggregation($serviceAggregation, $participant->veg); } if (null !== $participant->rentalInsurance && null !== $participant->rentalInsurance->price) { $this->addToServiceAggregation($serviceAggregation, $participant->rentalInsurance); } $insuranceToAggregate = $this->resolveInsuranceForAggregation($participant, $bookingDto); if (null !== $insuranceToAggregate && null !== $insuranceToAggregate->price && false === $insuranceToAggregate->isNoInsurance()) { $this->addInsuranceToServiceAggregation($serviceAggregation, $insuranceToAggregate); } $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) { $this->addToServiceAggregation($serviceAggregation, $service); } } } } /** @param array> $serviceAggregation */ private function addToServiceAggregation(array &$serviceAggregation, Service $service, int $quantity = 1): void { $serviceKey = $service->id.'_'.$service->label; if (false === isset($serviceAggregation[$serviceKey])) { $serviceAggregation[$serviceKey] = [ 'serviceId' => $service->id, 'label' => $service->label, 'unitPrice' => $service->price, 'participantCount' => 0, 'totalPrice' => 0.0, 'subType' => $service->subType, ]; } $serviceAggregation[$serviceKey]['participantCount'] += $quantity; $serviceAggregation[$serviceKey]['totalPrice'] += $service->price * $quantity; } /** @param array> $serviceAggregation */ private function addInsuranceToServiceAggregation(array &$serviceAggregation, Insurance $insurance, int $quantity = 1): void { $serviceKey = $insurance->id.'_'.$insurance->label; if (false === isset($serviceAggregation[$serviceKey])) { $serviceAggregation[$serviceKey] = [ 'serviceId' => $insurance->id, 'label' => $insurance->label, 'unitPrice' => $insurance->price, 'participantCount' => 0, 'totalPrice' => 0.0, 'subType' => $insurance->getSubType(), ]; } $serviceAggregation[$serviceKey]['participantCount'] += $quantity; $serviceAggregation[$serviceKey]['totalPrice'] += $insurance->price * $quantity; } /** * @param array $serviceData * * @return array */ private function createServicePricingLineItem(array $serviceData): array { return [ 'serviceId' => $serviceData['serviceId'] ?? null, 'label' => (string) ($serviceData['label'] ?? ''), 'unitPrice' => array_key_exists('unitPrice', $serviceData) ? $serviceData['unitPrice'] : null, 'participantCount' => (int) ($serviceData['participantCount'] ?? 0), 'totalPrice' => (float) ($serviceData['totalPrice'] ?? 0.0), 'subType' => $serviceData['subType'] ?? null, ]; } private function resolveInsuranceForAggregation(ParticipantDto $participant, BookingDto $bookingDto): ?Insurance { if (null !== $participant->insurance) { return $participant->insurance; } $applicant = $bookingDto->getParticipant(0); if (null === $applicant || false === $applicant->bulkInsuranceBooking || null === $applicant->insurance) { return null; } if (0 === $participant->index) { return $participant->insurance; } $selectableInsurances = $this->insuranceService->getSelectableInsurances($bookingDto->travel); $sameTypeInsurances = $this->insuranceService->filterByType( $selectableInsurances, $applicant->insurance ); $travelPrice = $this->participantPricingCalculator->calculateIndividualParticipantPriceExcludingInsurance( $bookingDto, $participant->index ); $eligibleInsurances = $this->insuranceService->getEligibleInsurances( $sameTypeInsurances, $participant, $bookingDto, $travelPrice ); return !empty($eligibleInsurances) ? array_values($eligibleInsurances)[0] : null; } }