wip: service descriptions and car license plate field

This commit is contained in:
Björn Fromme
2025-09-10 12:53:33 +02:00
parent 195246af89
commit a3ea02a136
12 changed files with 623 additions and 5 deletions
+1
View File
@@ -86,3 +86,4 @@ services:
- 'App\Form\Service\ParticipantPickupInboundFieldHandler'
- 'App\Form\Service\ParticipantParkingFieldHandler'
- 'App\Form\Service\ParticipantRentalInsuranceFieldHandler'
- 'App\Form\Service\ParticipantLicensePlateFieldHandler'
+3
View File
@@ -93,4 +93,7 @@ class Service
#[Groups(['api:single'])]
public ?string $rawAgeConstraintData = null;
#[Groups(['api:single', 'api:list'])]
public ?string $description = null;
}
+12
View File
@@ -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);
+3 -1
View File
@@ -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) {
+3 -2
View File
@@ -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
@@ -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;
}
}
@@ -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:
@@ -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;
}
}
@@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace App\Form\Service;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;
/**
* Handles processing of the licensePlate field for booking participants.
*
* This handler manages license plate input for participants who have selected
* parking services. The license plate field is only visible when parking is
* selected, creating a dependency chain where license plate depends on parking.
*
* The field accepts any string value without format validation, allowing for
* international license plate formats and variations.
*
* Dependencies: parking (for field visibility)
*/
class ParticipantLicensePlateFieldHandler extends AbstractParticipantFieldHandler
{
/**
* Returns the form field name this handler processes.
*
* @return string The field name 'licensePlate'
*/
public function getFieldName(): string
{
return 'licensePlate';
}
/**
* Returns the field dependencies for proper processing order.
*
* This handler depends on parking because license plate is only relevant
* when parking services are selected.
*
* @return string[] Array containing 'parking' dependency
*/
public function getDependencies(): array
{
return ['parking'];
}
/**
* Determines if this handler should process the field based on submitted data.
*
* For license plate fields, we always need to process to handle cases where
* the license plate should be cleared when parking is deselected. This ensures
* the participant DTO is updated correctly when parking availability changes.
*
* @param array<string, mixed> $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<string, mixed> $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;
}
}
@@ -92,6 +92,7 @@ class ParticipantRentalInsuranceFieldHandler extends AbstractParticipantFieldHan
// If no rentals are selected, clear rental insurance data
$participant->rentalInsuranceSelected = false;
$participant->rentalInsurance = null;
return;
}
+11
View File
@@ -177,6 +177,17 @@
}) }}
</div>
{% endif %}
{% if participant.licensePlate is defined %}
<div class="mt-4">
{{ form_row(participant.licensePlate, {
'attr': {
'hx-trigger': 'change',
'hx-post': path('app_booking_create_step_2_refresh'),
'hx-swap': 'none'
}
}) }}
</div>
{% endif %}
</div>
<div>
@@ -0,0 +1,187 @@
<?php
declare(strict_types=1);
namespace App\Tests\BusProNet\XmlParser;
use App\BusProNet\XmlParser\TravelParser;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DomCrawler\Crawler;
class TravelParserTest extends TestCase
{
private TravelParser $parser;
protected function setUp(): void
{
$this->parser = new TravelParser();
}
public function testParseServiceDescriptions(): void
{
$xmlContent = '<?xml version="1.0" encoding="utf-8"?>
<reisen>
<reise id="1" idbuspro="2187" code="SSTMR" erstellt_am="27.08.2025 09:09:40">
<termin id="1" idbuspro="11946" idprodukt="2187" termin="01.01.2030" bis="06.01.2030" code="SSTMR010130" reiseart="F">
<text>Test Travel</text>
<abpreis>659,00</abpreis>
<lei_sonstiges>
<leistung id="1" idbuspro="183658" unterart="SON" termin="01.01.2030" bis="06.01.2030" automatisch_buchen="False" pflicht="False">
<text>Begleitperson (gemäß Gruppenregelung)</text>
<hinweis>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.</hinweis>
<preis>0,00</preis>
<status>Frei</status>
</leistung>
<leistung id="2" idbuspro="183659" unterart="SON" termin="01.01.2030" bis="06.01.2030" automatisch_buchen="False" pflicht="False">
<text>Bettwäsche-Set inkl. Handtuch</text>
<hinweis>Das Bettwäsche-Set umfasst Spannbettlaken, Kissen- und Deckenbezug. Beim Handtuch handelt es sich um ein Duschtuch (ca. 70x140cm).</hinweis>
<preis>13,90</preis>
<status>Frei</status>
</leistung>
<leistung id="3" idbuspro="183656" unterart="SPA" termin="01.01.2030" bis="06.01.2030" automatisch_buchen="False" pflicht="False">
<text>Skipass 6 Tage (Erwachsene) (Jahrgang 1926-2006) inkl. Anreisetag</text>
<hinweis_stamm>JG:1926-2006</hinweis_stamm>
<preis>59,00</preis>
<status>Frei</status>
</leistung>
<leistung id="4" idbuspro="183660" unterart="VPF" termin="01.01.2030" bis="01.01.2030" automatisch_buchen="False" pflicht="False">
<text>Frühstück am Anreisetag</text>
<preis>8,90</preis>
<status>Frei</status>
</leistung>
</lei_sonstiges>
<lei_befoerderung>
<leistung id="5" idbuspro="183649" unterart="BUS" termin="30.12.2029" bis="01.01.2030" automatisch_buchen="False" pflicht="True">
<text>Bus-Hinfahrt</text>
<hinweis>Bus fährt nur bei ausreichender Teilnehmerzahl</hinweis>
<preis>0,00</preis>
<status>Frei</status>
<richtung>HIN</richtung>
</leistung>
</lei_befoerderung>
<hotel id="1" code="SSTGR" idbuspro="157047" ankunft="01.01.2030" abreise="06.01.2030">
<zimmer>
<preis zimmercode="4erDW" idbuspro_zimmer="95" zimmertext="4er Zimmer Dusche/WC" MinPax="4" MaxPax="4" naechte="5" kat="GR" vpcode="Lt. Auss." idbuspro_vp="2" vptext="Laut Ausschreibung" preis="689,00" status="Frei" verfuegbar="8" />
</zimmer>
</hotel>
</termin>
</reise>
</reisen>';
$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 = '<?xml version="1.0" encoding="utf-8"?>
<reisen>
<reise id="1" idbuspro="2187" code="SSTMR">
<termin id="1" idbuspro="11946" termin="01.01.2030" bis="06.01.2030" reiseart="F">
<text>Test Travel</text>
<abpreis>659,00</abpreis>
<lei_sonstiges>
<leistung id="1" idbuspro="183660" unterart="VPF">
<text>Service without description</text>
<preis>8,90</preis>
<status>Frei</status>
</leistung>
</lei_sonstiges>
<hotel id="1" idbuspro="157047">
<zimmer>
<preis zimmercode="4erDW" idbuspro_zimmer="95" zimmertext="4er Zimmer" MinPax="4" MaxPax="4" naechte="5" preis="689,00" status="Frei" verfuegbar="8" />
</zimmer>
</hotel>
</termin>
</reise>
</reisen>';
$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 = '<?xml version="1.0" encoding="utf-8"?>
<reisen>
<reise id="1" idbuspro="2187" code="SSTMR">
<termin id="1" idbuspro="11946" termin="01.01.2030" bis="06.01.2030" reiseart="F">
<text>Test Travel</text>
<abpreis>659,00</abpreis>
<lei_sonstiges>
<leistung id="1" idbuspro="183660" unterart="VPF">
<text>Service with empty description</text>
<hinweis></hinweis>
<preis>8,90</preis>
<status>Frei</status>
</leistung>
</lei_sonstiges>
<hotel id="1" idbuspro="157047">
<zimmer>
<preis zimmercode="4erDW" idbuspro_zimmer="95" zimmertext="4er Zimmer" MinPax="4" MaxPax="4" naechte="5" preis="689,00" status="Frei" verfuegbar="8" />
</zimmer>
</hotel>
</termin>
</reise>
</reisen>';
$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
}
}
@@ -0,0 +1,203 @@
<?php
declare(strict_types=1);
namespace App\Tests\Form\Service;
use App\BusProNet\Model\Travel;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\ParticipantDto;
use App\Form\Service\ParticipantLicensePlateFieldHandler;
use PHPUnit\Framework\TestCase;
class ParticipantLicensePlateFieldHandlerTest extends TestCase
{
private ParticipantLicensePlateFieldHandler $handler;
protected function setUp(): void
{
$this->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
}
}