# Transportation Services Implementation Plan ## Overview Implement comprehensive transportation services for the MyEP Next Booking system, supporting bus and self-organized (car/PKW) transportation with directional pickup selection, discount handling, and optional parking services. This implementation addresses BusProNet's inconsistent direction naming conventions while following established architectural patterns. ## Current State Analysis ### Existing Infrastructure ✅ **Transportation Data Model:** - `Service` model has `direction`, `subType`, `price` properties - `Travel` model has `transportationServices[]`, `pickupsTo[]`, `pickupsFro[]` - `Booking` model supports transportation service mapping - XML parsing handles transportation services and pickups - Data processing includes transportation service management **Form System Integration:** - `ParticipantDto` has transportation and pickup properties - Field handler registry supports service processing - Conditional field state system available - HTMX integration for real-time updates - Pricing integration system in place ### Direction Naming Inconsistencies 🔍 **Problem Identified:** BusProNet uses inconsistent direction codes across different contexts: 1. **Travel Data Context:** `'HIN'` and `'RUECK'` (full German words) 2. **Booking Data Context:** `'H'` and `'R'` (single letter abbreviations) 3. **Internal Properties:** `To`/`Fro` (archaic English) **Evidence:** - Comment in `BookingEditDto.php`: `"Different keys for direction used in booking data (H <=> HIN, R <=> RUECK)!"` - `Travel->getTransportationServicesByDirection('HIN'/'RUECK')` - `Booking->getTransportationServiceForParticipantAndDirection($index, 'H'/'R')` ### Missing Components 🚧 - Direction mapping utility for consistency - Transportation field handlers and options providers - Conditional pickup field logic (only show when bus selected) - Parking service integration (subtype PAR) - Modern English property naming (Outbound/Inbound) ## Implementation Strategy ### Phase 1: Foundation - Direction Mapping & Naming 🎯 #### 1.1 Create Direction Mapping Utility **File:** `src/BusProNet/Utility/DirectionMapper.php` ```php self::OUTBOUND_BOOKING, self::INBOUND_TRAVEL => self::INBOUND_BOOKING, default => throw new \InvalidArgumentException("Unknown travel direction: $travelDirection") }; } /** * Maps booking direction code to travel direction code. */ public static function bookingToTravel(string $bookingDirection): string { return match($bookingDirection) { self::OUTBOUND_BOOKING => self::OUTBOUND_TRAVEL, self::INBOUND_BOOKING => self::INBOUND_TRAVEL, default => throw new \InvalidArgumentException("Unknown booking direction: $bookingDirection") }; } /** * Maps direction code to English name. */ public static function toEnglish(string $direction): string { return match($direction) { self::OUTBOUND_TRAVEL, self::OUTBOUND_BOOKING => self::OUTBOUND, self::INBOUND_TRAVEL, self::INBOUND_BOOKING => self::INBOUND, default => throw new \InvalidArgumentException("Unknown direction: $direction") }; } /** * Gets all outbound direction codes. */ public static function getOutboundCodes(): array { return [self::OUTBOUND_TRAVEL, self::OUTBOUND_BOOKING]; } /** * Gets all inbound direction codes. */ public static function getInboundCodes(): array { return [self::INBOUND_TRAVEL, self::INBOUND_BOOKING]; } } ``` #### 1.2 Update ParticipantDto Properties **Current Properties (archaic naming):** ```php public ?Service $transportationServiceTo = null; public ?Service $transportationServiceFro = null; public ?Pickup $pickup = null; ``` **Updated Properties (modern English):** ```php public ?Service $transportationOutbound = null; // Maps to 'HIN'/'H' public ?Service $transportationInbound = null; // Maps to 'RUECK'/'R' public ?Pickup $pickupOutbound = null; // Maps to pickupsTo public ?Pickup $pickupInbound = null; // Maps to pickupsFro public ?Service $parking = null; // New parking service ``` #### 1.3 Update BookingEditDto Direction Mapping **Current Implementation:** ```php // Different keys for direction used in booking data (H <=> HIN, R <=> RUECK)! $participantData->transportationServiceTo = $booking ->getTransportationServiceForParticipantAndDirection($index, 'H'); $participantData->transportationServiceFro = $booking ->getTransportationServiceForParticipantAndDirection($index, 'R'); ``` **Updated Implementation:** ```php use App\BusProNet\Utility\DirectionMapper; $participantData->transportationOutbound = $booking ->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::OUTBOUND_BOOKING); $participantData->transportationInbound = $booking ->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::INBOUND_BOOKING); ``` ### Phase 2: Transportation Field Implementation 🚀 #### 2.1 Transportation Service Field Handlers **A. Outbound Transportation Handler** **File:** `src/Form/Service/ParticipantTransportationOutboundFieldHandler.php` ```php getParticipant($bookingDto, $participantIndex); if (null === $participant) { return; } $selectedTransportation = $this->getFieldValue($submittedData, $this->getFieldName()); // Get available outbound transportation services $availableServices = $bookingDto->travel->getTransportationServicesByDirection( DirectionMapper::OUTBOUND_TRAVEL, true // filter available ); // Validate and convert selection to Service object $validSelection = null; if (null !== $selectedTransportation) { if ($this->isServiceValidForParticipant($selectedTransportation, $availableServices, $bookingDto, $participantIndex)) { $validSelection = $this->findServiceInAvailableServices($selectedTransportation, $availableServices); } } // Update participant with validated selection $participant->transportationOutbound = $validSelection; } // ... validation methods similar to existing handlers } ``` **B. Inbound Transportation Handler** **File:** `src/Form/Service/ParticipantTransportationInboundFieldHandler.php` - Similar structure for inbound (RUECK) transportation - Field name: `transportationInbound` - Uses `DirectionMapper::INBOUND_TRAVEL` #### 2.2 Pickup Field Handlers **A. Outbound Pickup Handler** **File:** `src/Form/Service/ParticipantPickupOutboundFieldHandler.php` ```php getParticipant($bookingDto, $participantIndex); if (null === $participant) { return; } // Only process pickup if outbound transportation is bus if (null === $participant->transportationOutbound || 'BUS' !== $participant->transportationOutbound->subType) { $participant->pickupOutbound = null; // Clear pickup for non-bus transport return; } $selectedPickup = $this->getFieldValue($submittedData, $this->getFieldName()); // Validate pickup selection against available outbound pickups $validSelection = null; if (null !== $selectedPickup) { $availablePickups = $bookingDto->travel->pickupsTo; $validSelection = $this->findPickupInAvailable($selectedPickup, $availablePickups); } $participant->pickupOutbound = $validSelection; } // ... pickup validation methods } ``` **B. Inbound Pickup Handler** **File:** `src/Form/Service/ParticipantPickupInboundFieldHandler.php` - Similar structure for inbound pickup - Field name: `pickupInbound` - Depends on `transportationInbound` - Uses `travel->pickupsFro` #### 2.3 Parking Service Handler **File:** `src/Form/Service/ParticipantParkingFieldHandler.php` ```php getParticipant($bookingDto, $participantIndex); if (null === $participant) { return; } // Check if parking is applicable (at least one PKW direction) if (!$this->isParkingApplicable($participant)) { $participant->parking = null; // Clear parking for bus-only transport return; } $selectedParking = $this->getFieldValue($submittedData, $this->getFieldName()); // Get available parking services (subtype PAR) $availableParkingServices = $bookingDto->travel->getAdditionalServicesBySubTypes('PAR', true); $validSelection = null; if (null !== $selectedParking) { $validSelection = $this->findServiceInAvailableServices($selectedParking, $availableParkingServices); } $participant->parking = $validSelection; } private function isParkingApplicable($participant): bool { $outboundIsPkw = $participant->transportationOutbound?->subType === 'PKW'; $inboundIsPkw = $participant->transportationInbound?->subType === 'PKW'; return $outboundIsPkw || $inboundIsPkw; } } ``` #### 2.4 Field Options Provider Integration **Update:** `src/Form/Service/ParticipantFieldOptionsProvider.php` ```php protected function registerFieldOptionProviders(): void { // ... existing providers // Outbound Transportation $this->fieldOptionProviders['transportationOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Hinfahrt', 'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::OUTBOUND_TRAVEL), 'choice_label' => fn(Service $service) => $this->formatTransportationServiceLabel($service), 'choice_value' => 'id', 'expanded' => true, 'multiple' => false, 'required' => true, 'attr' => [ 'hx-post' => $this->urlGenerator->generate('booking_create_step_2_refresh'), 'hx-target' => '#booking-summary', 'hx-trigger' => 'change', ], ]; // Inbound Transportation $this->fieldOptionProviders['transportationInbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Rückfahrt', 'choices' => $bookingDto->travel->getTransportationServicesByDirection(DirectionMapper::INBOUND_TRAVEL), 'choice_label' => fn(Service $service) => $this->formatTransportationServiceLabel($service), 'choice_value' => 'id', 'expanded' => true, 'multiple' => false, 'required' => true, 'attr' => [ 'hx-post' => $this->urlGenerator->generate('booking_create_step_2_refresh'), 'hx-target' => '#booking-summary', 'hx-trigger' => 'change', ], ]; // Outbound Pickup (conditional) $this->fieldOptionProviders['pickupOutbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Zustieg Hinfahrt', 'choices' => $bookingDto->travel->pickupsTo, 'choice_label' => 'label', 'choice_value' => 'id', 'expanded' => false, // Dropdown for pickups 'multiple' => false, 'required' => true, 'placeholder' => 'Zustieg auswählen', ]; // Inbound Pickup (conditional) $this->fieldOptionProviders['pickupInbound'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Zustieg Rückfahrt', 'choices' => $bookingDto->travel->pickupsFro, 'choice_label' => 'label', 'choice_value' => 'id', 'expanded' => false, 'multiple' => false, 'required' => true, 'placeholder' => 'Zustieg auswählen', ]; // Parking (conditional) $this->fieldOptionProviders['parking'] = fn (BookingDtoInterface $bookingDto, int $participantIndex) => [ 'label' => 'Parkplatz', 'choices' => $bookingDto->travel->getAdditionalServicesBySubTypes('PAR', true), 'choice_label' => fn(Service $service) => $this->formatServiceLabelWithPrice($service), 'choice_value' => 'id', 'expanded' => true, 'multiple' => false, 'required' => false, ]; } /** * Format transportation service labels with type and pricing. */ private function formatTransportationServiceLabel(Service $service): string { $label = $service->label; // Add transportation type indicator $typeIndicator = match($service->subType) { 'BUS' => '🚌', 'PKW' => '🚗', default => '' }; if ($typeIndicator) { $label = $typeIndicator . ' ' . $label; } // Add pricing with discount indication if ($service->price > 0) { $label .= sprintf(' (+€%.2f)', $service->price); } elseif ($service->price < 0) { $label .= sprintf(' (-€%.2f Discount)', abs($service->price)); } // Add availability warning if limited if (null !== $service->available && $service->available <= 5) { $label .= sprintf(' (nur %d verfügbar)', $service->available); } return $label; } ``` ### Phase 3: Conditional Field States & UX 🎨 #### 3.1 Transportation Type Condition **File:** `src/Form/Service/Condition/TransportationTypeCondition.php` ```php getParticipant($participantIndex); if (null === $participant) { return false; } $transportationService = match($this->direction) { 'outbound' => $participant->transportationOutbound, 'inbound' => $participant->transportationInbound, default => null, }; if (null === $transportationService) { return false; } return $this->expectedType === $transportationService->subType; } public function getDependentFields(): array { return match($this->direction) { 'outbound' => ['transportationOutbound'], 'inbound' => ['transportationInbound'], default => [], }; } public function getDescription(): string { return sprintf('%s transportation is %s', ucfirst($this->direction), $this->expectedType); } } ``` #### 3.2 Update Field State Provider **Update:** `src/Form/Service/CreateFieldStateProvider.php` ```php use App\Form\Service\Condition\TransportationTypeCondition; use App\Form\Service\Condition\CompositeCondition; protected function registerFieldStateConditions(): void { // ... existing conditions // Show outbound pickup only when outbound transportation is BUS $this->fieldStateConditions['pickupOutbound'] = [ 'hidden' => CompositeCondition::not( new TransportationTypeCondition('outbound', 'BUS') ), ]; // Show inbound pickup only when inbound transportation is BUS $this->fieldStateConditions['pickupInbound'] = [ 'hidden' => CompositeCondition::not( new TransportationTypeCondition('inbound', 'BUS') ), ]; // Show parking only when at least one direction is PKW $this->fieldStateConditions['parking'] = [ 'hidden' => CompositeCondition::not( CompositeCondition::or( new TransportationTypeCondition('outbound', 'PKW'), new TransportationTypeCondition('inbound', 'PKW') ) ), ]; } ``` ### Phase 4: Form Integration & Service Configuration ⚙️ #### 4.1 Update Form Type **Update:** `src/Form/BookingCreateParticipantType.php` ```php // Add transportation fields to dynamic fields list $dynamicFields = [ 'assignedRoomId', 'courses', 'additionalServices', 'board', 'rentals', 'skiPass', 'transportationOutbound', // New 'transportationInbound', // New 'pickupOutbound', // New 'pickupInbound', // New 'parking', // New ]; ``` #### 4.2 Service Registration **Update:** `config/services.yaml` ```yaml # Transportation field handlers App\Form\Service\ParticipantTransportationOutboundFieldHandler: tags: - { name: 'app.participant_field_handler', field: 'transportationOutbound' } App\Form\Service\ParticipantTransportationInboundFieldHandler: tags: - { name: 'app.participant_field_handler', field: 'transportationInbound' } App\Form\Service\ParticipantPickupOutboundFieldHandler: tags: - { name: 'app.participant_field_handler', field: 'pickupOutbound' } App\Form\Service\ParticipantPickupInboundFieldHandler: tags: - { name: 'app.participant_field_handler', field: 'pickupInbound' } App\Form\Service\ParticipantParkingFieldHandler: tags: - { name: 'app.participant_field_handler', field: 'parking' } ``` #### 4.3 Constants Update **Update:** `src/BusProNet/Constants.php` ```php // Add parking token if not already defined public const TOKEN_PARKING = 'PAR'; // Transportation subtypes public const SUBTYPE_BUS = 'BUS'; public const SUBTYPE_PKW = 'PKW'; ``` ## UX Design & User Experience 🎯 ### Section Organization **Transportation will be organized in logical sections:** ```html

