calculateRoomPricing($bookingDto); $servicePricing = $this->calculateServicePricing($bookingDto); $grandTotal = $this->calculateGrandTotal($bookingDto); return [ 'rooms' => $roomPricing, 'services' => $servicePricing, 'grandTotal' => $grandTotal, ]; } /** * Calculates pricing for all selected rooms. * * @param BookingDtoInterface $bookingDto The booking data containing room selections * * @return array Array of room pricing data with labels, quantities, and totals */ public function calculateRoomPricing(BookingDtoInterface $bookingDto): array { $roomPricing = []; if (false === $bookingDto instanceof BookingCreateDto) { return $roomPricing; } $selectedRooms = $bookingDto->getSelectedRooms(); if (true === empty($selectedRooms)) { return $roomPricing; } foreach ($selectedRooms as $roomSelection) { $room = $this->getRoomById($bookingDto, $roomSelection->roomId); if (null === $room || null === $room->price) { continue; } $totalPrice = $roomSelection->quantity * $room->price; $roomPricing[] = [ 'roomId' => $room->id, 'label' => $room->label, 'quantity' => $roomSelection->quantity, 'unitPrice' => $room->price, 'totalPrice' => $totalPrice, ]; } return $roomPricing; } /** * Calculates pricing for all selected services across all participants, grouped by subtype. * * @param BookingDtoInterface $bookingDto The booking data containing participants and their service selections * * @return array Array of service groups with each group containing services of the same subtype */ public function calculateServicePricing(BookingDtoInterface $bookingDto): array { $participants = $bookingDto->getParticipants(); if (true === empty($participants)) { return []; } // Aggregate service selections across all participants $serviceAggregation = []; foreach ($participants as $participant) { $this->aggregateParticipantServices($participant, $serviceAggregation); } // Add transportation services as separate line items (Zustieg, Rabatt, Parkplatz) $transportationItems = $this->aggregateTransportationServices($bookingDto); foreach ($transportationItems as $key => $transportationItem) { $serviceAggregation[$key] = $transportationItem; } // Group services by subtype and convert to pricing format return $this->groupServicesBySubtype($serviceAggregation); } /** * Calculates the grand total for the entire booking. * * @param BookingDtoInterface $bookingDto The booking data to calculate total for * * @return float The grand total price */ public function calculateGrandTotal(BookingDtoInterface $bookingDto): float { $roomTotal = $this->calculateRoomTotal($bookingDto); $serviceTotal = $this->calculateServiceTotal($bookingDto); return $roomTotal + $serviceTotal; } /** * Calculates total price for all rooms. */ public function calculateRoomTotal(BookingDtoInterface $bookingDto): float { $roomPricing = $this->calculateRoomPricing($bookingDto); return array_sum(array_column($roomPricing, 'totalPrice')); } /** * Calculates total price for all services. */ public function calculateServiceTotal(BookingDtoInterface $bookingDto): float { $servicePricing = $this->calculateServicePricing($bookingDto); return array_sum(array_column($servicePricing, 'groupTotal')); } /** * Formats a price value for display with proper German formatting. * * @param float $price The price to format * * @return string Formatted price string (e.g., "123,45") */ public function formatPrice(float $price): string { return number_format($price, 2, ',', '.'); } /** * Formats a price with Euro symbol for display. * * @param float $price The price to format * * @return string Formatted price string with Euro symbol (e.g., "€123,45") */ public function formatPriceWithSymbol(float $price): string { return '€'.$this->formatPrice($price); } /** * Aggregates all transportation-related services and pricing into separate line items. * * Creates separate entries for: * - Zustieg: Sum of all pickup prices and base transportation costs (positive and negative) * - Parkplatz: Sum of all parking service prices * * Note: Transportation discounts will be handled generically by groupServicesBySubtype as "Beförderung - Rabatt" * * @param BookingDtoInterface $bookingDto The booking data containing participants * * @return array Array of transportation line items (Zustieg, Parkplatz) */ private function aggregateTransportationServices(BookingDtoInterface $bookingDto): array { $participants = $bookingDto->getParticipants(); $pickupTotal = 0.0; // All pickup prices and base transportation costs $parkingTotal = 0.0; // Parking service costs $pickupParticipants = 0; $parkingParticipants = 0; foreach ($participants as $participant) { $participantPickupCost = 0.0; $participantParkingCost = 0.0; // Transportation service pricing (outbound) if (null !== $participant->transportationOutbound && null !== $participant->transportationOutbound->price) { $participantPickupCost += $participant->transportationOutbound->price; } // Transportation service pricing (inbound) if (null !== $participant->transportationInbound && null !== $participant->transportationInbound->price) { $participantPickupCost += $participant->transportationInbound->price; } // Pickup pricing (outbound) if (null !== $participant->pickupOutbound && null !== $participant->pickupOutbound->price) { $participantPickupCost += $participant->pickupOutbound->price; } // Pickup pricing (inbound) if (null !== $participant->pickupInbound && null !== $participant->pickupInbound->price) { $participantPickupCost += $participant->pickupInbound->price; } // Parking service pricing if (null !== $participant->parkingService && null !== $participant->parkingService->price) { $participantParkingCost += $participant->parkingService->price; } // Aggregate participant totals (count participants who have any transportation costs) if (0.0 !== $participantPickupCost) { $pickupTotal += $participantPickupCost; ++$pickupParticipants; } if ($participantParkingCost > 0) { $parkingTotal += $participantParkingCost; ++$parkingParticipants; } } $transportationItems = []; // Add pickup entry (if any transportation/pickup costs exist) if (0.0 !== $pickupTotal) { $transportationItems['transportation_pickup'] = [ 'serviceId' => 'transportation_pickup', 'label' => 'Zustieg', 'unitPrice' => null, 'participantCount' => $pickupParticipants, 'totalPrice' => $pickupTotal, 'subType' => 'transportation', ]; } // Add parking entry (only if positive costs) if ($parkingTotal > 0) { $transportationItems['transportation_parking'] = [ 'serviceId' => 'transportation_parking', 'label' => 'Parkplatz', 'unitPrice' => null, 'participantCount' => $parkingParticipants, 'totalPrice' => $parkingTotal, 'subType' => 'transportation', ]; } return $transportationItems; } /** * Groups services by their subtypes for display, separating positive costs and discounts. * * Creates separate entries for positive costs and negative costs (discounts) within each service group. * For example: "Kurse" and "Kurse - Rabatt" if there are both positive and negative priced course services. * * @param array $serviceAggregation Aggregated service data * * @return array Grouped services by subtype with separate discount entries */ private function groupServicesBySubtype(array $serviceAggregation): array { $groupedServices = []; // First pass: separate positive and negative prices by subtype $servicesBySubtypeAndSign = []; foreach ($serviceAggregation as $serviceData) { if (0.0 === $serviceData['totalPrice']) { continue; // Skip zero-price services } $subType = $serviceData['subType'] ?? 'other'; // Normalize rental subtypes to avoid duplicate sections if (true === in_array($subType, Constants::TOKEN_RENTALS, true)) { $subType = 'rentals'; // Normalize all rental subtypes to a single key } $isDiscount = $serviceData['totalPrice'] < 0; // Create separate buckets for positive costs and discounts $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']; } // Second pass: create display groups foreach ($servicesBySubtypeAndSign as $bucketData) { $baseGroupName = $this->getGroupNameForSubtype($bucketData['subType']); $groupName = $bucketData['isDiscount'] ? $baseGroupName.' - Rabatt' : $baseGroupName; $groupedServices[] = [ 'groupName' => $groupName, 'services' => $bucketData['services'], 'groupTotal' => $bucketData['total'], ]; } return $groupedServices; } /** * Maps service subtypes to user-friendly group names. */ private function getGroupNameForSubtype(string $subType): string { $groupMapping = [ Constants::TOKEN_COURSES => 'Kurse', Constants::TOKEN_SKI_PASS => 'Skipässe', Constants::TOKEN_ADDITIONAL => 'Zusatzleistungen', Constants::TOKEN_BOARD => 'Verpflegung', Constants::TOKEN_RENTAL_INSURANCE => 'Leihmaterial-Versicherung', 'transportation' => 'Beförderung', 'rentals' => 'Leihmaterial', // Normalized rental subtype ]; // Handle rentals array (keep for backward compatibility with non-normalized subtypes) if (true === in_array($subType, Constants::TOKEN_RENTALS, true)) { return 'Leihmaterial'; } return $groupMapping[$subType] ?? 'Sonstige Leistungen'; } /** * Aggregates service selections from a single participant into the service aggregation array. */ private function aggregateParticipantServices(ParticipantDto $participant, array &$serviceAggregation): void { // Handle single service selections (skiPass, rentalInsurance) if (null !== $participant->skiPass && null !== $participant->skiPass->price) { $this->addToServiceAggregation($serviceAggregation, $participant->skiPass, 1); } if (null !== $participant->rentalInsurance && null !== $participant->rentalInsurance->price) { $this->addToServiceAggregation($serviceAggregation, $participant->rentalInsurance, 1); } // Handle 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) { $this->addToServiceAggregation($serviceAggregation, $service, 1); } } } } } /** * Adds a service to the aggregation array, incrementing count and updating total price. */ private function addToServiceAggregation(array &$serviceAggregation, Service $service, int $quantity): 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; } /** * Retrieves a room by ID from the booking's travel data. */ private function getRoomById(BookingCreateDto $bookingDto, ?int $roomId): ?Room { if (null === $roomId) { return null; } foreach ($bookingDto->travel->rooms as $room) { if ($room->id === $roomId) { return $room; } } return null; } }