feat: groups price calculator admin crud, booking/offer flow and api

This commit is contained in:
Björn Fromme
2026-08-03 15:25:10 +02:00
parent eabed8295a
commit 9d2aa11fdb
258 changed files with 16231 additions and 582 deletions
+119
View File
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace App\Controller\Groups;
use App\Entity\Groups\AccommodationBooking;
use App\Form\OfferAcceptConfirmationType;
use App\Htmx\HxTrait;
use App\Model\AccommodationBookingContext;
use App\Repository\Groups\AccommodationBookingRepository;
use App\Service\AccommodationBookingBreakdownCalculator;
use App\Service\AccommodationBookingLinkSigner;
use App\Service\AccommodationBookingService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class OfferController extends AbstractController
{
use HxTrait;
public function __construct(
private readonly AccommodationBookingRepository $bookingRepository,
private readonly AccommodationBookingLinkSigner $linkSigner,
private readonly AccommodationBookingBreakdownCalculator $breakdownCalculator,
private readonly AccommodationBookingService $bookingService,
) {
}
/**
* Signed-link entry point: validates the `t`/`_hash` query params once,
* authorizes the session for this booking, and redirects to the plain
* (session-gated) offer page — nothing downstream needs the signature again.
*/
#[Route(path: '/groups/booking/offer/{uuid}', name: 'app_groups_booking_offer', methods: ['GET'])]
public function access(string $uuid, Request $request): Response
{
$booking = $this->bookingRepository->findOneBy(['uuid' => $uuid]);
if (null === $booking || false === $this->linkSigner->isValidLinkRequest($request, $booking)) {
return $this->render('groups/booking/offer_unavailable.html.twig');
}
$this->linkSigner->authorizeSession($request, $booking);
return new RedirectResponse($this->generateUrl('app_groups_booking_offer_view', ['uuid' => $uuid]));
}
#[Route(path: '/groups/booking/offer/{uuid}/view', name: 'app_groups_booking_offer_view', methods: ['GET'])]
public function view(string $uuid, Request $request): Response
{
$booking = $this->bookingRepository->findOneBy(['uuid' => $uuid]);
if (null === $booking || false === $this->linkSigner->isSessionAuthorized($request, $booking)) {
return $this->render('groups/booking/offer_unavailable.html.twig');
}
$accommodation = $booking->getAccommodation() ?? throw $this->createNotFoundException('Booking has no accommodation.');
$priceBreakdown = $this->breakdownCalculator->compute($booking);
$ctx = new AccommodationBookingContext(
accommodation: $accommodation,
hotelCmsData: $this->bookingService->loadHotelCmsData($accommodation),
priceBreakdown: $priceBreakdown,
);
return $this->render('groups/booking/offer.html.twig', [
'booking' => $booking,
'priceBreakdown' => $priceBreakdown,
'ctx' => $ctx,
]);
}
#[Route(path: '/groups/booking/offer/{uuid}/confirm', name: 'app_groups_booking_offer_confirm', methods: ['GET', 'POST'])]
public function confirm(string $uuid, Request $request): Response
{
$booking = $this->bookingRepository->findOneBy(['uuid' => $uuid]);
if (null === $booking || false === $this->linkSigner->isSessionAuthorized($request, $booking)) {
return $this->redirectToOfferPage($request, $uuid);
}
if (!$booking->isInquiry()) {
return $this->redirectToOfferPage($request, $uuid);
}
$confirmationForm = $this->createForm(OfferAcceptConfirmationType::class, null, [
'terms_url' => $this->getParameter('terms_and_conditions_url'),
]);
$confirmationForm->handleRequest($request);
if ($confirmationForm->isSubmitted() && $confirmationForm->isValid()) {
$this->bookingService->acceptBooking($booking);
$this->addFlash('success', 'Deine Buchung ist bestätigt.');
return $this->redirectToOfferPage($request, $uuid);
}
return $this->render('groups/booking/_offer_accept_confirmation_modal.html.twig', [
'booking' => $booking,
'confirmationForm' => $confirmationForm,
]);
}
/**
* Sends the browser to a full reload of the (non-modal) offer page — this is
* triggered from an htmx-loaded modal, so a plain render/redirect here would
* get appended as an inert HTML fragment instead of actually navigating.
*/
private function redirectToOfferPage(Request $request, string $uuid): Response
{
$url = $this->generateUrl('app_groups_booking_offer_view', ['uuid' => $uuid]);
return $this->htmxRedirect($request, $url);
}
}