fix: streamlined discount display in all breakdowns

This commit is contained in:
Björn Fromme
2026-08-18 13:04:33 +02:00
parent 8dee65d0dc
commit ef24c9a616
10 changed files with 185 additions and 118 deletions
@@ -17,17 +17,60 @@ class AccommodationBookingBreakdownCalculator
/**
* Returns the authoritative stored snapshot, falling back to current prices only for
* bookings created before snapshot persistence was introduced.
* bookings created before snapshot persistence was introduced, enriched with the
* discount rows and the discounted total for rendering.
*
* @return array<string, mixed>|null null when no accommodation or dates are set
*/
public function compute(AccommodationBooking $booking): ?array
{
if (null !== $booking->getPriceBreakdown()) {
return $booking->getPriceBreakdown();
$breakdown = $booking->getPriceBreakdown() ?? $this->computeCurrent($booking);
return null !== $breakdown ? $this->withDiscounts($booking, $breakdown) : null;
}
/**
* Adds the discount rows and the price the customer actually pays. Derived on every read
* and never persisted — refreshPriceSnapshot() stores the raw breakdown from
* computeCurrent() — so the stored total and this one come out of the same code and
* cannot drift apart.
*
* @param array<string, mixed> $breakdown
*
* @return array<string, mixed> the breakdown plus `discounts` and `discountedTotal`
*/
public function withDiscounts(AccommodationBooking $booking, array $breakdown): array
{
$buckets = [
['Unterkunft', $booking->getAccommodationDiscount(), (int) ($breakdown['basePrice'] ?? 0) + (int) ($breakdown['additionalPersonsPrice'] ?? 0)],
['Verpflegung', $booking->getBoardServiceDiscount(), (int) ($breakdown['boardPrice'] ?? 0)],
['Zusatzleistungen', $booking->getAdditionalServicesDiscount(), (int) ($breakdown['servicesPrice'] ?? 0)],
];
$discounts = [];
$discountSum = 0;
foreach ($buckets as [$label, $percent, $base]) {
if (null === $percent) {
continue;
}
$amount = self::discountAmount($base, $percent);
$discountSum += $amount;
$discounts[] = ['label' => $label, 'percent' => $percent, 'amount' => $amount];
}
return $this->computeCurrent($booking);
$breakdown['discounts'] = $discounts;
// Deliberately not $booking->getTotalPrice(): refreshPriceSnapshot() calls this while
// computing the new total, where the stored one is still the outdated value.
$breakdown['discountedTotal'] = (int) ($breakdown['total'] ?? 0) - $discountSum;
return $breakdown;
}
private static function discountAmount(int $base, ?int $percent): int
{
return null !== $percent ? (int) round($base * $percent / 100) : 0;
}
/**
+3 -15
View File
@@ -356,28 +356,16 @@ class AccommodationBookingService
return;
}
$accommodationBase = (int) ($breakdown['basePrice'] ?? 0) + (int) ($breakdown['additionalPersonsPrice'] ?? 0);
$boardBase = (int) ($breakdown['boardPrice'] ?? 0);
$servicesBase = (int) ($breakdown['servicesPrice'] ?? 0);
$total = (int) ($breakdown['total'] ?? 0)
- $this->discountAmount($accommodationBase, $booking->getAccommodationDiscount())
- $this->discountAmount($boardBase, $booking->getBoardServiceDiscount())
- $this->discountAmount($servicesBase, $booking->getAdditionalServicesDiscount());
// The raw breakdown is what gets frozen; the discounted total comes from the same
// calculator the templates read, so the stored number always matches what is shown.
$booking->setPriceSnapshot(
$breakdown,
$total,
$this->breakdownCalculator->withDiscounts($booking, $breakdown)['discountedTotal'],
(string) ($breakdown['currency'] ?? $booking->getAccommodation()?->getCurrency() ?? 'EUR'),
self::PRICING_VERSION,
);
}
private function discountAmount(int $base, ?int $percent): int
{
return null !== $percent ? (int) round($base * $percent / 100) : 0;
}
public function sendNotificationEmail(AccommodationBooking $booking): void
{
$this->sendBookingEmail(