diff --git a/src/Controller/Booking/Create/Step1Controller.php b/src/Controller/Booking/Create/Step1Controller.php index 2072a05..b0ab360 100644 --- a/src/Controller/Booking/Create/Step1Controller.php +++ b/src/Controller/Booking/Create/Step1Controller.php @@ -9,10 +9,9 @@ use App\Controller\Booking\Traits\BookingExceptionHandlerTrait; use App\Form\BookingCreateStep1Type; use App\Form\Model\BookingDto; use App\Htmx\HxTrait; +use App\Service\BookingCreateContextFactory; use App\Service\BookingService; -use App\Service\BookingRoomSelectionService; use App\Service\BookingSessionService; -use App\Service\BookingSummaryDataService; use App\Service\RoomPricingCalculator; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -33,9 +32,8 @@ class Step1Controller extends AbstractController public function __construct( private readonly BookingService $bookingService, - private readonly BookingRoomSelectionService $roomSelectionService, private readonly BookingSessionService $bookingSessionService, - private readonly BookingSummaryDataService $summaryDataService, + private readonly BookingCreateContextFactory $createContextFactory, ) { } @@ -82,17 +80,14 @@ class Step1Controller extends AbstractController return $this->redirectToRoute('app_booking_create_step_2'); } - // Get complete summary data (pricing, rooms, CMS data) - $summaryData = $this->summaryDataService->getSummaryData($bookingCreateDto, RoomPricingCalculator::PRICING_MODE_SELECTION); - - $availableRooms = $bookingCreateDto->travel->getAvailableRooms(); - $groupedRooms = $this->roomSelectionService->groupRoomsBySelectionType($availableRooms); + $context = $this->createContextFactory->create( + $bookingCreateDto, + RoomPricingCalculator::PRICING_MODE_SELECTION + ); return $this->render('booking/create/step_1.html.twig', [ - 'bookingCreateDto' => $bookingCreateDto, - 'summaryData' => $summaryData, + 'bookingCreateContext' => $context, 'form' => $form->createView(), - 'groupedRooms' => $groupedRooms, ]); } @@ -103,7 +98,7 @@ 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->bookingService, $request); + $result = $this->getOrCreateBookingCreateDtoForHtmx($this->bookingSessionService, $request); if ($result instanceof Response) { return $result; } @@ -115,10 +110,10 @@ class Step1Controller extends AbstractController ]); $form->handleRequest($request); - // Get complete summary data (pricing, rooms, CMS data) - $summaryData = $this->summaryDataService->getSummaryData($bookingCreateDto, RoomPricingCalculator::PRICING_MODE_SELECTION); - $availableRooms = $bookingCreateDto->travel->getAvailableRooms(); - $groupedRooms = $this->roomSelectionService->groupRoomsBySelectionType($availableRooms); + $context = $this->createContextFactory->create( + $bookingCreateDto, + RoomPricingCalculator::PRICING_MODE_SELECTION + ); // The DTO is now updated with the latest selection. // We can now render the blocks with the fresh data. @@ -127,9 +122,7 @@ class Step1Controller extends AbstractController ['room_selection_form', 'booking_summary'], [ 'form' => $form->createView(), - 'bookingCreateDto' => $bookingCreateDto, - 'summaryData' => $summaryData, - 'groupedRooms' => $groupedRooms, + 'bookingCreateContext' => $context, ] ); } diff --git a/src/Controller/Booking/Create/Step2Controller.php b/src/Controller/Booking/Create/Step2Controller.php index 4e07dfa..ad82db8 100644 --- a/src/Controller/Booking/Create/Step2Controller.php +++ b/src/Controller/Booking/Create/Step2Controller.php @@ -9,11 +9,10 @@ use App\Controller\Booking\Traits\BookingCreateTrait; use App\Controller\Booking\Traits\BookingExceptionHandlerTrait; use App\Form\BookingCreateStep2Type; use App\Form\Model\BookingDto; +use App\Service\BookingCreateContextFactory; use App\Service\BookingService; use App\Service\BookingParticipantCountService; use App\Service\BookingSessionService; -use App\Service\BookingSummaryDataService; -use App\Service\ParticipantCardDataService; use App\Service\ParticipantPrepopulationService; use App\Service\RoomAssignmentService; use App\Service\TravelDataService; @@ -37,10 +36,9 @@ class Step2Controller extends AbstractController private readonly BookingService $bookingService, private readonly BookingParticipantCountService $participantCountService, private readonly BookingSessionService $bookingSessionService, - private readonly BookingSummaryDataService $summaryDataService, + private readonly BookingCreateContextFactory $createContextFactory, private readonly TravelDataService $travelDataService, private readonly RoomAssignmentService $roomAssignmentService, - private readonly ParticipantCardDataService $participantCardService, private readonly ParticipantPrepopulationService $prepopulationService, ) { } @@ -106,18 +104,11 @@ class Step2Controller extends AbstractController return $this->redirectToRoute('app_booking_create_step_3'); } - // Always generate card data with validation state to show completeness - $cardsData = $this->participantCardService->getAllCardsDataWithValidation($bookingCreateDto); - - // Get complete summary data (pricing, rooms, CMS data) - $summaryData = $this->summaryDataService->getSummaryData($bookingCreateDto); + $context = $this->createContextFactory->createWithParticipantCards($bookingCreateDto, $form->isSubmitted()); $templateData = [ 'form' => $form->createView(), - 'bookingDto' => $bookingCreateDto, - 'cardsData' => $cardsData, - 'summaryData' => $summaryData, - 'isSubmitted' => $form->isSubmitted(), + 'bookingCreateContext' => $context, ]; return $this->render('booking/create/step_2.html.twig', $templateData); diff --git a/src/Controller/Booking/Create/Step2ParticipantController.php b/src/Controller/Booking/Create/Step2ParticipantController.php index 10c26f5..7d67f90 100644 --- a/src/Controller/Booking/Create/Step2ParticipantController.php +++ b/src/Controller/Booking/Create/Step2ParticipantController.php @@ -7,12 +7,13 @@ namespace App\Controller\Booking\Create; use App\Form\BookingParticipantType; use App\Form\Model\BookingDto; use App\Htmx\HxTrait; +use App\Service\BookingCreateContextFactory; use App\Service\BookingService; use App\Service\BookingSessionService; -use App\Service\BookingSummaryDataService; use App\Service\ParticipantFormSupportService; use App\Service\ParticipantPrepopulationService; use App\Service\TravelDataService; +use App\Service\RoomPricingCalculator; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; @@ -29,7 +30,7 @@ class Step2ParticipantController extends AbstractController public function __construct( private readonly BookingService $bookingService, private readonly BookingSessionService $bookingSessionService, - private readonly BookingSummaryDataService $summaryDataService, + private readonly BookingCreateContextFactory $createContextFactory, private readonly TravelDataService $travelDataService, private readonly ParticipantPrepopulationService $prepopulationService, private readonly ParticipantFormSupportService $participantFormSupportService, @@ -128,11 +129,15 @@ class Step2ParticipantController extends AbstractController int $index, BookingDto $bookingDto, ): Response { + $bookingCreateContext = $this->createContextFactory->create( + $bookingDto, + RoomPricingCalculator::PRICING_MODE_SELECTION + ); + return $this->render('booking/create/step_2_participant.html.twig', [ 'form' => $form->createView(), 'participantIndex' => $index, - 'bookingDto' => $bookingDto, - 'summaryData' => $this->summaryDataService->getSummaryData($bookingDto), + 'bookingCreateContext' => $bookingCreateContext, 'refreshRouteName' => 'app_booking_create_step_2_participant_refresh', ]); } @@ -190,7 +195,10 @@ class Step2ParticipantController extends AbstractController $this->bookingSessionService->saveBookingDto($request, $bookingDto, $bookingDto->getMode()); - $summaryData = $this->summaryDataService->getSummaryData($bookingDto); + $bookingCreateContext = $this->createContextFactory->create( + $bookingDto, + RoomPricingCalculator::PRICING_MODE_SELECTION + ); $response = $this->htmxOobResponse( 'booking/_participant_form.html.twig', @@ -198,8 +206,7 @@ class Step2ParticipantController extends AbstractController [ 'form' => $form->createView(), 'participantIndex' => $index, - 'bookingDto' => $bookingDto, - 'summaryData' => $summaryData, + 'bookingCreateContext' => $bookingCreateContext, 'refreshRouteName' => $refreshRouteName, ] ); diff --git a/src/Controller/Booking/Create/Step3Controller.php b/src/Controller/Booking/Create/Step3Controller.php index 5bf2f56..025924e 100644 --- a/src/Controller/Booking/Create/Step3Controller.php +++ b/src/Controller/Booking/Create/Step3Controller.php @@ -13,11 +13,12 @@ use App\Controller\Booking\Traits\BookingExceptionHandlerTrait; use App\Form\BookingCreateStep3Type; use App\Form\Model\BookingDto; use App\Htmx\HxTrait; +use App\Service\BookingCreateContextFactory; use App\Service\BookingPriceCalculatorService; use App\Service\BookingPriceMismatchDiagnosticsService; use App\Service\BookingService; use App\Service\BookingSessionService; -use App\Service\BookingSummaryDataService; +use App\Service\RoomPricingCalculator; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\FormInterface; @@ -37,7 +38,7 @@ class Step3Controller extends AbstractController public function __construct( private readonly BookingService $bookingService, private readonly BookingSessionService $bookingSessionService, - private readonly BookingSummaryDataService $summaryDataService, + private readonly BookingCreateContextFactory $createContextFactory, private readonly BookingPriceCalculatorService $priceCalculator, private readonly BookingPriceMismatchDiagnosticsService $priceMismatchDiagnostics, private readonly ApiClient $apiClient, @@ -216,13 +217,14 @@ class Step3Controller extends AbstractController */ private function renderStepForm(BookingDto $bookingCreateDto, FormInterface $form): Response { - // Get complete summary data (pricing, rooms, CMS data) - $summaryData = $this->summaryDataService->getSummaryData($bookingCreateDto); + $context = $this->createContextFactory->create( + $bookingCreateDto, + RoomPricingCalculator::PRICING_MODE_ASSIGNMENT + ); return $this->render('booking/create/step_3.html.twig', [ - 'bookingCreateDto' => $bookingCreateDto, + 'bookingCreateContext' => $context, 'form' => $form->createView(), - 'summaryData' => $summaryData, ]); } diff --git a/src/Controller/Booking/Create/Step4Controller.php b/src/Controller/Booking/Create/Step4Controller.php index 4eead07..a1257a5 100644 --- a/src/Controller/Booking/Create/Step4Controller.php +++ b/src/Controller/Booking/Create/Step4Controller.php @@ -14,10 +14,10 @@ use App\Form\BookingCreateStep4Type; use App\Form\Model\BookingDto; use App\Htmx\HxTrait; use App\Entity\User; +use App\Service\BookingCreateContextFactory; use App\Service\BookingPriceCalculatorService; use App\Service\BookingService; use App\Service\BookingSessionService; -use App\Service\BookingSummaryDataService; use App\Service\Newsletter\MailjetNewsletterService; use App\Service\Newsletter\NewsletterDoubleOptInService; use Psr\Log\LoggerInterface; @@ -40,7 +40,7 @@ class Step4Controller extends AbstractController public function __construct( private readonly BookingService $bookingService, private readonly BookingSessionService $bookingSessionService, - private readonly BookingSummaryDataService $summaryDataService, + private readonly BookingCreateContextFactory $createContextFactory, private readonly BookingPriceCalculatorService $priceCalculator, private readonly ApiClient $apiClient, private readonly CacheInterface $cache, @@ -134,9 +134,9 @@ class Step4Controller extends AbstractController } // Success: Store booking data in flash for conversion tracking - $summaryData = $this->summaryDataService->getSummaryData($bookingCreateDto); + $bookingCreateContext = $this->createContextFactory->createWithParticipantPrices($bookingCreateDto); $this->addFlash('booking_number', $bookingResponse->bookingNumber); - $this->addFlash('booking_total', $summaryData->payableAmount); + $this->addFlash('booking_total', $bookingCreateContext->summaryData->payableAmount); $this->addFlash('booking_travel_name', $bookingCreateDto->travel->label); $this->clearTravelDataCache($bookingCreateDto); $this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_CREATE); @@ -182,14 +182,11 @@ class Step4Controller extends AbstractController ?string $newsletterTargetEmail, ): Response { - // Get complete summary data (pricing, rooms, CMS data) - $summaryData = $this->summaryDataService->getSummaryData($bookingCreateDto); + $bookingCreateContext = $this->createContextFactory->createWithParticipantPrices($bookingCreateDto); return $this->render('booking/create/step_4.html.twig', [ - 'bookingCreateDto' => $bookingCreateDto, + 'bookingCreateContext' => $bookingCreateContext, 'form' => $form->createView(), - 'summaryData' => $summaryData, - 'participantPrices' => $this->priceCalculator->calculateAllParticipantIndividualPrices($bookingCreateDto), 'newsletterOptInVisible' => $newsletterOptInVisible, 'newsletterTargetEmail' => $newsletterTargetEmail, ]); diff --git a/src/Controller/Booking/Edit/IndexController.php b/src/Controller/Booking/Edit/IndexController.php index 48f1dd9..5b846ab 100644 --- a/src/Controller/Booking/Edit/IndexController.php +++ b/src/Controller/Booking/Edit/IndexController.php @@ -4,9 +4,6 @@ declare(strict_types=1); namespace App\Controller\Booking\Edit; -use App\BusProNet\ApiClient; -use App\BusProNet\Exception\ApiClientException; -use App\BusProNet\Exception\TimeoutException; use App\BusProNet\Model\Notification; use App\Controller\Booking\Traits\BookingExceptionHandlerTrait; use App\Entity\User; @@ -16,13 +13,10 @@ use App\Form\Model\BookingDto; use App\Htmx\HxTrait; use App\Service\BookingEditDataLoaderService; use App\Service\BookingEditDraftService; +use App\Service\BookingEditContextFactory; use App\Service\BookingFingerprintService; -use App\Service\BookingEditSubmitGuardService; +use App\Service\BookingEditSubmitService; use App\Service\BookingSessionService; -use App\Service\BookingSummaryDataService; -use App\Service\ParticipantCardDataService; -use App\Service\TravelDataService; -use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -36,7 +30,7 @@ use Symfony\Component\Security\Http\Attribute\IsGranted; * - Card overview with lazy-loaded individual participant forms * - Handles canceled participants (status 'S') * - Applies mutability constraints via EditFieldStateProvider - * - Final submission calls ApiClient::updateBooking() + * - Final submission is delegated to BookingEditSubmitService */ class IndexController extends AbstractController { @@ -44,16 +38,12 @@ class IndexController extends AbstractController use HxTrait; public function __construct( - private readonly ApiClient $apiClient, private readonly BookingEditDataLoaderService $dataLoader, private readonly BookingEditDraftService $draftService, - private readonly TravelDataService $travelDataService, private readonly BookingSessionService $bookingSessionService, - private readonly BookingEditSubmitGuardService $submitGuard, private readonly BookingFingerprintService $fingerprintService, - private readonly BookingSummaryDataService $summaryDataService, - private readonly ParticipantCardDataService $participantCardService, - private readonly LoggerInterface $logger, + private readonly BookingEditContextFactory $editContextFactory, + private readonly BookingEditSubmitService $submitService, ) { } @@ -114,34 +104,26 @@ class IndexController extends AbstractController return $this->redirectToRoute('app_bookings'); } - // Fetch mutable data for form constraints - $mutableData = $this->travelDataService->getMutabilityData($bookingData->dateId); - // Create validation form (same pattern as CreateStep2Controller) $form = $this->createForm(BookingEditType::class, $bookingDto); $form->handleRequest($request); // Handle form submission (clicking "Buchung aktualisieren") if ($form->isSubmitted() && $form->isValid()) { - return $this->handleFormSubmission($request, $bookingDto, $id, $user); + return $this->submitService->handleSubmission($request, $bookingDto, $id, $user); } - // Always generate card data with validation state to show completeness - $cardsData = $this->participantCardService->getAllCardsDataWithValidation($bookingDto); - - // Get complete summary data (pricing, rooms, CMS data) - $summaryData = $this->summaryDataService->getSummaryData($bookingDto); + $bookingEditContext = $this->editContextFactory->createOverviewContext( + $bookingDto, + $bookingData, + $this->fingerprintService->isDirty($bookingDto), + $form->isSubmitted(), + $form->isSubmitted() && false === $form->isValid(), + ); $templateData = [ 'form' => $form->createView(), - 'bookingDto' => $bookingDto, - 'bookingData' => $bookingData, - 'mutableData' => $mutableData, - 'cardsData' => $cardsData, - 'summaryData' => $summaryData, - 'isDirty' => $this->fingerprintService->isDirty($bookingDto), - 'isSubmitted' => $form->isSubmitted(), - 'hasValidationErrors' => $form->isSubmitted() && false === $form->isValid(), + 'bookingEditContext' => $bookingEditContext, ]; return $this->render('booking/edit/index.html.twig', $templateData); @@ -208,97 +190,4 @@ class IndexController extends AbstractController return $this->redirectToRoute('app_bookings'); } - - /** - * Handles form submission for booking update. - */ - private function handleFormSubmission(Request $request, BookingDto $bookingDto, int $id, User $user): Response - { - $email = $user->getEmail(); - - $this->logger->info('Initiated booking update', [ - 'email' => $email, - 'booking_id' => $id, - ]); - - $this->dataLoader->invalidateBookingCache($id, $user); - $freshBookingData = $this->dataLoader->fetchBookingData($id, $user); - if (null === $freshBookingData || $freshBookingData instanceof Notification) { - $this->addFlash('error', 'Buchungsdaten konnten vor dem Speichern nicht neu geladen werden'); - $this->logger->warning('Failed to refresh booking before update submission', [ - 'email' => $email, - 'booking_id' => $id, - 'has_notification' => $freshBookingData instanceof Notification, - ]); - - return $this->redirectToRoute('app_booking_edit', ['id' => $id]); - } - - $bookingDto->booking = $freshBookingData; - - $mutableData = $this->travelDataService->getMutabilityData( - $freshBookingData->dateId, - forceRefresh: true - ); - if (null !== $mutableData) { - $this->travelDataService->patchMutability($bookingDto->travel, $mutableData); - } - - $immutableChangesReverted = $this->submitGuard->reconcileImmutableCategories($bookingDto, $freshBookingData); - if (true === $immutableChangesReverted) { - $this->bookingSessionService->saveBookingDto($request, $bookingDto, BookingDto::MODE_EDIT); - $this->addFlash('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('error', $response->message); - } else { - $this->addFlash('info', $response->message); - } - $this->logger->error('Booking update not successful', [ - 'email' => $email, - 'booking_id' => $id, - 'message' => $response->message, - ]); - } elseif (true === $response->success) { - // Invalidate cache and clear session on success - $this->dataLoader->invalidateBookingCache($id, $user); - $this->bookingSessionService->clearBookingDto($request, BookingDto::MODE_EDIT); - - // Delete draft on successful submission - $this->draftService->deleteDraft($user, $id); - - $this->addFlash('success', 'Buchung erfolgreich aktualisiert'); - $this->logger->info('Booking update successful', [ - 'email' => $email, - 'booking_id' => $id, - ]); - - return $this->redirectToRoute('app_booking_edit', ['id' => $id]); - } else { - // BookingUpdate received but success is false - $this->addFlash('error', $response->status ?? 'Buchung konnte nicht aktualisiert werden'); - $this->logger->warning('Booking update returned unsuccessful status', [ - 'email' => $email, - 'booking_id' => $id, - 'status' => $response->status, - 'valid' => $response->valid, - ]); - } - } catch (TimeoutException $e) { - $this->addFlash('error', 'Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut.'); - $this->logger->error('Booking update timeout', [ - 'email' => $email, - 'booking_id' => $id, - 'exception' => $e->getMessage(), - ]); - } catch (ApiClientException) { - $this->addFlash('error', 'Es ist ein Fehler in der Kommunikation mit dem Buchungssystem aufgetreten'); - } - - return $this->redirectToRoute('app_booking_edit', ['id' => $id]); - } } diff --git a/src/Controller/Booking/Edit/ParticipantController.php b/src/Controller/Booking/Edit/ParticipantController.php index fd899d3..1d020c2 100644 --- a/src/Controller/Booking/Edit/ParticipantController.php +++ b/src/Controller/Booking/Edit/ParticipantController.php @@ -11,8 +11,8 @@ use App\Form\BookingParticipantType; use App\Form\Model\BookingDto; use App\Htmx\HxTrait; use App\Service\BookingEditDataLoaderService; +use App\Service\BookingEditContextFactory; use App\Service\BookingEditDraftService; -use App\Service\BookingEditParticipantContextFactory; use App\Service\BookingSessionService; use App\Service\ParticipantFormSupportService; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -31,7 +31,7 @@ class ParticipantController extends AbstractController public function __construct( private readonly BookingEditDataLoaderService $dataLoader, private readonly BookingEditDraftService $draftService, - private readonly BookingEditParticipantContextFactory $participantContextFactory, + private readonly BookingEditContextFactory $editContextFactory, private readonly BookingSessionService $bookingSessionService, private readonly ParticipantFormSupportService $participantFormSupportService, ) { @@ -74,7 +74,7 @@ class ParticipantController extends AbstractController return $this->redirectToRoute('app_booking_edit', ['id' => $id]); } - $this->participantContextFactory->prepareBookingDto($bookingDto); + $this->editContextFactory->prepareBookingDto($bookingDto); $wrapper = $this->participantFormSupportService->createParticipantEditDto($bookingDto, $index); @@ -96,15 +96,12 @@ class ParticipantController extends AbstractController return $this->redirectToRoute('app_booking_edit', ['id' => $id]); } - $context = $this->participantContextFactory->create($bookingDto, $bookingData); + $context = $this->editContextFactory->createParticipantContext($bookingDto, $bookingData); - return $this->render('booking/edit/participant.html', [ + return $this->render('booking/edit/participant.html.twig', [ 'form' => $form->createView(), 'participantIndex' => $index, - 'bookingDto' => $context->bookingDto, - 'bookingData' => $context->bookingData, - 'mutableData' => $context->mutableData, - 'summaryData' => $context->summaryData, + 'bookingEditContext' => $context, 'refreshRouteName' => 'app_booking_edit_participant_refresh', 'refreshRouteParams' => ['id' => $id, 'index' => $index], 'cancelRouteName' => 'app_booking_edit', @@ -134,7 +131,7 @@ class ParticipantController extends AbstractController } $this->participantFormSupportService->ensureParticipantExists($bookingDto, $index); - $this->participantContextFactory->prepareBookingDto($bookingDto); + $this->editContextFactory->prepareBookingDto($bookingDto); $bookingData = $this->dataLoader->fetchBookingData($id, $user); @@ -149,20 +146,18 @@ class ParticipantController extends AbstractController $form->handleRequest($request); $notifications = $this->participantFormSupportService->collectAndClearNotifications($bookingDto); - $context = null !== $bookingData && !($bookingData instanceof Notification) - ? $this->participantContextFactory->create($bookingDto, $bookingData) - : null; + $context = $this->editContextFactory->createParticipantContext( + $bookingDto, + $bookingData instanceof Booking ? $bookingData : null + ); $response = $this->htmxOobResponse( 'booking/_participant_form.html.twig', ['participant_form', 'booking_summary'], [ - 'form' => $form, + 'form' => $form->createView(), 'participantIndex' => $index, - 'bookingDto' => $bookingDto, - 'bookingData' => $bookingData, - 'mutableData' => $context?->mutableData, - 'summaryData' => $context?->summaryData, + 'bookingEditContext' => $context, 'refreshRouteName' => 'app_booking_edit_participant_refresh', 'refreshRouteParams' => ['id' => $id, 'index' => $index], 'cancelRouteName' => 'app_booking_edit', diff --git a/src/Controller/Booking/Traits/BookingCreateTrait.php b/src/Controller/Booking/Traits/BookingCreateTrait.php index 5c31058..1d63028 100644 --- a/src/Controller/Booking/Traits/BookingCreateTrait.php +++ b/src/Controller/Booking/Traits/BookingCreateTrait.php @@ -48,23 +48,6 @@ trait BookingCreateTrait return $this->redirectToRoute($route); } - /** - * Prepares template variables for the booking summary sidebar. - * - * @return array Array containing all variables needed for the summary partial - */ - private function getSummaryVariables(BookingDto $bookingCreateDto): array - { - $summary = $this->summaryDataService->getSummaryData($bookingCreateDto); - - return [ - 'participantsCount' => $summary->participantCount, - 'pricingData' => $summary->pricingData, - 'assignmentCounts' => $summary->assignmentCounts, - 'groupedSelectedRooms' => $summary->groupedSelectedRooms, - ]; - } - /** * Handles API errors by logging and adding a flash message. */ diff --git a/src/Form/Model/BookingCreateContext.php b/src/Form/Model/BookingCreateContext.php new file mode 100644 index 0000000..309ac29 --- /dev/null +++ b/src/Form/Model/BookingCreateContext.php @@ -0,0 +1,28 @@ +, by_room: array} $groupedRooms + * @param array|null $cardsData + * @param array|null $participantPrices + */ + public function __construct( + public readonly BookingDto $bookingDto, + public readonly BookingSummaryDto $summaryData, + public readonly array $groupedRooms, + public readonly ?array $cardsData = null, + public readonly bool $isSubmitted = false, + public readonly ?array $participantPrices = null, + ) { + } +} diff --git a/src/Form/Model/BookingEditContext.php b/src/Form/Model/BookingEditContext.php new file mode 100644 index 0000000..15f7130 --- /dev/null +++ b/src/Form/Model/BookingEditContext.php @@ -0,0 +1,29 @@ +|null $cardsData + */ + public function __construct( + public readonly BookingDto $bookingDto, + public readonly ?Booking $bookingData, + public readonly ?BaseData $mutableData, + public readonly BookingSummaryDto $summaryData, + public readonly ?array $cardsData = null, + public readonly bool $isDirty = false, + public readonly bool $isSubmitted = false, + public readonly bool $hasValidationErrors = false, + ) { + } +} diff --git a/src/Form/Model/BookingEditParticipantContext.php b/src/Form/Model/BookingEditParticipantContext.php deleted file mode 100644 index 1f2c824..0000000 --- a/src/Form/Model/BookingEditParticipantContext.php +++ /dev/null @@ -1,22 +0,0 @@ -buildBaseContext($bookingDto, $pricingMode); + + return new BookingCreateContext( + bookingDto: $bookingDto, + summaryData: $baseContext['summaryData'], + groupedRooms: $baseContext['groupedRooms'], + ); + } + + public function createWithParticipantCards(BookingDto $bookingDto, bool $isSubmitted): BookingCreateContext + { + $baseContext = $this->buildBaseContext($bookingDto, RoomPricingCalculator::PRICING_MODE_SELECTION); + + return new BookingCreateContext( + bookingDto: $bookingDto, + summaryData: $baseContext['summaryData'], + groupedRooms: $baseContext['groupedRooms'], + cardsData: $this->participantCardDataService->getAllCardsDataWithValidation($bookingDto), + isSubmitted: $isSubmitted, + ); + } + + public function createWithParticipantPrices(BookingDto $bookingDto): BookingCreateContext + { + $baseContext = $this->buildBaseContext($bookingDto, RoomPricingCalculator::PRICING_MODE_ASSIGNMENT); + + return new BookingCreateContext( + bookingDto: $bookingDto, + summaryData: $baseContext['summaryData'], + groupedRooms: $baseContext['groupedRooms'], + participantPrices: $this->priceCalculator->calculateAllParticipantIndividualPrices($bookingDto), + ); + } + + /** + * @return array{summaryData: \App\Form\Model\BookingSummaryDto, groupedRooms: array{by_pax: array, by_room: array}} + */ + private function buildBaseContext(BookingDto $bookingDto, string $pricingMode): array + { + return [ + 'summaryData' => $this->summaryDataService->getSummaryData($bookingDto, $pricingMode), + 'groupedRooms' => $this->roomSelectionService->groupRoomsBySelectionType( + $bookingDto->travel->getAvailableRooms() + ), + ]; + } +} diff --git a/src/Service/BookingEditContextFactory.php b/src/Service/BookingEditContextFactory.php new file mode 100644 index 0000000..52eb529 --- /dev/null +++ b/src/Service/BookingEditContextFactory.php @@ -0,0 +1,63 @@ +travelDataService->enrichWithFreshAvailabilities($bookingDto->travel); + } + + private function createSummaryData(BookingDto $bookingDto): BookingSummaryDto + { + // Shared by the overview and participant contexts, including refresh fallback. + return $this->summaryDataService->getSummaryData($bookingDto); + } + + public function createParticipantContext(BookingDto $bookingDto, ?Booking $bookingData): BookingEditContext + { + return new BookingEditContext( + bookingDto: $bookingDto, + bookingData: $bookingData, + mutableData: $bookingData ? $this->travelDataService->getMutabilityData($bookingData->dateId) : null, + summaryData: $this->createSummaryData($bookingDto), + ); + } + + public function createOverviewContext( + BookingDto $bookingDto, + Booking $bookingData, + bool $isDirty, + bool $isSubmitted, + bool $hasValidationErrors, + ): BookingEditContext { + return new BookingEditContext( + bookingDto: $bookingDto, + bookingData: $bookingData, + mutableData: $this->travelDataService->getMutabilityData($bookingData->dateId), + summaryData: $this->createSummaryData($bookingDto), + cardsData: $this->participantCardDataService->getAllCardsDataWithValidation($bookingDto), + isDirty: $isDirty, + isSubmitted: $isSubmitted, + hasValidationErrors: $hasValidationErrors, + ); + } +} diff --git a/src/Service/BookingEditParticipantContextFactory.php b/src/Service/BookingEditParticipantContextFactory.php deleted file mode 100644 index a9114c0..0000000 --- a/src/Service/BookingEditParticipantContextFactory.php +++ /dev/null @@ -1,36 +0,0 @@ -travelDataService->enrichWithFreshAvailabilities($bookingDto->travel); - } - - public function create(BookingDto $bookingDto, Booking $bookingData): BookingEditParticipantContext - { - return new BookingEditParticipantContext( - bookingDto: $bookingDto, - bookingData: $bookingData, - mutableData: $this->travelDataService->getMutabilityData($bookingData->dateId), - summaryData: $this->summaryDataService->getSummaryData($bookingDto), - ); - } -} diff --git a/src/Service/BookingEditSubmitService.php b/src/Service/BookingEditSubmitService.php new file mode 100644 index 0000000..be7684f --- /dev/null +++ b/src/Service/BookingEditSubmitService.php @@ -0,0 +1,129 @@ +getEmail(); + + $this->logger->info('Initiated booking update', [ + 'email' => $email, + 'booking_id' => $bookingId, + ]); + + $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); + } + + $bookingDto->booking = $freshBookingData; + + $mutableData = $this->travelDataService->getMutabilityData( + $freshBookingData->dateId, + forceRefresh: true + ); + if (null !== $mutableData) { + $this->travelDataService->patchMutability($bookingDto->travel, $mutableData); + } + + $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) { + $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'); + $this->logger->warning('Booking update returned unsuccessful status', [ + 'email' => $email, + 'booking_id' => $bookingId, + 'status' => $response->status, + 'valid' => $response->valid, + ]); + } + } 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, + ]); + } catch (ApiClientException) { + $this->addFlash($request, 'error', 'Es ist ein Fehler in der Kommunikation mit dem Buchungssystem aufgetreten'); + } + + return $this->redirectToEdit($bookingId); + } + + private function addFlash(Request $request, string $type, string $message): void + { + $request->getSession()->getFlashBag()->add($type, $message); + } + + private function redirectToEdit(int $bookingId): RedirectResponse + { + return new RedirectResponse($this->urlGenerator->generate('app_booking_edit', ['id' => $bookingId])); + } +} diff --git a/templates/booking/_participant_form.html.twig b/templates/booking/_participant_form.html.twig index b2a7e2d..a9e71b0 100644 --- a/templates/booking/_participant_form.html.twig +++ b/templates/booking/_participant_form.html.twig @@ -140,6 +140,12 @@ {% endmacro %} +{# Shared by booking create and edit flows; callers should pass one of the flow contexts. #} +{% set bookingFlowContext = bookingEditContext|default(bookingCreateContext|default(null)) %} +{% set bookingDto = bookingFlowContext.bookingDto %} +{% set summaryData = bookingFlowContext.summaryData %} +{% set mutableData = bookingFlowContext.mutableData|default(null) %} + {# Standalone participant form view (replaces main content area) #} {% block participant_form %} {% form_theme form 'booking/_form_theme.html.twig' %} @@ -684,7 +690,7 @@ {% block booking_summary %}
{% include 'booking/_summary.html.twig' with { - 'bookingCreateDto': bookingDto, + 'bookingDto': bookingDto, 'summaryData': summaryData, 'mutableData': mutableData|default(null) } %} diff --git a/templates/booking/_summary.html.twig b/templates/booking/_summary.html.twig index 270cd63..f9c48f4 100644 --- a/templates/booking/_summary.html.twig +++ b/templates/booking/_summary.html.twig @@ -29,7 +29,7 @@ Reise - {{ bookingCreateDto.travel.label }} + {{ bookingDto.travel.label }} @@ -37,7 +37,7 @@ Zeitraum - {{ bookingCreateDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo|date('d.m.Y') }} + {{ bookingDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingDto.travel.dateTo|date('d.m.Y') }} @@ -45,7 +45,7 @@ Unterkunft - {{ bookingCreateDto.travel.hotel.name }} + {{ bookingDto.travel.hotel.name }} @@ -56,13 +56,13 @@ {{ summaryData.participantCount }} - {% if bookingCreateDto.bookingStatus != 'F' %} + {% if bookingDto.bookingStatus != 'F' %} Status - {{ bookingCreateDto.bookingStatus|map_status }} + {{ bookingDto.bookingStatus|map_status }} {% endif %} @@ -72,7 +72,7 @@ {% include 'booking/_summary_hotel.html.twig' %} {# Mutability information (edit mode only) #} - {% if bookingCreateDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') and mutableData %} + {% if bookingDto.mode == constant('App\\Form\\Model\\BookingDto::MODE_EDIT') and mutableData %}
Aktualisierung möglich bis @@ -201,7 +201,7 @@ {# Total Section #} {% if summaryData.totalPrice > 0 %} {# Show subtotal and voucher discounts when vouchers are applied #} - {% set acceptedVouchers = bookingCreateDto.getAcceptedVouchers() %} + {% set acceptedVouchers = bookingDto.getAcceptedVouchers() %} {% if acceptedVouchers and acceptedVouchers.hasVouchers() %}
diff --git a/templates/booking/create/step_1.html.twig b/templates/booking/create/step_1.html.twig index 11048c4..31ca0bb 100644 --- a/templates/booking/create/step_1.html.twig +++ b/templates/booking/create/step_1.html.twig @@ -43,8 +43,8 @@ {{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }} {% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}> {% include 'booking/_summary.html.twig' with { - 'bookingCreateDto': bookingCreateDto, - 'summaryData': summaryData + 'bookingDto': bookingCreateContext.bookingDto, + 'summaryData': bookingCreateContext.summaryData } %}
{% endblock %} @@ -65,19 +65,19 @@ {% block room_selection_form %}
{{ form_errors(form) }} - {% if groupedRooms.by_room is not empty %} + {% if bookingCreateContext.groupedRooms.by_room is not empty %}

Zimmer

- {% for roomId, room in groupedRooms.by_room %} + {% for roomId, room in bookingCreateContext.groupedRooms.by_room %} {{ _self.stepFormField(form.roomSelections[roomId]) }} {% endfor %} {% endif %} - {% if groupedRooms.by_pax is not empty %} + {% if bookingCreateContext.groupedRooms.by_pax is not empty %}

Betten

- {% for roomId, room in groupedRooms.by_pax %} + {% for roomId, room in bookingCreateContext.groupedRooms.by_pax %} {{ _self.stepFormField(form.roomSelections[roomId]) }} {% endfor %} {% endif %} diff --git a/templates/booking/create/step_2.html.twig b/templates/booking/create/step_2.html.twig index f993534..732f5e1 100644 --- a/templates/booking/create/step_2.html.twig +++ b/templates/booking/create/step_2.html.twig @@ -18,8 +18,8 @@ {{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }} {% if htmx_oob_swap|default(false) %} hx-swap-oob="true"{% endif %}> {% include 'booking/_summary.html.twig' with { - 'bookingCreateDto': bookingDto, - 'summaryData': summaryData + 'bookingDto': bookingCreateContext.bookingDto, + 'summaryData': bookingCreateContext.summaryData } %}
{% endblock %} @@ -45,13 +45,13 @@ {% endif %}
- {% for cardData in cardsData %} + {% for cardData in bookingCreateContext.cardsData %} {% include 'booking/_participant_card.html.twig' with { 'cardData': cardData, 'index': loop.index0, 'participantNumber': loop.index, 'mode': 'create', - 'isSubmitted': isSubmitted + 'isSubmitted': bookingCreateContext.isSubmitted } %} {% endfor %}
diff --git a/templates/booking/create/step_2_participant.html.twig b/templates/booking/create/step_2_participant.html.twig index db3f3e8..9b202e8 100644 --- a/templates/booking/create/step_2_participant.html.twig +++ b/templates/booking/create/step_2_participant.html.twig @@ -16,8 +16,8 @@ diff --git a/templates/booking/create/step_3.html.twig b/templates/booking/create/step_3.html.twig index 8a5eb82..9cb4f0c 100644 --- a/templates/booking/create/step_3.html.twig +++ b/templates/booking/create/step_3.html.twig @@ -18,8 +18,8 @@ {{ stimulus_controller('toggle', { 'open': false }, { 'closed': 'hidden', 'open': 'absolute inset-0 z-20', 'iconOpen': 'rotate-180' }) }} {% if htmx_oob_swap is defined and htmx_oob_swap %}hx-swap-oob="true"{% endif %}> {% include 'booking/_summary.html.twig' with { - 'bookingCreateDto': bookingCreateDto, - 'summaryData': summaryData + 'bookingDto': bookingCreateContext.bookingDto, + 'summaryData': bookingCreateContext.summaryData } %}
{% endblock %} diff --git a/templates/booking/create/step_4.html.twig b/templates/booking/create/step_4.html.twig index ae1d928..ebf6eaf 100644 --- a/templates/booking/create/step_4.html.twig +++ b/templates/booking/create/step_4.html.twig @@ -20,7 +20,7 @@
Gesamtpreis - {{ summaryData.payableAmount|format_currency('EUR') }} + {{ bookingCreateContext.summaryData.payableAmount|format_currency('EUR') }}
Buchungsübersicht @@ -35,7 +35,7 @@ Reise - {{ bookingCreateDto.travel.label }} + {{ bookingCreateContext.bookingDto.travel.label }} @@ -43,7 +43,7 @@ Zeitraum - {{ bookingCreateDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo|date('d.m.Y') }} + {{ bookingCreateContext.bookingDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateContext.bookingDto.travel.dateTo|date('d.m.Y') }} @@ -51,7 +51,7 @@ Unterkunft - {{ bookingCreateDto.travel.hotel.name }} + {{ bookingCreateContext.bookingDto.travel.hotel.name }} @@ -59,7 +59,7 @@ Teilnehmer - {{ summaryData.participantCount }} + {{ bookingCreateContext.summaryData.participantCount }} @@ -94,7 +94,7 @@ Reise - {{ bookingCreateDto.travel.label }} + {{ bookingCreateContext.bookingDto.travel.label }} @@ -102,7 +102,7 @@ Zeitraum - {{ bookingCreateDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateDto.travel.dateTo|date('d.m.Y') }} + {{ bookingCreateContext.bookingDto.travel.dateFrom|date('d.m.Y') }} - {{ bookingCreateContext.bookingDto.travel.dateTo|date('d.m.Y') }} @@ -110,7 +110,7 @@ Unterkunft - {{ bookingCreateDto.travel.hotel.name }} + {{ bookingCreateContext.bookingDto.travel.hotel.name }} @@ -118,7 +118,7 @@ Teilnehmer - {{ summaryData.participantCount }} + {{ bookingCreateContext.summaryData.participantCount }} @@ -132,18 +132,18 @@ {# Rooms Section #} - {% if summaryData.pricingData.rooms is not empty %} + {% if bookingCreateContext.summaryData.pricingData.rooms is not empty %}
Unterkunft
- {% for roomPricing in summaryData.pricingData.rooms %} + {% for roomPricing in bookingCreateContext.summaryData.pricingData.rooms %} {% for voucher in acceptedVouchers.vouchers %} @@ -498,12 +498,12 @@
Zu zahlen - {{ summaryData.payableAmount|format_currency('EUR') }} + {{ bookingCreateContext.summaryData.payableAmount|format_currency('EUR') }}
{% else %}
Gesamtpreis - {{ summaryData.pricingData.grandTotal|format_currency('EUR') }} + {{ bookingCreateContext.summaryData.pricingData.grandTotal|format_currency('EUR') }}
{% endif %} {% endif %} @@ -516,24 +516,24 @@
{{ roomPricing.quantity }}x {{ roomPricing.label }}
- {% if summaryData.assignmentCounts[roomPricing.roomId] is defined %} -
{{ summaryData.assignmentCounts[roomPricing.roomId] }} Person(en) belegt
+ {% if bookingCreateContext.summaryData.assignmentCounts[roomPricing.roomId] is defined %} +
{{ bookingCreateContext.summaryData.assignmentCounts[roomPricing.roomId] }} Person(en) belegt
{% endif %}
@@ -156,12 +156,12 @@ {% endif %} {# Services Section #} - {% if summaryData.pricingData.services is not empty %} + {% if bookingCreateContext.summaryData.pricingData.services is not empty %}
Leistungen
- {% for serviceGroup in summaryData.pricingData.services %} + {% for serviceGroup in bookingCreateContext.summaryData.pricingData.services %}
{{ serviceGroup.groupName }}
@@ -186,15 +186,15 @@ Teilnehmer - {% for participant in bookingCreateDto.participants %} + {% for participant in bookingCreateContext.bookingDto.participants %}
{{ participant.firstName }} {{ participant.lastName }} - {% if loop.first and not bookingCreateDto.isInternalAgencyBooking() %}(Anmelder:in){% endif %} + {% if loop.first and not bookingCreateContext.bookingDto.isInternalAgencyBooking() %}(Anmelder:in){% endif %} - {% if participantPrices is defined and participantPrices[loop.index0] is defined %} - {{ participantPrices[loop.index0]|format_currency('EUR') }} + {% if bookingCreateContext.participantPrices is defined and bookingCreateContext.participantPrices[loop.index0] is defined %} + {{ bookingCreateContext.participantPrices[loop.index0]|format_currency('EUR') }} {% endif %}
{# Personal Data - responsive grid #} @@ -242,7 +242,7 @@
{# Unterkunft Section #} - {% set assignedRoom = bookingCreateDto.travel.getRoomById(participant.assignedRoomId) %} + {% set assignedRoom = bookingCreateContext.bookingDto.travel.getRoomById(participant.assignedRoomId) %} {% if assignedRoom or participant.remarksRoom %}
Unterkunft @@ -462,8 +462,8 @@ {% endfor %} {# Grand Total #} - {% if summaryData.pricingData.grandTotal is defined and summaryData.pricingData.grandTotal > 0 %} - {% set acceptedVouchers = bookingCreateDto.getAcceptedVouchers() %} + {% if bookingCreateContext.summaryData.pricingData.grandTotal is defined and bookingCreateContext.summaryData.pricingData.grandTotal > 0 %} + {% set acceptedVouchers = bookingCreateContext.bookingDto.getAcceptedVouchers() %} {% if acceptedVouchers and acceptedVouchers.hasVouchers() %}
@@ -475,7 +475,7 @@ Gesamtpreis
- {{ summaryData.pricingData.grandTotal|format_currency('EUR') }} + {{ bookingCreateContext.summaryData.pricingData.grandTotal|format_currency('EUR') }}
- {% if bookingCreateDto.paymentMethod == constant('App\\BusProNet\\Constants::PAYMENT_METHOD_DEBIT') and bookingCreateDto.bankAccount %} + {% if bookingCreateContext.bookingDto.paymentMethod == constant('App\\BusProNet\\Constants::PAYMENT_METHOD_DEBIT') and bookingCreateContext.bookingDto.bankAccount %} {% endif %} @@ -572,7 +572,7 @@ Zurück {% endif %} diff --git a/templates/booking/edit/participant.html.twig b/templates/booking/edit/participant.html.twig index 217b305..f4363ad 100644 --- a/templates/booking/edit/participant.html.twig +++ b/templates/booking/edit/participant.html.twig @@ -12,9 +12,9 @@ @@ -27,13 +27,20 @@
{# Flash messages - must be inside main-content for HTMX swap to display them #} {% include '_partials/_flashes.html.twig' %} - {% include 'booking/_participant_form.html.twig' %} + {% include 'booking/_participant_form.html.twig' with { + 'bookingEditContext': bookingEditContext, + 'participantIndex': participantIndex, + 'refreshRouteName': refreshRouteName, + 'refreshRouteParams': refreshRouteParams|default({}), + 'cancelRouteName': cancelRouteName|default('app_booking_edit'), + 'cancelRouteParams': cancelRouteParams|default({id: bookingEditContext.bookingData.id}) + } %}
- diff --git a/tests/Service/BookingCreateContextFactoryTest.php b/tests/Service/BookingCreateContextFactoryTest.php new file mode 100644 index 0000000..93a3469 --- /dev/null +++ b/tests/Service/BookingCreateContextFactoryTest.php @@ -0,0 +1,215 @@ +dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-06'); + + $roomByRoom = new Room(); + $roomByRoom->id = 10; + $roomByRoom->label = 'Doppelzimmer'; + $roomByRoom->maxPax = 2; + $roomByRoom->available = 4; + $roomByRoom->status = 'Frei'; + + $roomByPax = new Room(); + $roomByPax->id = 11; + $roomByPax->label = '6-Bett Zimmer'; + $roomByPax->maxPax = 6; + $roomByPax->available = 2; + $roomByPax->status = 'Frei'; + + $travel->rooms = [$roomByRoom, $roomByPax]; + + $bookingDto = new BookingDto($travel, 157047); + $summaryData = $this->createMock(BookingSummaryDto::class); + + $summaryDataService = $this->createMock(BookingSummaryDataService::class); + $summaryDataService->expects($this->once()) + ->method('getSummaryData') + ->with( + $bookingDto, + RoomPricingCalculator::PRICING_MODE_SELECTION + ) + ->willReturn($summaryData); + + $roomSelectionService = $this->createMock(BookingRoomSelectionService::class); + $roomSelectionService->expects($this->once()) + ->method('groupRoomsBySelectionType') + ->with([$roomByRoom->id => $roomByRoom, $roomByPax->id => $roomByPax]) + ->willReturn([ + Room::SELECTION_TYPE_BY_PAX => [$roomByPax->id => $roomByPax], + Room::SELECTION_TYPE_BY_ROOM => [$roomByRoom->id => $roomByRoom], + ]); + + $participantCardDataService = $this->createMock(ParticipantCardDataService::class); + $participantCardDataService->expects($this->never()) + ->method('getAllCardsDataWithValidation'); + + $priceCalculator = $this->createMock(BookingPriceCalculatorService::class); + $priceCalculator->expects($this->never()) + ->method('calculateAllParticipantIndividualPrices'); + + $service = new BookingCreateContextFactory( + $roomSelectionService, + $participantCardDataService, + $summaryDataService, + $priceCalculator, + ); + + $context = $service->create($bookingDto, RoomPricingCalculator::PRICING_MODE_SELECTION); + + $this->assertInstanceOf(BookingCreateContext::class, $context); + $this->assertSame($bookingDto, $context->bookingDto); + $this->assertSame($summaryData, $context->summaryData); + $this->assertSame(null, $context->cardsData); + $this->assertFalse($context->isSubmitted); + $this->assertSame([ + Room::SELECTION_TYPE_BY_PAX => [$roomByPax->id => $roomByPax], + Room::SELECTION_TYPE_BY_ROOM => [$roomByRoom->id => $roomByRoom], + ], $context->groupedRooms); + } + + public function testCreateWithParticipantCardsBuildsStep2Context(): void + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-06'); + + $room = new Room(); + $room->id = 10; + $room->label = 'Doppelzimmer'; + $room->maxPax = 2; + $room->available = 4; + $room->status = 'Frei'; + + $travel->rooms = [$room]; + + $bookingDto = new BookingDto($travel, 157047); + $summaryData = $this->createMock(BookingSummaryDto::class); + $cardData = new ParticipantCardDataDto( + name: 'Max Mustermann', + email: 'max@example.test', + roomName: 'Doppelzimmer', + price: new ParticipantCardPriceDto(123.45, false), + isCanceled: false, + ); + + $summaryDataService = $this->createMock(BookingSummaryDataService::class); + $summaryDataService->expects($this->once()) + ->method('getSummaryData') + ->with($bookingDto, RoomPricingCalculator::PRICING_MODE_SELECTION) + ->willReturn($summaryData); + + $roomSelectionService = $this->createMock(BookingRoomSelectionService::class); + $roomSelectionService->expects($this->once()) + ->method('groupRoomsBySelectionType') + ->with([$room->id => $room]) + ->willReturn([ + Room::SELECTION_TYPE_BY_PAX => [], + Room::SELECTION_TYPE_BY_ROOM => [$room->id => $room], + ]); + + $participantCardDataService = $this->createMock(ParticipantCardDataService::class); + $participantCardDataService->expects($this->once()) + ->method('getAllCardsDataWithValidation') + ->with($bookingDto) + ->willReturn([$cardData]); + + $priceCalculator = $this->createMock(BookingPriceCalculatorService::class); + $priceCalculator->expects($this->never()) + ->method('calculateAllParticipantIndividualPrices'); + + $service = new BookingCreateContextFactory( + $roomSelectionService, + $participantCardDataService, + $summaryDataService, + $priceCalculator, + ); + + $context = $service->createWithParticipantCards($bookingDto, true); + + $this->assertInstanceOf(BookingCreateContext::class, $context); + $this->assertSame([$cardData], $context->cardsData); + $this->assertTrue($context->isSubmitted); + } + + public function testCreateWithParticipantPricesBuildsStep4Context(): void + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-06'); + + $room = new Room(); + $room->id = 10; + $room->label = 'Doppelzimmer'; + $room->maxPax = 2; + $room->available = 4; + $room->status = 'Frei'; + + $travel->rooms = [$room]; + + $bookingDto = new BookingDto($travel, 157047); + $summaryData = $this->createMock(BookingSummaryDto::class); + + $summaryDataService = $this->createMock(BookingSummaryDataService::class); + $summaryDataService->expects($this->once()) + ->method('getSummaryData') + ->with($bookingDto, RoomPricingCalculator::PRICING_MODE_ASSIGNMENT) + ->willReturn($summaryData); + + $roomSelectionService = $this->createMock(BookingRoomSelectionService::class); + $roomSelectionService->expects($this->once()) + ->method('groupRoomsBySelectionType') + ->with([$room->id => $room]) + ->willReturn([ + Room::SELECTION_TYPE_BY_PAX => [], + Room::SELECTION_TYPE_BY_ROOM => [$room->id => $room], + ]); + + $participantCardDataService = $this->createMock(ParticipantCardDataService::class); + $participantCardDataService->expects($this->never()) + ->method('getAllCardsDataWithValidation'); + + $priceCalculator = $this->createMock(BookingPriceCalculatorService::class); + $priceCalculator->expects($this->once()) + ->method('calculateAllParticipantIndividualPrices') + ->with($bookingDto) + ->willReturn([123.45]); + + $service = new BookingCreateContextFactory( + $roomSelectionService, + $participantCardDataService, + $summaryDataService, + $priceCalculator, + ); + + $context = $service->createWithParticipantPrices($bookingDto); + + $this->assertInstanceOf(BookingCreateContext::class, $context); + $this->assertSame([123.45], $context->participantPrices); + $this->assertNull($context->cardsData); + } +} diff --git a/tests/Service/BookingEditContextFactoryTest.php b/tests/Service/BookingEditContextFactoryTest.php new file mode 100644 index 0000000..0d8f133 --- /dev/null +++ b/tests/Service/BookingEditContextFactoryTest.php @@ -0,0 +1,164 @@ +dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-06'); + $bookingDto = new BookingDto($travel, 157047); + + $travelDataService = $this->createMock(TravelDataService::class); + $travelDataService->expects($this->once()) + ->method('enrichWithFreshAvailabilities') + ->with($travel); + + $service = new BookingEditContextFactory( + $this->createMock(ParticipantCardDataService::class), + $this->createMock(BookingSummaryDataService::class), + $travelDataService, + ); + + $service->prepareBookingDto($bookingDto); + } + + public function testCreateBuildsEditContext(): void + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-06'); + + $bookingDto = new BookingDto($travel, 157047); + $bookingDto->booking = new Booking(); + + $bookingData = new Booking(); + $bookingData->dateId = 1234; + + $mutableData = new BaseData([]); + $summaryData = $this->createMock(BookingSummaryDto::class); + + $summaryDataService = $this->createMock(BookingSummaryDataService::class); + $summaryDataService->expects($this->once()) + ->method('getSummaryData') + ->with($bookingDto) + ->willReturn($summaryData); + + $travelDataService = $this->createMock(TravelDataService::class); + $travelDataService->expects($this->once()) + ->method('getMutabilityData') + ->with(1234) + ->willReturn($mutableData); + + $service = new BookingEditContextFactory( + $this->createMock(ParticipantCardDataService::class), + $summaryDataService, + $travelDataService, + ); + + $context = $service->createParticipantContext($bookingDto, $bookingData); + + $this->assertInstanceOf(BookingEditContext::class, $context); + $this->assertSame($bookingDto, $context->bookingDto); + $this->assertSame($bookingData, $context->bookingData); + $this->assertSame($mutableData, $context->mutableData); + $this->assertSame($summaryData, $context->summaryData); + } + + public function testCreateParticipantContextWithoutBookingDataFallsBackToSummaryOnly(): void + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-06'); + + $bookingDto = new BookingDto($travel, 157047); + $summaryData = $this->createMock(BookingSummaryDto::class); + + $summaryDataService = $this->createMock(BookingSummaryDataService::class); + $summaryDataService->expects($this->once()) + ->method('getSummaryData') + ->with($bookingDto) + ->willReturn($summaryData); + + $travelDataService = $this->createMock(TravelDataService::class); + $travelDataService->expects($this->never()) + ->method('getMutabilityData'); + + $service = new BookingEditContextFactory( + $this->createMock(ParticipantCardDataService::class), + $summaryDataService, + $travelDataService, + ); + + $context = $service->createParticipantContext($bookingDto, null); + + $this->assertInstanceOf(BookingEditContext::class, $context); + $this->assertSame($bookingDto, $context->bookingDto); + $this->assertNull($context->bookingData); + $this->assertNull($context->mutableData); + $this->assertSame($summaryData, $context->summaryData); + } + + public function testCreateOverviewContextBuildsEditOverviewPayload(): void + { + $travel = new Travel(); + $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); + $travel->dateTo = new \DateTimeImmutable('2030-01-06'); + + $bookingDto = new BookingDto($travel, 157047); + $bookingData = new Booking(); + $bookingData->dateId = 1234; + + $mutableData = new BaseData([]); + $summaryData = $this->createMock(BookingSummaryDto::class); + $cardsData = []; + + $participantCardDataService = $this->createMock(ParticipantCardDataService::class); + $participantCardDataService->expects($this->once()) + ->method('getAllCardsDataWithValidation') + ->with($bookingDto) + ->willReturn($cardsData); + + $summaryDataService = $this->createMock(BookingSummaryDataService::class); + $summaryDataService->expects($this->once()) + ->method('getSummaryData') + ->with($bookingDto) + ->willReturn($summaryData); + + $travelDataService = $this->createMock(TravelDataService::class); + $travelDataService->expects($this->once()) + ->method('getMutabilityData') + ->with(1234) + ->willReturn($mutableData); + + $service = new BookingEditContextFactory( + $participantCardDataService, + $summaryDataService, + $travelDataService, + ); + + $context = $service->createOverviewContext($bookingDto, $bookingData, true, false, true); + + $this->assertInstanceOf(BookingEditContext::class, $context); + $this->assertSame($cardsData, $context->cardsData); + $this->assertTrue($context->isDirty); + $this->assertFalse($context->isSubmitted); + $this->assertTrue($context->hasValidationErrors); + } +} diff --git a/tests/Service/BookingEditParticipantContextFactoryTest.php b/tests/Service/BookingEditParticipantContextFactoryTest.php deleted file mode 100644 index f477dc6..0000000 --- a/tests/Service/BookingEditParticipantContextFactoryTest.php +++ /dev/null @@ -1,77 +0,0 @@ -dateFrom = new \DateTimeImmutable('2030-01-01'); - $travel->dateTo = new \DateTimeImmutable('2030-01-06'); - $bookingDto = new BookingDto($travel, 157047); - - $travelDataService = $this->createMock(TravelDataService::class); - $travelDataService->expects($this->once()) - ->method('enrichWithFreshAvailabilities') - ->with($travel); - - $service = new BookingEditParticipantContextFactory( - $this->createMock(BookingSummaryDataService::class), - $travelDataService, - ); - - $service->prepareBookingDto($bookingDto); - } - - public function testCreateBuildsEditContext(): void - { - $travel = new Travel(); - $travel->dateFrom = new \DateTimeImmutable('2030-01-01'); - $travel->dateTo = new \DateTimeImmutable('2030-01-06'); - - $bookingDto = new BookingDto($travel, 157047); - $bookingDto->booking = new Booking(); - - $bookingData = new Booking(); - $bookingData->dateId = 1234; - - $mutableData = new BaseData([]); - $summaryData = $this->createMock(BookingSummaryDto::class); - - $summaryDataService = $this->createMock(BookingSummaryDataService::class); - $summaryDataService->expects($this->once()) - ->method('getSummaryData') - ->with($bookingDto) - ->willReturn($summaryData); - - $travelDataService = $this->createMock(TravelDataService::class); - $travelDataService->expects($this->once()) - ->method('getMutabilityData') - ->with(1234) - ->willReturn($mutableData); - - $service = new BookingEditParticipantContextFactory($summaryDataService, $travelDataService); - - $context = $service->create($bookingDto, $bookingData); - - $this->assertInstanceOf(BookingEditParticipantContext::class, $context); - $this->assertSame($bookingDto, $context->bookingDto); - $this->assertSame($bookingData, $context->bookingData); - $this->assertSame($mutableData, $context->mutableData); - $this->assertSame($summaryData, $context->summaryData); - } -} diff --git a/tests/Service/BookingEditSubmitServiceTest.php b/tests/Service/BookingEditSubmitServiceTest.php new file mode 100644 index 0000000..7762fe8 --- /dev/null +++ b/tests/Service/BookingEditSubmitServiceTest.php @@ -0,0 +1,482 @@ +createRequestWithSession(); + $user = $this->createUser(); + $bookingDto = $this->createBookingDto(); + + $dataLoader = $this->createMock(BookingEditDataLoaderService::class); + $dataLoader->expects($this->once()) + ->method('invalidateBookingCache') + ->with(42, $user); + $dataLoader->expects($this->once()) + ->method('fetchBookingData') + ->with(42, $user) + ->willReturn(null); + + $service = $this->createService( + dataLoader: $dataLoader, + ); + + $response = $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') + ); + } + + /** + * @dataProvider updateFailureProvider + */ + public function testHandleSubmissionReturnsRedirectWhenUpdateThrows( + \Throwable $exception, + string $expectedFlashType, + string $expectedFlashMessage, + ): void { + $request = $this->createRequestWithSession(); + $user = $this->createUser(); + $bookingDto = $this->createBookingDto(); + $freshBookingData = $this->createFreshBooking(); + + $dataLoader = $this->createMock(BookingEditDataLoaderService::class); + $dataLoader->expects($this->exactly(1)) + ->method('invalidateBookingCache') + ->with(42, $user); + $dataLoader->expects($this->once()) + ->method('fetchBookingData') + ->with(42, $user) + ->willReturn($freshBookingData); + + $travelDataService = $this->createMock(TravelDataService::class); + $travelDataService->expects($this->once()) + ->method('getMutabilityData') + ->with(1234, true, true) + ->willReturn(null); + $travelDataService->expects($this->never()) + ->method('patchMutability'); + + $submitGuard = $this->createMock(BookingEditSubmitGuardService::class); + $submitGuard->expects($this->once()) + ->method('reconcileImmutableCategories') + ->with($bookingDto, $freshBookingData) + ->willReturn(false); + + $apiClient = $this->createMock(\App\BusProNet\ApiClient::class); + $apiClient->expects($this->once()) + ->method('updateBooking') + ->with($bookingDto, true) + ->willThrowException($exception); + + $service = $this->createService( + apiClient: $apiClient, + dataLoader: $dataLoader, + travelDataService: $travelDataService, + submitGuard: $submitGuard, + ); + + $response = $service->handleSubmission($request, $bookingDto, 42, $user); + + $this->assertSame('/bookings/42/edit', $response->headers->get('Location')); + $this->assertSame([$expectedFlashMessage], $request->getSession()->getFlashBag()->get($expectedFlashType)); + } + + public function testHandleSubmissionReturnsRedirectWhenUpdateIsUnsuccessful(): void + { + $request = $this->createRequestWithSession(); + $user = $this->createUser(); + $bookingDto = $this->createBookingDto(); + $freshBookingData = $this->createFreshBooking(); + + $dataLoader = $this->createMock(BookingEditDataLoaderService::class); + $dataLoader->expects($this->once()) + ->method('invalidateBookingCache') + ->with(42, $user); + $dataLoader->expects($this->once()) + ->method('fetchBookingData') + ->with(42, $user) + ->willReturn($freshBookingData); + + $travelDataService = $this->createMock(TravelDataService::class); + $travelDataService->expects($this->once()) + ->method('getMutabilityData') + ->with(1234, true, true) + ->willReturn(null); + $travelDataService->expects($this->never()) + ->method('patchMutability'); + + $submitGuard = $this->createMock(BookingEditSubmitGuardService::class); + $submitGuard->expects($this->once()) + ->method('reconcileImmutableCategories') + ->with($bookingDto, $freshBookingData) + ->willReturn(false); + + $apiClient = $this->createMock(\App\BusProNet\ApiClient::class); + $bookingUpdate = new BookingUpdate(); + $bookingUpdate->success = false; + $bookingUpdate->status = 'BPN-FAIL'; + $apiClient->expects($this->once()) + ->method('updateBooking') + ->with($bookingDto, true) + ->willReturn($bookingUpdate); + + $service = $this->createService( + apiClient: $apiClient, + dataLoader: $dataLoader, + travelDataService: $travelDataService, + submitGuard: $submitGuard, + ); + + $response = $service->handleSubmission($request, $bookingDto, 42, $user); + + $this->assertSame('/bookings/42/edit', $response->headers->get('Location')); + $this->assertSame(['BPN-FAIL'], $request->getSession()->getFlashBag()->get('error')); + } + + public function testHandleSubmissionStoresInfoForNonErrorNotification(): void + { + $this->assertNotificationFlash(new Notification(650, 'Alles gut'), 'info'); + } + + public function testHandleSubmissionStoresErrorForErrorNotification(): void + { + $this->assertNotificationFlash(new Notification(500, 'Kaputt'), 'error'); + } + + public function testHandleSubmissionClearsSessionAndDraftOnSuccessfulUpdate(): void + { + $request = $this->createRequestWithSession(); + $user = $this->createUser(); + $bookingDto = $this->createBookingDto(); + $freshBookingData = $this->createFreshBooking(); + + $dataLoader = $this->createMock(BookingEditDataLoaderService::class); + $dataLoader->expects($this->exactly(2)) + ->method('invalidateBookingCache') + ->with(42, $user); + $dataLoader->expects($this->once()) + ->method('fetchBookingData') + ->with(42, $user) + ->willReturn($freshBookingData); + + $travelDataService = $this->createMock(TravelDataService::class); + $travelDataService->expects($this->once()) + ->method('getMutabilityData') + ->with(1234, true, true) + ->willReturn(null); + $travelDataService->expects($this->never()) + ->method('patchMutability'); + + $submitGuard = $this->createMock(BookingEditSubmitGuardService::class); + $submitGuard->expects($this->once()) + ->method('reconcileImmutableCategories') + ->with($bookingDto, $freshBookingData) + ->willReturn(false); + + $apiClient = $this->createMock(\App\BusProNet\ApiClient::class); + $bookingUpdate = new BookingUpdate(); + $bookingUpdate->success = true; + $apiClient->expects($this->once()) + ->method('updateBooking') + ->with($bookingDto, true) + ->willReturn($bookingUpdate); + + $bookingSessionService = $this->createMock(BookingSessionService::class); + $bookingSessionService->expects($this->once()) + ->method('clearBookingDto') + ->with($request, BookingDto::MODE_EDIT); + $bookingSessionService->expects($this->never()) + ->method('saveBookingDto'); + + $draftService = $this->createMock(BookingEditDraftService::class); + $draftService->expects($this->once()) + ->method('deleteDraft') + ->with($user, 42); + + $service = $this->createService( + apiClient: $apiClient, + dataLoader: $dataLoader, + draftService: $draftService, + travelDataService: $travelDataService, + submitGuard: $submitGuard, + bookingSessionService: $bookingSessionService, + ); + + $response = $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($freshBookingData, $bookingDto->booking); + } + + public function testHandleSubmissionPersistsSessionWhenImmutableFieldsWereReverted(): void + { + $request = $this->createRequestWithSession(); + $user = $this->createUser(); + $bookingDto = $this->createBookingDto(); + $freshBookingData = $this->createFreshBooking(); + + $dataLoader = $this->createMock(BookingEditDataLoaderService::class); + $dataLoader->expects($this->exactly(2)) + ->method('invalidateBookingCache') + ->with(42, $user); + $dataLoader->expects($this->once()) + ->method('fetchBookingData') + ->with(42, $user) + ->willReturn($freshBookingData); + + $travelDataService = $this->createMock(TravelDataService::class); + $travelDataService->expects($this->once()) + ->method('getMutabilityData') + ->with(1234, true, true) + ->willReturn(null); + $travelDataService->expects($this->never()) + ->method('patchMutability'); + + $submitGuard = $this->createMock(BookingEditSubmitGuardService::class); + $submitGuard->expects($this->once()) + ->method('reconcileImmutableCategories') + ->with($bookingDto, $freshBookingData) + ->willReturn(true); + + $apiClient = $this->createMock(\App\BusProNet\ApiClient::class); + $bookingUpdate = new BookingUpdate(); + $bookingUpdate->success = true; + $apiClient->expects($this->once()) + ->method('updateBooking') + ->with($bookingDto, true) + ->willReturn($bookingUpdate); + + $bookingSessionService = $this->createMock(BookingSessionService::class); + $bookingSessionService->expects($this->once()) + ->method('saveBookingDto') + ->with($request, $bookingDto, BookingDto::MODE_EDIT); + $bookingSessionService->expects($this->once()) + ->method('clearBookingDto') + ->with($request, BookingDto::MODE_EDIT); + + $draftService = $this->createMock(BookingEditDraftService::class); + $draftService->expects($this->once()) + ->method('deleteDraft') + ->with($user, 42); + + $service = $this->createService( + apiClient: $apiClient, + dataLoader: $dataLoader, + draftService: $draftService, + travelDataService: $travelDataService, + submitGuard: $submitGuard, + bookingSessionService: $bookingSessionService, + ); + + $response = $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')); + } + + public function testHandleSubmissionReturnsRedirectOnTimeout(): void + { + $this->assertExceptionFlash(new TimeoutException('slow'), 'Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut.'); + } + + public function testHandleSubmissionReturnsRedirectOnApiClientException(): void + { + $this->assertExceptionFlash(new ApiClientException('boom'), 'Es ist ein Fehler in der Kommunikation mit dem Buchungssystem aufgetreten'); + } + + 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'], + ]; + } + + private function assertNotificationFlash(Notification $notification, string $expectedType): void + { + $request = $this->createRequestWithSession(); + $user = $this->createUser(); + $bookingDto = $this->createBookingDto(); + $freshBookingData = $this->createFreshBooking(); + + $dataLoader = $this->createMock(BookingEditDataLoaderService::class); + $dataLoader->expects($this->once()) + ->method('invalidateBookingCache') + ->with(42, $user); + $dataLoader->expects($this->once()) + ->method('fetchBookingData') + ->with(42, $user) + ->willReturn($freshBookingData); + + $travelDataService = $this->createMock(TravelDataService::class); + $travelDataService->expects($this->once()) + ->method('getMutabilityData') + ->with(1234, true, true) + ->willReturn(null); + $travelDataService->expects($this->never()) + ->method('patchMutability'); + + $submitGuard = $this->createMock(BookingEditSubmitGuardService::class); + $submitGuard->expects($this->once()) + ->method('reconcileImmutableCategories') + ->with($bookingDto, $freshBookingData) + ->willReturn(false); + + $apiClient = $this->createMock(\App\BusProNet\ApiClient::class); + $apiClient->expects($this->once()) + ->method('updateBooking') + ->with($bookingDto, true) + ->willReturn($notification); + + $service = $this->createService( + apiClient: $apiClient, + dataLoader: $dataLoader, + travelDataService: $travelDataService, + submitGuard: $submitGuard, + ); + + $response = $service->handleSubmission($request, $bookingDto, 42, $user); + + $this->assertSame('/bookings/42/edit', $response->headers->get('Location')); + $this->assertSame([$notification->message], $request->getSession()->getFlashBag()->get($expectedType)); + } + + private function assertExceptionFlash(\Throwable $exception, string $expectedMessage): void + { + $request = $this->createRequestWithSession(); + $user = $this->createUser(); + $bookingDto = $this->createBookingDto(); + $freshBookingData = $this->createFreshBooking(); + + $dataLoader = $this->createMock(BookingEditDataLoaderService::class); + $dataLoader->expects($this->once()) + ->method('invalidateBookingCache') + ->with(42, $user); + $dataLoader->expects($this->once()) + ->method('fetchBookingData') + ->with(42, $user) + ->willReturn($freshBookingData); + + $travelDataService = $this->createMock(TravelDataService::class); + $travelDataService->expects($this->once()) + ->method('getMutabilityData') + ->with(1234, true, true) + ->willReturn(null); + $travelDataService->expects($this->never()) + ->method('patchMutability'); + + $submitGuard = $this->createMock(BookingEditSubmitGuardService::class); + $submitGuard->expects($this->once()) + ->method('reconcileImmutableCategories') + ->with($bookingDto, $freshBookingData) + ->willReturn(false); + + $apiClient = $this->createMock(\App\BusProNet\ApiClient::class); + $apiClient->expects($this->once()) + ->method('updateBooking') + ->with($bookingDto, true) + ->willThrowException($exception); + + $service = $this->createService( + apiClient: $apiClient, + dataLoader: $dataLoader, + travelDataService: $travelDataService, + submitGuard: $submitGuard, + ); + + $response = $service->handleSubmission($request, $bookingDto, 42, $user); + + $this->assertSame('/bookings/42/edit', $response->headers->get('Location')); + $this->assertSame([$expectedMessage], $request->getSession()->getFlashBag()->get('error')); + } + + private function createService( + ?\App\BusProNet\ApiClient $apiClient = null, + ?BookingEditDataLoaderService $dataLoader = null, + ?BookingEditDraftService $draftService = null, + ?TravelDataService $travelDataService = null, + ?BookingEditSubmitGuardService $submitGuard = null, + ?BookingSessionService $bookingSessionService = null, + ): BookingEditSubmitService { + return new BookingEditSubmitService( + $apiClient ?? $this->createMock(\App\BusProNet\ApiClient::class), + $dataLoader ?? $this->createMock(BookingEditDataLoaderService::class), + $draftService ?? $this->createMock(BookingEditDraftService::class), + $travelDataService ?? $this->createMock(TravelDataService::class), + $submitGuard ?? $this->createMock(BookingEditSubmitGuardService::class), + $bookingSessionService ?? $this->createMock(BookingSessionService::class), + $this->createUrlGenerator(), + $this->createMock(LoggerInterface::class), + ); + } + + private function createBookingDto(): BookingDto + { + return new BookingDto(new Travel(), 1); + } + + private function createFreshBooking(): Booking + { + $booking = new Booking(); + $booking->dateId = 1234; + + return $booking; + } + + private function createUser(): User + { + $user = new User('user@example.com'); + $user->setPassword('secret'); + + return $user; + } + + private function createRequestWithSession(): Request + { + $request = Request::create('/bookings/42/edit', Request::METHOD_POST); + $request->setSession(new Session(new MockArraySessionStorage())); + + return $request; + } + + private function createUrlGenerator(): UrlGeneratorInterface + { + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $urlGenerator->method('generate') + ->with('app_booking_edit', ['id' => 42]) + ->willReturn('/bookings/42/edit'); + + return $urlGenerator; + } +}
- {% if bookingCreateDto.paymentMethod == constant('App\\BusProNet\\Constants::PAYMENT_METHOD_DEBIT') %} + {% if bookingCreateContext.bookingDto.paymentMethod == constant('App\\BusProNet\\Constants::PAYMENT_METHOD_DEBIT') %} Lastschrift (Einzugsermächtigungsverfahren) {% else %} Überweisung {% endif %}
IBAN: - {{ bookingCreateDto.bankAccount.iban }} + {{ bookingCreateContext.bookingDto.bankAccount.iban }}
Kontoinhaber: - {{ bookingCreateDto.bankAccount.accountHolder }} + {{ bookingCreateContext.bookingDto.bankAccount.accountHolder }}