Files
myep/docs/technical-documentation.md
T

52 KiB

Technical Documentation

Customer Portal for Ski Travel Company

This document provides comprehensive technical documentation for experienced PHP and Symfony developers working with this codebase. It covers the architecture, components, and implementation details necessary to navigate and extend the application.


Table of Contents

  1. Architecture Overview
  2. Directory Structure & Namespaces
  3. BusProNet API Integration
  4. Controllers
  5. Service Layer
  6. Form System
  7. Security
  8. Database & Entities
  9. Frontend Integration
  10. CLI Commands
  11. Logging & Debugging
  12. Validation
  13. BusProNet API Quirks & Workarounds

1. Architecture Overview

1.1 System Overview

This is a Symfony 6.4 web application serving as a customer portal for a ski travel company. The application integrates with the BusProNet API to manage customer data, bookings, and travel information.

┌─────────────────────────────────────────────────────────────────────────────┐
│                              User Interface                                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────┐ │
│  │   Browser   │  │    HTMX     │  │  Stimulus   │  │   TailwindCSS       │ │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  └─────────────────────┘ │
└─────────┼────────────────┼────────────────┼─────────────────────────────────┘
          │                │                │
          ▼                ▼                ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                           Symfony Application                                │
│  ┌─────────────────────────────────────────────────────────────────────────┐│
│  │                          Controllers                                     ││
│  │  Security │ Booking/Create │ Booking/Edit │ Account │ API │ Admin       ││
│  └─────────────────────────────────────────────────────────────────────────┘│
│  ┌─────────────────────────────────────────────────────────────────────────┐│
│  │                          Service Layer                                   ││
│  │  BookingService │ TravelDataService │ PricingCalculators │ Insurance    ││
│  └─────────────────────────────────────────────────────────────────────────┘│
│  ┌─────────────────────────────────────────────────────────────────────────┐│
│  │                          Form System                                     ││
│  │  Form Types │ Field Handlers │ Conditions │ State Providers │ DTOs      ││
│  └─────────────────────────────────────────────────────────────────────────┘│
│  ┌─────────────────────────────────────────────────────────────────────────┐│
│  │                       BusProNet Integration                              ││
│  │  ApiClient │ XmlParsers │ DataProcessors │ XmlLoaders │ Models          ││
│  └─────────────────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────────────────┘
          │                                              │
          ▼                                              ▼
┌─────────────────────────┐                 ┌─────────────────────────────────┐
│      Database           │                 │       BusProNet API             │
│  User │ LogEntry │ Draft│                 │  Socket-based XML Protocol      │
└─────────────────────────┘                 └─────────────────────────────────┘

1.2 Key Application Flows

Multi-Step Booking Creation

Step 1 (Room Selection)
    │
    ▼
Step 2 (Participant Details + Services)
    │
    ▼
Step 3 (Payment - API Inquiry Validation)
    │
    ▼
Step 4 (Confirmation - API Booking Submission)
    │
    ▼
Success Page

Booking Edit Flow

Load Booking from API
    │
    ├─ Apply Saved Draft (if exists)
    │
    ▼
Card-based Participant Editing
    │
    ├─ Auto-save Draft on Each Participant
    │
    ▼
Submit Update to API
    │
    ▼
Delete Draft on Success

1.3 Core Technologies

Component Technology
Framework Symfony 6.4
PHP Version 8.1+
Database Doctrine ORM
Frontend Stimulus, HTMX, TailwindCSS
Build Webpack Encore
API Communication Socket-based XML
Caching Symfony Cache (TagAware)

2. Directory Structure & Namespaces

2.1 Source Directory (src/)

src/
├── Admin/                    # EasyAdmin custom fields
│   └── Field/JsonDataField.php
├── BusProNet/               # BusProNet API integration layer
│   ├── ApiClient.php        # Main API client
│   ├── Constants.php        # Service tokens and constants
│   ├── DataProcessor/       # DTO ↔ API payload transformation
│   ├── DataProvider/        # Data providers (countries)
│   ├── Exception/           # API-specific exceptions
│   ├── Form/                # Form choice loaders
│   ├── Model/               # Data models (39 classes)
│   ├── Traits/              # Shared traits
│   ├── Utility/             # Helper utilities
│   ├── XmlLoader/           # Cached XML file loaders
│   └── XmlParser/           # XML response parsers (28 classes)
├── Command/                 # CLI commands (9 commands)
├── Controller/              # HTTP controllers
│   ├── Account/             # Account management
│   ├── Admin/               # EasyAdmin controllers
│   ├── Api/                 # OAuth2-protected JSON endpoints
│   ├── Booking/             # Booking workflows
│   │   ├── Create/          # Multi-step creation
│   │   ├── Edit/            # Booking editing
│   │   └── Traits/          # Shared controller logic
│   ├── RegistrationController.php
│   ├── ResetPasswordController.php
│   └── SecurityController.php
├── Entity/                  # Doctrine entities
│   ├── User.php
│   ├── LogEntry.php
│   └── BookingEditDraft.php
├── EventListener/           # Symfony event listeners
├── Exception/               # Application exceptions
├── Form/                    # Form system
│   ├── Model/               # DTOs (BookingDto, ParticipantDto, etc.)
│   ├── Service/             # Field handlers & conditions
│   │   └── Condition/       # 22+ condition classes
│   ├── DataTransformer/     # Form data transformers
│   └── Extension/           # Form extensions (XSS protection)
├── Htmx/                    # HTMX utilities
├── Logger/                  # Custom Monolog handlers/processors
├── Model/                   # Application models
├── Repository/              # Doctrine repositories
├── Security/                # Authentication & authorization
│   └── Voter/               # Custom voters
├── Service/                 # Business logic services (21 services)
├── Twig/                    # Template extensions
└── Validator/               # Custom validation constraints
    └── Constraints/         # Constraint classes

