feat: additional confirmation cycle and status for groups inquiries

This commit is contained in:
Björn Fromme
2026-08-18 12:48:17 +02:00
parent 5045846eed
commit 8dee65d0dc
28 changed files with 677 additions and 95 deletions
+49 -17
View File
@@ -267,10 +267,14 @@ class AccommodationBookingService
$booking->setMinorsCount($dto->minorsCount);
$booking->setChildrenCount($dto->childrenCount);
// An inquiry arrives live — the office has nothing to prepare first, so it goes
// straight to Offen. A direct booking is binding on arrival, hence Bestätigt.
// straight to Offen. A direct booking has been committed to by the customer but
// still awaits the office's validation, hence Eingegangen rather than Bestätigt.
$isInquiry = $dto->forceInquiry || $this->computeInquiryStatus($dto, $prices)->isInquiry;
$booking->setType($isInquiry ? AccommodationBookingType::Inquiry : AccommodationBookingType::Booking);
$booking->setStatus($isInquiry ? AccommodationBookingStatus::Open : AccommodationBookingStatus::Accepted);
$booking->setStatus($isInquiry ? AccommodationBookingStatus::Open : AccommodationBookingStatus::Received);
if (!$isInquiry) {
$booking->setAcceptedAt(new \DateTimeImmutable());
}
// Freeze board service as scalar fields (no FK)
if (null !== $dto->selectedBoardServiceId) {
@@ -333,7 +337,12 @@ class AccommodationBookingService
$booking = $this->persist($dto, $accommodation, $prices, $services);
$this->issueAccessLinkForDirectBooking($booking);
$this->sendNotificationEmail($booking);
$this->sendCustomerConfirmationEmail($booking);
// A direct booking gets no customer mail here: it only reaches the customer once the
// office has validated it and confirmed via confirmBooking(). Acknowledging an
// inquiry is a different matter — it promises nothing, so it still goes out.
if ($booking->isInquiry()) {
$this->sendCustomerConfirmationEmail($booking);
}
return $booking;
}
@@ -409,11 +418,15 @@ class AccommodationBookingService
return;
}
$breakdown = $this->breakdownCalculator->compute($booking);
try {
$this->mailer->createAndSendEmail(
[
'booking' => $booking,
'accessLink' => $this->accessLinkOrNull($booking),
'priceBreakdown' => $breakdown,
'currency' => $booking->getPricingCurrency() ?? $breakdown['currency'] ?? null,
],
[
'from' => $this->accommodationEmail,
@@ -467,13 +480,9 @@ class AccommodationBookingService
*/
public function sendCustomerConfirmationEmail(AccommodationBooking $booking): void
{
if ($booking->isAccepted()) {
$subject = 'Deine Buchung ist bestätigt';
} elseif (null !== $booking->getAccessLinkIssuedAt()) {
$subject = 'Dein Angebot ist bereit';
} else {
$subject = 'Deine Anfrage ist bei uns eingegangen';
}
$subject = null !== $booking->getAccessLinkIssuedAt()
? 'Dein Angebot ist bereit'
: 'Deine Anfrage ist bei uns eingegangen';
$this->sendBookingEmail(
$booking,
@@ -496,11 +505,14 @@ class AccommodationBookingService
}
/**
* Accepts an open offer and notifies office and customer. The type stays Anfrage —
* only the status moves to Bestätigt, so an offer-originated booking remains
* Accepts an open offer and notifies the office. The type stays Anfrage —
* only the status moves to Eingegangen, so an offer-originated booking remains
* distinguishable from a direct one.
* Idempotent — a no-op (including no emails) if the offer is not open any more.
*
* The customer deliberately gets no mail here: nothing is confirmed until the office
* has validated the booking and released it via confirmBooking().
*
* @param ?string $remarks null leaves the stored remark untouched, a string replaces it,
* an empty string clears it
*/
@@ -515,12 +527,32 @@ class AccommodationBookingService
$booking->setRemarks('' === $trimmed ? null : $trimmed);
}
$booking->setStatus(AccommodationBookingStatus::Accepted);
$booking->setStatus(AccommodationBookingStatus::Received);
$booking->setAcceptedAt(new \DateTimeImmutable());
$this->entityManager->flush();
$this->sendOfferAcceptedNotificationEmail($booking);
$this->sendOfferAcceptedCustomerEmail($booking);
}
/**
* Explicit office action after the requested services and capacities have been validated:
* this is the moment the booking becomes binding for the customer, and the only place the
* booking confirmation email is sent.
* Idempotent — a no-op (including no email) for anything that is not awaiting validation.
*/
public function confirmBooking(AccommodationBooking $booking): void
{
if (!$booking->isReceived()) {
return;
}
$booking->setStatus(AccommodationBookingStatus::Confirmed);
$booking->setConfirmedAt(new \DateTimeImmutable());
$this->entityManager->flush();
// The confirmation links to the booking view, so make sure a link exists.
$this->issueAccessLink($booking);
$this->sendBookingConfirmedCustomerEmail($booking);
}
public function sendOfferAcceptedNotificationEmail(AccommodationBooking $booking): void
@@ -534,14 +566,14 @@ class AccommodationBookingService
);
}
public function sendOfferAcceptedCustomerEmail(AccommodationBooking $booking): void
public function sendBookingConfirmedCustomerEmail(AccommodationBooking $booking): void
{
$this->sendBookingEmail(
$booking,
$booking->getEmail(),
'Deine Buchung ist bestätigt',
'email/offer_accepted_customer.html.twig',
'Failed to send offer accepted customer email',
'email/booking_confirmed_customer.html.twig',
'Failed to send booking confirmed customer email',
);
}