114 lines
4.1 KiB
PHP
114 lines
4.1 KiB
PHP
<?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` 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]) {
|
||
$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];
|
||
}
|
||
|
||
$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;
|
||
}
|
||
|
||
/**
|
||
* 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(),
|
||
);
|
||
}
|
||
}
|