2.2 Configuration (config/)

config/
├── packages/                # Bundle configurations
│   ├── security.yaml        # Firewalls, access control
│   ├── league_oauth2_server.yaml
│   └── monolog.yaml         # Logging configuration
├── routes/                  # Route definitions
├── services.yaml            # Service definitions, BPN config
├── secret/                  # RSA keys for encryption
└── bundles.php              # Enabled bundles

2.3 Assets (assets/)

assets/
├── app.js                   # Main entry point
├── bootstrap.js             # Stimulus initialization
├── loading.js               # HTMX loading indicator
├── controllers/             # Stimulus controllers (10)
├── styles/                  # TailwindCSS styles
│   ├── app.css              # Main stylesheet
│   └── components/          # Component styles
├── images/                  # Static images
├── fonts/                   # Web fonts (Lato)
└── favicon/                 # Favicon files

3. BusProNet API Integration

3.1 Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                        Application Layer                            │
└─────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────┐
│                         ApiClient                                   │
│  src/BusProNet/ApiClient.php                                        │
│  - Socket connection management with load balancing                 │
│  - MD5 authentication (username + password + date + type)           │
│  - Automatic retry on busy server                                   │
│  - XML debugging dumps                                              │
└─────────────────────────────────────────────────────────────────────┘
                    │                               │
                    ▼                               ▼
┌──────────────────────────────┐    ┌──────────────────────────────────┐
│     DataProcessor/           │    │        XmlParser/                │
│  - BookingDataProcessor      │    │  - ApiResponseParser (router)    │
│  - BookingPayloadBuilder     │    │  - TravelParser, BookingParser   │
│  - ParticipantServiceProcessor│   │  - PersonalDataParser, etc.      │
└──────────────────────────────┘    └──────────────────────────────────┘
         (DTO → API payload)                (XML → Model objects)

3.2 ApiClient

Location: src/BusProNet/ApiClient.php

Connection & Authentication

// Load balancing across multiple ports
$this->selectedPort = $this->config['bpn_api_ports'][array_rand($this->config['bpn_api_ports'])];

// MD5-based authentication key
private function createKey(string $username, string $password, string $type): string
{
    $date = (new \DateTimeImmutable())->format('Ymd');
    return md5($username.$password.$date.$type);
}

Request Types

Constant Value Purpose
TYPE_CUSTOMER_DATA KUNDENKONTO Customer account operations
TYPE_BOOKING BUCHUNG Create new bookings
TYPE_BOOKING_UPDATE BUCHUNGAENDERUNG Modify existing bookings
TYPE_PRODUCT_DATA PRODUKTDATEN Travel/product details
TYPE_AVAILABILITY VERFUEGBARKEIT Service availability
TYPE_PURCHASE_VOUCHER GUTSCHEINPRUEFUNGEINLOESUNG Validate purchase vouchers
TYPE_PROMO_VOUCHER AKTIONSGUTSCHEIN Validate promotional codes

Key Methods

Method Return Type Purpose
getPersonalData(email, password) PersonalData|Notification Authenticate and fetch profile
getBookings(email, password) BaseData|Notification List customer bookings
getBooking(email, password, id) Booking|Notification Single booking details
getTravelData(travelId, hotelId) Travel|Notification Travel package details
createBookingInquiry(BookingDto) BookingResponse|Notification Validate booking (phase 1)
createBooking(BookingDto) BookingResponse|Notification Submit booking (phase 2)
updateBooking(BookingDto) BookingUpdate|Notification Modify existing booking

Two-Phase Booking Process

  1. Inquiry Phase (createBookingInquiry): Validates data and returns pricing without creating a booking
  2. Booking Phase (createBooking): Creates the actual booking after inquiry validation

Timeout Configuration

Setting Default Purpose
connection_timeout 5s Socket connect
stream_timeout 30s Read/write operations
total_timeout 45s Entire operation including retries
busy_retry_attempts 3 Retry count for busy server
busy_retry_delay 1s Delay between retries

3.3 Response Parsing

Router: src/BusProNet/XmlParser/ApiResponseParser.php

Routes XML responses to specialized parsers based on response type and sub-type.

Parser Classes

