chore: further cleanup and refactor
This commit is contained in:
@@ -15,6 +15,8 @@ use App\BusProNet\Constants;
|
||||
*/
|
||||
class Booking
|
||||
{
|
||||
public const string STATUS_CANCELED = 'S';
|
||||
|
||||
public ?int $id = null;
|
||||
public ?int $agencyId = null;
|
||||
public ?int $bookingNumber = null;
|
||||
|
||||
@@ -15,6 +15,10 @@ namespace App\BusProNet\Model;
|
||||
*/
|
||||
class BookingResponse
|
||||
{
|
||||
public const string BOOKING_POSSIBLE = 'möglich';
|
||||
public const string BOOKING_IMPOSSIBLE = 'nicht möglich';
|
||||
public const string BOOKING_PROCESSED = 'erfolgt';
|
||||
|
||||
/**
|
||||
* @param string $status Booking status (möglich|erfolgt)
|
||||
* @param int|null $bookingNumber Booking number (BPN XML: vorgang)
|
||||
@@ -37,7 +41,7 @@ class BookingResponse
|
||||
*/
|
||||
public function isInquiryValid(): bool
|
||||
{
|
||||
return 'möglich' === $this->status;
|
||||
return self::BOOKING_POSSIBLE === $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +49,7 @@ class BookingResponse
|
||||
*/
|
||||
public function isBookingSuccessful(): bool
|
||||
{
|
||||
return 'erfolgt' === $this->status;
|
||||
return self::BOOKING_PROCESSED === $this->status;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Booking\Create;
|
||||
|
||||
use App\Exception\BookingSessionNotFoundException;
|
||||
use App\Exception\HotelNotInTravelException;
|
||||
use App\Exception\TravelNotFoundException;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Service\BookingSessionManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Shared base controller for booking creation steps.
|
||||
*/
|
||||
abstract class AbstractBookingCreateController extends AbstractController
|
||||
{
|
||||
protected function loadBookingCreateDto(BookingSessionManager $bookingSessionService, Request $request): BookingDto
|
||||
{
|
||||
return $bookingSessionService->getOrCreateBookingCreateDto($request);
|
||||
}
|
||||
|
||||
protected function createBookingCreateFailureResponse(\Throwable $exception, bool $htmx): Response
|
||||
{
|
||||
if (true === $htmx) {
|
||||
return $this->createBookingCreateHtmxFailureResponse();
|
||||
}
|
||||
|
||||
return $this->createBookingCreateRedirectFailureResponse($exception);
|
||||
}
|
||||
|
||||
protected function validateStepAccess(BookingDto $bookingCreateDto, int $expectedStep): ?RedirectResponse
|
||||
{
|
||||
// Allow access to current step or any previous step
|
||||
if ($expectedStep > $bookingCreateDto->currentStep) {
|
||||
$this->addFlash('error', 'Bitte erst die vorherigen Schritte abschließen.');
|
||||
|
||||
return $this->redirectToCurrentStep($bookingCreateDto);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles API errors by logging and adding a flash message.
|
||||
*
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
protected function handleApiError(
|
||||
LoggerInterface $logger,
|
||||
string $logMessage,
|
||||
array $context,
|
||||
string $flashMessage,
|
||||
): void {
|
||||
$logger->error($logMessage, $context);
|
||||
$this->addFlash('error', $flashMessage);
|
||||
}
|
||||
|
||||
private function createBookingCreateRedirectFailureResponse(\Throwable $exception): Response
|
||||
{
|
||||
$message = match (true) {
|
||||
$exception instanceof BookingSessionNotFoundException => 'Deine Buchungssitzung ist abgelaufen. Bitte starte eine neue Buchung.',
|
||||
$exception instanceof TravelNotFoundException => 'Reisedaten sind nicht (mehr) verfügbar.',
|
||||
$exception instanceof HotelNotInTravelException => 'Das gewählte Hotel ist für diese Reise nicht verfügbar.',
|
||||
default => 'Die Buchung konnte nicht geladen werden.',
|
||||
};
|
||||
|
||||
$this->addFlash('error', $message);
|
||||
|
||||
return $this->redirectToRoute('app_booking_create_error');
|
||||
}
|
||||
|
||||
private function createBookingCreateHtmxFailureResponse(): Response
|
||||
{
|
||||
return new Response('', 400);
|
||||
}
|
||||
|
||||
private function redirectToCurrentStep(BookingDto $bookingCreateDto): RedirectResponse
|
||||
{
|
||||
$route = match ($bookingCreateDto->currentStep) {
|
||||
2 => 'app_booking_create_step_2',
|
||||
3 => 'app_booking_create_step_3',
|
||||
4 => 'app_booking_create_step_4',
|
||||
default => 'app_booking_create_step_1',
|
||||
};
|
||||
|
||||
return $this->redirectToRoute($route);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Booking\Create;
|
||||
|
||||
use App\Controller\Booking\Traits\BookingCreateTrait;
|
||||
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
|
||||
use App\Exception\BookingSessionNotFoundException;
|
||||
use App\Exception\HotelNotInTravelException;
|
||||
use App\Exception\TravelNotFoundException;
|
||||
use App\Form\BookingCreateStep1Type;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Htmx\HxTrait;
|
||||
@@ -13,7 +14,6 @@ use App\Service\BookingConfigurator;
|
||||
use App\Service\BookingCreateContextFactory;
|
||||
use App\Service\BookingSessionManager;
|
||||
use App\Service\RoomPricingCalculator;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
@@ -24,10 +24,8 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
* This controller manages room selection functionality where users
|
||||
* choose the types and quantities of rooms for their booking.
|
||||
*/
|
||||
class Step1Controller extends AbstractController
|
||||
class Step1Controller extends AbstractBookingCreateController
|
||||
{
|
||||
use BookingCreateTrait;
|
||||
use BookingExceptionHandlerTrait;
|
||||
use HxTrait;
|
||||
|
||||
public function __construct(
|
||||
@@ -43,11 +41,11 @@ class Step1Controller extends AbstractController
|
||||
#[Route('/bookings/create/rooms', name: 'app_booking_create_step_1')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
try {
|
||||
$bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request);
|
||||
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) {
|
||||
return $this->createBookingCreateFailureResponse($exception, false);
|
||||
}
|
||||
$bookingCreateDto = $result;
|
||||
|
||||
// Get or create baseline snapshot for change detection
|
||||
$oldRoomSelectionSnapshot = $this->bookingSessionService->getOrCreateBaselineSnapshot($request, $bookingCreateDto);
|
||||
@@ -98,11 +96,11 @@ class Step1Controller extends AbstractController
|
||||
#[Route('/bookings/create/refresh', name: 'app_booking_create_step_1_refresh', methods: ['POST'])]
|
||||
public function refresh(Request $request): Response
|
||||
{
|
||||
$result = $this->getOrCreateBookingCreateDtoForHtmx($this->bookingSessionService, $request);
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
try {
|
||||
$bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request);
|
||||
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) {
|
||||
return $this->createBookingCreateFailureResponse($exception, true);
|
||||
}
|
||||
$bookingCreateDto = $result;
|
||||
|
||||
// Process form data without validation to capture current state
|
||||
$form = $this->createForm(BookingCreateStep1Type::class, $bookingCreateDto, [
|
||||
|
||||
@@ -5,19 +5,20 @@ declare(strict_types=1);
|
||||
namespace App\Controller\Booking\Create;
|
||||
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Controller\Booking\Traits\BookingCreateTrait;
|
||||
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
|
||||
use App\Exception\BookingSessionNotFoundException;
|
||||
use App\Exception\HotelNotInTravelException;
|
||||
use App\Exception\TravelNotFoundException;
|
||||
use App\Entity\User;
|
||||
use App\Form\BookingCreateStep2Type;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Form\Model\ParticipantDto;
|
||||
use App\Form\Model\RoomSelectionDto;
|
||||
use App\Service\BookingConfigurator;
|
||||
use App\Service\BookingCreateContextFactory;
|
||||
use App\Service\BookingSessionManager;
|
||||
use App\Service\ParticipantDataPrefiller;
|
||||
use App\Service\RoomAssigner;
|
||||
use App\Service\TravelDataProvider;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
@@ -28,11 +29,8 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
* This controller uses a card overview and lazy-loaded individual participant forms
|
||||
* for better performance and UX with large groups (50+ participants).
|
||||
*/
|
||||
class Step2Controller extends AbstractController
|
||||
class Step2Controller extends AbstractBookingCreateController
|
||||
{
|
||||
use BookingCreateTrait;
|
||||
use BookingExceptionHandlerTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly BookingConfigurator $bookingService,
|
||||
private readonly BookingSessionManager $bookingSessionService,
|
||||
@@ -49,15 +47,14 @@ class Step2Controller extends AbstractController
|
||||
#[Route('/bookings/create/participants', name: 'app_booking_create_step_2')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
// Load or create booking DTO
|
||||
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
try {
|
||||
$bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request);
|
||||
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) {
|
||||
return $this->createBookingCreateFailureResponse($exception, false);
|
||||
}
|
||||
$bookingCreateDto = $result;
|
||||
|
||||
// Validate step access
|
||||
if ($redirect = $this->validateStepAccess($bookingCreateDto, 2)) {
|
||||
if (null !== $redirect = $this->validateStepAccess($bookingCreateDto, 2)) {
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
@@ -132,7 +129,9 @@ class Step2Controller extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<int, \App\Form\Model\RoomSelectionDto> $roomSelections */
|
||||
/**
|
||||
* @param array<int, RoomSelectionDto> $roomSelections
|
||||
*/
|
||||
private function calculateParticipantsCount(array $roomSelections, Travel $travelData): int
|
||||
{
|
||||
$participantsCount = 0;
|
||||
|
||||
@@ -8,8 +8,9 @@ use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\TimeoutException;
|
||||
use App\BusProNet\Model\BookingResponse;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\Controller\Booking\Traits\BookingCreateTrait;
|
||||
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
|
||||
use App\Exception\BookingSessionNotFoundException;
|
||||
use App\Exception\HotelNotInTravelException;
|
||||
use App\Exception\TravelNotFoundException;
|
||||
use App\Form\BookingCreateStep3Type;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Htmx\HxTrait;
|
||||
@@ -20,7 +21,6 @@ use App\Service\BookingPriceMismatchAnalyzer;
|
||||
use App\Service\BookingSessionManager;
|
||||
use App\Service\RoomPricingCalculator;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -29,10 +29,8 @@ use Symfony\Component\Routing\Attribute\Route;
|
||||
/**
|
||||
* Handles the third step of the booking creation process (payment method selection).
|
||||
*/
|
||||
class Step3Controller extends AbstractController
|
||||
class Step3Controller extends AbstractBookingCreateController
|
||||
{
|
||||
use BookingCreateTrait;
|
||||
use BookingExceptionHandlerTrait;
|
||||
use HxTrait;
|
||||
|
||||
public function __construct(
|
||||
@@ -52,14 +50,14 @@ class Step3Controller extends AbstractController
|
||||
#[Route('/bookings/create/payment', name: 'app_booking_create_step_3')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
try {
|
||||
$bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request);
|
||||
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) {
|
||||
return $this->createBookingCreateFailureResponse($exception, false);
|
||||
}
|
||||
$bookingCreateDto = $result;
|
||||
|
||||
// Validate step access
|
||||
if ($redirect = $this->validateStepAccess($bookingCreateDto, 3)) {
|
||||
if (null !== $redirect = $this->validateStepAccess($bookingCreateDto, 3)) {
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
@@ -203,11 +201,11 @@ class Step3Controller extends AbstractController
|
||||
#[Route('/bookings/create/payment/refresh', name: 'app_booking_create_step_3_refresh', methods: ['POST'])]
|
||||
public function refresh(Request $request): Response
|
||||
{
|
||||
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
try {
|
||||
$bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request);
|
||||
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) {
|
||||
return $this->createBookingCreateFailureResponse($exception, true);
|
||||
}
|
||||
$bookingCreateDto = $result;
|
||||
|
||||
$form = $this->createForm(BookingCreateStep3Type::class, $bookingCreateDto, [
|
||||
'validation_groups' => false,
|
||||
@@ -248,7 +246,7 @@ class Step3Controller extends AbstractController
|
||||
private function shouldFallbackToInquiryMode(BookingResponse $response): bool
|
||||
{
|
||||
// Check for "nicht möglich" status + inquiry suggestion in message
|
||||
if ('nicht möglich' !== $response->status) {
|
||||
if (BookingResponse::BOOKING_IMPOSSIBLE !== $response->status) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ namespace App\Controller\Booking\Create;
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\TimeoutException;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\Controller\Booking\Traits\BookingCreateTrait;
|
||||
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
|
||||
use App\Entity\User;
|
||||
use App\Exception\BookingSessionNotFoundException;
|
||||
use App\Exception\HotelNotInTravelException;
|
||||
use App\Exception\NewsletterProviderException;
|
||||
use App\Exception\TravelNotFoundException;
|
||||
use App\Form\BookingCreateStep4Type;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Htmx\HxTrait;
|
||||
@@ -20,7 +21,6 @@ use App\Service\BookingSessionManager;
|
||||
use App\Service\MailjetApiClient;
|
||||
use App\Service\NewsletterManager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
@@ -30,10 +30,8 @@ use Symfony\Contracts\Cache\CacheInterface;
|
||||
/**
|
||||
* Handles the fourth step of the booking creation process (confirmation).
|
||||
*/
|
||||
class Step4Controller extends AbstractController
|
||||
class Step4Controller extends AbstractBookingCreateController
|
||||
{
|
||||
use BookingCreateTrait;
|
||||
use BookingExceptionHandlerTrait;
|
||||
use HxTrait;
|
||||
|
||||
public function __construct(
|
||||
@@ -54,14 +52,14 @@ class Step4Controller extends AbstractController
|
||||
#[Route('/bookings/create/confirmation', name: 'app_booking_create_step_4')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$result = $this->getOrCreateBookingCreateDto($this->bookingSessionService, $request);
|
||||
if ($result instanceof Response) {
|
||||
return $result;
|
||||
try {
|
||||
$bookingCreateDto = $this->loadBookingCreateDto($this->bookingSessionService, $request);
|
||||
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $exception) {
|
||||
return $this->createBookingCreateFailureResponse($exception, false);
|
||||
}
|
||||
$bookingCreateDto = $result;
|
||||
|
||||
// Validate step access
|
||||
if ($redirect = $this->validateStepAccess($bookingCreateDto, 4)) {
|
||||
if (null !== $redirect = $this->validateStepAccess($bookingCreateDto, 4)) {
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Controller\Booking\Edit;
|
||||
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\Controller\Booking\Traits\BookingExceptionHandlerTrait;
|
||||
use App\Entity\User;
|
||||
use App\Exception\TravelNotFoundException;
|
||||
use App\Form\BookingEditType;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Model\BookingEditSubmissionResult;
|
||||
use App\Htmx\HxTrait;
|
||||
use App\Service\BookingChangeTracker;
|
||||
use App\Service\BookingEditContextFactory;
|
||||
@@ -34,7 +34,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
*/
|
||||
class IndexController extends AbstractController
|
||||
{
|
||||
use BookingExceptionHandlerTrait;
|
||||
use HxTrait;
|
||||
|
||||
public function __construct(
|
||||
@@ -43,7 +42,7 @@ class IndexController extends AbstractController
|
||||
private readonly BookingSessionManager $bookingSessionService,
|
||||
private readonly BookingChangeTracker $fingerprintService,
|
||||
private readonly BookingEditContextFactory $editContextFactory,
|
||||
private readonly BookingEditSubmitter $submitService,
|
||||
private readonly BookingEditSubmitter $formSubmitter,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -53,32 +52,32 @@ class IndexController extends AbstractController
|
||||
* Clears any existing session data and API cache to ensure fresh data
|
||||
* is loaded, then redirects to the main edit page.
|
||||
*/
|
||||
#[Route('/bookings/{id}/edit/start', name: 'app_booking_edit_start', requirements: ['id' => '\d+'])]
|
||||
#[Route('/bookings/{bookingId}/edit/start', name: 'app_booking_edit_start', requirements: ['bookingId' => '\d+'])]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function start(int $id, Request $request): Response
|
||||
public function start(int $bookingId, Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT);
|
||||
$this->dataLoader->invalidateBookingCache($id, $user);
|
||||
$this->dataLoader->invalidateBookingCache($bookingId, $user);
|
||||
|
||||
return $this->redirectToRoute('app_booking_edit', ['id' => $id]);
|
||||
return $this->redirectToRoute('app_booking_edit', ['bookingId' => $bookingId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display participant cards overview.
|
||||
*/
|
||||
#[Route('/bookings/{id}/edit', name: 'app_booking_edit', requirements: ['id' => '\d+'])]
|
||||
#[Route('/bookings/{bookingId}/edit', name: 'app_booking_edit', requirements: ['bookingId' => '\d+'])]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function index(int $id, Request $request): Response
|
||||
public function index(int $bookingId, Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
// Load form data from session (or API on first load)
|
||||
try {
|
||||
$bookingDto = $this->dataLoader->loadFormData($request, $id, $user);
|
||||
$bookingDto = $this->dataLoader->loadFormData($request, $bookingId, $user);
|
||||
} catch (TravelNotFoundException) {
|
||||
$this->addFlash('error', 'Reisedaten sind nicht (mehr) verfügbar');
|
||||
|
||||
@@ -92,12 +91,12 @@ class IndexController extends AbstractController
|
||||
}
|
||||
|
||||
// Show flash message if draft was restored
|
||||
if (true === $this->dataLoader->wasDraftRestored()) {
|
||||
if (true === $this->dataLoader->isDraftRestored()) {
|
||||
$this->addFlash('info', 'Dein zuvor gespeicherter Entwurf wurde wiederhergestellt.');
|
||||
}
|
||||
|
||||
// Fetch booking data for display (surcharges, canceled status, etc.)
|
||||
$bookingData = $this->dataLoader->fetchBookingData($id, $user);
|
||||
$bookingData = $this->dataLoader->fetchBookingData($bookingId, $user);
|
||||
if (null === $bookingData || $bookingData instanceof Notification) {
|
||||
$this->addFlash('error', 'Buchungsdaten nicht (mehr) verfügbar');
|
||||
|
||||
@@ -110,7 +109,10 @@ class IndexController extends AbstractController
|
||||
|
||||
// Handle form submission (clicking "Buchung aktualisieren")
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
return $this->submitService->handleSubmission($request, $bookingDto, $id, $user);
|
||||
$submissionResult = $this->formSubmitter->handleSubmission($request, $bookingDto, $bookingId, $user);
|
||||
$this->applySubmissionResultFlashes($submissionResult);
|
||||
|
||||
return $this->redirectToRoute('app_booking_edit', ['bookingId' => $bookingId]);
|
||||
}
|
||||
|
||||
$bookingEditContext = $this->editContextFactory->createOverviewContext(
|
||||
@@ -129,6 +131,39 @@ class IndexController extends AbstractController
|
||||
return $this->render('booking/edit/index.html.twig', $templateData);
|
||||
}
|
||||
|
||||
private function applySubmissionResultFlashes(BookingEditSubmissionResult $result): void
|
||||
{
|
||||
if (true === $result->immutableChangesReverted) {
|
||||
$this->addFlash('info', 'Einige Änderungen wurden verworfen, da sie aktuell nicht mehr änderbar sind.');
|
||||
}
|
||||
|
||||
switch ($result->status) {
|
||||
case BookingEditSubmissionResult::STATUS_BOOKING_DATA_RELOAD_FAILED:
|
||||
$this->addFlash('error', 'Buchungsdaten konnten vor dem Speichern nicht neu geladen werden');
|
||||
break;
|
||||
case BookingEditSubmissionResult::STATUS_NOTIFICATION_ERROR:
|
||||
$this->addFlash('error', $result->message ?? 'Es ist ein Fehler in der Kommunikation mit dem Buchungssystem aufgetreten');
|
||||
break;
|
||||
case BookingEditSubmissionResult::STATUS_NOTIFICATION_INFO:
|
||||
if (null !== $result->message && '' !== trim($result->message)) {
|
||||
$this->addFlash('info', $result->message);
|
||||
}
|
||||
break;
|
||||
case BookingEditSubmissionResult::STATUS_SUCCESS:
|
||||
$this->addFlash('success', 'Buchung erfolgreich aktualisiert');
|
||||
break;
|
||||
case BookingEditSubmissionResult::STATUS_UNSUCCESSFUL:
|
||||
$this->addFlash('error', $result->message ?? 'Buchung konnte nicht aktualisiert werden');
|
||||
break;
|
||||
case BookingEditSubmissionResult::STATUS_TIMEOUT:
|
||||
$this->addFlash('error', 'Die Anfrage hat zu lange gedauert. Bitte versuche es erneut.');
|
||||
break;
|
||||
case BookingEditSubmissionResult::STATUS_API_CLIENT_ERROR:
|
||||
$this->addFlash('error', 'Es ist ein Fehler in der Kommunikation mit dem Buchungssystem aufgetreten');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads booking data from API, discarding all session changes.
|
||||
*
|
||||
@@ -136,12 +171,12 @@ class IndexController extends AbstractController
|
||||
* POST: Clears session and redirects to reload the booking
|
||||
*/
|
||||
#[Route(
|
||||
path: '/bookings/{id}/edit/reload',
|
||||
path: '/bookings/{bookingId}/edit/reload',
|
||||
name: 'app_booking_edit_reload',
|
||||
requirements: ['id' => '\d+']
|
||||
requirements: ['bookingId' => '\d+']
|
||||
)]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function reloadFromApi(int $id, Request $request): Response
|
||||
public function reloadFromApi(int $bookingId, Request $request): Response
|
||||
{
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
// Clear session to discard all changes
|
||||
@@ -150,15 +185,15 @@ class IndexController extends AbstractController
|
||||
// Delete draft since user explicitly chose to discard changes
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$this->draftService->deleteDraft($user, $id);
|
||||
$this->draftService->deleteDraft($user, $bookingId);
|
||||
|
||||
$this->addFlash('success', 'Änderungen verworfen, Daten neu geladen');
|
||||
|
||||
return $this->redirectToRoute('app_booking_edit', ['id' => $id]);
|
||||
return $this->redirectToRoute('app_booking_edit', ['bookingId' => $bookingId]);
|
||||
}
|
||||
|
||||
return $this->render('booking/edit/modal_reload.html.twig', [
|
||||
'bookingId' => $id,
|
||||
'bookingId' => $bookingId,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -169,12 +204,12 @@ class IndexController extends AbstractController
|
||||
* Shows a flash message informing about the saved draft.
|
||||
*/
|
||||
#[Route(
|
||||
path: '/bookings/{id}/edit/cancel',
|
||||
path: '/bookings/{bookingId}/edit/cancel',
|
||||
name: 'app_booking_edit_cancel',
|
||||
requirements: ['id' => '\d+']
|
||||
requirements: ['bookingId' => '\d+']
|
||||
)]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function cancelEdit(int $id, Request $request): Response
|
||||
public function cancelEdit(int $bookingId, Request $request): Response
|
||||
{
|
||||
// Clear session state
|
||||
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT);
|
||||
@@ -182,7 +217,7 @@ class IndexController extends AbstractController
|
||||
// Check if a draft exists to show appropriate message
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$draft = $this->draftService->findDraft($user, $id);
|
||||
$draft = $this->draftService->findDraft($user, $bookingId);
|
||||
|
||||
if (null !== $draft) {
|
||||
$this->addFlash('info', 'Deine Änderungen wurden als Entwurf gespeichert.');
|
||||
|
||||
@@ -41,12 +41,12 @@ class ParticipantController extends AbstractController
|
||||
* Edit single participant form.
|
||||
*/
|
||||
#[Route(
|
||||
path: '/bookings/{id}/edit/participants/{index}',
|
||||
path: '/bookings/{bookingId}/edit/participants/{index}',
|
||||
name: 'app_booking_edit_participant',
|
||||
requirements: ['id' => '\d+', 'index' => '\d+']
|
||||
requirements: ['bookingId' => '\d+', 'index' => '\d+']
|
||||
)]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function editParticipant(int $id, int $index, Request $request): Response
|
||||
public function editParticipant(int $bookingId, int $index, Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
@@ -56,12 +56,12 @@ class ParticipantController extends AbstractController
|
||||
if (null === $bookingDto) {
|
||||
$this->addFlash('error', 'Sitzung abgelaufen. Bitte neu laden.');
|
||||
|
||||
return $this->redirectToRoute('app_booking_edit', ['id' => $id]);
|
||||
return $this->redirectToRoute('app_booking_edit', ['bookingId' => $bookingId]);
|
||||
}
|
||||
|
||||
$this->participantFormSupportService->ensureParticipantExists($bookingDto, $index);
|
||||
|
||||
$bookingData = $this->dataLoader->fetchBookingData($id, $user);
|
||||
$bookingData = $this->dataLoader->fetchBookingData($bookingId, $user);
|
||||
if (null === $bookingData || $bookingData instanceof Notification) {
|
||||
$this->addFlash('error', 'Buchungsdaten nicht (mehr) verfügbar');
|
||||
|
||||
@@ -71,7 +71,7 @@ class ParticipantController extends AbstractController
|
||||
if ($this->isParticipantCanceled($bookingData, $index)) {
|
||||
$this->addFlash('warning', 'Stornierte Teilnehmer können nicht bearbeitet werden');
|
||||
|
||||
return $this->redirectToRoute('app_booking_edit', ['id' => $id]);
|
||||
return $this->redirectToRoute('app_booking_edit', ['bookingId' => $bookingId]);
|
||||
}
|
||||
|
||||
$this->editContextFactory->prepareBookingDto($bookingDto);
|
||||
@@ -90,10 +90,10 @@ class ParticipantController extends AbstractController
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->bookingSessionService->saveBookingDto($request, $bookingDto, BookingDto::MODE_EDIT);
|
||||
$this->draftService->saveDraft($user, $id, $bookingDto);
|
||||
$this->draftService->saveDraft($user, $bookingId, $bookingDto);
|
||||
$this->addNotificationsAsFlashMessages($notifications);
|
||||
|
||||
return $this->redirectToRoute('app_booking_edit', ['id' => $id]);
|
||||
return $this->redirectToRoute('app_booking_edit', ['bookingId' => $bookingId]);
|
||||
}
|
||||
|
||||
$context = $this->editContextFactory->createParticipantContext($bookingDto, $bookingData);
|
||||
@@ -106,9 +106,9 @@ class ParticipantController extends AbstractController
|
||||
'summaryData' => $context->summaryData,
|
||||
'mutableData' => $context->mutableData,
|
||||
'refreshRouteName' => 'app_booking_edit_participant_refresh',
|
||||
'refreshRouteParams' => ['id' => $id, 'index' => $index],
|
||||
'refreshRouteParams' => ['bookingId' => $bookingId, 'index' => $index],
|
||||
'cancelRouteName' => 'app_booking_edit',
|
||||
'cancelRouteParams' => ['id' => $id],
|
||||
'cancelRouteParams' => ['bookingId' => $bookingId],
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -116,13 +116,13 @@ class ParticipantController extends AbstractController
|
||||
* HTMX endpoint for refreshing participant form without validation.
|
||||
*/
|
||||
#[Route(
|
||||
path: '/bookings/{id}/edit/participants/{index}/refresh',
|
||||
path: '/bookings/{bookingId}/edit/participants/{index}/refresh',
|
||||
name: 'app_booking_edit_participant_refresh',
|
||||
requirements: ['id' => '\d+', 'index' => '\d+'],
|
||||
requirements: ['bookingId' => '\d+', 'index' => '\d+'],
|
||||
methods: ['POST']
|
||||
)]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function refreshParticipantForm(int $id, int $index, Request $request): Response
|
||||
public function refreshParticipantForm(int $bookingId, int $index, Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
@@ -136,7 +136,7 @@ class ParticipantController extends AbstractController
|
||||
$this->participantFormSupportService->ensureParticipantExists($bookingDto, $index);
|
||||
$this->editContextFactory->prepareBookingDto($bookingDto);
|
||||
|
||||
$bookingData = $this->dataLoader->fetchBookingData($id, $user);
|
||||
$bookingData = $this->dataLoader->fetchBookingData($bookingId, $user);
|
||||
|
||||
$wrapper = $this->participantFormSupportService->createParticipantEditDto($bookingDto, $index);
|
||||
|
||||
@@ -165,9 +165,9 @@ class ParticipantController extends AbstractController
|
||||
'summaryData' => $context->summaryData,
|
||||
'mutableData' => $context->mutableData,
|
||||
'refreshRouteName' => 'app_booking_edit_participant_refresh',
|
||||
'refreshRouteParams' => ['id' => $id, 'index' => $index],
|
||||
'refreshRouteParams' => ['bookingId' => $bookingId, 'index' => $index],
|
||||
'cancelRouteName' => 'app_booking_edit',
|
||||
'cancelRouteParams' => ['id' => $id],
|
||||
'cancelRouteParams' => ['bookingId' => $bookingId],
|
||||
]
|
||||
);
|
||||
|
||||
@@ -192,6 +192,6 @@ class ParticipantController extends AbstractController
|
||||
|
||||
private function isParticipantCanceled(Booking $bookingData, int $index): bool
|
||||
{
|
||||
return 'S' === ($bookingData->participantsStatus[$index] ?? null);
|
||||
return Booking::STATUS_CANCELED === ($bookingData->participantsStatus[$index] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Booking\Traits;
|
||||
|
||||
use App\Form\Model\BookingDto;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
||||
/**
|
||||
* Provides common functionality for booking creation controllers.
|
||||
*
|
||||
* This trait contains shared validation and redirect logic used across
|
||||
* all booking creation steps to ensure consistent behavior and reduce
|
||||
* code duplication.
|
||||
*/
|
||||
trait BookingCreateTrait
|
||||
{
|
||||
/**
|
||||
* Validates step access and returns redirect response if necessary.
|
||||
*
|
||||
* @return RedirectResponse|null Returns redirect response if validation fails, null if access is allowed
|
||||
*/
|
||||
private function validateStepAccess(BookingDto $bookingCreateDto, int $expectedStep): ?RedirectResponse
|
||||
{
|
||||
// Allow access to current step or any previous step
|
||||
if ($expectedStep > $bookingCreateDto->currentStep) {
|
||||
$this->addFlash('error', 'Bitte erst die vorherigen Schritte abschließen.');
|
||||
|
||||
return $this->redirectToCurrentStep($bookingCreateDto);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirects to the current step based on the DTO's currentStep.
|
||||
*/
|
||||
private function redirectToCurrentStep(BookingDto $bookingCreateDto): RedirectResponse
|
||||
{
|
||||
$route = match ($bookingCreateDto->currentStep) {
|
||||
2 => 'app_booking_create_step_2',
|
||||
3 => 'app_booking_create_step_3',
|
||||
4 => 'app_booking_create_step_4',
|
||||
default => 'app_booking_create_step_1',
|
||||
};
|
||||
|
||||
return $this->redirectToRoute($route);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles API errors by logging and adding a flash message.
|
||||
*/
|
||||
/**
|
||||
* @param array<string, mixed> $context
|
||||
*/
|
||||
private function handleApiError(
|
||||
LoggerInterface $logger,
|
||||
string $logMessage,
|
||||
array $context,
|
||||
string $flashMessage,
|
||||
): void {
|
||||
$logger->error($logMessage, $context);
|
||||
$this->addFlash('error', $flashMessage);
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Booking\Traits;
|
||||
|
||||
use App\Exception\BookingSessionNotFoundException;
|
||||
use App\Exception\HotelNotInTravelException;
|
||||
use App\Exception\TravelNotFoundException;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Service\BookingSessionManager;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Provides centralized exception handling for booking controllers.
|
||||
*
|
||||
* This trait contains common exception handling logic for booking operations
|
||||
* including user-friendly error messages and appropriate redirects.
|
||||
*/
|
||||
trait BookingExceptionHandlerTrait
|
||||
{
|
||||
/**
|
||||
* Safely retrieves booking DTO with centralized exception handling.
|
||||
*
|
||||
* Handles all booking-related exceptions and provides appropriate user feedback
|
||||
* by redirecting to the error page with flash messages.
|
||||
*/
|
||||
protected function getOrCreateBookingCreateDto(BookingSessionManager $bookingSessionService, Request $request): BookingDto|RedirectResponse
|
||||
{
|
||||
try {
|
||||
return $bookingSessionService->getOrCreateBookingCreateDto($request);
|
||||
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $e) {
|
||||
$this->addFlash('error', 'Deine Buchungssitzung ist abgelaufen. Bitte starte eine neue Buchung.');
|
||||
|
||||
return $this->redirectToRoute('app_booking_create_error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely retrieves booking DTO for HTMX requests with lightweight error responses.
|
||||
*
|
||||
* Returns empty 400 responses for HTMX requests when exceptions occur,
|
||||
* allowing the frontend to handle errors appropriately.
|
||||
*/
|
||||
protected function getOrCreateBookingCreateDtoForHtmx(BookingSessionManager $bookingSessionService, Request $request): mixed
|
||||
{
|
||||
try {
|
||||
return $bookingSessionService->getOrCreateBookingCreateDto($request);
|
||||
} catch (BookingSessionNotFoundException|TravelNotFoundException|HotelNotInTravelException $e) {
|
||||
return new Response('', 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
final readonly class BookingEditSubmissionResult
|
||||
{
|
||||
public const string STATUS_BOOKING_DATA_RELOAD_FAILED = 'booking_data_reload_failed';
|
||||
public const string STATUS_NOTIFICATION_ERROR = 'notification_error';
|
||||
public const string STATUS_NOTIFICATION_INFO = 'notification_info';
|
||||
public const string STATUS_SUCCESS = 'success';
|
||||
public const string STATUS_UNSUCCESSFUL = 'unsuccessful';
|
||||
public const string STATUS_TIMEOUT = 'timeout';
|
||||
public const string STATUS_API_CLIENT_ERROR = 'api_client_error';
|
||||
|
||||
public function __construct(
|
||||
public string $status,
|
||||
public ?string $message = null,
|
||||
public bool $immutableChangesReverted = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ class BookingEditDataLoader
|
||||
{
|
||||
public const string CACHE_TAG_USER_PREFIX = 'user_bookings_';
|
||||
|
||||
private bool $draftWasRestored = false;
|
||||
private bool $draftRestored = false;
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
@@ -48,9 +48,9 @@ class BookingEditDataLoader
|
||||
* This flag is reset on each call to loadFormData() and can be used
|
||||
* by the controller to show a flash message to the user.
|
||||
*/
|
||||
public function wasDraftRestored(): bool
|
||||
public function isDraftRestored(): bool
|
||||
{
|
||||
return $this->draftWasRestored;
|
||||
return $this->draftRestored;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +71,7 @@ class BookingEditDataLoader
|
||||
*/
|
||||
public function loadFormData(Request $request, int $bookingId, User $user): ?BookingDto
|
||||
{
|
||||
$this->draftWasRestored = false;
|
||||
$this->draftRestored = false;
|
||||
|
||||
$formData = $this->bookingSessionService->getBookingDto($request, BookingDto::MODE_EDIT);
|
||||
|
||||
@@ -154,7 +154,7 @@ class BookingEditDataLoader
|
||||
if (null !== $draft) {
|
||||
$applied = $this->draftService->applyDraftToDto($draft, $formData, $travelData);
|
||||
if (true === $applied) {
|
||||
$this->draftWasRestored = true;
|
||||
$this->draftRestored = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,12 +9,10 @@ use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Exception\TimeoutException;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\Entity\User;
|
||||
use App\Model\BookingEditSubmissionResult;
|
||||
use App\Form\Model\BookingDto;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Session\FlashBagAwareSessionInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* Handles the final booking edit submission workflow.
|
||||
@@ -28,12 +26,11 @@ class BookingEditSubmitter
|
||||
private readonly TravelDataProvider $travelDataService,
|
||||
private readonly BookingEditSubmitGuard $submitGuard,
|
||||
private readonly BookingSessionManager $bookingSessionService,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function handleSubmission(Request $request, BookingDto $bookingDto, int $bookingId, User $user): RedirectResponse
|
||||
public function handleSubmission(Request $request, BookingDto $bookingDto, int $bookingId, User $user): BookingEditSubmissionResult
|
||||
{
|
||||
$email = $user->getEmail();
|
||||
|
||||
@@ -45,14 +42,15 @@ class BookingEditSubmitter
|
||||
$this->dataLoader->invalidateBookingCache($bookingId, $user);
|
||||
$freshBookingData = $this->dataLoader->fetchBookingData($bookingId, $user);
|
||||
if (null === $freshBookingData || $freshBookingData instanceof Notification) {
|
||||
$this->addFlash($request, 'error', 'Buchungsdaten konnten vor dem Speichern nicht neu geladen werden');
|
||||
$this->logger->warning('Failed to refresh booking before update submission', [
|
||||
'email' => $email,
|
||||
'booking_id' => $bookingId,
|
||||
'has_notification' => $freshBookingData instanceof Notification,
|
||||
]);
|
||||
|
||||
return $this->redirectToEdit($bookingId);
|
||||
return new BookingEditSubmissionResult(
|
||||
BookingEditSubmissionResult::STATUS_BOOKING_DATA_RELOAD_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
$bookingDto->booking = $freshBookingData;
|
||||
@@ -68,66 +66,69 @@ class BookingEditSubmitter
|
||||
$immutableChangesReverted = $this->submitGuard->reconcileImmutableCategories($bookingDto, $freshBookingData);
|
||||
if (true === $immutableChangesReverted) {
|
||||
$this->bookingSessionService->saveBookingDto($request, $bookingDto, BookingDto::MODE_EDIT);
|
||||
$this->addFlash($request, 'info', 'Einige Änderungen wurden verworfen, da sie aktuell nicht mehr änderbar sind.');
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->apiClient->updateBooking($bookingDto, true);
|
||||
if ($response instanceof Notification) {
|
||||
if (true === $response->isError()) {
|
||||
$this->addFlash($request, 'error', $response->message);
|
||||
} else {
|
||||
$this->addFlash($request, 'info', $response->message);
|
||||
}
|
||||
$this->logger->error('Booking update not successful', [
|
||||
'email' => $email,
|
||||
'booking_id' => $bookingId,
|
||||
'message' => $response->message,
|
||||
]);
|
||||
} elseif (true === $response->success) {
|
||||
return new BookingEditSubmissionResult(
|
||||
true === $response->isError()
|
||||
? BookingEditSubmissionResult::STATUS_NOTIFICATION_ERROR
|
||||
: BookingEditSubmissionResult::STATUS_NOTIFICATION_INFO,
|
||||
$response->message,
|
||||
$immutableChangesReverted,
|
||||
);
|
||||
}
|
||||
|
||||
if (true === $response->success) {
|
||||
$this->dataLoader->invalidateBookingCache($bookingId, $user);
|
||||
$this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT);
|
||||
$this->draftService->deleteDraft($user, $bookingId);
|
||||
|
||||
$this->addFlash($request, 'success', 'Buchung erfolgreich aktualisiert');
|
||||
$this->logger->info('Booking update successful', [
|
||||
'email' => $email,
|
||||
'booking_id' => $bookingId,
|
||||
]);
|
||||
|
||||
return $this->redirectToEdit($bookingId);
|
||||
} else {
|
||||
$this->addFlash($request, 'error', $response->status ?? 'Buchung konnte nicht aktualisiert werden');
|
||||
return new BookingEditSubmissionResult(
|
||||
BookingEditSubmissionResult::STATUS_SUCCESS,
|
||||
null,
|
||||
$immutableChangesReverted,
|
||||
);
|
||||
}
|
||||
|
||||
$this->logger->warning('Booking update returned unsuccessful status', [
|
||||
'email' => $email,
|
||||
'booking_id' => $bookingId,
|
||||
'status' => $response->status,
|
||||
'valid' => $response->valid,
|
||||
]);
|
||||
}
|
||||
|
||||
return new BookingEditSubmissionResult(
|
||||
BookingEditSubmissionResult::STATUS_UNSUCCESSFUL,
|
||||
$response->status ?? 'Buchung konnte nicht aktualisiert werden',
|
||||
$immutableChangesReverted,
|
||||
);
|
||||
} catch (TimeoutException) {
|
||||
$this->addFlash($request, 'error', 'Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut.');
|
||||
$this->logger->error('Booking update timeout', [
|
||||
'email' => $email,
|
||||
'booking_id' => $bookingId,
|
||||
]);
|
||||
return new BookingEditSubmissionResult(
|
||||
BookingEditSubmissionResult::STATUS_TIMEOUT,
|
||||
null,
|
||||
$immutableChangesReverted,
|
||||
);
|
||||
} catch (ApiClientException) {
|
||||
$this->addFlash($request, 'error', 'Es ist ein Fehler in der Kommunikation mit dem Buchungssystem aufgetreten');
|
||||
return new BookingEditSubmissionResult(
|
||||
BookingEditSubmissionResult::STATUS_API_CLIENT_ERROR,
|
||||
null,
|
||||
$immutableChangesReverted,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->redirectToEdit($bookingId);
|
||||
}
|
||||
|
||||
private function addFlash(Request $request, string $type, string $message): void
|
||||
{
|
||||
$session = $request->getSession();
|
||||
if ($session instanceof FlashBagAwareSessionInterface) {
|
||||
$session->getFlashBag()->add($type, $message);
|
||||
}
|
||||
}
|
||||
|
||||
private function redirectToEdit(int $bookingId): RedirectResponse
|
||||
{
|
||||
return new RedirectResponse($this->urlGenerator->generate('app_booking_edit', ['id' => $bookingId]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
{# Build edit URL for linking #}
|
||||
{% if mode == 'edit' %}
|
||||
{% set editUrl = path('app_booking_edit_participant', {id: bookingId, index: index}) %}
|
||||
{% set editUrl = path('app_booking_edit_participant', {bookingId: bookingId, index: index}) %}
|
||||
{% else %}
|
||||
{% set editUrl = path('app_booking_create_step_2_participant', {index: index}) %}
|
||||
{% endif %}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</h1>
|
||||
{% if bookingEditContext.isDirty %}
|
||||
<button type="button"
|
||||
hx-get="{{ path('app_booking_edit_reload', {id: bookingEditContext.bookingData.id}) }}"
|
||||
hx-get="{{ path('app_booking_edit_reload', {bookingId: bookingEditContext.bookingData.id}) }}"
|
||||
hx-target="body"
|
||||
hx-swap="beforeend"
|
||||
class="button button--small button--secondary">
|
||||
@@ -85,7 +85,7 @@
|
||||
{# Fixed footer #}
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
|
||||
<div class="flex justify-between" hx-disinherit="*">
|
||||
<a href="{{ bookingEditContext.isDirty ? path('app_booking_edit_cancel', {id: bookingEditContext.bookingData.id}) : path('app_bookings') }}"
|
||||
<a href="{{ bookingEditContext.isDirty ? path('app_booking_edit_cancel', {bookingId: bookingEditContext.bookingData.id}) : path('app_bookings') }}"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-8 h-8 md:w-10 md:h-10">
|
||||
<svg class="w-6 h-6 md:w-8 md:h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
</a>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<button type="button"
|
||||
hx-post="{{ path('app_booking_edit_reload', {id: bookingId}) }}"
|
||||
hx-post="{{ path('app_booking_edit_reload', {bookingId: bookingId}) }}"
|
||||
hx-target="body"
|
||||
class="button button--small button--primary">
|
||||
Ja
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</div>
|
||||
<div class="shrink-0 bg-primary-dark px-4 lg:px-8 py-2 lg:py-4 z-20">
|
||||
<div class="flex justify-between">
|
||||
<a href="{{ path('app_booking_edit', {id: bookingEditContext.bookingData.id}) }}"
|
||||
<a href="{{ path('app_booking_edit', {bookingId: bookingEditContext.bookingData.id}) }}"
|
||||
class="inline-flex items-center justify-center bg-gray-200 rounded-md w-8 h-8 md:w-10 md:h-10"
|
||||
{{ qa_attribute('btn-cancel') }}>
|
||||
<svg class="w-6 h-6 md:w-8 md:h-8" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="200" y1="56" x2="56" y2="200" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="200" y1="200" x2="56" y2="56" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</table>
|
||||
<div class="flex space-x-2">
|
||||
{% if booking.editable %}
|
||||
<a href="{{ path('app_booking_edit_start', { 'id': booking.id }) }}"
|
||||
<a href="{{ path('app_booking_edit_start', { bookingId: booking.id }) }}"
|
||||
class="button button--primary button--small"
|
||||
title="Buchung bearbeiten">
|
||||
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M92.69,216H48a8,8,0,0,1-8-8V163.31a8,8,0,0,1,2.34-5.65L165.66,34.34a8,8,0,0,1,11.31,0L221.66,79a8,8,0,0,1,0,11.31L98.34,213.66A8,8,0,0,1,92.69,216Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="136" y1="64" x2="192" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="164" y1="92" x2="68" y2="188" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><line x1="95.49" y1="215.49" x2="40.51" y2="160.51" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Controller\Booking\Edit;
|
||||
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Controller\Booking\Edit\IndexController;
|
||||
use App\Entity\User;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Model\BookingEditSubmissionResult;
|
||||
use App\Service\BookingChangeTracker;
|
||||
use App\Service\BookingEditContextFactory;
|
||||
use App\Service\BookingEditDataLoader;
|
||||
use App\Service\BookingEditDraftManager;
|
||||
use App\Service\BookingEditSubmitter;
|
||||
use App\Service\BookingSessionManager;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class IndexControllerTest extends TestCase
|
||||
{
|
||||
public function testIndexAppliesSubmissionResultFlashesAndRedirectsBackToEditPage(): void
|
||||
{
|
||||
$this->assertSubmissionResultFlashes(
|
||||
new BookingEditSubmissionResult(
|
||||
BookingEditSubmissionResult::STATUS_SUCCESS,
|
||||
null,
|
||||
true
|
||||
),
|
||||
[
|
||||
['info', 'Einige Änderungen wurden verworfen, da sie aktuell nicht mehr änderbar sind.'],
|
||||
['success', 'Buchung erfolgreich aktualisiert'],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider submissionResultProvider
|
||||
*
|
||||
* @param array<int, array{0: string, 1: string}> $expectedFlashes
|
||||
* @phpstan-param array<int, array{0: string, 1: string}> $expectedFlashes
|
||||
*/
|
||||
public function testIndexAppliesErrorSubmissionResultFlashesAndRedirectsBackToEditPage(
|
||||
BookingEditSubmissionResult $submissionResult,
|
||||
array $expectedFlashes,
|
||||
): void {
|
||||
$this->assertSubmissionResultFlashes($submissionResult, $expectedFlashes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{0: BookingEditSubmissionResult, 1: array<int, array{0: string, 1: string}>}>
|
||||
*/
|
||||
public static function submissionResultProvider(): array
|
||||
{
|
||||
return [
|
||||
'notification error' => [
|
||||
new BookingEditSubmissionResult(
|
||||
BookingEditSubmissionResult::STATUS_NOTIFICATION_ERROR,
|
||||
'BPN-FAIL',
|
||||
false
|
||||
),
|
||||
[
|
||||
['error', 'BPN-FAIL'],
|
||||
],
|
||||
],
|
||||
'timeout' => [
|
||||
new BookingEditSubmissionResult(
|
||||
BookingEditSubmissionResult::STATUS_TIMEOUT,
|
||||
null,
|
||||
false
|
||||
),
|
||||
[
|
||||
['error', 'Die Anfrage hat zu lange gedauert. Bitte versuche es erneut.'],
|
||||
],
|
||||
],
|
||||
'reload failed' => [
|
||||
new BookingEditSubmissionResult(
|
||||
BookingEditSubmissionResult::STATUS_BOOKING_DATA_RELOAD_FAILED,
|
||||
null,
|
||||
false
|
||||
),
|
||||
[
|
||||
['error', 'Buchungsdaten konnten vor dem Speichern nicht neu geladen werden'],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function createUser(): User
|
||||
{
|
||||
return new User('[email protected]');
|
||||
}
|
||||
|
||||
private function createBookingDto(): BookingDto
|
||||
{
|
||||
$travel = new Travel();
|
||||
$travel->id = 1234;
|
||||
|
||||
$bookingDto = new BookingDto($travel, 77);
|
||||
$bookingDto->booking = new Booking();
|
||||
$bookingDto->booking->id = 42;
|
||||
|
||||
return $bookingDto;
|
||||
}
|
||||
|
||||
private function createBooking(): Booking
|
||||
{
|
||||
$booking = new Booking();
|
||||
$booking->id = 42;
|
||||
$booking->dateId = 1234;
|
||||
|
||||
return $booking;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{0: string, 1: string}> $expectedFlashes
|
||||
*/
|
||||
private function assertSubmissionResultFlashes(
|
||||
BookingEditSubmissionResult $submissionResult,
|
||||
array $expectedFlashes,
|
||||
): void {
|
||||
$request = Request::create('/bookings/42/edit', 'POST');
|
||||
$user = $this->createUser();
|
||||
$bookingDto = $this->createBookingDto();
|
||||
$bookingData = $this->createBooking();
|
||||
$form = $this->createMock(FormInterface::class);
|
||||
|
||||
$form->expects($this->once())
|
||||
->method('handleRequest')
|
||||
->with($request)
|
||||
->willReturnSelf();
|
||||
$form->expects($this->once())
|
||||
->method('isSubmitted')
|
||||
->willReturn(true);
|
||||
$form->expects($this->once())
|
||||
->method('isValid')
|
||||
->willReturn(true);
|
||||
|
||||
$dataLoader = $this->createMock(BookingEditDataLoader::class);
|
||||
$dataLoader->expects($this->once())
|
||||
->method('loadFormData')
|
||||
->with($request, 42, $user)
|
||||
->willReturn($bookingDto);
|
||||
$dataLoader->expects($this->once())
|
||||
->method('fetchBookingData')
|
||||
->with(42, $user)
|
||||
->willReturn($bookingData);
|
||||
$dataLoader->expects($this->once())
|
||||
->method('isDraftRestored');
|
||||
|
||||
$submitter = $this->createMock(BookingEditSubmitter::class);
|
||||
$submitter->expects($this->once())
|
||||
->method('handleSubmission')
|
||||
->with($request, $bookingDto, 42, $user)
|
||||
->willReturn($submissionResult);
|
||||
|
||||
$controller = new TestableIndexController(
|
||||
$dataLoader,
|
||||
$this->createMock(BookingEditDraftManager::class),
|
||||
$this->createMock(BookingSessionManager::class),
|
||||
$this->createMock(BookingChangeTracker::class),
|
||||
$this->createMock(BookingEditContextFactory::class),
|
||||
$submitter,
|
||||
$user,
|
||||
$form,
|
||||
);
|
||||
|
||||
$response = $controller->index(42, $request);
|
||||
|
||||
$this->assertInstanceOf(RedirectResponse::class, $response);
|
||||
$this->assertSame('/bookings/42/edit', $response->headers->get('Location'));
|
||||
$this->assertSame($expectedFlashes, $controller->flashes);
|
||||
}
|
||||
}
|
||||
|
||||
final class TestableIndexController extends IndexController
|
||||
{
|
||||
/**
|
||||
* @var array<int, array{string, mixed}>
|
||||
*/
|
||||
public array $flashes = [];
|
||||
|
||||
/**
|
||||
* @var FormInterface<mixed>
|
||||
*/
|
||||
private readonly FormInterface $form;
|
||||
|
||||
/**
|
||||
* @param FormInterface<mixed> $form
|
||||
*/
|
||||
public function __construct(
|
||||
BookingEditDataLoader $dataLoader,
|
||||
BookingEditDraftManager $draftService,
|
||||
BookingSessionManager $bookingSessionService,
|
||||
BookingChangeTracker $fingerprintService,
|
||||
BookingEditContextFactory $editContextFactory,
|
||||
BookingEditSubmitter $formSubmitter,
|
||||
private readonly User $user,
|
||||
FormInterface $form,
|
||||
) {
|
||||
$this->form = $form;
|
||||
|
||||
parent::__construct(
|
||||
$dataLoader,
|
||||
$draftService,
|
||||
$bookingSessionService,
|
||||
$fingerprintService,
|
||||
$editContextFactory,
|
||||
$formSubmitter,
|
||||
);
|
||||
}
|
||||
|
||||
protected function getUser(): UserInterface
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FormInterface<mixed>
|
||||
*/
|
||||
protected function createForm(string $type, mixed $data = null, array $options = []): FormInterface
|
||||
{
|
||||
return $this->form;
|
||||
}
|
||||
|
||||
protected function addFlash(string $type, mixed $message): void
|
||||
{
|
||||
$this->flashes[] = [$type, $message];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $parameters
|
||||
*/
|
||||
protected function redirectToRoute(string $route, array $parameters = [], int $status = 302): RedirectResponse
|
||||
{
|
||||
$location = match ($route) {
|
||||
'app_booking_edit' => '/bookings/'.$parameters['bookingId'].'/edit',
|
||||
default => '/'.$route,
|
||||
};
|
||||
|
||||
return new RedirectResponse($location, $status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $parameters
|
||||
*/
|
||||
protected function render(string $view, array $parameters = [], ?Response $response = null): Response
|
||||
{
|
||||
throw new \LogicException('Render should not be called in this test.');
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\Entity\User;
|
||||
use App\Form\Model\BookingDto;
|
||||
use App\Model\BookingEditSubmissionResult;
|
||||
use App\Service\BookingEditDataLoader;
|
||||
use App\Service\BookingEditDraftManager;
|
||||
use App\Service\BookingEditSubmitGuard;
|
||||
@@ -23,11 +24,10 @@ use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
class BookingEditSubmitterTest extends TestCase
|
||||
{
|
||||
public function testHandleSubmissionReturnsRedirectWhenFreshBookingDataCannotBeLoaded(): void
|
||||
public function testHandleSubmissionReturnsResultWhenFreshBookingDataCannotBeLoaded(): void
|
||||
{
|
||||
$request = $this->createRequestWithSession();
|
||||
$user = $this->createUser();
|
||||
@@ -46,22 +46,18 @@ class BookingEditSubmitterTest extends TestCase
|
||||
dataLoader: $dataLoader,
|
||||
);
|
||||
|
||||
$response = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
|
||||
$this->assertSame('/bookings/42/edit', $response->headers->get('Location'));
|
||||
$this->assertSame(
|
||||
['Buchungsdaten konnten vor dem Speichern nicht neu geladen werden'],
|
||||
$request->getSession()->getFlashBag()->get('error')
|
||||
);
|
||||
$this->assertSame(BookingEditSubmissionResult::STATUS_BOOKING_DATA_RELOAD_FAILED, $result->status);
|
||||
$this->assertFalse($result->immutableChangesReverted);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider updateFailureProvider
|
||||
*/
|
||||
public function testHandleSubmissionReturnsRedirectWhenUpdateThrows(
|
||||
public function testHandleSubmissionReturnsResultWhenUpdateThrows(
|
||||
\Throwable $exception,
|
||||
string $expectedFlashType,
|
||||
string $expectedFlashMessage,
|
||||
string $expectedStatus,
|
||||
): void {
|
||||
$request = $this->createRequestWithSession();
|
||||
$user = $this->createUser();
|
||||
@@ -104,13 +100,14 @@ class BookingEditSubmitterTest extends TestCase
|
||||
submitGuard: $submitGuard,
|
||||
);
|
||||
|
||||
$response = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
|
||||
$this->assertSame('/bookings/42/edit', $response->headers->get('Location'));
|
||||
$this->assertSame([$expectedFlashMessage], $request->getSession()->getFlashBag()->get($expectedFlashType));
|
||||
$this->assertSame($expectedStatus, $result->status);
|
||||
$this->assertNull($result->message);
|
||||
$this->assertFalse($result->immutableChangesReverted);
|
||||
}
|
||||
|
||||
public function testHandleSubmissionReturnsRedirectWhenUpdateIsUnsuccessful(): void
|
||||
public function testHandleSubmissionReturnsResultWhenUpdateIsUnsuccessful(): void
|
||||
{
|
||||
$request = $this->createRequestWithSession();
|
||||
$user = $this->createUser();
|
||||
@@ -156,20 +153,21 @@ class BookingEditSubmitterTest extends TestCase
|
||||
submitGuard: $submitGuard,
|
||||
);
|
||||
|
||||
$response = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
|
||||
$this->assertSame('/bookings/42/edit', $response->headers->get('Location'));
|
||||
$this->assertSame(['BPN-FAIL'], $request->getSession()->getFlashBag()->get('error'));
|
||||
$this->assertSame(BookingEditSubmissionResult::STATUS_UNSUCCESSFUL, $result->status);
|
||||
$this->assertSame('BPN-FAIL', $result->message);
|
||||
$this->assertFalse($result->immutableChangesReverted);
|
||||
}
|
||||
|
||||
public function testHandleSubmissionStoresInfoForNonErrorNotification(): void
|
||||
{
|
||||
$this->assertNotificationFlash(new Notification(650, 'Alles gut'), 'info');
|
||||
$this->assertNotificationResult(new Notification(650, 'Alles gut'), 'info');
|
||||
}
|
||||
|
||||
public function testHandleSubmissionStoresErrorForErrorNotification(): void
|
||||
{
|
||||
$this->assertNotificationFlash(new Notification(500, 'Kaputt'), 'error');
|
||||
$this->assertNotificationResult(new Notification(500, 'Kaputt'), 'error');
|
||||
}
|
||||
|
||||
public function testHandleSubmissionClearsSessionAndDraftOnSuccessfulUpdate(): void
|
||||
@@ -231,10 +229,10 @@ class BookingEditSubmitterTest extends TestCase
|
||||
bookingSessionService: $bookingSessionService,
|
||||
);
|
||||
|
||||
$response = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
|
||||
$this->assertSame('/bookings/42/edit', $response->headers->get('Location'));
|
||||
$this->assertSame(['Buchung erfolgreich aktualisiert'], $request->getSession()->getFlashBag()->get('success'));
|
||||
$this->assertSame(BookingEditSubmissionResult::STATUS_SUCCESS, $result->status);
|
||||
$this->assertFalse($result->immutableChangesReverted);
|
||||
$this->assertSame($freshBookingData, $bookingDto->booking);
|
||||
}
|
||||
|
||||
@@ -298,32 +296,31 @@ class BookingEditSubmitterTest extends TestCase
|
||||
bookingSessionService: $bookingSessionService,
|
||||
);
|
||||
|
||||
$response = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
|
||||
$this->assertSame('/bookings/42/edit', $response->headers->get('Location'));
|
||||
$this->assertSame(['Einige Änderungen wurden verworfen, da sie aktuell nicht mehr änderbar sind.'], $request->getSession()->getFlashBag()->get('info'));
|
||||
$this->assertSame(['Buchung erfolgreich aktualisiert'], $request->getSession()->getFlashBag()->get('success'));
|
||||
$this->assertSame(BookingEditSubmissionResult::STATUS_SUCCESS, $result->status);
|
||||
$this->assertTrue($result->immutableChangesReverted);
|
||||
}
|
||||
|
||||
public function testHandleSubmissionReturnsRedirectOnTimeout(): void
|
||||
public function testHandleSubmissionReturnsResultOnTimeout(): void
|
||||
{
|
||||
$this->assertExceptionFlash(new TimeoutException('slow'), 'Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut.');
|
||||
$this->assertExceptionResult(new TimeoutException('slow'), BookingEditSubmissionResult::STATUS_TIMEOUT);
|
||||
}
|
||||
|
||||
public function testHandleSubmissionReturnsRedirectOnApiClientException(): void
|
||||
public function testHandleSubmissionReturnsResultOnApiClientException(): void
|
||||
{
|
||||
$this->assertExceptionFlash(new ApiClientException('boom'), 'Es ist ein Fehler in der Kommunikation mit dem Buchungssystem aufgetreten');
|
||||
$this->assertExceptionResult(new ApiClientException('boom'), BookingEditSubmissionResult::STATUS_API_CLIENT_ERROR);
|
||||
}
|
||||
|
||||
public static function updateFailureProvider(): array
|
||||
{
|
||||
return [
|
||||
'timeout' => [new TimeoutException('slow'), 'error', 'Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut.'],
|
||||
'api-client' => [new ApiClientException('boom'), 'error', 'Es ist ein Fehler in der Kommunikation mit dem Buchungssystem aufgetreten'],
|
||||
'timeout' => [new TimeoutException('slow'), BookingEditSubmissionResult::STATUS_TIMEOUT],
|
||||
'api-client' => [new ApiClientException('boom'), BookingEditSubmissionResult::STATUS_API_CLIENT_ERROR],
|
||||
];
|
||||
}
|
||||
|
||||
private function assertNotificationFlash(Notification $notification, string $expectedType): void
|
||||
private function assertNotificationResult(Notification $notification, string $expectedType): void
|
||||
{
|
||||
$request = $this->createRequestWithSession();
|
||||
$user = $this->createUser();
|
||||
@@ -366,13 +363,14 @@ class BookingEditSubmitterTest extends TestCase
|
||||
submitGuard: $submitGuard,
|
||||
);
|
||||
|
||||
$response = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
|
||||
$this->assertSame('/bookings/42/edit', $response->headers->get('Location'));
|
||||
$this->assertSame([$notification->message], $request->getSession()->getFlashBag()->get($expectedType));
|
||||
$this->assertSame($this->resolveExpectedStatus($expectedType), $result->status);
|
||||
$this->assertSame($notification->message, $result->message);
|
||||
$this->assertFalse($result->immutableChangesReverted);
|
||||
}
|
||||
|
||||
private function assertExceptionFlash(\Throwable $exception, string $expectedMessage): void
|
||||
private function assertExceptionResult(\Throwable $exception, string $expectedStatus): void
|
||||
{
|
||||
$request = $this->createRequestWithSession();
|
||||
$user = $this->createUser();
|
||||
@@ -415,10 +413,11 @@ class BookingEditSubmitterTest extends TestCase
|
||||
submitGuard: $submitGuard,
|
||||
);
|
||||
|
||||
$response = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
$result = $service->handleSubmission($request, $bookingDto, 42, $user);
|
||||
|
||||
$this->assertSame('/bookings/42/edit', $response->headers->get('Location'));
|
||||
$this->assertSame([$expectedMessage], $request->getSession()->getFlashBag()->get('error'));
|
||||
$this->assertSame($expectedStatus, $result->status);
|
||||
$this->assertNull($result->message);
|
||||
$this->assertFalse($result->immutableChangesReverted);
|
||||
}
|
||||
|
||||
private function createService(
|
||||
@@ -436,7 +435,6 @@ class BookingEditSubmitterTest extends TestCase
|
||||
$travelDataService ?? $this->createMock(TravelDataProvider::class),
|
||||
$submitGuard ?? $this->createMock(BookingEditSubmitGuard::class),
|
||||
$bookingSessionService ?? $this->createMock(BookingSessionManager::class),
|
||||
$this->createUrlGenerator(),
|
||||
$this->createMock(LoggerInterface::class),
|
||||
);
|
||||
}
|
||||
@@ -470,13 +468,13 @@ class BookingEditSubmitterTest extends TestCase
|
||||
return $request;
|
||||
}
|
||||
|
||||
private function createUrlGenerator(): UrlGeneratorInterface
|
||||
private function resolveExpectedStatus(string $expectedType): string
|
||||
{
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$urlGenerator->method('generate')
|
||||
->with('app_booking_edit', ['id' => 42])
|
||||
->willReturn('/bookings/42/edit');
|
||||
|
||||
return $urlGenerator;
|
||||
return match ($expectedType) {
|
||||
'error' => BookingEditSubmissionResult::STATUS_NOTIFICATION_ERROR,
|
||||
'info' => BookingEditSubmissionResult::STATUS_NOTIFICATION_INFO,
|
||||
default => $expectedType,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user