From 577f7445aba1c8ab1a8e731ec3aa030a4351eb5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Thu, 6 Aug 2026 12:05:03 +0200 Subject: [PATCH] fix: correctly calculate undersubscription fees --- src/Service/GroupsPriceCalculator.php | 15 +++- tests/Service/GroupsPriceCalculatorTest.php | 88 +++++++++++++++++++++ 2 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 tests/Service/GroupsPriceCalculatorTest.php diff --git a/src/Service/GroupsPriceCalculator.php b/src/Service/GroupsPriceCalculator.php index 833dbc8..cae205c 100644 --- a/src/Service/GroupsPriceCalculator.php +++ b/src/Service/GroupsPriceCalculator.php @@ -244,10 +244,17 @@ class GroupsPriceCalculator $shortTermSurcharge = (int) round(($basePrice + $additionalPersonsPrice) * self::SHORT_TERM_FACTORS[$nights] / 100); } - // Rule 4: undersubscription surcharge (only when board is selected) + // Board is only billable — and only triggers the catering surcharge — when it costs + // something. A free "Selbstversorgung" board is stored as 0 (BoardService::$price is + // non-nullable), so a plain null check would charge the surcharge for it. + $paidBoardPricePerPersonNight = ($boardServicePricePerPersonNight ?? 0) > 0 + ? $boardServicePricePerPersonNight + : null; + + // Rule 4: undersubscription surcharge (only when a paid board is selected) $undersubscriptionSurcharge = 0; $undersubscriptionThreshold = null; - if ($boardServicePricePerPersonNight !== null) { + if ($paidBoardPricePerPersonNight !== null) { $surcharge30 = (int) round(('CHF' === $currency ? $this->config['undersubscription30Chf'] : $this->config['undersubscription30Eur']) * 100); $surcharge40 = (int) round(('CHF' === $currency ? $this->config['undersubscription40Chf'] : $this->config['undersubscription40Eur']) * 100); @@ -262,8 +269,8 @@ class GroupsPriceCalculator // Rule 5: board price $boardPrice = 0; - if ($boardServicePricePerPersonNight !== null) { - $boardPrice = $boardServicePricePerPersonNight * $effectivePax * $nights; + if ($paidBoardPricePerPersonNight !== null) { + $boardPrice = $paidBoardPricePerPersonNight * $effectivePax * $nights; } // Rule 6: additional services diff --git a/tests/Service/GroupsPriceCalculatorTest.php b/tests/Service/GroupsPriceCalculatorTest.php new file mode 100644 index 0000000..1ab8780 --- /dev/null +++ b/tests/Service/GroupsPriceCalculatorTest.php @@ -0,0 +1,88 @@ +calculate(30, 0); + + self::assertSame(0, $result['undersubscriptionSurcharge']); + self::assertNull($result['undersubscriptionThreshold']); + self::assertSame(0, $result['boardPrice']); + } + + public function testNoBoardDoesNotTriggerUndersubscriptionSurcharge(): void + { + $result = $this->calculate(30, null); + + self::assertSame(0, $result['undersubscriptionSurcharge']); + self::assertNull($result['undersubscriptionThreshold']); + self::assertSame(0, $result['boardPrice']); + } + + public function testPaidBoardBelow40TriggersTheLowerThresholdSurcharge(): void + { + $result = $this->calculate(30, 1500); + + self::assertSame(30 * 500 * self::NIGHTS, $result['undersubscriptionSurcharge']); + self::assertSame(40, $result['undersubscriptionThreshold']); + self::assertSame(1500 * 30 * self::NIGHTS, $result['boardPrice']); + } + + public function testPaidBoardBelow50TriggersTheUpperThresholdSurcharge(): void + { + $result = $this->calculate(45, 1500); + + self::assertSame(45 * 250 * self::NIGHTS, $result['undersubscriptionSurcharge']); + self::assertSame(50, $result['undersubscriptionThreshold']); + } + + /** + * @return array + */ + private function calculate(int $paxCount, ?int $boardServicePrice): array + { + $dateFrom = new \DateTimeImmutable('2026-07-01'); + $dateTo = $dateFrom->modify(sprintf('+%d days', self::NIGHTS)); + + $price = (new AccommodationPrice()) + ->setDateFrom($dateFrom->modify('-1 month')) + ->setDateTo($dateTo->modify('+1 month')) + ->setIncludedPax(20) + ->setPricePerNight(100_00) + ->setPriceAdditionalPerson(10_00) + ->setMinNights(1); + + $calculator = new GroupsPriceCalculator(new PriceTimelineBuilder(), [ + 'runningCostsEur' => 0, + 'runningCostsChf' => 0, + 'undersubscription30Eur' => 5.0, + 'undersubscription30Chf' => 6.0, + 'undersubscription40Eur' => 2.5, + 'undersubscription40Chf' => 3.0, + ]); + + return $calculator->calculateFromSnapshots( + $paxCount, + 0, + self::NIGHTS, + $dateFrom, + $dateTo, + [$price], + $boardServicePrice, + [], + 'EUR', + ); + } +}