Parser Output Model Purpose
TravelParser Travel Travel packages with services, rooms, pickups
BookingParser Booking Complete booking with participants
PersonalDataParser PersonalData Customer profile and address
RoomsParser Room[] Hotel room information
ServicesParser Service[] Additional services and transportation
InsuranceParser Insurance[] Insurance options
PickupsParser Pickup[] Pickup/dropoff locations
PurchaseVoucherParser PurchaseVoucher Voucher validation results
PromoVoucherParser PromoVoucher Promo code validation results

3.4 Data Models

Location: src/BusProNet/Model/

Travel

class Travel
{
    public ?int $id = null;
    public ?int $hotelId = null;
    public ?\DateTimeImmutable $dateFrom = null;
    public ?\DateTimeImmutable $dateTo = null;
    public array $additionalServices = [];
    public array $transportationServices = [];
    public array $rooms = [];
    public array $pickups = [];
    public array $dropOffs = [];
    public array $insurances = [];

    public function getAdditionalServicesBySubTypes(mixed $subTypes): array;
    public function getTransportationServicesByDirection(string $direction): array;
    public function getAvailableRooms(): array;
    public function requiresInquiryBooking(): bool;
}

Service Token Constants

Token Meaning
KUR Courses (ski lessons)
SPA Ski passes
SON Additional services
VPF Board/catering
VER-VE8 Equipment rentals
PAR Parking
RRV, PAK Insurance types
LVS Rental insurance

3.5 Data Processors

Location: src/BusProNet/DataProcessor/

Class Purpose
BookingDataProcessor Orchestrates booking data transformation
BookingPayloadBuilder Constructs nested XML payload structure
ParticipantServiceProcessor Maps participant service selections
ServiceMappingCollector Collects service IDs by type
PersonalDataSynchronizer Syncs personal data between applicant/participants
PickupPlanningTransformer Transforms pickup planning data

3.6 XML Loaders

Location: src/BusProNet/XmlLoader/

Load data from pre-cached XML files for performance:

Loader Purpose
TravelLoader Travel packages
HotelLoader Hotel details
InsuranceLoader Insurance options
PickupLoader Pickup locations
AgencyLoader Agency information

4. Controllers

4.1 Authentication Controllers

SecurityController (/)

  • GET / - Login page with booking flow detection
  • GET /logout - Logout endpoint

Detects OAuth2 authorization requests and booking flow context from session.

RegistrationController (/registration)

  • POST /registration - Handle customer registration via BusProNet API

ResetPasswordController (/reset-password)

  • POST /reset-password - Password reset request via BusProNet API

4.2 Booking Creation Flow

Create/IndexController

  • GET /bookings/create - Entry point with HTMX loading
  • POST /bookings/create/init - Initialize fresh booking session
  • POST /bookings/cancel - Cancel and redirect to return URL
  • GET /bookings/create/error - Error display page

Validates query parameters (dateId, hotelId, agency), resolves agency codes, creates fresh BookingDto.

Create/Step1Controller (Room Selection)

  • GET /bookings/create/rooms - Room selection form
  • POST /bookings/create/refresh - HTMX refresh for sidebar

Validates step access, detects room changes, updates booking status for inquiry mode.

Create/Step2Controller (Participant Details)

  • GET /bookings/create/participants - Participant cards overview
  • GET/POST /bookings/create/participants/{index} - Edit single participant
  • POST /bookings/create/participants/{index}/refresh - HTMX form refresh

Card-based UI for scalability (50+ participants). Auto-assigns rooms, preselects mandatory services, saves drafts.

Create/Step3Controller (Payment Validation)

  • GET /bookings/create/payment - Payment form
  • POST /bookings/create/payment/refresh - HTMX refresh

Calls createBookingInquiry() for validation, compares calculated vs. API prices.

Create/Step4Controller (Confirmation)

  • GET /bookings/create/confirmation - Final confirmation

Calls createBooking() for submission, clears caches, stores booking number.

Create/SuccessController

  • GET /bookings/create/success - Success page with booking number

4.3 Booking Management

Booking/IndexController

  • GET /bookings - List user's bookings

Booking/DownloadController

  • GET /bookings/{id}/documents - Download booking documents
  • GET /bookings/{id}/invoice - Download invoice/statement

Booking/Edit/IndexController

  • GET /bookings/{id}/edit/start - Clear cache, redirect to edit
  • GET /bookings/{id}/edit - Edit participant cards
  • GET/POST /bookings/{id}/edit/participants/{index} - Edit single participant
  • POST /bookings/{id}/edit/reload - Reload from API (discard changes)
  • POST /bookings/{id}/edit/cancel - Cancel edit (preserve draft)

Similar card-based UI as Step 2. Auto-saves drafts, uses fingerprinting for change detection.

4.4 Account Controllers

Account/IndexController

  • GET /account - Account dashboard (requires ROLE_USER)

Account/PersonalDataController

  • GET /personal-data - View/edit personal data
  • POST /personal-data/newsletter - Toggle newsletter subscription

4.5 API Controllers (OAuth2 Protected)

All under /api prefix, require OAuth2 authentication.

