From 849a19b67ab0c886d13458ea1bb98e10f6029d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Fri, 9 Oct 2026 12:32:57 +0200 Subject: [PATCH] fix: exclude cancelled participants' services from price breakdown --- src/Service/BookingConfigurator.php | 3 +- src/Service/BookingPricingAssembler.php | 19 ++++++++-- tests/Service/BookingConfiguratorBabyTest.php | 21 +++++++++++ tests/Service/BookingPricingAssemblerTest.php | 35 +++++++++++++++++++ 4 files changed, 74 insertions(+), 4 deletions(-) diff --git a/src/Service/BookingConfigurator.php b/src/Service/BookingConfigurator.php index 97251464..32725eed 100644 --- a/src/Service/BookingConfigurator.php +++ b/src/Service/BookingConfigurator.php @@ -288,7 +288,8 @@ class BookingConfigurator int $participantIndex, ParticipantDto $participant, ): bool { - if (null === $participant->dateOfBirth) { + // A cancelled participant's services are no longer billed, so nothing may be added for them + if (true === $participant->isCanceled() || null === $participant->dateOfBirth) { return false; } diff --git a/src/Service/BookingPricingAssembler.php b/src/Service/BookingPricingAssembler.php index 587a1fa1..53b3459f 100644 --- a/src/Service/BookingPricingAssembler.php +++ b/src/Service/BookingPricingAssembler.php @@ -63,7 +63,8 @@ class BookingPricingAssembler /** * 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). + * Only includes services from billed participants: not cancelled, and eligible (those with available + * skipasses for their age). * * @return array> */ @@ -78,7 +79,7 @@ class BookingPricingAssembler $serviceAggregation = []; foreach ($participants as $participantIndex => $participant) { - if (false === $this->participantEligibilityChecker->isParticipantEligible($bookingDto, $participantIndex)) { + if (false === $this->isParticipantBilled($bookingDto, $participantIndex, $participant)) { continue; } @@ -142,7 +143,7 @@ class BookingPricingAssembler $parkingParticipants = 0; foreach ($participants as $participantIndex => $participant) { - if (false === $this->participantEligibilityChecker->isParticipantEligible($bookingDto, $participantIndex)) { + if (false === $this->isParticipantBilled($bookingDto, $participantIndex, $participant)) { continue; } @@ -306,6 +307,18 @@ class BookingPricingAssembler return $groupedServices; } + /** + * Whether the participant's services count towards the breakdown. + * + * A cancelled participant is billed only through BusPro's Stornokosten surcharge. Their services + * are never sent back to BusPro, so counting them would put the breakdown above BusPro's total. + */ + private function isParticipantBilled(BookingDto $bookingDto, int $participantIndex, ParticipantDto $participant): bool + { + return false === $participant->isCanceled() + && true === $this->participantEligibilityChecker->isParticipantEligible($bookingDto, $participantIndex); + } + private function getGroupNameForSubtype(string $subType): string { if (true === in_array($subType, Constants::TOKEN_RENTALS, true)) { diff --git a/tests/Service/BookingConfiguratorBabyTest.php b/tests/Service/BookingConfiguratorBabyTest.php index 3689760c..10a480ea 100644 --- a/tests/Service/BookingConfiguratorBabyTest.php +++ b/tests/Service/BookingConfiguratorBabyTest.php @@ -100,6 +100,27 @@ class BookingConfiguratorBabyTest extends TestCase $this->assertSame('Mandatory Service', $participant->additionalServices[0]->label); } + public function testPreselectMandatoryServicesSkipsCanceledParticipant(): void + { + $travel = $this->createTravelWithMandatoryServices(); + $bookingDto = new BookingDto($travel, 1); + + $participant = new ParticipantDto(); + $participant->index = 0; + $participant->status = 'S'; + $participant->dateOfBirth = $travel->dateFrom->modify('-25 years'); + $participant->additionalServices = []; + $bookingDto->participants[0] = $participant; + + $this->participantEligibilityService + ->method('isParticipantEligible') + ->willReturn(true); + + $this->bookingService()->preselectDefaultServices($bookingDto); + + $this->assertSame([], $participant->additionalServices, 'Mandatory services should not be preselected for cancelled participants'); + } + public function testPreselectMandatoryServicesIncludesAdultParticipant(): void { $travel = $this->createTravelWithMandatoryServices(); diff --git a/tests/Service/BookingPricingAssemblerTest.php b/tests/Service/BookingPricingAssemblerTest.php index 9c309980..af5be498 100644 --- a/tests/Service/BookingPricingAssemblerTest.php +++ b/tests/Service/BookingPricingAssemblerTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Tests\Service; +use App\BusProNet\Constants; use App\BusProNet\Model\Booking; use App\BusProNet\Model\Insurance; use App\BusProNet\Model\Pickup; @@ -358,6 +359,40 @@ class BookingPricingAssemblerTest extends TestCase $this->assertEqualsWithDelta($group['groupTotal'], $participantTotal, 0.001, 'Summary and per-participant totals must agree'); } + public function testServicePricingExcludesCanceledParticipants(): void + { + $travel = new Travel(); + $travel->insurances = []; + + $bookingDto = new BookingDto($travel, 2); + $bookingDto->participants = []; + + foreach (['F', 'S'] as $index => $status) { + $mandatoryService = new Service(); + $mandatoryService->id = 1; + $mandatoryService->label = 'Kurtaxe'; + $mandatoryService->subType = Constants::TOKEN_ADDITIONAL; + $mandatoryService->mandatory = true; + $mandatoryService->price = 12.5; + + $participant = new ParticipantDto(); + $participant->index = $index; + $participant->status = $status; + $participant->additionalServices = [$mandatoryService]; + $participant->transportationOutbound = $this->createTransportationService(203222, 'Bus-Hinfahrt', 'BUS'); + $participant->pickup = $this->createBoardingPoint(18, 17.9); + $bookingDto->participants[$index] = $participant; + } + + $servicePricing = $this->assembler->calculateServicePricing($bookingDto); + + $this->assertCount(2, $servicePricing); + foreach ($servicePricing as $group) { + $this->assertSame(1, $group['services'][0]['participantCount'], $group['groupName'].' counts only the active participant'); + } + $this->assertEqualsWithDelta(12.5 + 17.9, array_sum(array_column($servicePricing, 'groupTotal')), 0.001); + } + public function testSurchargePricingOmitsZeroAmountSurcharges(): void { $booking = new Booking();