From a019c7dae0f1e1bbca0e6e94c9126f670f9f5dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 2 Sep 2025 17:52:42 +0200 Subject: [PATCH] wip: prepare transportation services --- ...SPORTATION_SERVICES_IMPLEMENTATION_PLAN.md | 872 ++++++++++++++++++ src/BusProNet/Utility/DirectionMapper.php | 118 +++ src/Form/Model/BookingEditDto.php | 30 +- src/Form/Model/ParticipantDto.php | 13 + 4 files changed, 1025 insertions(+), 8 deletions(-) create mode 100644 docs/TRANSPORTATION_SERVICES_IMPLEMENTATION_PLAN.md create mode 100644 src/BusProNet/Utility/DirectionMapper.php diff --git a/docs/TRANSPORTATION_SERVICES_IMPLEMENTATION_PLAN.md b/docs/TRANSPORTATION_SERVICES_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..e98563b --- /dev/null +++ b/docs/TRANSPORTATION_SERVICES_IMPLEMENTATION_PLAN.md @@ -0,0 +1,872 @@ +# 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 \ No newline at end of file diff --git a/src/BusProNet/Utility/DirectionMapper.php b/src/BusProNet/Utility/DirectionMapper.php new file mode 100644 index 0000000..fbf2790 --- /dev/null +++ b/src/BusProNet/Utility/DirectionMapper.php @@ -0,0 +1,118 @@ + 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. + * + * @param string $bookingDirection The booking direction code ('H' or 'R') + * + * @return string The travel direction code ('HIN' or 'RUECK') + * + * @throws \InvalidArgumentException When direction code is not recognized + */ + 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"), + }; + } + + /** + * Gets all outbound direction codes. + * + * @return string[] Array of outbound direction codes in all formats + */ + public static function getOutboundCodes(): array + { + return [self::OUTBOUND_TRAVEL, self::OUTBOUND_BOOKING]; + } + + /** + * Gets all inbound direction codes. + * + * @return string[] Array of inbound direction codes in all formats + */ + public static function getInboundCodes(): array + { + return [self::INBOUND_TRAVEL, self::INBOUND_BOOKING]; + } + + /** + * Checks if a direction code is outbound. + * + * @param string $direction The direction code to check + * + * @return bool True if the direction is outbound, false otherwise + */ + public static function isOutbound(string $direction): bool + { + return in_array($direction, self::getOutboundCodes(), true); + } + + /** + * Checks if a direction code is inbound. + * + * @param string $direction The direction code to check + * + * @return bool True if the direction is inbound, false otherwise + */ + public static function isInbound(string $direction): bool + { + return in_array($direction, self::getInboundCodes(), true); + } + + /** + * Validates that a direction code is recognized. + * + * @param string $direction The direction code to validate + * + * @return bool True if the direction is valid, false otherwise + */ + public static function isValid(string $direction): bool + { + return self::isOutbound($direction) || self::isInbound($direction); + } +} diff --git a/src/Form/Model/BookingEditDto.php b/src/Form/Model/BookingEditDto.php index 0a936e7..0af129b 100644 --- a/src/Form/Model/BookingEditDto.php +++ b/src/Form/Model/BookingEditDto.php @@ -6,6 +6,7 @@ use App\BusProNet\Constants; use App\BusProNet\Model\Booking; use App\BusProNet\Model\PersonalData; use App\BusProNet\Model\Travel; +use App\BusProNet\Utility\DirectionMapper; use Symfony\Component\Validator\Constraints as Assert; class BookingEditDto implements BookingDtoInterface @@ -34,24 +35,37 @@ class BookingEditDto implements BookingDtoInterface $participantData->courses = $booking ->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_COURSES); - + // Skipass is single selection - take first item from array or null $skipasses = $booking ->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_SKI_PASS); $participantData->skiPass = !empty($skipasses) ? $skipasses[0] : null; - + $participantData->additionalServices = $booking ->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_ADDITIONAL); $participantData->board = $booking ->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_BOARD); $participantData->rentals = $booking ->getAdditionalServicesForParticipantByGroup($index, Constants::TOKEN_RENTALS); - // Different keys for direction used in booking data (H <=> HIN, R <=> RUECK)! - $participantData->transportationServiceTo = $booking - ->getTransportationServiceForParticipantAndDirection($index, 'H'); - $participantData->transportationServiceFro = $booking - ->getTransportationServiceForParticipantAndDirection($index, 'R'); - $participantData->pickup = $booking->getPickupForParticipant($index); + // Transportation services using improved direction mapping + // Direction mapping handles BusProNet's inconsistent codes (H <=> HIN, R <=> RUECK) + $outboundTransportation = $booking + ->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::OUTBOUND_BOOKING); + $inboundTransportation = $booking + ->getTransportationServiceForParticipantAndDirection($index, DirectionMapper::INBOUND_BOOKING); + + // Set new improved property names + $participantData->transportationOutbound = $outboundTransportation; + $participantData->transportationInbound = $inboundTransportation; + + // Backward compatibility: also set deprecated properties + $participantData->transportationServiceTo = $outboundTransportation; + $participantData->transportationServiceFro = $inboundTransportation; + + // Pickup handling (currently only supports outbound pickup) + $pickupOutbound = $booking->getPickupForParticipant($index); + $participantData->pickupOutbound = $pickupOutbound; + $participantData->pickup = $pickupOutbound; // Backward compatibility $instance->participants[$index] = $participantData; } diff --git a/src/Form/Model/ParticipantDto.php b/src/Form/Model/ParticipantDto.php index 4c07d45..1b311ed 100644 --- a/src/Form/Model/ParticipantDto.php +++ b/src/Form/Model/ParticipantDto.php @@ -51,6 +51,19 @@ class ParticipantDto public ?Service $skiPass = null; public array $board = []; public array $rentals = []; + + // Transportation services with improved naming (outbound/inbound) + public ?Service $transportationOutbound = null; + public ?Service $transportationInbound = null; + + // Pickup locations for each direction + public ?Pickup $pickupOutbound = null; + public ?Pickup $pickupInbound = null; + + // Parking service for self-organized transportation + public ?Service $parking = null; + + // Deprecated properties for backward compatibility - will be removed in future version public ?Service $transportationServiceTo = null; public ?Service $transportationServiceFro = null; public ?Pickup $pickup = null;