Controller Routes Purpose
UserinfoController GET /api/userinfo User profile (scope-filtered)
ProductController GET /api/products Product listing
TravelController GET /api/travels/* Travel data endpoints
HotelController GET /api/hotels/* Hotel information
PickupController GET/POST /api/pickups* Pickup locations and planning
CountryController GET /api/countries Country list
CrmAttributeController GET /api/crm-attributes User CRM attributes
ContactFormController POST /api/contactform Contact form submission
LastUpdateController GET /api/last-update Data sync timestamp

4.6 Admin Controllers (EasyAdmin)

Controller Entity Purpose
DashboardController - Admin panel home
UserCrudController User User management (read-only)
BookingEditDraftCrudController BookingEditDraft Draft management + export
LogEntryCrudController LogEntry Activity logging
XmlDumpController - API request debugging

4.7 Shared Traits

Trait Purpose
BookingCreateTrait Step validation, redirects, error handling
BookingExceptionHandlerTrait Safe DTO retrieval with error messages
ParticipantCardFlowTrait Card UI, form creation, notifications

5. Service Layer

5.1 Booking Services

BookingService

Location: src/Service/BookingService.php

Orchestrates booking workflow, session management, participant calculations.

Method Purpose
startFreshBooking() Initialize new booking
getOrCreateBookingCreateDto() Session DTO management
ensureCorrectNumberOfParticipants() Sync participant count
preselectMandatoryServices() Pre-select required services
updateBookingStatusFromRoomSelection() Update inquiry status
getParticipantsCount() Calculate from room selections
groupRoomsBySelectionType() Group by "by_pax" or "by_room"

BookingEditDataLoaderService

Loads booking data for edit mode with automatic draft restoration.

Method Purpose
loadFormData() Load or initialize for editing
initializeFromApi() Fresh load with draft application
fetchBookingData() Cached API fetch (5-min TTL)
invalidateUserBookingCaches() Bulk cache invalidation

BookingEditDraftService

Draft persistence for edit sessions.

Method Purpose
findDraft() Retrieve existing draft
saveDraft() Create or update draft
deleteDraft() Remove after submission
applyDraftToDto() Merge draft onto API data

5.2 Pricing Calculators

BookingPriceCalculatorService

Facade for all pricing calculations.

Method Purpose
getPricingBreakdown() Complete breakdown with surcharges
calculateGrandTotal() Total booking price
calculateRoomPricing() Room costs only
calculateServicePricing() Service costs only
calculateIndividualParticipantPrice() Per-participant total

Supporting Calculators

Class Purpose
RoomPricingCalculator Room cost calculations
ServicePricingCalculator Service aggregation by subtype
ParticipantPricingCalculator Per-participant with caching

5.3 Travel Data Services

TravelDataService

Unified interface for travel data from XML files and API.

Method Purpose
getTravelData() Primary method with source selection
getTravelDataFromXml() Load from cached XML
getTravelDataFromApi() Load from live API
getMutabilityData() Mutable services flags (12h cache)
getAvailabilityData() Service availability (10-min cache)
enrichWithFreshAvailabilities() Ensure current data

InsuranceService

Consolidated insurance operations with eligibility filtering.

Method Purpose
getSelectableInsurances() Non-complementary options
getEligibleInsurances() Filtered by participant criteria
filterByType() Group by subType + familyInsurance
reassignInsuranceForPriceChange() Find compatible insurance
batchAssignInsuranceToParticipants() Bulk assignment

VoucherValidationService

Voucher validation with API caching (15-min TTL).

Method Purpose
validatePurchaseVoucher() Validate redemption codes
validatePromoVoucher() Validate promo codes with context

5.4 Supporting Services

Service Purpose
ParticipantEligibilityService Check booking eligibility by skipass availability
ServiceAvailabilityCalculator Track dynamic availability within session
RoomAssignmentService Automatic room assignment
BookingSummaryDataService Consolidate sidebar display data
ParticipantCardDataService Extract card display data
BookingFingerprintService SHA-256 change detection
BookingExportService Excel export from drafts
CmsDataService CMS data retrieval (hotel images)
XmlDumpService API debugging file management

6. Form System

6.1 Architecture Overview

The form system implements sophisticated conditional field visibility with dynamic state management.

┌─────────────────────────────────────────────────────────────────────┐
│                        Form Submission                              │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│               ParticipantFieldHandlerRegistry                       │
│  - Topological sort by dependencies                                 │
│  - Execute handlers in correct order                                │
│  - Sync DTO back to submitted data                                  │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    Field Handlers (20+)                             │
│  DateOfBirth → SkiPass → Rentals → Insurance → ...                 │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│              Field State Providers                                  │
│  CreateFieldStateProvider │ EditFieldStateProvider                  │
│  - Evaluate conditions for each field                               │
│  - Determine: hidden, static_text, readonly, disabled, required     │
└─────────────────────────────────────────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────────┐
│                    Conditions (22+)                                 │
│  CompositeCondition (AND/OR/NOT)                                    │
│  AgeRangeCondition │ SkiPassSelectionCondition │ ...               │
└─────────────────────────────────────────────────────────────────────┘

6.2 Form Types

Booking Flow Forms

Form Type Purpose
BookingCreateStep1Type Room selection
BookingCreateStep2Type Embeds BookingParticipantType
BookingCreateStep3Type Additional participants
BookingCreateStep4Type Confirmation/payment
BookingEditType Edit mode root form
BookingEditParticipantType Edit individual participants
BookingParticipantType Base participant form (create + edit)

Supporting Forms

Form Type Purpose
AddressType Street, postal code, city, country
BodyDimensionsType Height, weight, shoe size
RoomAssignmentType Room selection
PersonalDataType Customer profile
PaymentType Payment information
RegistrationType User registration
BankAccountType Bank details

6.3 DTOs

ParticipantDto

class ParticipantDto
{
    // Personal Data
    public ?string $firstName = null;
    public ?string $lastName = null;
    public ?string $gender = null;
    public ?\DateTimeImmutable $dateOfBirth = null;
    public ?Address $address = null;

    // Service Selections
    public ?int $assignedRoomId = null;
    public array $courses = [];
    public array $additionalServices = [];
    public ?Service $skiPass = null;
    public array $rentals = [];
    public ?Service $transportationOutbound = null;
    public ?Service $transportationInbound = null;
    public ?Pickup $pickup = null;

    // Insurance
    public ?Insurance $insurance = null;
    public bool $bulkInsuranceBooking = false;

    // Vouchers
    public ?string $purchaseVoucherCode = null;
    public ?string $promoVoucherCode = null;
}

BookingDto

class BookingDto
{
    public const MODE_CREATE = 'create';
    public const MODE_EDIT = 'edit';

    public string $mode = self::MODE_CREATE;
    public ?Travel $travel = null;
    public ?Booking $booking = null;
    public array $participants = [];
    public array $selectedRooms = [];
    public ?int $agencyId = null;
    public ?string $paymentMethod = null;
}

6.4 Field Handler System

Handler Registry

Location: src/Form/Service/ParticipantFieldHandlerRegistry.php

Uses topological sort (Kahn's algorithm) to execute handlers in dependency order.

interface ParticipantFieldHandlerInterface
{
    public function getFieldName(): string;
    public function getDependencies(): array;
    public function shouldProcess(...): bool;
    public function processField(...): void;
    public function getFieldStateModifications(...): array;
}

Handler Implementations

Handler Field Dependencies
ParticipantDateOfBirthFieldHandler dateOfBirth None (first)
ParticipantSkiPassFieldHandler skiPass dateOfBirth
ParticipantRentalsFieldHandler rentals dateOfBirth, skiPass
ParticipantCoursesFieldHandler courses dateOfBirth
ParticipantBoardFieldHandler board dateOfBirth
ParticipantTransportationOutboundFieldHandler transportationOutbound dateOfBirth
ParticipantTransportationInboundFieldHandler transportationInbound dateOfBirth
ParticipantInsuranceFieldHandler insurance All price-affecting fields
ParticipantBulkInsuranceFieldHandler bulkInsuranceBooking None
ParticipantPickupFieldHandler pickup transportation
ParticipantParkingFieldHandler parking transportationOutbound
ParticipantPromoVoucherFieldHandler promoVoucherCode None
ParticipantPurchaseVoucherFieldHandler purchaseVoucherCode None

6.5 Condition System

CompositeCondition

CompositeCondition::and(condition1, condition2);
CompositeCondition::or(condition1, condition2);
CompositeCondition::not(condition);

Core Conditions

Condition Purpose
FirstParticipantCondition Is applicant (index 0)
MultipleParticipantsCondition 2+ participants
DateOfBirthProvidedCondition Date of birth is set
AgeRangeCondition(min, max) Age in range
BabyAgeCondition Age 0-2
SkiPassSelectionCondition Ski pass selected
RentalSelectionCondition Rentals selected
RoomSelectionCondition(['codes']) Specific room type
ServiceSubTypeCondition::equals(field, type) Service type match
BookingEligibilityCondition Has eligible services
BulkInsuranceBookingCondition Bulk insurance active
PersonalDataMutabilityCondition BPN mutability flag
BookingModeCondition(mode) Create or edit mode

6.6 Field State Providers

State Behavior
hidden Field excluded from form
static_text Rendered as read-only text
readonly Visible but not editable
disabled User cannot interact
required Field is mandatory

Example: Insurance Field Visibility (Create Mode)

// Hidden if:
// - Date of birth not provided OR
// - Bulk insurance active for dependents OR
// - Participant ineligible (no skipasses available)
CompositeCondition::or(
    CompositeCondition::not(new DateOfBirthProvidedCondition()),
    new BulkInsuranceBookingCondition(),
    new BookingEligibilityCondition()
)

7. Security

7.1 Authentication

BpnAuthenticator

Location: src/Security/BpnAuthenticator.php

Custom authenticator that validates credentials against BusProNet API.

Login Form → MD5 Hash Password → ApiClient::getPersonalData()
    │
    ├─ Valid → Create/Update User Entity
    │          Get CRM Attributes (roles, hotel codes)
    │          Encrypt password with RSA
    │          Store in database
    │
    └─ Invalid → CustomUserMessageAuthenticationException

User Entity Password Handling

  • Plain password hashed with MD5 for API authentication
  • MD5 hash encrypted with RSA before database storage
  • Decrypted when needed for subsequent API calls

7.2 Encryption

Location: src/Security/Crypt.php

Uses spatie/crypto library with RSA asymmetric encryption.

Method Purpose
encrypt(string) Encrypt with private key
decrypt(string) Decrypt with public key
sign(string) Create digital signature
verify(string, signature) Verify signature

Keys stored in config/secret/ directory.

7.3 Authorization

BookingVoter

Location: src/Security/Voter/BookingVoter.php

This voter currently exists as legacy authorization logic and is not actively invoked by booking controllers.

Current booking routes (/bookings/*) are guarded with ROLE_USER, while final document access checks are enforced by BusProNet.

7.4 OAuth2

Configured via League OAuth2 Server for API endpoints.

  • Access Token TTL: 10 minutes
  • Scopes: email, id, profile, roles, api
  • Grants: Authorization code, client credentials

API Protection

# config/packages/security.yaml
api:
    pattern: ^/api
    security: true
    stateless: true
    oauth2: true

7.5 Access Control

Path Role
^/authorize IS_AUTHENTICATED_REMEMBERED
^/admin ROLE_ADMIN
^/ PUBLIC_ACCESS

8. Database & Entities

8.1 User Entity

Location: src/Entity/User.php

Property Type Purpose
email string (unique) User identifier
password string (nullable) RSA-encrypted API password
personId int (nullable) BusProNet person ID
addressId int (nullable) BusProNet address ID
roles array (JSON) CRM roles
hotelCodes array (JSON) Associated hotels
lastLoginAt DateTimeImmutable Audit timestamp

8.2 LogEntry Entity

Location: src/Entity/LogEntry.php

Property Type Purpose
channel string Log channel (core, bpn, auth)
message string Log message
context array (JSON) Contextual data
extra array (JSON) Request ID, URI, user info
createdAt DateTimeImmutable Timestamp

8.3 BookingEditDraft Entity

Location: src/Entity/BookingEditDraft.php

Property Type Purpose
user User (ManyToOne) Draft owner
bookingId int BusProNet booking ID
bookingNumber int (nullable) User-facing number (vorgang)
travelDate DateTimeImmutable For cleanup queries
dateId int (nullable) Travel date ID
hotelId int (nullable) Hotel ID
formData array (JSON) Saved form values
createdAt DateTimeImmutable Creation timestamp
updatedAt DateTimeImmutable Last update

Unique constraint: (user_id, booking_id)

8.4 Repositories

LogEntryRepository

public function deleteOlderThan(DateTimeImmutable $threshold): int;

BookingEditDraftRepository

public function findByUserAndBooking(User $user, int $bookingId): ?BookingEditDraft;
public function deleteByUserAndBooking(User $user, int $bookingId): void;
public function deleteExpiredDrafts(): int;

9. Frontend Integration

9.1 Stimulus Controllers

Location: assets/controllers/

Controller Purpose
modal_controller Modal dialog management
toggle_controller Collapsible sections with state persistence
checkbox_toggle_controller Container visibility based on checkboxes
select_toggle_controller Container visibility based on select values
form_collection_controller Dynamic add/remove form fields
step_input_controller Numeric spinner input
birthday_controller Date of birth validation/completion event
toast_controller Toast notifications via Toastify
tooltip_controller Tooltips via Tippy.js
backbutton_controller Browser back navigation

9.2 HTMX Integration

HxRedirectResponse

// src/Htmx/HxRedirectResponse.php
class HxRedirectResponse extends Response
{
    public function __construct(string $url)
    {
        parent::__construct('', Response::HTTP_OK, ['HX-Redirect' => $url]);
    }
}

Triggers full-page navigation without CORS issues.

HxTrait

// Out-of-band swap for multiple sections
protected function htmxOobResponse(
    string $templateName,
    array $blockNames,
    array $context = [],
    ?string $pushUrl = null
): Response;

// Intelligent redirect for HTMX and regular requests
protected function htmxRedirect(Request $request, string $url): Response;

HTMX Configuration

// assets/app.js
htmx.config.includeIndicatorStyles = false;
htmx.config.historyEnabled = true;
htmx.config.historyCacheSize = 10;
htmx.config.allowScriptTags = false;
htmx.config.withCredentials = true;
htmx.config.timeout = 50000;

9.3 Asset Pipeline

Webpack Encore Configuration

Encore
    .setOutputPath('public/build/')
    .setPublicPath('/build')
    .addEntry('app', './assets/app.js')
    .enableStimulusBridge('./assets/controllers.json')
    .enablePostCssLoader()
    .enableVersioning(Encore.isProduction())

TailwindCSS

/* assets/styles/app.css */
@import "tailwindcss/base";
@import "_base.css";
@import "tailwindcss/components";
@import "_components.css";
@import "tailwindcss/utilities";

