Files
myep/src/Service/AccommodationBookingBreakdownCalculator.php
T

133 lines
5.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\Groups\AccommodationBooking;
use App\Repository\Groups\AccommodationPriceRepository;
class AccommodationBookingBreakdownCalculator
{
public function __construct(
private readonly GroupsPriceCalculator $priceCalculator,
private readonly AccommodationPriceRepository $priceRepo,
) {
}
/**
* Returns the authoritative stored snapshot, falling back to current prices only for
* 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
{
$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`, `discountSubtotal`,
* `totalDiscountDetails` and `discountedTotal`
*/
public function withDiscounts(AccommodationBooking $booking, array $breakdown): array
{
// The labels carry their own "Rabatt" prefix so that every row renders as plain
// {label} {percent} %, including the freely named discount on the total below.
$buckets = [
['Rabatt Unterkunft', $booking->getAccommodationDiscount(), (int) ($breakdown['basePrice'] ?? 0) + (int) ($breakdown['additionalPersonsPrice'] ?? 0)],
['Rabatt Verpflegung', $booking->getBoardServiceDiscount(), (int) ($breakdown['boardPrice'] ?? 0)],
['Rabatt Zusatzleistungen', $booking->getAdditionalServicesDiscount(), (int) ($breakdown['servicesPrice'] ?? 0)],
];
$discounts = [];
$discountSum = 0;
foreach ($buckets as [$label, $percent, $base]) {
$amount = self::discountAmount($base, $percent);
// Nothing to show for a discount of 0 %, or one on a bucket this booking has no
// price in — a " 0,00 €" row only raises questions.
if (0 === $amount) {
continue;
}
$discountSum += $amount;
$discounts[] = ['label' => $label, 'percent' => $percent, 'amount' => $amount];
}
// Deliberately not $booking->getTotalPrice(): refreshPriceSnapshot() calls this while
// computing the new total, where the stored one is still the outdated value.
$subtotal = (int) ($breakdown['total'] ?? 0) - $discountSum;
// Compounds on the already-reduced subtotal rather than on the gross total: the discount
// is granted on what the customer would otherwise pay, not on a figure no longer asked.
$totalDiscountAmount = self::discountAmount($subtotal, $booking->getTotalDiscount());
$breakdown['discounts'] = $discounts;
$breakdown['totalDiscountDetails'] = 0 !== $totalDiscountAmount
? [
'label' => (string) $booking->getTotalDiscountLabel(),
'percent' => $booking->getTotalDiscount(),
'amount' => $totalDiscountAmount,
]
: null;
// Only worth a row when it sits between two sets of discounts — with no section discount
// above it, a subtotal would merely restate the total one line up.
$breakdown['discountSubtotal'] = 0 !== $totalDiscountAmount && [] !== $discounts ? $subtotal : null;
$breakdown['discountedTotal'] = $subtotal - $totalDiscountAmount;
return $breakdown;
}
private static function discountAmount(int $base, ?int $percent): int
{
return null !== $percent ? (int) round($base * $percent / 100) : 0;
}
/**
* Calculates against the current catalog. Use only while creating or explicitly editing
* a booking, or as a compatibility fallback for records created before price snapshots.
*
* @return array<string, mixed>|null
*/
public function computeCurrent(AccommodationBooking $booking): ?array
{
$accommodation = $booking->getAccommodation();
$dateFrom = $booking->getDateFrom();
$dateTo = $booking->getDateTo();
if (null === $accommodation || null === $dateFrom || null === $dateTo) {
return null;
}
$prices = $this->priceRepo->findByHotelCodeAndDateRange(
$accommodation->getCalendarCode() ?? '',
$dateFrom,
$dateTo,
);
return $this->priceCalculator->calculateFromSnapshots(
$booking->getPaxCount(),
$booking->getMinorsCount(),
$booking->getNights(),
$dateFrom,
$dateTo,
$prices,
$booking->getBoardServicePrice(),
$booking->getAdditionalServices(),
$accommodation->getCurrency(),
);
}
}