wip: group inquiries and booking

This commit is contained in:
Björn Fromme committed 2026-07-16 17:13:26 +02:00
1 parent 18a890e50e
commit e7bfb19ac9
71 files changed
+2731 -512

No files matched your search

+72 -16
View File
@@ -22,6 +22,8 @@ use Psr\Log\LoggerInterface;
class AccommodationBookingService
{
private const int PRICING_VERSION = 1;
public function __construct(
private readonly AccommodationRepository $accommodationRepo,
private readonly AccommodationPriceRepository $priceRepo,
@@ -99,9 +101,12 @@ class AccommodationBookingService
}
/**
* Fetches hotel name, image, and address from the CMS for display in the sidebar.
* Fetches hotel details from the CMS for display in the booking flow — name, images,
* address, descriptive text blocks, amenity icons, and the surrounding region's info
* (name, coordinates, webcam, ski area text, stats, images).
*
* @return array<string, mixed>|null null on CMS error or missing code
* @return array<string, mixed>|null null on CMS error or missing code, see
* {@see CmsDataProvider::getHotelDetails()} for the shape
*/
public function loadHotelCmsData(Accommodation $accommodation): ?array
{
@@ -187,7 +192,7 @@ class AccommodationBookingService
$candidates = array_values(array_filter(
$prices,
fn(AccommodationPrice $p) => $p->getDateFrom() <= $firstDay && $p->getDateTo() >= $firstDay,
fn (AccommodationPrice $p) => $p->getDateFrom() <= $firstDay && $p->getDateTo() >= $firstDay,
));
$winner = $this->priceTimelineBuilder->resolveWinner($candidates);
@@ -224,14 +229,14 @@ class AccommodationBookingService
* Board and additional services are stored as frozen snapshots — independent of
* the catalog entities so that future catalog edits do not affect existing bookings.
*
* @param AccommodationPrice[] $prices
* @param AccommodationPrice[] $prices
* @param array{additionalServices: AdditionalService[], boardServices: mixed[]} $services
*/
public function persist(
AccommodationBookingDto $dto,
Accommodation $accommodation,
array $prices,
array $services,
Accommodation $accommodation,
array $prices,
array $services,
): AccommodationBooking {
$booking = new AccommodationBooking();
$booking->setAccommodation($accommodation);
@@ -281,20 +286,72 @@ class AccommodationBookingService
$booking->setCity($dto->city);
$booking->setRemarks($dto->remarks);
$this->refreshPriceSnapshot($booking);
$this->entityManager->persist($booking);
$this->entityManager->flush();
return $booking;
}
/** @param array<string, mixed> $priceBreakdown */
public function sendNotificationEmail(AccommodationBooking $booking, array $priceBreakdown = []): void
/**
* Persists the booking and sends the notification/confirmation emails in one go.
*
* @param AccommodationPrice[] $prices
* @param array{additionalServices: AdditionalService[], boardServices: mixed[]} $services
*/
public function finalizeBooking(
AccommodationBookingDto $dto,
Accommodation $accommodation,
array $prices,
array $services,
): AccommodationBooking {
$booking = $this->persist($dto, $accommodation, $prices, $services);
$this->issueAccessLinkForDirectBooking($booking);
$this->sendNotificationEmail($booking);
$this->sendCustomerConfirmationEmail($booking);
return $booking;
}
public function refreshPriceSnapshot(AccommodationBooking $booking): void
{
$breakdown = $this->breakdownCalculator->computeCurrent($booking);
if (null === $breakdown) {
$booking->clearPriceSnapshot();
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());
$booking->setPriceSnapshot(
$breakdown,
$total,
(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
{
try {
$this->mailer->createAndSendEmail(
[
'booking' => $booking,
'priceBreakdown' => $priceBreakdown,
'priceBreakdown' => $this->breakdownCalculator->compute($booking) ?? [],
'accessLink' => $this->accessLinkOrNull($booking),
],
[
@@ -362,15 +419,14 @@ class AccommodationBookingService
}
/**
* Explicit admin action: (re)issues the access link — invalidating any previously issued
* link for this booking — and sends the customer confirmation email, now including it.
* Explicit admin action: (re)issues the access link, invalidating any previously issued
* link for this booking. No email side effect — sending is a separate, explicit admin
* action via sendCustomerConfirmationEmail().
*/
public function regenerateAccessLinkAndNotifyCustomer(AccommodationBooking $booking): void
public function regenerateAccessLink(AccommodationBooking $booking): void
{
$booking->setAccessLinkIssuedAt(new \DateTimeImmutable());
$this->entityManager->flush();
$this->sendCustomerConfirmationEmail($booking);
}
/**
@@ -421,7 +477,7 @@ class AccommodationBookingService
$prices = $this->priceRepo->findByHotelCodeAndDateRange($hotelCode, $dateFrom, $dateTo);
$candidates = array_values(array_filter(
$prices,
fn(AccommodationPrice $price): bool => $price->getDateFrom() <= $dateFrom && $price->getDateTo() >= $dateFrom,
fn (AccommodationPrice $price): bool => $price->getDateFrom() <= $dateFrom && $price->getDateTo() >= $dateFrom,
));
return $this->priceTimelineBuilder->resolveWinner($candidates)?->getIncludedPax() ?? 1;