Files
myep/docs/TRANSPORTATION_SERVICES_IMPLEMENTATION_PLAN.md
T

31 KiB

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

declare(strict_types=1);

namespace App\BusProNet\Utility;

/**
 * Handles direction mapping between BusProNet's inconsistent direction codes.
 * 
 * BusProNet uses different direction codes in different contexts:
 * - Travel data: 'HIN' (outbound), 'RUECK' (inbound) 
 * - Booking data: 'H' (outbound), 'R' (inbound)
 * 
 * This utility provides consistent mapping between formats.
 */
final class DirectionMapper 
{
    // Travel data format (full German words)
    public const OUTBOUND_TRAVEL = 'HIN';
    public const INBOUND_TRAVEL = 'RUECK';
    
    // Booking data format (single letter abbreviations)  
    public const OUTBOUND_BOOKING = 'H';
    public const INBOUND_BOOKING = 'R';
    
    // English naming for internal use
    public const OUTBOUND = 'outbound';
    public const INBOUND = 'inbound';
    
    /**
     * Maps travel direction code to booking direction code.
     */
    public static function travelToBooking(string $travelDirection): string
    {
        return match($travelDirection) {
            self::OUTBOUND_TRAVEL => 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):

public ?Service $transportationServiceTo = null;
public ?Service $transportationServiceFro = null;
public ?Pickup $pickup = null;

Updated Properties (modern English):

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:

// 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:

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

declare(strict_types=1);

namespace App\Form\Service;

use App\BusProNet\Utility\DirectionMapper;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;

/**
 * Handles processing of outbound transportation service selection.
 *
 * Manages outbound (HIN) transportation options including bus and 
 * self-organized (PKW) services with pricing and availability validation.
 */
class ParticipantTransportationOutboundFieldHandler extends AbstractParticipantFieldHandler
{
    public function getFieldName(): string
    {
        return 'transportationOutbound';
    }

    public function getDependencies(): array
    {
        return ['dateOfBirth']; // For age-based discounts
    }

    public function shouldProcess(array $submittedData, int $participantIndex): bool
    {
        return true; // Always process to handle deselection
    }

    public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
    {
        $participant = $this->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

declare(strict_types=1);

namespace App\Form\Service;

use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;

/**
 * Handles outbound pickup location selection.
 *
 * Pickup selection is only processed when outbound transportation 
 * is bus type. Automatically clears pickup when transportation 
 * changes to self-organized (PKW).
 */
class ParticipantPickupOutboundFieldHandler extends AbstractParticipantFieldHandler
{
    public function getFieldName(): string
    {
        return 'pickupOutbound';
    }

    public function getDependencies(): array
    {
        return ['transportationOutbound']; // Must process transportation first
    }

    public function shouldProcess(array $submittedData, int $participantIndex): bool
    {
        return true; // Always process to handle clearing
    }

    public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
    {
        $participant = $this->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

declare(strict_types=1);

namespace App\Form\Service;

use App\BusProNet\Constants;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Abstract\AbstractParticipantFieldHandler;

/**
 * Handles parking service selection for self-organized transportation.
 *
 * Parking is only available when at least one direction uses 
 * self-organized (PKW) transportation.
 */
class ParticipantParkingFieldHandler extends AbstractParticipantFieldHandler
{
    public function getFieldName(): string
    {
        return 'parking';
    }

    public function getDependencies(): array
    {
        return ['transportationOutbound', 'transportationInbound'];
    }

    public function shouldProcess(array $submittedData, int $participantIndex): bool
    {
        return true; // Always process for state changes
    }

    public function processField(array $submittedData, BookingDtoInterface $bookingDto, int $participantIndex): void
    {
        $participant = $this->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 = DirectionMapper::SUBTYPE_CAR_API === $participant->transportationOutbound?->subType;
        $inboundIsPkw = DirectionMapper::SUBTYPE_CAR_API === $participant->transportationInbound?->subType;
        
        return $outboundIsPkw || $inboundIsPkw;
    }
}

2.4 Field Options Provider Integration

Update: src/Form/Service/ParticipantFieldOptionsProvider.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) {
        DirectionMapper::SUBTYPE_BUS_API => '🚌',
        DirectionMapper::SUBTYPE_CAR_API => '🚗',
        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 Service Sub-Type Condition

File: src/Form/Service/Condition/ServiceSubTypeCondition.php

<?php

declare(strict_types=1);

namespace App\Form\Service\Condition;

use App\BusProNet\Model\Service;
use App\Form\Model\BookingDtoInterface;
use App\Form\Service\Contract\FieldConditionInterface;

/**
 * Condition that evaluates service sub-types.
 *
 * This condition enables field state logic based on the sub-type property
 * of Service objects. It can be used with any service field (transportation,
 * additional services, etc.) to show/hide or enable/disable dependent fields
 * based on the selected service type.
 */
class ServiceSubTypeCondition implements FieldConditionInterface
{
    public function __construct(
        private readonly string $serviceFieldName,
        private readonly string $operator,
        private readonly string|array $expectedSubType,
    ) {}

    public function evaluate(BookingDtoInterface $bookingDto, int $participantIndex, array $formData): bool
    {
        $participant = $bookingDto->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

use App\BusProNet\Utility\DirectionMapper;
use App\Form\Service\Condition\ServiceSubTypeCondition;

protected function registerFieldStateConditions(): void
{
    // ... existing conditions

    // Transportation-related field conditions

    // Hide outbound pickup when transportation is not BUS
    $this->fieldStateConditions['pickupOutbound'] = [
        'hidden' => ServiceSubTypeCondition::notEquals('transportationOutbound', DirectionMapper::SUBTYPE_BUS_API),
    ];

    // Hide inbound pickup when transportation is not BUS
    $this->fieldStateConditions['pickupInbound'] = [
        'hidden' => ServiceSubTypeCondition::notEquals('transportationInbound', DirectionMapper::SUBTYPE_BUS_API),
    ];

    // Hide parking when outbound transportation is not PKW (car)
    // Parking is offered at holiday destination for those arriving by car
    $this->fieldStateConditions['parking'] = [
        'hidden' => ServiceSubTypeCondition::notEquals('transportationOutbound', DirectionMapper::SUBTYPE_CAR_API),
    ];
}

Phase 4: Form Integration & Service Configuration ⚙️

4.1 Update Form Type

Update: src/Form/BookingCreateParticipantType.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

    # 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/Utility/DirectionMapper.php

// Transportation service sub-types (API format - German abbreviations)
public const SUBTYPE_BUS_API = 'BUS';
public const SUBTYPE_CAR_API = 'PKW';

// Transportation service sub-types (internal format - English)
public const SUBTYPE_BUS = 'BUS';
public const SUBTYPE_CAR = 'CAR';

/**
 * Maps API transportation sub-type to internal sub-type.
 */
public static function apiToInternal(string $apiSubType): string
{
    return match ($apiSubType) {
        self::SUBTYPE_BUS_API => self::SUBTYPE_BUS,
        self::SUBTYPE_CAR_API => self::SUBTYPE_CAR,
        default => throw new \InvalidArgumentException("Unknown API sub-type: $apiSubType"),
    };
}

/**
 * Maps internal transportation sub-type to API sub-type.
 */
public static function internalToApi(string $internalSubType): string
{
    return match ($internalSubType) {
        self::SUBTYPE_BUS => self::SUBTYPE_BUS_API,
        self::SUBTYPE_CAR => self::SUBTYPE_CAR_API,
        default => throw new \InvalidArgumentException("Unknown internal sub-type: $internalSubType"),
    };
}

UX Design & User Experience 🎯

Section Organization

Transportation will be organized in logical sections:

<!-- Outbound Transportation Section -->
<div class="form-section" data-section="transportation-outbound">
    <h4>🚌 Hinfahrt (Outbound Transportation)</h4>
    
    <div class="transportation-options">
        <!-- Radio buttons for transportation type -->
        {{ form_row(form.transportationOutbound) }}
    </div>
    
    <!-- Conditional pickup field (shown only for bus) -->
    <div class="pickup-selection" data-conditional="bus-outbound">
        {{ form_row(form.pickupOutbound) }}
    </div>
</div>

<!-- Inbound Transportation Section -->
<div class="form-section" data-section="transportation-inbound">
    <h4>🚗 Rückfahrt (Inbound Transportation)</h4>
    
    <div class="transportation-options">
        {{ form_row(form.transportationInbound) }}
    </div>
    
    <!-- Conditional pickup field -->
    <div class="pickup-selection" data-conditional="bus-inbound">
        {{ form_row(form.pickupInbound) }}
    </div>
</div>

<!-- Parking Section (conditional) -->
<div class="form-section" data-section="parking" data-conditional="pkw-selected">
    <h4>🅿️ Parken (Parking)</h4>
    {{ form_row(form.parking) }}
</div>

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 (removed unused toEnglish method)
  • Update ParticipantDto properties
  • Update BookingEditDto mapping
  • Create transportation field handlers (Outbound/Inbound)
  • Create pickup field handlers with conditional logic
  • Add parking service handler for self-organized transport
  • Add TOKEN_PARKING constant
  • Update ParticipantFieldOptionsProvider for transportation services
  • Integrate transportation fields into BookingCreateParticipantType
  • Create ServiceSubTypeCondition for field state management
  • Update field state provider with transportation conditions
  • Implement transportation type mapping for API/internal consistency

Sprint 2: Advanced Features (Week 2)

  • 🚧 Add pricing integration for transportation services
  • 🚧 Update HTMX integration for real-time transportation updates

Sprint 3: Testing & Deployment (Week 3)

  • Unit testing for all components
  • Integration testing for form flow
  • Manual testing scenarios
  • Performance optimization

Key Implementation Highlights 🌟

Transportation Type Mapping System

Problem Solved: BusProNet uses German abbreviations ('PKW') while internal code should use English terminology ('CAR') for consistency.

Solution: Enhanced DirectionMapper utility with bidirectional mapping:

  • API Format: SUBTYPE_CAR_API = 'PKW', SUBTYPE_BUS_API = 'BUS'
  • Internal Format: SUBTYPE_CAR = 'CAR', SUBTYPE_BUS = 'BUS'
  • Mapping Methods: apiToInternal(), internalToApi(), validation helpers

Generic Service Sub-Type Condition

Achievement: Created reusable ServiceSubTypeCondition instead of transportation-specific logic:

  • Supports multiple operators: equals, notEquals, in, notIn
  • Works with any service field, not just transportation
  • Handles both API and internal sub-type values
  • Provides static factory methods for common use cases

Conditional UX Logic

Smart Field Visibility:

  • Pickup Fields: Only visible when respective transportation is BUS
  • Parking Field: Only visible when outbound transportation is CAR (PKW)
  • Uses API constants since Service objects contain API values
  • Proper business logic: parking needed at destination for car arrivals

Backward Compatibility

  • Maintained all existing property names with deprecation notices
  • API integration continues using BusProNet's expected format
  • Internal code uses clean English naming
  • Seamless migration path for existing functionality

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