From a3ea02a136aa886994069abf9ca9429fdff17a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Wed, 10 Sep 2025 12:53:33 +0200 Subject: [PATCH] wip: service descriptions and car license plate field --- config/services.yaml | 1 + src/BusProNet/Model/Service.php | 3 + src/BusProNet/XmlParser/TravelParser.php | 12 ++ src/Form/BookingCreateParticipantType.php | 4 +- src/Form/Model/ParticipantDto.php | 7 +- src/Form/Service/CreateFieldStateProvider.php | 5 + .../ParticipantFieldOptionsProvider.php | 74 +++++++ .../ParticipantLicensePlateFieldHandler.php | 118 ++++++++++ ...ParticipantRentalInsuranceFieldHandler.php | 3 +- templates/booking/create_step_2.html.twig | 11 + .../BusProNet/XmlParser/TravelParserTest.php | 187 ++++++++++++++++ ...articipantLicensePlateFieldHandlerTest.php | 203 ++++++++++++++++++ 12 files changed, 623 insertions(+), 5 deletions(-) create mode 100644 src/Form/Service/ParticipantLicensePlateFieldHandler.php create mode 100644 tests/BusProNet/XmlParser/TravelParserTest.php create mode 100644 tests/Form/Service/ParticipantLicensePlateFieldHandlerTest.php diff --git a/config/services.yaml b/config/services.yaml index 6e58be4..3a1f526 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -86,3 +86,4 @@ services: - 'App\Form\Service\ParticipantPickupInboundFieldHandler' - 'App\Form\Service\ParticipantParkingFieldHandler' - 'App\Form\Service\ParticipantRentalInsuranceFieldHandler' + - 'App\Form\Service\ParticipantLicensePlateFieldHandler' diff --git a/src/BusProNet/Model/Service.php b/src/BusProNet/Model/Service.php index 077c661..ea6c30d 100644 --- a/src/BusProNet/Model/Service.php +++ b/src/BusProNet/Model/Service.php @@ -93,4 +93,7 @@ class Service #[Groups(['api:single'])] public ?string $rawAgeConstraintData = null; + + #[Groups(['api:single', 'api:list'])] + public ?string $description = null; } diff --git a/src/BusProNet/XmlParser/TravelParser.php b/src/BusProNet/XmlParser/TravelParser.php index 4a9966b..503bab1 100644 --- a/src/BusProNet/XmlParser/TravelParser.php +++ b/src/BusProNet/XmlParser/TravelParser.php @@ -136,6 +136,12 @@ class TravelParser extends AbstractParser ->stringToFloat($this->getStringOrNullValue($serviceNode->filterXPath('//preis'))); $service->status = $this->getStringOrNullValue($serviceNode->filterXPath('//status')); + // Parse optional description from hinweis node + $description = $this->getStringOrNullValue($serviceNode->filterXPath('//hinweis')); + if (null !== $description && '' !== trim($description)) { + $service->description = $description; + } + // Parse age constraints $this->parseServiceAgeConstraints($serviceNode, $service); @@ -173,6 +179,12 @@ class TravelParser extends AbstractParser $service->direction = $this->getStringOrNullValue($serviceNode->filterXPath('//richtung')); $service->status = $this->getStringOrNullValue($serviceNode->filterXPath('//status')); + // Parse optional description from hinweis node + $description = $this->getStringOrNullValue($serviceNode->filterXPath('//hinweis')); + if (null !== $description && '' !== trim($description)) { + $service->description = $description; + } + if (null !== $timeFrom = $serviceNode->attr('uhrzeit_von')) { $service->timeFrom = $timeFrom; $service->dayTime = (new DayTimeUtility())->mapTime($timeFrom); diff --git a/src/Form/BookingCreateParticipantType.php b/src/Form/BookingCreateParticipantType.php index 1d1efcb..d8240f5 100644 --- a/src/Form/BookingCreateParticipantType.php +++ b/src/Form/BookingCreateParticipantType.php @@ -180,6 +180,7 @@ class BookingCreateParticipantType extends AbstractType 'pickupOutbound', 'pickupInbound', 'parking', + 'licensePlate', ]; foreach ($dynamicFields as $fieldName) { @@ -211,7 +212,7 @@ class BookingCreateParticipantType extends AbstractType $this->addBaseFields($form, $bookingDto, $participantIndex); // Rebuild dynamic fields - $dynamicFields = ['assignedRoomId', 'remarksRoom', 'courses', 'additionalServices', 'board', 'rentals', 'rentalInsurance', 'skiPass', 'transportationOutbound', 'transportationInbound', 'pickupOutbound', 'pickupInbound', 'parking']; + $dynamicFields = ['assignedRoomId', 'remarksRoom', 'courses', 'additionalServices', 'board', 'rentals', 'rentalInsurance', 'skiPass', 'transportationOutbound', 'transportationInbound', 'pickupOutbound', 'pickupInbound', 'parking', 'licensePlate']; foreach ($dynamicFields as $fieldName) { if ($form->has($fieldName)) { $form->remove($fieldName); @@ -241,6 +242,7 @@ class BookingCreateParticipantType extends AbstractType 'pickupOutbound' => ChoiceType::class, 'pickupInbound' => ChoiceType::class, 'parking' => CheckboxType::class, + 'licensePlate' => TextType::class, ]; foreach ($dynamicFields as $fieldName => $fieldType) { diff --git a/src/Form/Model/ParticipantDto.php b/src/Form/Model/ParticipantDto.php index 6705a4f..45710df 100644 --- a/src/Form/Model/ParticipantDto.php +++ b/src/Form/Model/ParticipantDto.php @@ -7,7 +7,6 @@ use App\BusProNet\Model\Pickup; use App\BusProNet\Model\Service; use App\Validator\Constraints as AppAssert; use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Validator\Context\ExecutionContextInterface; #[AppAssert\Participant(groups: ['booking_edit'])] class ParticipantDto @@ -51,7 +50,7 @@ class ParticipantDto public array $board = []; public array $rentals = []; public ?Service $rentalInsurance = null; - + // Rental insurance checkbox state (boolean: true if rental insurance requested) public bool $rentalInsuranceSelected = false; @@ -69,6 +68,9 @@ class ParticipantDto // Parking service object for pricing calculation (new, contains actual service with pricing) public ?Service $parkingService = null; + // License plate for participants with parking (optional, visible only when parking is selected) + public ?string $licensePlate = null; + // Deprecated properties for backward compatibility - will be removed in future version public ?Service $transportationServiceTo = null; public ?Service $transportationServiceFro = null; @@ -105,5 +107,4 @@ class ParticipantDto { return 'O' === $this->status; } - } diff --git a/src/Form/Service/CreateFieldStateProvider.php b/src/Form/Service/CreateFieldStateProvider.php index 0577353..30b67c1 100644 --- a/src/Form/Service/CreateFieldStateProvider.php +++ b/src/Form/Service/CreateFieldStateProvider.php @@ -118,6 +118,11 @@ class CreateFieldStateProvider extends AbstractFieldStateProvider 'hidden' => CompositeCondition::not($rentalCondition), ]; + // Show license plate only when parking is selected (hidden by default) + $this->fieldStateConditions['licensePlate'] = [ + 'hidden' => FieldValueCondition::equals('parking', false), + ]; + // Example field state conditions would be registered here // For demonstration purposes, here are some example patterns: diff --git a/src/Form/Service/ParticipantFieldOptionsProvider.php b/src/Form/Service/ParticipantFieldOptionsProvider.php index 3b00dea..68c093f 100644 --- a/src/Form/Service/ParticipantFieldOptionsProvider.php +++ b/src/Form/Service/ParticipantFieldOptionsProvider.php @@ -108,6 +108,20 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider ), 'choice_value' => 'id', 'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service), + 'choice_attr' => function (?Service $service) { + if (null === $service) { + return []; + } + + $attributes = []; + + // Add service description as data attribute for frontend use + if (null !== $service->description && '' !== trim($service->description)) { + $attributes['data-description'] = $service->description; + } + + return $attributes; + }, ]; // Additional services field provider - provides age-appropriate additional services with mandatory pre-selection @@ -141,6 +155,11 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider $attributes['title'] = 'Diese Leistung ist nicht abwählbar'; } + // Add service description as data attribute for frontend use + if (null !== $service->description && '' !== trim($service->description)) { + $attributes['data-description'] = $service->description; + } + return $attributes; }, ]; @@ -181,6 +200,20 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider ), 'choice_value' => 'id', 'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service), + 'choice_attr' => function (?Service $service) { + if (null === $service) { + return []; + } + + $attributes = []; + + // Add service description as data attribute for frontend use + if (null !== $service->description && '' !== trim($service->description)) { + $attributes['data-description'] = $service->description; + } + + return $attributes; + }, ]; // Rental insurance field provider - provides rental insurance options when rental services are selected @@ -188,6 +221,19 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider 'label' => $this->getRentalInsuranceCheckboxLabel($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true, true)), 'required' => false, 'property_path' => 'rentalInsuranceSelected', + 'help' => $this->getRentalInsuranceDescription($bookingDto->travel->getAdditionalServicesBySubTypes(Constants::TOKEN_RENTAL_INSURANCE, true, true)), + ]; + + // License plate field provider - provides text input for vehicle license plate when parking is selected + $this->fieldOptionProviders['licensePlate'] = fn (BookingDtoInterface $bookingDto, int $participantIndex, array $options = []) => [ + 'label' => 'Kennzeichen', + 'required' => false, + 'attr' => [ + 'placeholder' => 'z.B. AB-CD 123', + 'maxlength' => 20, + ], + 'help' => 'Bitte gib das Kennzeichen deines Fahrzeugs an. Du kannst es aber auch später nachreichen.', + 'clean_xss' => true, ]; // Skipass field provider - provides age-appropriate skipass options from travel data filtered by date range @@ -207,6 +253,20 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider ), 'choice_value' => 'id', 'choice_label' => fn (?Service $service) => $this->formatServiceLabelWithPrice($service), + 'choice_attr' => function (?Service $service) { + if (null === $service) { + return []; + } + + $attributes = []; + + // Add service description as data attribute for frontend use + if (null !== $service->description && '' !== trim($service->description)) { + $attributes['data-description'] = $service->description; + } + + return $attributes; + }, ]; // Room remarks field provider - provides textarea for room-specific remarks (only for 'mbz' rooms) @@ -494,6 +554,20 @@ class ParticipantFieldOptionsProvider extends AbstractFieldOptionsProvider return 'Leihmaterial-Versicherung'; } $rentalInsuranceService = reset($rentalInsuranceServices); // Get the first (and only) rental insurance service + return $this->formatServiceLabelWithPrice($rentalInsuranceService); } + + /** + * Gets the rental insurance description for help text. + */ + private function getRentalInsuranceDescription(array $rentalInsuranceServices): ?string + { + if (empty($rentalInsuranceServices)) { + return null; + } + $rentalInsuranceService = reset($rentalInsuranceServices); // Get the first (and only) rental insurance service + + return $rentalInsuranceService->description; + } } diff --git a/src/Form/Service/ParticipantLicensePlateFieldHandler.php b/src/Form/Service/ParticipantLicensePlateFieldHandler.php new file mode 100644 index 0000000..503186d --- /dev/null +++ b/src/Form/Service/ParticipantLicensePlateFieldHandler.php @@ -0,0 +1,118 @@ + $submittedData The submitted participant form data + * @param int $participantIndex The index of the participant being processed + * + * @return bool Always returns true for license plate fields + */ + public function shouldProcess(array $submittedData, int $participantIndex): bool + { + return true; // Always process to handle deselection cases + } + + /** + * Processes the licensePlate field for a specific participant. + * + * This method extracts the license plate value from submitted form data + * and updates the participant DTO with the value. If parking is not selected, + * the license plate is automatically cleared to maintain data consistency. + * + * @param array $submittedData The submitted participant form data + * @param BookingDtoInterface $bookingDto The booking DTO to update (create or edit) + * @param int $participantIndex The index of the participant being processed + */ + public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void + { + // Safely get the participant object, returning early if not found + $participant = $this->getParticipant($bookingDto, $participantIndex); + if (null === $participant) { + return; + } + + // Check if parking is selected - license plate only relevant with parking + if (false === $participant->parking) { + // If parking is not selected, clear license plate data + $participant->licensePlate = null; + + return; + } + + // Extract license plate value from submitted data + $licensePlateValue = $this->getFieldValue($submittedData, $this->getFieldName()); + + // Store the license plate value (null if empty/not provided) + $participant->licensePlate = $this->sanitizeLicensePlateValue($licensePlateValue); + } + + /** + * Sanitizes the license plate value. + * + * Handles basic cleanup of the license plate input by trimming whitespace + * and converting empty strings to null for consistent data handling. + * + * @param mixed $value The raw license plate value from form submission + * + * @return string|null The sanitized license plate value, or null if empty + */ + private function sanitizeLicensePlateValue(mixed $value): ?string + { + if (null === $value || false === is_string($value)) { + return null; + } + + $trimmedValue = trim($value); + + return '' === $trimmedValue ? null : $trimmedValue; + } +} diff --git a/src/Form/Service/ParticipantRentalInsuranceFieldHandler.php b/src/Form/Service/ParticipantRentalInsuranceFieldHandler.php index 3d9baad..07f0415 100644 --- a/src/Form/Service/ParticipantRentalInsuranceFieldHandler.php +++ b/src/Form/Service/ParticipantRentalInsuranceFieldHandler.php @@ -87,11 +87,12 @@ class ParticipantRentalInsuranceFieldHandler extends AbstractParticipantFieldHan // Check if rental insurance should be visible based on rental selections $hasRentals = false === empty($participant->rentals); - + if (false === $hasRentals) { // If no rentals are selected, clear rental insurance data $participant->rentalInsuranceSelected = false; $participant->rentalInsurance = null; + return; } diff --git a/templates/booking/create_step_2.html.twig b/templates/booking/create_step_2.html.twig index 236634b..d191ce2 100644 --- a/templates/booking/create_step_2.html.twig +++ b/templates/booking/create_step_2.html.twig @@ -177,6 +177,17 @@ }) }} {% endif %} + {% if participant.licensePlate is defined %} +
+ {{ form_row(participant.licensePlate, { + 'attr': { + 'hx-trigger': 'change', + 'hx-post': path('app_booking_create_step_2_refresh'), + 'hx-swap': 'none' + } + }) }} +
+ {% endif %}
diff --git a/tests/BusProNet/XmlParser/TravelParserTest.php b/tests/BusProNet/XmlParser/TravelParserTest.php new file mode 100644 index 0000000..30caf67 --- /dev/null +++ b/tests/BusProNet/XmlParser/TravelParserTest.php @@ -0,0 +1,187 @@ +parser = new TravelParser(); + } + + public function testParseServiceDescriptions(): void + { + $xmlContent = ' + + + + Test Travel + 659,00 + + + Begleitperson (gemäß Gruppenregelung) + Gilt für Betreuende, Lehrkräfte, Gruppenleitungen oder andere berechtigte Begleitpersonen laut Buchungsregelung. Bei Unklarheiten bitte zunächst ohne Angabe buchen – eine spätere Ergänzung ist möglich. + 0,00 + Frei + + + Bettwäsche-Set inkl. Handtuch + Das Bettwäsche-Set umfasst Spannbettlaken, Kissen- und Deckenbezug. Beim Handtuch handelt es sich um ein Duschtuch (ca. 70x140cm). + 13,90 + Frei + + + Skipass 6 Tage (Erwachsene) (Jahrgang 1926-2006) inkl. Anreisetag + JG:1926-2006 + 59,00 + Frei + + + Frühstück am Anreisetag + 8,90 + Frei + + + + + Bus-Hinfahrt + Bus fährt nur bei ausreichender Teilnehmerzahl + 0,00 + Frei + HIN + + + + + + + + + +'; + + $crawler = new Crawler($xmlContent); + $travelNode = $crawler->filterXPath('//reise/termin')->first(); + $travel = $this->parser->parse($travelNode); + + // Test additional services descriptions + $additionalServices = $travel->additionalServices; + + // Service with description (ID 183658 - Begleitperson) + $this->assertArrayHasKey(183658, $additionalServices); + $begleitpersonService = $additionalServices[183658]; + $this->assertSame('Begleitperson (gemäß Gruppenregelung)', $begleitpersonService->label); + $this->assertSame( + 'Gilt für Betreuende, Lehrkräfte, Gruppenleitungen oder andere berechtigte Begleitpersonen laut Buchungsregelung. Bei Unklarheiten bitte zunächst ohne Angabe buchen – eine spätere Ergänzung ist möglich.', + $begleitpersonService->description + ); + + // Service with description (ID 183659 - Bettwäsche) + $this->assertArrayHasKey(183659, $additionalServices); + $bettwascheService = $additionalServices[183659]; + $this->assertSame('Bettwäsche-Set inkl. Handtuch', $bettwascheService->label); + $this->assertSame( + 'Das Bettwäsche-Set umfasst Spannbettlaken, Kissen- und Deckenbezug. Beim Handtuch handelt es sich um ein Duschtuch (ca. 70x140cm).', + $bettwascheService->description + ); + + // Service without description (ID 183656 - Skipass, has hinweis_stamm but no hinweis) + $this->assertArrayHasKey(183656, $additionalServices); + $skipassService = $additionalServices[183656]; + $this->assertSame('Skipass 6 Tage (Erwachsene) (Jahrgang 1926-2006) inkl. Anreisetag', $skipassService->label); + $this->assertNull($skipassService->description); // No hinweis node for this service + $this->assertSame('JG:1926-2006', $skipassService->rawAgeConstraintData); // But has age constraints + + // Service without description (ID 183660 - Frühstück) + $this->assertArrayHasKey(183660, $additionalServices); + $fruehstueckService = $additionalServices[183660]; + $this->assertSame('Frühstück am Anreisetag', $fruehstueckService->label); + $this->assertNull($fruehstueckService->description); // No hinweis node + + // Test transportation services descriptions + $transportationServices = $travel->transportationServices; + $this->assertArrayHasKey(183649, $transportationServices); + $busService = $transportationServices[183649]; + $this->assertSame('Bus-Hinfahrt', $busService->label); + $this->assertSame('Bus fährt nur bei ausreichender Teilnehmerzahl', $busService->description); + } + + public function testParseServiceWithoutDescriptions(): void + { + $xmlContent = ' + + + + Test Travel + 659,00 + + + Service without description + 8,90 + Frei + + + + + + + + + +'; + + $crawler = new Crawler($xmlContent); + $travelNode = $crawler->filterXPath('//reise/termin')->first(); + $travel = $this->parser->parse($travelNode); + + $additionalServices = $travel->additionalServices; + $this->assertArrayHasKey(183660, $additionalServices); + $service = $additionalServices[183660]; + $this->assertSame('Service without description', $service->label); + $this->assertNull($service->description); // No hinweis node should result in null description + } + + public function testParseServiceWithEmptyDescription(): void + { + $xmlContent = ' + + + + Test Travel + 659,00 + + + Service with empty description + + 8,90 + Frei + + + + + + + + + +'; + + $crawler = new Crawler($xmlContent); + $travelNode = $crawler->filterXPath('//reise/termin')->first(); + $travel = $this->parser->parse($travelNode); + + $additionalServices = $travel->additionalServices; + $this->assertArrayHasKey(183660, $additionalServices); + $service = $additionalServices[183660]; + $this->assertSame('Service with empty description', $service->label); + $this->assertNull($service->description); // Empty hinweis should result in null description + } +} diff --git a/tests/Form/Service/ParticipantLicensePlateFieldHandlerTest.php b/tests/Form/Service/ParticipantLicensePlateFieldHandlerTest.php new file mode 100644 index 0000000..a2dfa78 --- /dev/null +++ b/tests/Form/Service/ParticipantLicensePlateFieldHandlerTest.php @@ -0,0 +1,203 @@ +handler = new ParticipantLicensePlateFieldHandler(); + } + + public function testGetFieldName(): void + { + $this->assertSame('licensePlate', $this->handler->getFieldName()); + } + + public function testGetDependencies(): void + { + $this->assertSame(['parking'], $this->handler->getDependencies()); + } + + public function testShouldProcessAlwaysReturnsTrue(): void + { + $this->assertTrue($this->handler->shouldProcess([], 0)); + $this->assertTrue($this->handler->shouldProcess(['some' => 'data'], 5)); + } + + public function testProcessFieldWithoutParticipant(): void + { + $travel = new Travel(); + $bookingDto = new BookingCreateDto($travel, 1); + $submittedData = ['licensePlate' => 'AB-CD 123']; + + $this->handler->processField($submittedData, $bookingDto, 0); + + // Should handle gracefully when participant doesn't exist + $this->expectNotToPerformAssertions(); + } + + public function testProcessFieldClearsLicensePlateWhenParkingNotSelected(): void + { + $participant = new ParticipantDto(); + $participant->parking = false; // Parking not selected + $participant->licensePlate = 'AB-CD 123'; // Should be cleared + + $travel = new Travel(); + $bookingDto = new BookingCreateDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['licensePlate' => 'XY-ZZ 999']; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertNull($participant->licensePlate); + } + + public function testProcessFieldSetsLicensePlateWhenParkingSelected(): void + { + $participant = new ParticipantDto(); + $participant->parking = true; // Parking selected + + $travel = new Travel(); + $bookingDto = new BookingCreateDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['licensePlate' => 'AB-CD 123']; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertSame('AB-CD 123', $participant->licensePlate); + } + + public function testProcessFieldHandlesEmptyLicensePlate(): void + { + $participant = new ParticipantDto(); + $participant->parking = true; // Parking selected + + $travel = new Travel(); + $bookingDto = new BookingCreateDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['licensePlate' => '']; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertNull($participant->licensePlate); + } + + public function testProcessFieldHandlesNullLicensePlate(): void + { + $participant = new ParticipantDto(); + $participant->parking = true; // Parking selected + + $travel = new Travel(); + $bookingDto = new BookingCreateDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['licensePlate' => null]; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertNull($participant->licensePlate); + } + + public function testProcessFieldTrimsWhitespace(): void + { + $participant = new ParticipantDto(); + $participant->parking = true; // Parking selected + + $travel = new Travel(); + $bookingDto = new BookingCreateDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['licensePlate' => ' AB-CD 123 ']; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertSame('AB-CD 123', $participant->licensePlate); + } + + public function testProcessFieldHandlesWhitespaceOnlyAsEmpty(): void + { + $participant = new ParticipantDto(); + $participant->parking = true; // Parking selected + + $travel = new Travel(); + $bookingDto = new BookingCreateDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = ['licensePlate' => ' ']; + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertNull($participant->licensePlate); + } + + public function testProcessFieldHandlesNonStringValues(): void + { + $participant = new ParticipantDto(); + $participant->parking = true; // Parking selected + + $travel = new Travel(); + $bookingDto = new BookingCreateDto($travel, 1); + $bookingDto->participants = [$participant]; + + // Test with integer value + $submittedData = ['licensePlate' => 123]; + $this->handler->processField($submittedData, $bookingDto, 0); + $this->assertNull($participant->licensePlate); + + // Test with array value + $submittedData = ['licensePlate' => ['invalid']]; + $this->handler->processField($submittedData, $bookingDto, 0); + $this->assertNull($participant->licensePlate); + } + + public function testProcessFieldHandlesMissingLicensePlateField(): void + { + $participant = new ParticipantDto(); + $participant->parking = true; // Parking selected + + $travel = new Travel(); + $bookingDto = new BookingCreateDto($travel, 1); + $bookingDto->participants = [$participant]; + + $submittedData = []; // No licensePlate field + + $this->handler->processField($submittedData, $bookingDto, 0); + + $this->assertNull($participant->licensePlate); + } + + public function testProcessFieldWithDifferentParticipantIndex(): void + { + $participant1 = new ParticipantDto(); + $participant1->parking = false; + + $participant2 = new ParticipantDto(); + $participant2->parking = true; + + $travel = new Travel(); + $bookingDto = new BookingCreateDto($travel, 1); + $bookingDto->participants = [$participant1, $participant2]; + + $submittedData = ['licensePlate' => 'AB-CD 123']; + + // Process for participant at index 1 + $this->handler->processField($submittedData, $bookingDto, 1); + + $this->assertNull($participant1->licensePlate); // Should not be affected + $this->assertSame('AB-CD 123', $participant2->licensePlate); // Should be set + } +}