1441 lines
51 KiB
Markdown
1441 lines
51 KiB
Markdown
# 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](#1-architecture-overview)
|
|
2. [Directory Structure & Namespaces](#2-directory-structure--namespaces)
|
|
3. [BusProNet API Integration](#3-buspro-net-api-integration)
|
|
4. [Controllers](#4-controllers)
|
|
5. [Service Layer](#5-service-layer)
|
|
6. [Form System](#6-form-system)
|
|
7. [Security](#7-security)
|
|
8. [Database & Entities](#8-database--entities)
|
|
9. [Frontend Integration](#9-frontend-integration)
|
|
10. [CLI Commands](#10-cli-commands)
|
|
11. [Logging & Debugging](#11-logging--debugging)
|
|
12. [Validation](#12-validation)
|
|
13. [BusProNet API Quirks & Workarounds](#13-buspro-net-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
|
|
|
|
```php
|
|
// 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
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
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.
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
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)
|
|
|
|
```php
|
|
// 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`
|
|
|
|
| Operation | Requirements |
|
|
|-----------|--------------|
|
|
| `VIEW` | User owns booking (addressId match) |
|
|
| `EDIT` | User owns booking AND booking is editable |
|
|
|
|
### 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
|
|
|
|
```yaml
|
|
# 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
|
|
|
|
```php
|
|
public function deleteOlderThan(DateTimeImmutable $threshold): int;
|
|
```
|
|
|
|
#### BookingEditDraftRepository
|
|
|
|
```php
|
|
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
|
|
|
|
```php
|
|
// 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
|
|
|
|
```php
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
Encore
|
|
.setOutputPath('public/build/')
|
|
.setPublicPath('/build')
|
|
.addEntry('app', './assets/app.js')
|
|
.enableStimulusBridge('./assets/controllers.json')
|
|
.enablePostCssLoader()
|
|
.enableVersioning(Encore.isProduction())
|
|
```
|
|
|
|
#### TailwindCSS
|
|
|
|
```css
|
|
/* assets/styles/app.css */
|
|
@import "tailwindcss/base";
|
|
@import "_base.css";
|
|
@import "tailwindcss/components";
|
|
@import "_components.css";
|
|
@import "tailwindcss/utilities";
|
|
```
|
|
|
|
### 9.4 NPM Scripts
|
|
|
|
```json
|
|
{
|
|
"dev": "encore dev",
|
|
"watch": "encore dev --watch",
|
|
"build": "encore production --progress"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 10. CLI Commands
|
|
|
|
### 10.1 BusProNet API Commands
|
|
|
|
#### app:bpn:fetch-travel
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
php bin/console app:draft:cleanup [--dry-run]
|
|
```
|
|
|
|
Deletes booking edit drafts for past travels.
|
|
|
|
#### app:draft:inspect
|
|
|
|
```bash
|
|
php bin/console app:draft:inspect [--days|-d <n>] [--booking-id|-b <id>] [--delete]
|
|
```
|
|
|
|
Inspects and optionally deletes drafts.
|
|
|
|
#### app:draft:backfill-booking-number
|
|
|
|
```bash
|
|
php bin/console app:draft:backfill-booking-number [--dry-run]
|
|
```
|
|
|
|
Populates missing booking numbers from API.
|
|
|
|
#### app:cleanup:log-entries
|
|
|
|
```bash
|
|
php bin/console app:cleanup:log-entries [--retention|-r <months>]
|
|
```
|
|
|
|
Removes log entries older than retention period (default: 6 months).
|
|
|
|
#### app:cleanup:xml-dumps
|
|
|
|
```bash
|
|
php bin/console app:cleanup:xml-dumps
|
|
```
|
|
|
|
Removes XML debug dumps older than 3 days.
|
|
|
|
### 10.3 Setup Commands
|
|
|
|
#### app:crypto:generate-keys
|
|
|
|
```bash
|
|
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.
|
|
|
|
```php
|
|
// 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
|
|
|
|
```php
|
|
// 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`
|
|
|
|
```php
|
|
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`
|
|
|
|
```php
|
|
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`
|
|
|
|
```php
|
|
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`
|
|
|
|
```php
|
|
// 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`
|
|
|
|
```php
|
|
$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 |
|