9.4 NPM Scripts

{
  "dev": "encore dev",
  "watch": "encore dev --watch",
  "build": "encore production --progress"
}

10. CLI Commands

10.1 BusProNet API Commands

app:bpn:fetch-travel

php bin/console app:bpn:fetch-travel <travel-id> [--filename|-f <name>] [--dry-run]

Fetches travel data from BusProNet API and saves to XML export directory.

app:bpn:replay

php bin/console app:bpn:replay <file> [--dry-run|-d] [--output|-o <file>]

Replays stored XML requests against the API for debugging.

app:bpn:xml-sync

php bin/console app:bpn:xml-sync [--force|-f] [--dry-run]

Synchronizes XML export files from remote SFTP. Invalidates caches on completion.

10.2 Maintenance Commands

app:draft:cleanup

php bin/console app:draft:cleanup [--dry-run]

Deletes booking edit drafts for past travels.

app:draft:inspect

php bin/console app:draft:inspect [--days|-d <n>] [--booking-id|-b <id>] [--delete]

Inspects and optionally deletes drafts.

app:cleanup:log-entries

php bin/console app:cleanup:log-entries [--retention|-r <months>]

Removes log entries older than retention period (default: 6 months).

app:cleanup:xml-dumps

php bin/console app:cleanup:xml-dumps

