wip: service descriptions and car license plate field
This commit is contained in:
@@ -86,3 +86,4 @@ services:
|
||||
- 'App\Form\Service\ParticipantPickupInboundFieldHandler'
|
||||
- 'App\Form\Service\ParticipantParkingFieldHandler'
|
||||
- 'App\Form\Service\ParticipantRentalInsuranceFieldHandler'
|
||||
- 'App\Form\Service\ParticipantLicensePlateFieldHandler'
|
||||
|
||||
@@ -93,4 +93,7 @@ class Service
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public ?string $rawAgeConstraintData = null;
|
||||
|
||||
#[Groups(['api:single', 'api:list'])]
|
||||
public ?string $description = null;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user