From 5ef3bb9cbf6fb2465d5f9c7a9914bd4c5408740b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Fri, 17 Oct 2025 18:46:25 +0200 Subject: [PATCH] wip: finalize implementation --- src/BusProNet/Model/Insurance.php | 7 +- src/BusProNet/Traits/SortByPriceTrait.php | 2 +- src/BusProNet/XmlParser/InsuranceParser.php | 7 +- .../Booking/Create/Step1Controller.php | 4 +- .../Booking/Create/Step2Controller.php | 27 +++--- .../Booking/Create/Step3Controller.php | 31 +++---- .../Booking/Create/Step4Controller.php | 24 +----- .../Booking/Traits/BookingCreateTrait.php | 18 ++++ .../Traits/ParticipantCardFlowTrait.php | 1 - src/Form/BookingParticipantType.php | 54 ++++++------ src/Form/InsuranceChoiceType.php | 86 ------------------- src/Form/Model/ParticipantDto.php | 2 +- .../ParticipantFieldOptionsProvider.php | 16 +++- .../ParticipantInsuranceFieldHandler.php | 20 ++--- .../ParticipantSkiPassFieldHandler.php | 7 +- ...ipantTransportationInboundFieldHandler.php | 3 - ...pantTransportationOutboundFieldHandler.php | 3 - src/Service/ParticipantCardDataService.php | 2 +- templates/booking/create/step_4.html.twig | 16 +--- .../BookingDataProcessorTest.php | 6 +- 20 files changed, 112 insertions(+), 224 deletions(-) delete mode 100644 src/Form/InsuranceChoiceType.php diff --git a/src/BusProNet/Model/Insurance.php b/src/BusProNet/Model/Insurance.php index 899cc89..f1b1aa3 100644 --- a/src/BusProNet/Model/Insurance.php +++ b/src/BusProNet/Model/Insurance.php @@ -17,7 +17,7 @@ use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; class Insurance { #[Groups(['api:single', 'api:list'])] - public string|int|null $id = null; + public string|null $id = null; #[Groups(['api:single', 'api:list'])] public ?string $code = null; @@ -105,11 +105,6 @@ class Insurance */ public array $individualPrice = []; - public function __toString(): string - { - return (string) $this->id; - } - /** * Returns the subType, with virtual 'PKG' subType for insurance packages. * diff --git a/src/BusProNet/Traits/SortByPriceTrait.php b/src/BusProNet/Traits/SortByPriceTrait.php index 1e691e4..f178d23 100644 --- a/src/BusProNet/Traits/SortByPriceTrait.php +++ b/src/BusProNet/Traits/SortByPriceTrait.php @@ -34,7 +34,7 @@ trait SortByPriceTrait */ protected function sortByPrice(array $items): array { - usort($items, function (object $a, object $b) { + uasort($items, function (object $a, object $b) { $priceA = $a->price ?? 0; $priceB = $b->price ?? 0; diff --git a/src/BusProNet/XmlParser/InsuranceParser.php b/src/BusProNet/XmlParser/InsuranceParser.php index 2a95316..de6cfbe 100644 --- a/src/BusProNet/XmlParser/InsuranceParser.php +++ b/src/BusProNet/XmlParser/InsuranceParser.php @@ -33,7 +33,6 @@ class InsuranceParser extends AbstractParser $individualInsurances = []; $xmlContent->filterXPath('//versicherungen/versicherung') ->each(function (Crawler $node) use (&$individualInsurances, &$insurances) { - $id = (int) $node->attr('idbuspro'); // Individual insurances have int IDs $isComplementary = $this->getBoolAttributeValue($node->attr('zusatzversicherung')); // Parse all individual insurances for reference lookup @@ -110,7 +109,7 @@ class InsuranceParser extends AbstractParser // Basic identifiers - packages have string IDs with 'P' prefix, individual insurances have int IDs $idValue = $node->attr('idbuspro'); - $insurance->id = $isPackage ? $idValue : (int) $idValue; + $insurance->id = $idValue; $insurance->code = $node->attr('code'); $insurance->label = $this->normalizeInsuranceLabel($node->attr('bezeichnung')); $insurance->subType = $node->attr('unterart'); @@ -169,7 +168,7 @@ class InsuranceParser extends AbstractParser ->each(function (Crawler $packageNode) use (&$referencedIds) { $packageNode->filterXPath('.//enthalteneversicherung') ->each(function (Crawler $refNode) use (&$referencedIds) { - $referencedIds[] = (int) $refNode->attr('idbuspro'); // Referenced insurances are always int IDs + $referencedIds[] = $refNode->attr('idbuspro'); }); }); @@ -188,7 +187,7 @@ class InsuranceParser extends AbstractParser $ids = []; $node->filterXPath('.//enthalteneversicherungen/enthalteneversicherung') ->each(function (Crawler $insuranceNode) use (&$ids) { - $ids[] = (int) $insuranceNode->attr('idbuspro'); // Individual insurance IDs are always int + $ids[] = $insuranceNode->attr('idbuspro'); // Individual insurance IDs are always int }); return $ids; diff --git a/src/Controller/Booking/Create/Step1Controller.php b/src/Controller/Booking/Create/Step1Controller.php index 9745959..f167015 100644 --- a/src/Controller/Booking/Create/Step1Controller.php +++ b/src/Controller/Booking/Create/Step1Controller.php @@ -65,7 +65,7 @@ class Step1Controller extends AbstractController $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { + if (true === $form->isSubmitted() && true === $form->isValid()) { if ($this->bookingService->hasRoomSelectionChanged($oldRoomSelectionSnapshot, $bookingCreateDto)) { $this->bookingService->resetParticipantAssignments($bookingCreateDto); } @@ -99,7 +99,7 @@ class Step1Controller extends AbstractController * without validation and returning freshly rendered blocks. */ #[Route('/bookings/create/refresh', name: 'app_booking_create_step_1_refresh', methods: ['POST'])] - public function refreshRoomSelection(Request $request): Response + public function refresh(Request $request): Response { $result = $this->getOrCreateBookingCreateDtoForHtmx($this->bookingService, $request); if ($result instanceof Response) { diff --git a/src/Controller/Booking/Create/Step2Controller.php b/src/Controller/Booking/Create/Step2Controller.php index bba9057..420c7d6 100644 --- a/src/Controller/Booking/Create/Step2Controller.php +++ b/src/Controller/Booking/Create/Step2Controller.php @@ -11,6 +11,7 @@ use App\Controller\Booking\Traits\ParticipantValidationTrait; use App\Form\BookingCreateStep2Type; use App\Form\Model\BookingDto; use App\Form\Model\ParticipantDto; +use App\Form\Service\ParticipantFieldOptionsProvider; use App\Htmx\HxTrait; use App\Service\BookingPriceCalculatorService; use App\Service\BookingService; @@ -42,6 +43,7 @@ class Step2Controller extends AbstractController private readonly TravelDataService $travelDataService, private readonly RoomAssignmentService $roomAssignmentService, private readonly ParticipantCardDataService $participantCardService, + private readonly ParticipantFieldOptionsProvider $fieldOptionsProvider, ) { } @@ -58,14 +60,14 @@ class Step2Controller extends AbstractController } $bookingCreateDto = $result; - // Enrich with fresh availability data - $this->enrichWithFreshAvailabilities($bookingCreateDto); - // Validate step access if ($redirect = $this->validateStepAccess($bookingCreateDto, 2)) { return $redirect; } + // Enrich with fresh availability data + $this->enrichWithFreshAvailabilities($bookingCreateDto); + // Ensure correct number of participants $this->ensureCorrectNumberOfParticipants($bookingCreateDto); @@ -84,8 +86,8 @@ class Step2Controller extends AbstractController ]); $form->handleRequest($request); - // Handle form submission (clicking "Weiter") - if ($form->isSubmitted() && $form->isValid()) { + // Handle form submission + if (true === $form->isSubmitted() && true === $form->isValid()) { // All participants validated successfully, update current step $bookingCreateDto->currentStep = 3; $this->bookingService->saveBookingDto($request, $bookingCreateDto, BookingDto::MODE_CREATE); @@ -96,7 +98,7 @@ class Step2Controller extends AbstractController // Extract participant indices with validation errors $participantErrors = []; - if ($form->isSubmitted() && false === $form->isValid()) { + if (true === $form->isSubmitted() && false === $form->isValid()) { $participantErrors = $this->extractParticipantErrorIndices($form); } @@ -118,7 +120,7 @@ class Step2Controller extends AbstractController 'participantErrors' => $participantErrors, ]; - // If HTMX request, render only blocks to avoid layout duplication + // HTMX request: render blocks only if ($this->isHxRequest($request)) { return $this->htmxOobResponse( 'booking/create/step_2.html.twig', @@ -148,6 +150,9 @@ class Step2Controller extends AbstractController throw $this->createNotFoundException(sprintf('Participant at index %d does not exist', $index)); } + // Enrich with fresh availability data + $this->enrichWithFreshAvailabilities($bookingDto); + // Create form with booking_context option $form = $this->createParticipantForm($bookingDto, $index, [ 'validation_groups' => ['booking_create_step_2'], @@ -155,7 +160,7 @@ class Step2Controller extends AbstractController $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { + if (true === $form->isSubmitted() && true === $form->isValid()) { // Save BookingDto to session $this->bookingService->saveBookingDto($request, $bookingDto, BookingDto::MODE_CREATE); @@ -199,14 +204,14 @@ class Step2Controller extends AbstractController { $bookingDto = $this->loadBookingDtoOrFail($request, BookingDto::MODE_CREATE); - // Enrich with fresh availability data - $this->enrichWithFreshAvailabilities($bookingDto); - // Validate participant index if (false === isset($bookingDto->participants[$index])) { throw $this->createNotFoundException(sprintf('Participant at index %d does not exist', $index)); } + // Enrich with fresh availability data + $this->enrichWithFreshAvailabilities($bookingDto); + // Use trait method for refresh handling return $this->handleParticipantRefresh( $request, diff --git a/src/Controller/Booking/Create/Step3Controller.php b/src/Controller/Booking/Create/Step3Controller.php index 05f7251..0dd3a61 100644 --- a/src/Controller/Booking/Create/Step3Controller.php +++ b/src/Controller/Booking/Create/Step3Controller.php @@ -64,13 +64,14 @@ class Step3Controller extends AbstractController ]); $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { + if (true === $form->isSubmitted() && true === $form->isValid()) { try { // Validate booking data with API (inquiry) + $bookingCreateDto->bookingStatus = 'A'; $inquiryResponse = $this->apiClient->createBookingInquiry($bookingCreateDto); if ($inquiryResponse instanceof Notification) { - return $this->handleInquiryError( + return $this->handleApiError( 'Booking inquiry failed', ['message' => $inquiryResponse->message], 'Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.', @@ -80,7 +81,7 @@ class Step3Controller extends AbstractController } if (false === $inquiryResponse->isInquiryValid()) { - return $this->handleInquiryError( + return $this->handleApiError( 'Booking inquiry validation failed', ['status' => $inquiryResponse->status], 'Buchung konnte nicht validiert werden.', @@ -94,7 +95,7 @@ class Step3Controller extends AbstractController $calculatedTotal = $this->priceCalculator->calculateGrandTotal($bookingCreateDto); if ($apiTotal !== $calculatedTotal) { - return $this->handleInquiryError( + return $this->handleApiError( 'Price mismatch detected - payload incomplete', [ 'apiTotal' => $apiTotal, @@ -111,9 +112,13 @@ class Step3Controller extends AbstractController $bookingCreateDto->currentStep = 4; $this->bookingService->saveBookingCreateDto($request, $bookingCreateDto); + if ($inquiryResponse->message) { + $this->addFlash('info', $inquiryResponse->message); + } + return $this->hxRedirect($request, $this->generateUrl('app_booking_create_step_4')); } catch (\Exception $e) { - return $this->handleInquiryError( + return $this->handleApiError( 'Booking inquiry exception', [ 'exception' => $e->getMessage(), @@ -151,22 +156,6 @@ class Step3Controller extends AbstractController return $this->renderStepForm($bookingCreateDto, $form); } - /** - * Handles inquiry errors by logging, adding flash message, and rendering the form. - */ - private function handleInquiryError( - string $logMessage, - array $context, - string $flashMessage, - BookingDto $bookingCreateDto, - FormInterface $form, - ): Response { - $this->logger->error($logMessage, $context); - $this->addFlash('error', $flashMessage); - - return $this->renderStepForm($bookingCreateDto, $form); - } - /** * Renders the step 3 form with standard template variables. */ diff --git a/src/Controller/Booking/Create/Step4Controller.php b/src/Controller/Booking/Create/Step4Controller.php index 61dd19b..04bbecb 100644 --- a/src/Controller/Booking/Create/Step4Controller.php +++ b/src/Controller/Booking/Create/Step4Controller.php @@ -62,13 +62,13 @@ class Step4Controller extends AbstractController ]); $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { + if (true === $form->isSubmitted() && true === $form->isValid()) { try { // Submit final booking (already validated in Step 3) $bookingResponse = $this->apiClient->createBooking($bookingCreateDto); if ($bookingResponse instanceof Notification) { - return $this->handleBookingError( + return $this->handleApiError( 'Booking creation failed - API notification', ['message' => $bookingResponse->message], $bookingResponse->message, @@ -78,7 +78,7 @@ class Step4Controller extends AbstractController } if (false === $bookingResponse->isBookingSuccessful()) { - return $this->handleBookingError( + return $this->handleApiError( 'Booking creation unsuccessful', ['status' => $bookingResponse->status], 'Buchung konnte nicht erstellt werden.', @@ -93,7 +93,7 @@ class Step4Controller extends AbstractController return $this->hxRedirect($request, $this->generateUrl('app_booking_create_success')); } catch (\Exception $e) { - return $this->handleBookingError( + return $this->handleApiError( 'Booking creation exception', [ 'exception' => $e->getMessage(), @@ -109,22 +109,6 @@ class Step4Controller extends AbstractController return $this->renderStepForm($bookingCreateDto, $form); } - /** - * Handles booking errors by logging, adding flash message, and rendering the form. - */ - private function handleBookingError( - string $logMessage, - array $context, - string $flashMessage, - BookingDto $bookingCreateDto, - FormInterface $form, - ): Response { - $this->logger->error($logMessage, $context); - $this->addFlash('error', $flashMessage); - - return $this->renderStepForm($bookingCreateDto, $form); - } - /** * Renders the step 4 form with standard template variables. */ diff --git a/src/Controller/Booking/Traits/BookingCreateTrait.php b/src/Controller/Booking/Traits/BookingCreateTrait.php index 3c46169..f05af34 100644 --- a/src/Controller/Booking/Traits/BookingCreateTrait.php +++ b/src/Controller/Booking/Traits/BookingCreateTrait.php @@ -5,7 +5,9 @@ declare(strict_types=1); namespace App\Controller\Booking\Traits; use App\Form\Model\BookingDto; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Response; /** * Provides common functionality for booking creation controllers. @@ -78,4 +80,20 @@ trait BookingCreateTrait 'groupedSelectedRooms' => $groupedSelectedRooms, ]; } + + /** + * Handles API errors by logging, adding flash message, and rendering the form. + */ + private function handleApiError( + string $logMessage, + array $context, + string $flashMessage, + BookingDto $bookingCreateDto, + FormInterface $form, + ): Response { + $this->logger->error($logMessage, $context); + $this->addFlash('error', $flashMessage); + + return $this->renderStepForm($bookingCreateDto, $form); + } } diff --git a/src/Controller/Booking/Traits/ParticipantCardFlowTrait.php b/src/Controller/Booking/Traits/ParticipantCardFlowTrait.php index c8fbf16..54d8ca3 100644 --- a/src/Controller/Booking/Traits/ParticipantCardFlowTrait.php +++ b/src/Controller/Booking/Traits/ParticipantCardFlowTrait.php @@ -67,7 +67,6 @@ trait ParticipantCardFlowTrait // Merge default options with provided options $formOptions = array_merge([ 'booking_context' => $bookingDto, - 'edit_mode' => BookingDto::MODE_EDIT === $bookingDto->getMode(), ], $options); return $this->createForm(BookingParticipantType::class, $participant, $formOptions); diff --git a/src/Form/BookingParticipantType.php b/src/Form/BookingParticipantType.php index e49fa9d..d84d1f2 100644 --- a/src/Form/BookingParticipantType.php +++ b/src/Form/BookingParticipantType.php @@ -37,26 +37,21 @@ class BookingParticipantType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options): void { + // Capture booking context for use in event listeners + /** @var BookingDto $bookingContext */ + $bookingContext = $options['booking_context']; + // Select field state provider based on edit_mode option - $this->fieldStateProvider = $options['edit_mode'] + $this->fieldStateProvider = BookingDto::MODE_EDIT === $bookingContext->getMode() ? $this->editFieldStateProvider : $this->createFieldStateProvider; - // Capture booking context for use in event listeners - $bookingContext = $options['booking_context']; - $builder ->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($bookingContext) { $this->onPreSetData($event, $bookingContext); }) ->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($bookingContext) { - // Process field handlers FIRST (before form binding and validation) - // This ensures data is cleaned before Symfony processes it - if (null !== $bookingContext) { - $this->processFieldHandlers($event, $bookingContext); - } - - // Then rebuild fields with updated states + $this->processFieldHandlers($event, $bookingContext); $this->onPreSubmit($event, $bookingContext); }); } @@ -99,12 +94,13 @@ class BookingParticipantType extends AbstractType { /** @var ParticipantDto|null $participantData */ $participantData = $event->getData(); - $form = $event->getForm(); if (null === $participantData) { return; } + $form = $event->getForm(); + // Card flow: BookingDto passed via options // Accordion flow (if we had one): traverse form tree $bookingDto = $bookingContext ?? $this->fieldStateProvider->getBookingDtoFromForm($form); @@ -128,10 +124,6 @@ class BookingParticipantType extends AbstractType $submittedData = $event->getData(); $form = $event->getForm(); - if (false === is_array($submittedData)) { - return; - } - // Card flow: BookingDto passed via options // Accordion flow (if we had one): traverse form tree $bookingDto = $bookingContext ?? $this->fieldStateProvider->getBookingDtoFromForm($form); @@ -148,6 +140,8 @@ class BookingParticipantType extends AbstractType // Rebuild all fields with updated states based on submitted data $this->rebuildFieldsWithStates($form, $bookingDto, $participantData->index, $submittedData); + + $event->setData($submittedData); } /** @@ -219,13 +213,17 @@ class BookingParticipantType extends AbstractType * @param FormInterface $form The form to modify * @param BookingDto $bookingDto The booking data for context * @param int $participantIndex The participant index - * @param array $formData Submitted form data for state calculation + * @param array $submittedData Submitted form data for state calculation */ - private function removeExcludedFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array $formData = []): void + private function removeExcludedFields(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array &$submittedData = []): void { foreach (ParticipantDto::DYNAMIC_FIELDS as $fieldName) { - if ($form->has($fieldName) && !$this->fieldStateProvider->shouldIncludeField($fieldName, $bookingDto, $participantIndex, $formData)) { + if ( + true === $form->has($fieldName) + && false === $this->fieldStateProvider->shouldIncludeField($fieldName, $bookingDto, $participantIndex, $submittedData) + ) { $form->remove($fieldName); + unset($submittedData[$fieldName]); } } } @@ -233,13 +231,11 @@ class BookingParticipantType extends AbstractType /** * Rebuilds all fields with updated states based on submitted data. */ - private function rebuildFieldsWithStates(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array $submittedData): void + private function rebuildFieldsWithStates(FormInterface $form, BookingDto $bookingDto, int $participantIndex, array &$submittedData): void { // First, remove fields that should be excluded entirely $this->removeExcludedFields($form, $bookingDto, $participantIndex, $submittedData); - // Clear the form and rebuild from scratch with updated states - // Rebuild base fields with updated states $baseFields = ['firstName', 'lastName', 'dateOfBirth', 'gender', 'nationality', 'email', 'mobile', 'address', 'bodyDimensions']; foreach ($baseFields as $fieldName) { @@ -286,16 +282,16 @@ class BookingParticipantType extends AbstractType // Insurance fields only available in create mode (API limitation) if (BookingDto::MODE_CREATE === $bookingDto->getMode()) { $dynamicFields['bulkInsuranceBooking'] = CheckboxType::class; - $dynamicFields['insurance'] = InsuranceChoiceType::class; + $dynamicFields['insurance'] = ChoiceType::class; } foreach ($dynamicFields as $fieldName => $fieldType) { - if ($this->fieldOptionsProvider->hasFieldOptions($fieldName)) { - // Check if field should be included in the form at all - if (false === $this->fieldStateProvider->shouldIncludeField($fieldName, $bookingDto, $participantIndex)) { - continue; - } + // Check if field should be included in the form at all + if (false === $this->fieldStateProvider->shouldIncludeField($fieldName, $bookingDto, $participantIndex)) { + continue; + } + if ($this->fieldOptionsProvider->hasFieldOptions($fieldName)) { // Get base field options $fieldOptions = $this->fieldOptionsProvider->getFieldOptions($fieldName, $bookingDto, $participantIndex); @@ -377,12 +373,10 @@ class BookingParticipantType extends AbstractType $resolver->setDefaults([ 'data_class' => ParticipantDto::class, 'selected_rooms' => [], - 'edit_mode' => false, 'booking_context' => null, ]); $resolver->setAllowedTypes('selected_rooms', 'array'); - $resolver->setAllowedTypes('edit_mode', 'bool'); $resolver->setAllowedTypes('booking_context', ['null', BookingDto::class]); } } diff --git a/src/Form/InsuranceChoiceType.php b/src/Form/InsuranceChoiceType.php deleted file mode 100644 index 5b33440..0000000 --- a/src/Form/InsuranceChoiceType.php +++ /dev/null @@ -1,86 +0,0 @@ -setRequired('insurances'); - $resolver->setAllowedTypes('insurances', 'array'); - - $resolver->setDefault('choices', function (Options $options) { - // Prepend "no insurance" option to eligible insurances - return array_merge( - [0 => null], - $options['insurances'] - ); - }); - - $resolver->setDefault('choice_value', function ($insurance) { - // Handle "no insurance" option (null value at index 0) - // instanceof check required because closures in configureOptions receive mixed types - if ($insurance instanceof Insurance) { - return (string) $insurance->id; - } - return ''; - }); - - $resolver->setDefault('choice_label', function ($insurance) { - // Handle "no insurance" option (null value at index 0) - // instanceof check required because closures in configureOptions receive mixed types - if (!$insurance instanceof Insurance) { - return 'Keine Versicherung'; - } - - $label = $insurance->label; - - if (null !== $insurance->price && $insurance->price > 0) { - $label .= sprintf(' (€%s)', number_format($insurance->price, 2, ',', '.')); - } - - return $label; - }); - } - - public function buildView(FormView $view, FormInterface $form, array $options): void - { - // Index insurances by ID for template lookup - $insurances = []; - foreach ($options['insurances'] as $insurance) { - if ($insurance instanceof Insurance) { - $insurances[(string) $insurance->id] = $insurance; - } - } - - // Pass Insurance objects to template indexed by choice value - $view->vars['insurances'] = $insurances; - } - - public function getParent(): string - { - return ChoiceType::class; - } - - public function getBlockPrefix(): string - { - return 'insurance_choice'; - } -} diff --git a/src/Form/Model/ParticipantDto.php b/src/Form/Model/ParticipantDto.php index 09d7dd1..4eadb91 100644 --- a/src/Form/Model/ParticipantDto.php +++ b/src/Form/Model/ParticipantDto.php @@ -186,7 +186,7 @@ class ParticipantDto * * @return bool True if an insurance is selected */ - public function hasInsurance(): bool + public function hasInsuranceSelected(): bool { return null !== $this->insurance; } diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index be225ca..87a6524 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace App\Form\Service; use App\BusProNet\Constants; +use App\BusProNet\Model\Insurance; use App\BusProNet\Model\Pickup; use App\BusProNet\Model\Service; use App\BusProNet\Utility\DirectionMapper; @@ -45,13 +46,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider * @param ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory Factory for creating room choice loaders * @param ServiceAvailabilityCalculator $serviceAvailabilityCalculator Service for calculating dynamic availability * @param InsuranceMatchingService $insuranceMatchingService Service for matching insurances to participants - * @param UrlGeneratorInterface $urlGenerator URL generator for HTMX endpoints */ public function __construct( private readonly ParticipantRoomChoiceLoaderFactory $roomChoiceLoaderFactory, private readonly ServiceAvailabilityCalculator $serviceAvailabilityCalculator, private readonly InsuranceMatchingService $insuranceMatchingService, - private readonly UrlGeneratorInterface $urlGenerator, ) { parent::__construct(); } @@ -435,7 +434,18 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider 'multiple' => false, 'expanded' => true, 'required' => false, - 'insurances' => $this->getEligibleInsurances($bookingDto, $participantIndex), + 'choices' => $this->getEligibleInsurances($bookingDto, $participantIndex), + 'choice_value' => 'id', + 'choice_label' => function (?Insurance $insurance) { + $label = $insurance->label; + + if (null !== $insurance->price && $insurance->price > 0) { + $label .= sprintf(' (€%s)', number_format($insurance->price, 2, ',', '.')); + } + + return $label; + }, + 'placeholder' => 'Keine Versicherung gewünscht', ]; // Future field providers would be added here, for example: diff --git a/src/Form/Service/ParticipantInsuranceFieldHandler.php b/src/Form/Service/ParticipantInsuranceFieldHandler.php index 6cf2b54..14abe92 100644 --- a/src/Form/Service/ParticipantInsuranceFieldHandler.php +++ b/src/Form/Service/ParticipantInsuranceFieldHandler.php @@ -76,7 +76,7 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler /** * Determines if this handler should process the field based on submitted data. * - * Insurance handler should NOT process in edit mode because the BPN API does not + * Insurance handler should NOT procefss in edit mode because the BPN API does not * return insurance data. In edit mode, insurance data must be preserved as-is * and passed through to the update endpoint unchanged. * @@ -172,8 +172,6 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler // Insurance not found - clear selection $participant->insurance = null; } - - return; } // Handle form resubmission with existing insurance (automatic reassignment check) @@ -241,15 +239,15 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler /** * Finds an insurance by ID from the available insurances array. * - * @param array $insurances Array of available insurances - * @param string|int $insuranceId The insurance ID to find + * @param array $insurances Array of available insurances + * @param string $insuranceId The insurance ID to find * * @return Insurance|null The found insurance or null if not found */ - private function findInsuranceById(array $insurances, string|int $insuranceId): ?Insurance + private function findInsuranceById(array $insurances, string $insuranceId): ?Insurance { foreach ($insurances as $insurance) { - if ($insurance->id === $insuranceId || (string) $insurance->id === (string) $insuranceId) { + if ($insurance->id === $insuranceId) { return $insurance; } } @@ -268,7 +266,7 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler private function isInsuranceInList(Insurance $targetInsurance, array $insuranceList): bool { foreach ($insuranceList as $insurance) { - if ($insurance->id === $targetInsurance->id || (string) $insurance->id === (string) $targetInsurance->id) { + if ($insurance->id === $targetInsurance->id) { return true; } } @@ -282,14 +280,14 @@ class ParticipantInsuranceFieldHandler extends AbstractParticipantFieldHandler * This is used to distinguish between a new user selection and a form resubmission * with the existing insurance selection (e.g., when user adds rentals that change travel price). * - * @param string|int $selectedInsuranceId The insurance ID from form submission + * @param string $selectedInsuranceId The insurance ID from form submission * @param Insurance $currentInsurance The currently assigned insurance from DTO * * @return bool True if they represent the same insurance */ - private function isSameInsurance(string|int $selectedInsuranceId, Insurance $currentInsurance): bool + private function isSameInsurance(string $selectedInsuranceId, Insurance $currentInsurance): bool { - return (string) $currentInsurance->id === (string) $selectedInsuranceId; + return $currentInsurance->id === $selectedInsuranceId; } /** diff --git a/src/Form/Service/ParticipantSkiPassFieldHandler.php b/src/Form/Service/ParticipantSkiPassFieldHandler.php index b78802c..8ee764f 100644 --- a/src/Form/Service/ParticipantSkiPassFieldHandler.php +++ b/src/Form/Service/ParticipantSkiPassFieldHandler.php @@ -79,10 +79,9 @@ class ParticipantSkiPassFieldHandler extends AbstractParticipantFieldHandler * selection. If the skipass is no longer appropriate for the participant's * age or exceeds the travel date range, it is automatically cleared. * - * @param array $submittedData The submitted participant form data - * @param string $mode The booking mode (BookingDto::MODE_CREATE or MODE_EDIT) - * @param BookingDto $bookingDto The booking DTO to update (create or edit) - * @param int $participantIndex The index of the participant being processed + * @param array $submittedData The submitted participant form data + * @param BookingDto $bookingDto The booking DTO to update (create or edit) + * @param int $participantIndex The index of the participant being processed */ public function processField(array $submittedData, BookingDto $bookingDto, int $participantIndex): void { diff --git a/src/Form/Service/ParticipantTransportationInboundFieldHandler.php b/src/Form/Service/ParticipantTransportationInboundFieldHandler.php index 8fb1171..e5566fa 100644 --- a/src/Form/Service/ParticipantTransportationInboundFieldHandler.php +++ b/src/Form/Service/ParticipantTransportationInboundFieldHandler.php @@ -68,9 +68,6 @@ class ParticipantTransportationInboundFieldHandler extends AbstractParticipantFi // Update participant with validated selection $participant->transportationInbound = $validSelection; - - // Backward compatibility: also update deprecated property - $participant->transportationServiceFro = $validSelection; } /** diff --git a/src/Form/Service/ParticipantTransportationOutboundFieldHandler.php b/src/Form/Service/ParticipantTransportationOutboundFieldHandler.php index f289a2f..1222a97 100644 --- a/src/Form/Service/ParticipantTransportationOutboundFieldHandler.php +++ b/src/Form/Service/ParticipantTransportationOutboundFieldHandler.php @@ -68,9 +68,6 @@ class ParticipantTransportationOutboundFieldHandler extends AbstractParticipantF // Update participant with validated selection $participant->transportationOutbound = $validSelection; - - // Backward compatibility: also update deprecated property - $participant->transportationServiceTo = $validSelection; } /** diff --git a/src/Service/ParticipantCardDataService.php b/src/Service/ParticipantCardDataService.php index f18012d..24b06ea 100644 --- a/src/Service/ParticipantCardDataService.php +++ b/src/Service/ParticipantCardDataService.php @@ -75,7 +75,7 @@ class ParticipantCardDataService $name = trim($firstName.' '.$lastName); if ('' === $name) { - return sprintf('Teilnehmer %d', $index + 1); + return 0 === $index ? 'Anmelder:in' : sprintf('Teilnehmer:in %d', $index); } return $name; diff --git a/templates/booking/create/step_4.html.twig b/templates/booking/create/step_4.html.twig index e647266..4bdaa4e 100644 --- a/templates/booking/create/step_4.html.twig +++ b/templates/booking/create/step_4.html.twig @@ -5,20 +5,6 @@ {% block content %}

Neue Buchung

- {% if bookingCreateDto.bookingStatus == 'A' %} -
-
- - - -
-

Achtung: Buchung auf Anfrage

-

Diese Buchung erfolgt auf Anfrage. Nach der Absendung wird deine Anfrage geprüft. Du erhältst anschließend eine verbindliche Buchungsbestätigung oder eine Absage per E-Mail.

-
-
-
- {% endif %} -
{% include '_partials/_flashes.html.twig' %} @@ -313,4 +299,4 @@ {{ form_end(form) }}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php b/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php index a5e0b7a..a95233e 100644 --- a/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php +++ b/tests/BusProNet/DataProcessor/BookingDataProcessorTest.php @@ -16,6 +16,7 @@ use App\BusProNet\Model\Service; use App\BusProNet\Model\Travel; use App\Form\Model\BookingDto; use App\Form\Model\ParticipantDto; +use App\Service\InsuranceMatchingService; use PHPUnit\Framework\TestCase; /** @@ -30,7 +31,10 @@ class BookingDataProcessorTest extends TestCase protected function setUp(): void { - $this->processor = new BookingDataProcessor(); + // Create a mock InsuranceMatchingService + $insuranceMatchingService = $this->createMock(InsuranceMatchingService::class); + + $this->processor = new BookingDataProcessor($insuranceMatchingService); } public function testCreateUpdateRequestPayloadWithCompleteData(): void