Removes XML debug dumps older than 3 days.

app:cleanup:newsletter-opt-in-requests

php bin/console app:cleanup:newsletter-opt-in-requests

Removes expired pending newsletter double opt-in requests.

  • Request TTL is controlled by NEWSLETTER_CONFIRMATION_TTL_HOURS (default: 1 hour).
  • Expired pending requests are removed immediately when they are encountered (new request / confirmation attempt).
  • Scheduled cleanup removes all currently expired pending requests (expires_at <= now) as a safety net.
  • Confirmed requests are converted into durable per-list newsletter consent records and then deleted.

app:mailjet:newsletter-webhook

php bin/console app:mailjet:newsletter-webhook register
php bin/console app:mailjet:newsletter-webhook remove 123
php bin/console app:mailjet:newsletter-webhook deactivate

Creates or deletes the Mailjet unsub callback for POST /webhooks/mailjet/newsletter.

  • Uses APP_BASE_URL by default and appends the webhook path.
  • The command prepends mailjet:<password>@ to the URL before registering it with Mailjet.
  • Configure the plain password in MAILJET_WEBHOOK_BASIC_PASSWORD; Symfony uses it for the webhook firewall.
  • Pass --url to target a different full webhook URL.
  • register prints the API response payload so you can note the callback ID.
  • remove and deactivate require the callback ID returned by Mailjet.

