Files
myep/docs/TRANSPORTATION_SERVICES_IMPLEMENTATION_PLAN.md
T

28 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 = $participant->transportationOutbound?->subType === 'PKW';
        $inboundIsPkw = $participant->transportationInbound?->subType === 'PKW';
        
        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) {
        '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

declare(strict_types=1);

namespace App\Form\Service\Condition;

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

/**
 * Condition that evaluates transportation service type.
 *
 * Used to show/hide fields based on whether transportation 
 * is bus or self-organized (PKW) for specific directions.
 */
class TransportationTypeCondition implements FieldConditionInterface
{
    public function __construct(
        private readonly string $direction, // 'outbound' or 'inbound'
        private readonly string $expectedType, // 'BUS' or 'PKW'
    ) {}

    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\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

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

<!-- 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
  • 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