fix: exclude cancelled participants' services from price breakdown

This commit is contained in:
fromme committed 2026-10-09 12:33:43 +02:00
1 parent 553ec901f3
commit 849a19b67a
4 files changed
+74 -4

No files matched your search

+2 -1
View File
@@ -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;
}
+16 -3
View File
@@ -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<int, array<string, mixed>>
*/
@@ -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)) {
@@ -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();
@@ -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();