10.3 Setup Commands

app:crypto:generate-keys

php bin/console app:crypto:generate-keys

Generates RSA keypair for encryption in configured path.


11. Logging & Debugging

11.1 Log Channels

Channel Purpose
core Core application logic
bpn BusProNet API interactions
auth Authentication/security events

11.2 Custom Processors

Location: src/Logger/

Processor Adds to Record
RequestIdProcessor extra['request_id']
RequestInfoProcessor extra['uri'], extra['method']
UserDataProcessor extra['user']['username'], extra['user']['roles']

11.3 Database Handler

Location: src/Logger/DatabaseHandler.php

Persists INFO+ level logs to LogEntry entity. Replaces message placeholders with context values.

11.4 XML Debugging

When debug mode is enabled, API requests/responses are dumped to var/bpn/:

r-00m8z7k58a-8k1pvd9q_1_request.xml
r-00m8z7k58a-8k1pvd9q_1_response.xml

View via Admin Panel: Admin → Log → XML Dumps action.


12. Validation

12.1 Custom Constraints

Location: src/Validator/Constraints/

ParticipantValidator

Cross-field validation for participant data.

// Requires pickup when bus transportation selected
if (($hasOutboundBus || $hasInboundBus) && null === $participant->pickup) {
    $this->context->buildViolation('Bitte auswählen')
        ->atPath('pickup')
        ->addViolation();
}

RoomSelectionValidator

  • At least one room must be selected
  • Baby rooms cannot be booked standalone
// Baby room + regular room required
if ($hasBabyRoom && false === $hasRegularRoom) {
    $this->context->buildViolation($constraint->onlyBabyRoomsMessage)
        ->addViolation();
}

PromoVoucherValidator

Validates promo codes against BusProNet API with travel and price context.

PurchaseVoucherValidator

Validates purchase voucher codes for existence and remaining balance.


13. BusProNet API Quirks & Workarounds

This section documents known API behaviors and the solutions implemented.

13.1 Server Busy - Immediate Connection Close

Issue: Server may close socket immediately without data when busy.

Solution: Automatic retry with configurable attempts (default: 3) and delay (default: 1s).

Location: src/BusProNet/ApiClient.php:625-659