🚌 Hinfahrt (Outbound Transportation)

{{ form_row(form.transportationOutbound) }}
{{ form_row(form.pickupOutbound) }}

🚗 Rückfahrt (Inbound Transportation)

{{ form_row(form.transportationInbound) }}
{{ form_row(form.pickupInbound) }}

🅿️ Parken (Parking)

{{ form_row(form.parking) }}
``` ### Progressive Disclosure Features 1. **Smart Field Visibility:** - Pickup fields only appear when bus is selected - Parking only appears when PKW is selected - Smooth transitions using existing HTMX integration 2. **Visual Indicators:** - Transportation type icons (🚌 bus, 🚗 car) - Pricing with discount indicators - Availability warnings for limited services - Required field indicators 3. **Real-time Feedback:** - Pricing updates immediately - Pickup/parking fields show/hide smoothly - Booking summary reflects transportation selections - Validation feedback on selection changes ## Pricing Integration 💰 ### Transportation Service Pricing - **Bus Services:** Standard pricing per direction - **PKW (Self-organized):** Often negative prices (discounts) - **Parking:** Additional cost for PKW travelers - **Combined Pricing:** Total transportation cost = outbound + inbound + parking ### Service Label Examples - `🚌 Bus nach München (+€45,00)` - `🚗 Eigenanreise (-€20,00 Discount)` - `🅿️ Parkplatz Hotel (+€15,00)` - `🚌 Bus Hinfahrt (nur 3 verfügbar)` ## Data Flow & Validation 🔄 ### Form Submission Flow 1. **Transportation Selection:** User selects outbound/inbound transport 2. **Conditional Fields Update:** Pickup/parking fields show/hide via HTMX 3. **Field Handler Processing:** Services validated against age/availability 4. **Pricing Calculation:** Total transportation cost calculated 5. **Booking Summary Update:** Summary reflects all transportation selections ### Validation Rules - **Transportation Required:** Both directions must have transportation - **Pickup Required:** When bus is selected, pickup is mandatory - **Parking Optional:** Available only with PKW transportation - **Service Availability:** Validate against available quantities - **Date Constraints:** Services must be valid for travel dates ## Testing Strategy 🧪 ### Unit Testing Focus 1. **Direction Mapper:** Test all direction code conversions 2. **Field Handlers:** Test transportation/pickup processing logic 3. **Conditional States:** Test pickup/parking visibility logic 4. **Service Validation:** Test availability and age constraints ### Integration Testing 1. **Form Flow:** Complete transportation selection workflow 2. **HTMX Updates:** Real-time field visibility and pricing updates 3. **Data Processing:** Transportation data for BPN API submission 4. **Backward Compatibility:** Ensure existing booking edit still works ### Manual Testing Scenarios 1. **Bus Transportation:** Select bus both directions with pickups 2. **Mixed Transportation:** Bus one direction, PKW other direction 3. **PKW Transportation:** Self-organized both directions with parking 4. **Limited Availability:** Test behavior with limited service availability 5. **Discount Services:** Verify negative pricing for PKW options ## Migration Strategy 🔄 ### Backward Compatibility 1. **Property Mapping:** Update existing code using old property names 2. **Direction Constants:** Maintain compatibility with existing direction codes 3. **Data Import:** Handle existing bookings with old property structure 4. **API Consistency:** Ensure BPN XML submission uses correct direction codes ### Deployment Steps 1. **Phase 1:** Deploy direction mapper and updated properties 2. **Phase 2:** Deploy field handlers and form integration 3. **Phase 3:** Deploy UX improvements and conditional states 4. **Phase 4:** Deploy pricing integration and final testing ## Implementation Timeline 📅 ### Sprint 1: Foundation (Week 1) - ✅ Create documentation - ✅ Implement DirectionMapper utility - ✅ Update ParticipantDto properties - ✅ Update BookingEditDto mapping ### Sprint 2: Core Implementation (Week 2) - Create transportation field handlers - Create pickup field handlers - Add parking service handler - Update field options provider ### Sprint 3: UX & Integration (Week 3) - Implement conditional field states - Update form integration - Add transportation service configuration - Implement HTMX real-time updates ### Sprint 4: Testing & Polish (Week 4) - Add pricing integration - Comprehensive testing - UX refinements - Documentation updates ## Success Criteria ✅ ### Technical Success - [ ] Direction mapping handles all BPN inconsistencies correctly - [ ] Transportation services integrate with existing pricing system - [ ] Conditional pickup/parking fields work seamlessly - [ ] HTMX updates provide smooth UX - [ ] Field handlers follow established patterns - [ ] Backward compatibility maintained ### UX Success - [ ] Clear separation of outbound/inbound transportation - [ ] Progressive disclosure prevents overwhelming users - [ ] Visual indicators for discounts and availability - [ ] Real-time pricing feedback - [ ] Intuitive field organization - [ ] Mobile-responsive transportation selection ### Business Success - [ ] Support for complex transportation scenarios - [ ] Parking space booking integration - [ ] Discount management for self-organized travel - [ ] Data integrity for BPN API submission - [ ] Scalable architecture for future enhancements --- **Last Updated:** 2025-09-02 **Status:** 🚧 Implementation in Progress **Next Phase:** Direction Mapping & Naming Foundation