From bd7b70e4658094352078fe2b325a484dc368afc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Fri, 26 Sep 2025 15:32:22 +0200 Subject: [PATCH] feat: correct calculation and display of individual pricing --- .../Booking/CreateStep2Controller.php | 6 + src/Service/BookingPriceCalculatorService.php | 131 +++++++- templates/booking/create_step_2.html.twig | 9 +- .../BookingPriceCalculatorServiceTest.php | 318 ++++++++++++++++++ 4 files changed, 462 insertions(+), 2 deletions(-) create mode 100644 tests/Service/BookingPriceCalculatorServiceTest.php diff --git a/src/Controller/Booking/CreateStep2Controller.php b/src/Controller/Booking/CreateStep2Controller.php index 8a05ac5..eb69e6c 100644 --- a/src/Controller/Booking/CreateStep2Controller.php +++ b/src/Controller/Booking/CreateStep2Controller.php @@ -8,6 +8,7 @@ use App\Controller\Traits\HtmxControllerTrait; use App\Form\BookingCreateStep2Type; use App\Form\Model\BookingCreateDto; use App\Form\Model\ParticipantDto; +use App\Service\BookingPriceCalculatorService; use App\Service\BookingService; use App\Service\TravelDataService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -29,6 +30,7 @@ class CreateStep2Controller extends AbstractController public function __construct( private readonly BookingService $bookingService, + private readonly BookingPriceCalculatorService $priceCalculator, private readonly TravelDataService $travelDataService, ) { } @@ -80,12 +82,14 @@ class CreateStep2Controller extends AbstractController $roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto); $availableRooms = $bookingCreateDto->travel->getAvailableRooms(); $groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($bookingCreateDto->getSelectedRooms(), $availableRooms); + $participantPrices = $this->priceCalculator->calculateAllParticipantIndividualPrices($bookingCreateDto); return $this->render('booking/create_step_2.html.twig', [ 'bookingCreateDto' => $bookingCreateDto, 'participantsCount' => $participantsCount, 'pricingData' => $summary['pricing'], 'assignmentCounts' => $roomAssignmentCounts, + 'participantPrices' => $participantPrices, 'form' => $form->createView(), 'groupedSelectedRooms' => $groupedSelectedRooms, ]); @@ -132,6 +136,7 @@ class CreateStep2Controller extends AbstractController $roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto); $availableRooms = $bookingCreateDto->travel->getAvailableRooms(); $groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($bookingCreateDto->getSelectedRooms(), $availableRooms); + $participantPrices = $this->priceCalculator->calculateAllParticipantIndividualPrices($bookingCreateDto); // The DTO is now updated with the latest selection and submitted data has been cleaned. // We can now render the blocks with the fresh data. @@ -144,6 +149,7 @@ class CreateStep2Controller extends AbstractController 'participantsCount' => $participantsCount, 'pricingData' => $summary['pricing'], 'assignmentCounts' => $roomAssignmentCounts, + 'participantPrices' => $participantPrices, 'groupedSelectedRooms' => $groupedSelectedRooms, ] ); diff --git a/src/Service/BookingPriceCalculatorService.php b/src/Service/BookingPriceCalculatorService.php index 4491229..b6d1eda 100644 --- a/src/Service/BookingPriceCalculatorService.php +++ b/src/Service/BookingPriceCalculatorService.php @@ -66,12 +66,17 @@ class BookingPriceCalculatorService continue; } - $totalPrice = $roomSelection->quantity * $room->price; + // Calculate participant count for this room selection + $participantCount = $room->minPax * $roomSelection->quantity; + + // Each participant pays the full room price + $totalPrice = $participantCount * $room->price; $roomPricing[] = [ 'roomId' => $room->id, 'label' => $room->label, 'quantity' => $roomSelection->quantity, + 'participantCount' => $participantCount, 'unitPrice' => $room->price, 'totalPrice' => $totalPrice, ]; @@ -171,6 +176,68 @@ class BookingPriceCalculatorService return '€'.$this->formatPrice($price); } + /** + * 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 BookingDtoInterface $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(BookingDtoInterface $bookingDto, int $participantIndex): float + { + if (false === $bookingDto instanceof BookingCreateDto) { + return 0.0; + } + + $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->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 + $totalPrice += $this->calculateParticipantServiceTotal($participant); + + return $totalPrice; + } + + /** + * Calculates individual prices for all participants in a booking. + * + * @param BookingDtoInterface $bookingDto The booking data containing all participants + * + * @return array Array indexed by participant index containing individual prices + */ + public function calculateAllParticipantIndividualPrices(BookingDtoInterface $bookingDto): array + { + if (false === $bookingDto instanceof BookingCreateDto) { + return []; + } + + $participantPrices = []; + $participants = $bookingDto->getParticipants(); + + foreach ($participants as $index => $participant) { + $participantPrices[$index] = $this->calculateIndividualParticipantPrice($bookingDto, $index); + } + + return $participantPrices; + } + /** * Aggregates all transportation-related services and pricing into separate line items. * @@ -405,6 +472,68 @@ class BookingPriceCalculatorService $serviceAggregation[$serviceKey]['totalPrice'] += $service->price * $quantity; } + /** + * Calculates the total service cost for a single participant. + * + * @param ParticipantDto $participant The participant to calculate services for + * + * @return float The total service cost for this participant + */ + private function calculateParticipantServiceTotal(ParticipantDto $participant): float + { + $serviceTotal = 0.0; + + // Single service selections + if (null !== $participant->skiPass && null !== $participant->skiPass->price) { + $serviceTotal += $participant->skiPass->price; + } + + if (null !== $participant->rentalInsurance && null !== $participant->rentalInsurance->price) { + $serviceTotal += $participant->rentalInsurance->price; + } + + // Transportation services + if (null !== $participant->transportationOutbound && null !== $participant->transportationOutbound->price) { + $serviceTotal += $participant->transportationOutbound->price; + } + + if (null !== $participant->transportationInbound && null !== $participant->transportationInbound->price) { + $serviceTotal += $participant->transportationInbound->price; + } + + if (null !== $participant->pickupOutbound && null !== $participant->pickupOutbound->price) { + $serviceTotal += $participant->pickupOutbound->price; + } + + if (null !== $participant->pickupInbound && null !== $participant->pickupInbound->price) { + $serviceTotal += $participant->pickupInbound->price; + } + + if (null !== $participant->parkingService && null !== $participant->parkingService->price) { + $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) { + $serviceTotal += $service->price; + } + } + } + } + + return $serviceTotal; + } + /** * Retrieves a room by ID from the booking's travel data. */ diff --git a/templates/booking/create_step_2.html.twig b/templates/booking/create_step_2.html.twig index d191ce2..6e0965c 100644 --- a/templates/booking/create_step_2.html.twig +++ b/templates/booking/create_step_2.html.twig @@ -16,7 +16,14 @@
- Teilnehmer:in {{ loop.index }} +
+ Teilnehmer:in {{ loop.index }} + {% if participantPrices is defined and participantPrices[loop.index0] is defined and participantPrices[loop.index0] > 0.0 %} + + €{{ participantPrices[loop.index0]|number_format(2, ',', '.') }} + + {% endif %} +