private function executeWithRetry(callable $operation, ?string $type = null): mixed
{
    for ($attempt = 1; $attempt <= $maxAttempts; ++$attempt) {
        try {
            return $operation();
        } catch (ImmediateConnectionCloseException $e) {
            if ($attempt < $maxAttempts) {
                sleep($retryDelay);
            }
        }
    }
}

13.2 Pickup API Limitation

Issue: API only supports pickup submission via outbound (zustiege) section. When outbound is car but inbound is bus, pickup cannot be properly submitted.

Solution:

  • Pickup field shown when either direction is bus
  • Pickup pricing only calculated when outbound is bus
  • Summary shows "Zustieg (inkl.)" without price for inbound-only bus

Documentation: docs/pickup-api-limitation.md

13.3 Insurance Not Available in Edit Mode

Issue: API does not return insurance data in booking responses.

Solution: Insurance field skipped in edit mode. Original insurance preserved and passed through unchanged.

Location: src/Form/Service/ParticipantInsuranceFieldHandler.php:96-100

public function shouldProcess(...): bool
{
    if (BookingDto::MODE_EDIT === $mode) {
        return false;  // Skip processing entirely in edit mode
    }
}

13.4 Minimal Data for First Participant

Issue: API may return full data only in <anmelder> but minimal/empty in <teilnehmer id="1">.

Solution: Copy address from applicant if first participant has empty address.

Location: src/BusProNet/DataProcessor/BookingDataProcessor.php:71-81

13.5 Automatic Customer Matching

Issue: Including customer IDs prevents automatic matching by personal data.

Solution: Exclude personId and addressId from create mode payload.

Location: src/BusProNet/DataProcessor/BookingPayloadBuilder.php:247-249

13.6 Personal Data Mutability Flag

Issue: Per-participant aenderungmoeglich flag controls editability.

Solution: Hide personal data fields and render as static text when not mutable.

Exception: Internal agency bookings (code '0004') ignore mutability.

Location: src/Form/Service/Condition/PersonalDataMutabilityCondition.php

13.7 Synthetic "No Insurance" Option

Issue: Insurance is optional but requires explicit choice for legal compliance.

Solution: Inject synthetic option with ID '0' that satisfies validation but is excluded from API transmission.

Location: src/BusProNet/Model/Insurance.php:19-26

public const NO_INSURANCE_ID = '0';

public function isNoInsurance(): bool
{
    return self::NO_INSURANCE_ID === $this->id;
}

13.8 Goodwill (Kulanz) Vouchers

Issue: Goodwill vouchers must be treated differently from regular purchase vouchers.

Solution: Detect by type="Kulanz", send as <aktionscode> per participant instead of aggregated.

Location: src/BusProNet/Model/PurchaseVoucher.php:42-49

13.9 API-Applied Discounts

Issue: API may apply automatic discounts (group discounts) not known to local calculator.

Solution: Extract ERM-type negative price items from response and account for them in price comparison.

Location: src/BusProNet/Model/BookingResponse.php:106-120

13.10 Insurance Auto-Reassignment

Issue: Insurance may become ineligible when participant price changes.

Solution: Automatically reassign to same type with appropriate price tier, preserving user intent.

Location: src/Form/Service/ParticipantInsuranceFieldHandler.php:115-224

13.11 Address Data Cloning

Issue: Shared address references could cause unintended modifications.

Solution: Clone address objects when loading bookings and creating new instances during updates.

Locations:

  • src/BusProNet/DataProcessor/BookingDataProcessor.php:43-48
  • src/BusProNet/DataProcessor/PersonalDataSynchronizer.php:60-71

13.12 Company Bookings - Missing Fields

Issue: Travel agencies create bookings with company data lacking required personal fields.

Solution: Fill missing mandatory fields with sensible defaults.

Location: src/BusProNet/DataProcessor/PersonalDataSynchronizer.php:91-122

// Default values for company applicants
$applicant->firstName = 'Anmelder';
$applicant->gender = 'D';
$applicant->nationality = 'D';
$applicant->dateOfBirth = new \DateTimeImmutable('-20 years');
$applicant->mobile = '12345';

13.13 Message Length Header

Issue: All API responses are prefixed with 10-byte message length.

Solution: Strip first 10 bytes from all socket responses.

Location: src/BusProNet/ApiClient.php:695-696

$xml = substr($response, 10);

Appendix: Key File Locations

Configuration

File Purpose
config/services.yaml Service definitions, BPN config
config/packages/security.yaml Firewalls, access control
config/packages/monolog.yaml Logging configuration
config/secret/ RSA encryption keys

Entry Points

File Purpose
src/Controller/SecurityController.php Login entry
src/Controller/Booking/Create/IndexController.php Booking creation entry
src/BusProNet/ApiClient.php API communication
assets/app.js Frontend entry

Core Business Logic

File Purpose
src/Service/BookingService.php Booking orchestration
src/Service/BookingPriceCalculatorService.php Pricing facade
src/Form/Service/ParticipantFieldHandlerRegistry.php Form field processing
src/BusProNet/DataProcessor/BookingDataProcessor.php DTO ↔ API transformation