chore: consolidate documentation

This commit is contained in:
Björn Fromme
2025-10-24 12:17:42 +02:00
parent 293574187f
commit b326510b04
2 changed files with 0 additions and 835 deletions
-498
View File
@@ -1,498 +0,0 @@
# MyEP Next Booking - Project Overview
**Symfony 6.4 travel booking application** integrating with Bus Pro Net (BPN) XML API.
## Core Architecture
### Multi-Step Booking Flow
1. **Step 1**: Room selection and dates (`Create\Step1Controller`)
2. **Step 2**: Participant details with card-based UI (`Create\Step2Controller`)
3. **Step 3**: Payment method selection (`Create\Step3Controller`)
4. **Step 4**: Final confirmation with comprehensive pricing breakdown and submission to BPN API (`Create\Step4Controller`)
- Displays complete pricing breakdown by category (rooms, services grouped by type)
- Shows per-participant total pricing in card headers
- Displays individual service prices inline with each booked service
- Three-level pricing transparency: aggregate, participant, and service-level
5. **Edit Flow**: Similar card-based UI for existing bookings (`Edit\IndexController`)
### Card-Based UI Pattern (Production)
- **Overview**: Grid of participant cards with lazy-loaded individual forms
- **Performance**: Handles 50+ participants efficiently via HTMX
- **Controllers**:
- `Create\Step2Controller` - create flow with validation before step 3
- `Edit\IndexController` - edit flow with validation before API submission
- **Shared Logic**:
- `ParticipantCardFlowTrait` - Card rendering and form handling
- `ParticipantCardDataService` - Card data generation with optional validation state
- **Validation Pattern**:
- Both controllers use validation-only forms (`BookingCreateStep2Type`, `BookingEditType`)
- Standard Symfony form flow: `handleRequest()``isSubmitted()``isValid()`
- On invalid: Cards show red border with "unvollständige oder fehlerhafte Daten" badge
- Validation state determined via `ParticipantCardDataService::getAllCardsDataWithValidation()`
- On valid: Proceed to next step (create) or submit to API (edit)
## Key Architectural Layers
### BusProNet Integration (`src/BusProNet/`)
- `ApiClient` - XML API communication
- `XmlParser/` - Response parsers (travels, hotels, bookings)
- `XmlLoader/` - Data loaders with caching
- `DataProcessor/` - Transform API data to DTOs
### Form System (`src/Form/`)
- **DTOs**: `BookingCreateDto`, `ParticipantDto`, `ParticipantEditDto` (session-stored)
- `ParticipantEditDto` wrapper enables cross-participant validation (e.g., email uniqueness)
- Wrapper contains participant being edited + full booking context for validation
- **Field Handlers**: 15+ specialized handlers in `src/Form/Service/`
- Registered via service tags with dependency resolution
- Process in dependency order via `ParticipantFieldHandlerRegistry`
- `ParticipantDateOfBirthFieldHandler` handles array input from BirthdayType widget
- **Conditional Fields**: Universal condition system (`FieldConditionInterface`)
- Age-based, field-dependent, service-specific conditions
- Applied via `CreateFieldStateProvider` / `EditFieldStateProvider`
- **HTMX Integration**: Real-time updates for dynamic fields
- **Email Uniqueness Validation**: Adult participants (16+) must have unique email addresses
- Children (under 16) exempt from validation, can share emails with adults
- Implemented via `ParticipantEditDto::validateEmailUniqueness()` callback validator
- Case-insensitive comparison with whitespace normalization
- Validation groups: `booking_create`, `booking_edit`
- **Date of Birth Field**: Uses `BirthdayType` with three text inputs (day, month, year)
- Widget: `widget: 'text'` renders separate text fields instead of dropdowns
- Input format: `input: 'datetime_immutable'` for immutable date handling
- Custom theme: `birthday_widget` block in `templates/forms.html.twig` handles rendering
- German format: Day.Month.Year with hardcoded placeholders ("Tag", "Monat", "Jahr")
- Field order explicitly set to `['day', 'month', 'year']` for German date convention
- HTMX attributes applied to parent container for form refresh on change
### Service Layer (`src/Service/`)
- `BookingService` - Core booking workflow
- `BookingPriceCalculatorService` - Comprehensive pricing calculations
- `getPricingBreakdown()` - Complete pricing breakdown with rooms and services grouped by type
- `calculateAllParticipantIndividualPrices()` - Per-participant total pricing
- `calculateIndividualParticipantPrice()` - Single participant total (room + all services)
- `calculateServicePricing()` - Service aggregation with grouping by subtype
- Powers sidebar summary and Step 4 confirmation pricing display
- `BookingFingerprintService` - Dirty state detection for edit mode
- `TravelDataService` - API integration and caching
- `ParticipantCardDataService` - Card display data with optional validation state
- `getAllCardsData()` - Basic card data without validation
- `getAllCardsDataWithValidation()` - Card data enriched with validation state for error display
- `InsuranceService` - Consolidated insurance operations (eligibility, type filtering, reassignment) with request-scoped caching
- `RoomAssignmentService` - Automatic room assignment with intelligent conflict resolution
### Room Reassignment with Conflict Resolution
The room assignment system allows participants to freely select any room from Step 1 selections, with automatic conflict resolution when capacity is exceeded.
**Key Features:**
- **Flexible Selection**: All rooms from Step 1 always appear in participant dropdown, regardless of current capacity
- **Auto-Assignment**: When exactly ONE room type selected, all participants auto-assigned via `RoomAssignmentService`
- **Field Display**: Single room type hides dropdown field entirely, displays room label as read-only text
- **Conflict Resolution**: When participant selects room at capacity, system automatically unassigns minimum participants needed
- **Unassignment Priority**: Highest index participants unassigned first (keeps applicant and early participants stable)
- **User Notifications**: Unassigned participants receive warning notifications via toast system
**Implementation:**
- `ParticipantRoomChoiceLoader`: Always includes all selected rooms (no capacity filtering)
- `ParticipantAssignedRoomFieldHandler`: Detects conflicts and resolves via auto-unassignment
- `detectRoomCapacityConflict()`: Calculates if assignment would exceed capacity
- `resolveRoomCapacityConflict()`: Unassigns minimum participants (highest index first)
- Generates notifications for unassigned participants only (not for user-initiated assignment)
- Handles missing field gracefully when single room type (field excluded from form)
- `RoomAssignmentService`: Auto-assigns when single room type selected at DTO level
- `CreateFieldStateProvider`: Hides `assignedRoomId` field when `SingleRoomTypeCondition` is true
- `BookingDto::getSingleRoomLabel()`: Returns room label for template display when single room type
- Template: Conditionally renders dropdown (multiple rooms) or read-only label (single room)
**Example Scenario:**
- User selects 1x "Doppelzimmer" (capacity 2), 3 participants
- Participants 0 and 1 auto-assigned to "Doppelzimmer"
- Participant 2 manually selects "Doppelzimmer" (at capacity)
- System automatically unassigns Participant 1 (highest index)
- Participant 2 gets assigned, Participant 1 receives warning notification
## Critical Patterns
### Field Handler Pattern
```php
// Handlers process in dependency order (topological sort)
// Example: insurance handler depends on ALL price-affecting fields
$this->fieldHandlerRegistry->processFieldsForParticipant(
$participantData,
$bookingDto,
$index
);
```
**Important**: Field handlers use "sync pattern" - only sync fields present in original submission to avoid validation errors.
### HTMX Block-Based Rendering
```php
// All HTMX swaps target #main-content with innerHTML
// OOB swaps for sidebar: #booking-summary
return $this->htmxOobResponse(
'booking/_participant_form.html.twig',
['participant_form', 'booking_summary'],
$templateData
);
```
### Service Enrichment Pattern
Services from BPN API may lack complete data (especially prices). Always enrich from travel data:
```php
// BookingDataProcessor::enrichParticipantServicesFromTravel()
// Looks up each service in travel data and replaces with full version
```
### Notification System
Field handlers generate notifications (auto-changes) → collected by controller → sent via HX-Trigger → displayed as toasts.
**Implementation:**
- `ParticipantDto::$notifications` - Array of notification messages (keyed by MD5 hash for deduplication)
- `ParticipantDto::addNotification($type, $message, $id = null)` - Add notification with auto-generated or custom ID
- Field handlers call `addNotification()` when making automatic changes (e.g., insurance reassignment, rental clearing)
- Controllers collect notifications via `collectParticipantNotifications()` after field processing
- Notifications sent to frontend via `HX-Trigger` response header with `showNotifications` event
- Frontend `toast_controller.js` displays notifications using toastify-js library
- Notification types: `info`, `warning`, `success` (mapped to Tailwind color classes)
### Floating-Point Precision in Price Calculations
**Critical**: All monetary comparisons must account for floating-point arithmetic accumulation errors.
**The Problem:**
- Prices are parsed from XML as German-formatted strings (e.g., `"1.812,70"`)
- Multiple price additions (rooms + services + insurances) accumulate tiny precision errors (`~1e-15` per operation)
- With bulk insurance and deep calculation chains, errors compound to `~1e-13` or larger
- Example: API returns `1812.7`, calculation produces `1812.6999999999998`
**The Solution:**
- **Always round monetary values to 2 decimal places before comparison**
- Use `round($price, 2)` for cent precision (standard for EUR currency)
- Never use strict equality (`===` or `!==`) on unrounded float prices
**Implementation Example (Step3Controller:93-94):**
```php
// CORRECT: Round to cent precision before comparison
$apiTotal = round($inquiryResponse->totalPrice ?? 0.0, 2);
$calculatedTotal = round($this->priceCalculator->calculateGrandTotal($bookingCreateDto), 2);
if ($apiTotal !== $calculatedTotal) {
// Handle mismatch
}
// WRONG: Direct float comparison (will fail due to precision errors)
if ($inquiryResponse->totalPrice !== $this->priceCalculator->calculateGrandTotal($bookingCreateDto)) {
// This comparison is unreliable!
}
```
**Important Notes:**
- Rounding eliminates precision errors smaller than 1 cent (€0.01)
- The BPN API returns prices already rounded to cent precision
- This is the industry-standard approach for financial calculations
- Only affects comparison logic - does not alter actual price calculation flow
### Dirty State Detection (Edit Mode)
Fingerprint-based change detection to warn users about unsaved modifications:
- **BookingFingerprintService** generates SHA-256 hash of all mutable booking data
- Original fingerprint stored in `BookingDto::$originalFingerprint` on API load
- `isDirty()` compares current state with original to detect changes
- Yellow warning banner displays when changes detected
- Update button conditionally shown only when dirty
- Fingerprint persists in session, survives page refreshes
- Resets on submission, "Änderungen verwerfen", or "Zurück" actions
**Unavailable Services Handling:**
- Services with `available <= 0` included in edit mode (not filtered out)
- `ParticipantFieldOptionsProvider::shouldMakeServiceReadonly()` implements intelligent readonly logic:
- **Create mode**: Uses standard availability calculator (filters out unavailable services)
- **Edit mode**: Services participants already have remain editable even if now fully booked
- **Edit mode**: Unavailable services participant doesn't have are marked readonly
- Prevents fingerprint false positives when services become fully booked during editing session
- Ensures service IDs remain consistent in form submissions for accurate dirty detection
## Key Service Dependencies
### Field Handler Execution Order
Critical for correct pricing and auto-reassignment:
1. Age-dependent fields (dateOfBirth)
2. Price-affecting services (skiPass, rentals, courses, board, transportation, pickup, parking)
3. **Insurance handler LAST** (depends on all price-affecting fields)
4. Bulk insurance handler (applicant only)
### Insurance System
- **3-pass parsing**: Referenced IDs → Individual insurances → Packages with family detection
- **Auto-reassignment**: Maintains insurance type when price tier changes
- **Age constraints**: Absolute age (at travel date) vs birth year
- **Hydration**: `TravelDataService::hydrateInsurancePackageRelationships()` rebuilds package relationships after cache deserialization
- **ID Type**: Insurance IDs are strings (not integers) - ensure all test fixtures use string IDs
- **Mutability**: Insurances are always readonly in edit mode (API limitation) - see `InsuranceMutabilityCondition`
### Transportation Services
- **Unified pickup field**: Single field for both directions (BPN API limitation)
- **Conditional visibility**: Pickup vs parking fields mutually exclusive
- **Direction mapping**: `DirectionMapper` translates API ↔ internal codes
- **Discount replacement logic**: Automatic correction of incompatible transportation combinations
- When inbound bus selected with discounted PKW outbound: system automatically replaces with regular PKW
- When inbound changes from bus to PKW: system automatically restores discounted PKW if available
- Form options update dynamically to show only valid PKW variant
- User notified via toast messages about automatic changes
- Implemented via `ParticipantTransportationDiscountReplacementFieldHandler`
- Prevents invalid pricing where user gets both PKW discount and bus pricing
## Important Field Dependencies
### Ski Pass → Rentals → Insurance → Body Dimensions
- Rentals filtered by ski pass duration (exact date matching)
- Rental insurance only shown when rentals selected
- Body dimensions only shown when rentals selected
- All cleared automatically when dependencies removed
### Date of Birth → Age-Dependent Services
- Courses, additional services, board, insurance hidden until DOB provided
- Age evaluated at travel start date, not current date
- Dual constraint types: absolute_age, birth_year, mixed
- **Array input handling**: `ParticipantDateOfBirthFieldHandler` processes array `['day' => X, 'month' => Y, 'year' => Z]` from three-field widget
- **Date normalization**: Handler converts array to `DateTimeImmutable` using `sprintf()` with proper formatting
### Bulk Insurance Booking
- Applicant enables bulk → applies to all participants
- `BulkInsuranceBookingCondition` hides dependent participant insurance fields
- Uses `InsuranceService::batchAssignInsuranceToParticipants()` for price tier matching
### Room Assignment Field
- **Single room type**: Field completely hidden, room label displayed as read-only text
- **Multiple room types**: Dropdown shown with all selected rooms from Step 1
- `SingleRoomTypeCondition` controls field visibility in `CreateFieldStateProvider`
- Auto-assignment by `RoomAssignmentService` happens at DTO level, independent of form field
- Field handler gracefully handles missing field when excluded from form
## Data Flow
### Create Flow
1. Load/create DTO from session
2. Enrich with fresh API availability data
3. Auto-assign rooms, preselect mandatory services
4. Render cards → user edits participant → field handlers process → save to session
5. Validation check before step 3
6. Final submission to BPN API
### Edit Flow
1. Load booking from BPN API on first visit
2. Generate fingerprint of initial state for dirty detection
3. Store in session with `MODE_EDIT`
4. Apply mutability constraints via `EditFieldStateProvider`
5. Same card-based UI as create flow
6. Handle canceled participants (status 'S')
7. Display warning banner when unsaved changes detected
8. **Validation on submission**: Form wraps cards, validates all participants before API call
9. Submit changes back to BPN API (only if validation passes)
10. Staleness warnings after 5 minutes
11. **Session cleanup**: "Zurück" button clears session via `app_booking_edit_cancel` action
## Common Development Tasks
### Adding a New Field Handler
1. Create handler class extending `AbstractParticipantFieldHandler`
2. Implement `shouldProcess()`, `process()`, `getDependencies()`
3. Register in `services.yaml` with `participant.field_handler` tag
4. Add field options to `ParticipantFieldOptionsProvider`
5. Add conditional logic to `CreateFieldStateProvider` if needed
6. Update template with HTMX refresh triggers
### Adding a New Conditional Field
1. Create condition class implementing `FieldConditionInterface`
2. Register in `CreateFieldStateProvider::registerFieldStateConditions()`
3. Use composite conditions for complex logic (AND/OR/NOT)
### Debugging Field Handler Issues
- Check execution order in `ParticipantFieldHandlerRegistry` (topological sort)
- Verify `shouldProcess()` logic for mode awareness
- Ensure dependencies declared correctly
- Check sync pattern: only sync fields in original submission
### Adding Cross-Participant Validation
1. Add validation method to `ParticipantEditDto` (wrapper DTO)
2. Use `#[Assert\Callback]` attribute with appropriate validation groups
3. Access booking context via `$this->bookingContext` for cross-participant checks
4. Access current participant via `$this->participant`
5. Validation errors automatically displayed on participant cards via red border and badge
### Writing Tests
- **Insurance IDs**: Always use strings, not integers (e.g., `'100'` not `100`)
- **Room properties**: Use `$label` property, not `$name`
- **Participant names**: Index 0 expects "Anmelder:in", others expect "Teilnehmer:in N" (1-based)
- **Mock dependencies**: Ensure all constructor dependencies have mocks. Note: `InsuranceService` is stateless with no dependencies (no mocking required)
- **Insurance mutability**: In edit mode, insurances are always readonly (API limitation)
- **Notification arrays**: Use associative keys (MD5 hashes), not numeric indices - use `reset($notifications)` to get first notification
## File Locations
### Key Design Patterns
**Service Availability in Edit Mode:**
Edit mode requires special handling of unavailable services to prevent fingerprint false positives:
```php
// ParticipantFieldOptionsProvider - intelligent availability filtering
$bookingDto->travel->getAdditionalServicesBySubTypes(
Constants::TOKEN_COURSES,
BookingDto::MODE_CREATE === $bookingDto->getMode() // Only filter in create mode
)
// shouldMakeServiceReadonly() - context-aware readonly logic
// In edit mode: service readonly ONLY if unavailable AND participant doesn't have it
private function shouldMakeServiceReadonly(Service $service, BookingDto $bookingDto, int $participantIndex, string $fieldName): bool
{
if (BookingDto::MODE_CREATE === $bookingDto->getMode()) {
return $this->isServiceUnavailableForParticipant($service, $bookingDto, $participantIndex);
}
// Edit mode: allow keeping services participant already has
if (null !== $service->available && $service->available > 0) {
return false;
}
$participantHasService = match ($fieldName) {
'courses' => $this->hasServiceById($participant->courses, $service->id),
'additionalServices' => $this->hasServiceById($participant->additionalServices, $service->id),
// ... other field types
};
return !$participantHasService; // Readonly only if participant doesn't have it
}
```
**Session Lifecycle Management:**
Edit mode session requires proper cleanup to prevent dirty state persistence:
- **Entry**: `IndexController::loadFormData()` initializes from API with original fingerprint
- **Exit (save)**: `IndexController::index()` clears session on successful API update
- **Exit (discard)**: `IndexController::reloadFromApi()` clears session and reloads from API
- **Exit (cancel)**: `IndexController::cancelEdit()` clears session when user clicks "Zurück"
- **Validation**: Dirty state persists across page refreshes until explicit action taken
### Controllers
**Create Namespace** (`src/Controller/Booking/Create/`):
- `IndexController.php` - Booking session initialization and error handling
- `Step1Controller.php` - Room selection and dates
- `Step2Controller.php` - Participant details with card-based UI
- `Step3Controller.php` - Payment method selection
- `Step4Controller.php` - Final confirmation with comprehensive pricing display and API submission
- Integrates `BookingPriceCalculatorService` for complete pricing breakdown
- Calculates per-participant individual prices via `calculateAllParticipantIndividualPrices()`
- Template displays three-level pricing: aggregate breakdown, per-participant totals, and inline service prices
- All service prices shown in blue (text-blue-700) with German formatting
- Zero prices hidden per project standards
- `SuccessController.php` - Success page after booking completion
**Edit Namespace** (`src/Controller/Booking/Edit/`):
- `IndexController.php` - Edit flow with card-based UI, validation, and session management
- `index()` - Main edit view with dirty state detection
- `editParticipant()` - Individual participant form editing
- `refreshParticipantForm()` - HTMX refresh without validation
- `reloadFromApi()` - Discard changes and reload from API
- `cancelEdit()` - Clean session exit to bookings list
**Root Booking Namespace** (`src/Controller/Booking/`):
- `IndexController.php` - Bookings list
- `DownloadController.php` - Booking document downloads
**Shared Traits** (`src/Controller/Booking/Traits/`):
- `ParticipantCardFlowTrait` - Card rendering, form creation, summary calculation
- Creates `ParticipantEditDto` wrapper for email uniqueness validation
- Wraps participant form type with booking context for cross-participant validation
- `BookingCreateTrait` - Create flow helpers
- `BookingDataTrait` - API data fetching
- `BookingExceptionHandlerTrait` - Error handling
### Templates
**Create Flow**:
- `templates/booking/create/step_1.html.twig` - Room selection
- `templates/booking/create/step_2.html.twig` - Participant cards
- `templates/booking/create/step_3.html.twig` - Payment method
- `templates/booking/create/step_4.html.twig` - Final confirmation with comprehensive pricing display
- **Aggregate pricing breakdown**: Rooms and services grouped by type with totals
- **Per-participant cards**: Individual total price in header, all personal details and service selections
- **Inline service pricing**: Each service shows price in blue (€X,XX format) next to label
- **Payment summary**: Selected payment method and bank details (if debit)
- **Confirmation checkbox**: Final acceptance before API submission
- `templates/booking/create/success.html.twig` - Success page
- `templates/booking/create/error.html.twig` - Error page
**Edit Flow**:
- `templates/booking/edit/index.html.twig` - Edit with participant cards
**Shared Components**:
- `templates/booking/_participant_card.html.twig` - Individual participant card
- Displays validation state with red border and "unvollständige oder fehlerhafte Daten" badge
- Requires `isValid` and `errorMessages` in cardData for validation display
- `templates/booking/_participant_form.html.twig` - Participant edit form
- `templates/booking/_summary.html.twig` - Pricing summary sidebar
### Services
- `src/Service/BookingService.php` - Core workflow and session management
- `src/Service/BookingFingerprintService.php` - Dirty state detection via SHA-256 fingerprinting
- `src/Service/ParticipantCardDataService.php` - Card data generation
- `src/Form/Service/ParticipantFieldHandlerRegistry.php` - Handler orchestration
- `src/Form/Service/ParticipantFieldOptionsProvider.php` - Field configuration with mode-aware availability
- `src/Form/Service/CreateFieldStateProvider.php` - Conditional field states
- `src/Form/Service/EditFieldStateProvider.php` - Edit mode mutability constraints
### Field Handlers
- `src/Form/Service/Participant*FieldHandler.php` (16+ handlers)
- Transportation (outbound, inbound, discount replacement), pickup, parking, ski pass, rentals, insurance, bulk insurance, etc.
- `ParticipantTransportationDiscountReplacementFieldHandler` - Automatic discount replacement for invalid transportation combinations
## Testing
```bash
./vendor/bin/phpunit # All tests (234 tests, 588 assertions)
./vendor/bin/phpunit tests/Service/ # Service layer
./vendor/bin/phpunit tests/BusProNet/ # API integration
./vendor/bin/phpunit tests/Form/ # Form processing and field handlers
/opt/homebrew/bin/php-cs-fixer fix --rules=@Symfony # Code style (Symfony ruleset)
```
**Test Coverage Areas:**
- BusProNet data loaders and processors
- XML parsers (travels, hotels, bookings, insurances)
- Form DTOs and field handlers (including `ParticipantEditDto` wrapper)
- Service layer (pricing, insurance matching, room assignment, card data generation)
- Conditional field system
- Cross-participant validation (email uniqueness)
- Utility classes
## Development Environment
```bash
ddev start # Start DDEV
ddev composer install # Install dependencies
ddev exec bin/console cache:clear # Clear cache
ddev logs # Read PHP error logs
ddev exec "php -r 'opcache_reset()';" # Clear opcache after code changes
```
## Important Notes
- **Room prices are per person**
- **Room model uses `$label` property** (not `$name`) - ensure test fixtures use correct property
- **Zero prices display without suffix** (e.g., "Vollpension" not "Vollpension (€0,00)")
- **All services sorted by price** (cheapest first) via `SortByPriceTrait`
- **Field sync pattern critical**: Only sync fields in original submission to avoid "extra fields" errors
- **Insurance handler requires mode awareness**: Skips processing in edit mode (API doesn't return insurance data)
- **Insurance IDs are strings**: All insurance IDs must be strings, not integers (type safety)
- **Clear opcache after code changes** affecting hydration or serialization
- **HTMX targeting consistency**: All swaps target `#main-content` with `innerHTML`, sidebar via OOB swap
- **Validation pattern**: Both create and edit flows use validation-only forms that wrap card UI for standard Symfony form handling
- **Card validation display**: Uses `ParticipantCardDataService::getAllCardsDataWithValidation()` to enrich cards with validation state
- **Cross-participant validation**: Implemented via `ParticipantEditDto` wrapper with callback validators
- **Email uniqueness**: Adults (16+) require unique emails; children exempt from validation
- **Edit mode service availability**: Services with `available <= 0` remain visible and editable for participants who already have them (prevents fingerprint false positives)
- **Session cleanup on exit**: All exit paths from edit mode (save, discard, cancel) properly clear session to reset dirty state
- **Participant naming convention**: Index 0 is "Anmelder:in", others are "Teilnehmer:in N" (1-based, not 0-based)
- **Notification array structure**: Uses associative keys (MD5 hashes) for deduplication, not numeric indices
- **Floating-point price comparisons**: ALWAYS use `round($price, 2)` before comparing monetary values to avoid precision errors (see "Floating-Point Precision in Price Calculations" section)
## References
- **Project conventions**: `../CLAUDE.md` (root level)
- **User preferences**: `~/.claude/CLAUDE.md`
- **Documentation index**: `README.md` (this directory)
-337
View File
@@ -1,337 +0,0 @@
# MyEP Next Booking System
A sophisticated Symfony 6.4 travel booking application that integrates with Bus Pro Net (BPN) XML API for comprehensive travel management. The system handles complex multi-step booking workflows with dynamic participant forms, conditional field logic, and real-time pricing display.
## 🚀 Quick Start
### Prerequisites
- PHP 8.1+
- Composer
- Node.js & npm
- DDEV (recommended for local development)
### Installation
```bash
# Clone the repository
git clone [repository-url]
cd myep-next-booking
# Install dependencies
composer install
npm install
# Start local development environment
ddev start
# Run database migrations
bin/console doctrine:migrations:migrate
# Generate OAuth2 keys
bin/console app:generate-keys
# Compile assets
npm run dev
```
## 🏗️ Architecture Overview
### Multi-Step Booking Flow
1. **Step 1**: Room selection with quantities and dates
2. **Step 2**: Participant details with conditional fields and service selection
3. **Step 3**: Final confirmation and submission to BPN API
### Core Components
#### BusProNet Integration (`src/BusProNet/`)
- **ApiClient**: XML API communication layer
- **XmlParser/**: Response parsing for travels, hotels, bookings
- **XmlLoader/**: Data loading with caching
- **DataProcessor/**: API data transformation
#### Advanced Form System (`src/Form/Service/`)
- **Conditional Field States**: Dynamic field behavior based on participant data
- **Service Field Handlers**: Modular field processing with dependency resolution
- **Transportation Services**: Comprehensive pickup, parking, and transportation options
- **Real-time Updates**: HTMX integration for seamless UX
#### Service Architecture (`src/Service/`)
- **BookingService**: Core booking workflow management
- **TravelDataService**: Travel data access and caching
- **BookingPriceCalculatorService**: Real-time pricing calculations
## 🎯 Key Features
### ✅ Implemented Features
#### Multi-Step Booking Workflow
- Room selection with dynamic pricing display
- Participant registration with conditional fields
- Service selection (board, ski passes, courses, rentals)
- Real-time booking summary with pricing
#### Transportation Services
- **Transportation Selection**: Bus vs. car transport options
- **Pickup Services**: Location-based pickup with conditional visibility
- **Parking Services**: Self-organized transport parking options
- **Direction Mapping**: Outbound/inbound transportation handling
#### Advanced Form System
- **Conditional Field States**: Age-based, value-dependent field visibility
- **Dynamic Field Options**: Context-aware choice generation
- **HTMX Integration**: Real-time form updates without page refresh
- **XSS Protection**: Built-in security measures
#### Pricing & Display
- **Inline Pricing**: Service costs displayed in form options
- **Real-time Calculations**: Live pricing updates via HTMX
- **Smart Formatting**: Zero-price services handled gracefully
- **Unified Summary**: Integrated booking and pricing display
#### Service Integration
- **Field Handler Registry**: Modular field processing system
- **Service Registration**: Automatic field handler discovery
- **Dependency Resolution**: Smart field interdependency handling
### 🔄 Ongoing Development
- Age-based field constraints
- Enhanced pricing features (discounts, taxes)
- Advanced booking management
- Extended BPN API integration
## 🛠️ Development
### Commands
```bash
# Development workflow
npm run dev # Development build
npm run watch # Watch mode
npm run build # Production build
# Testing
bin/phpunit # All tests
./vendor/bin/phpunit tests/Service/ # Specific directory
./vendor/bin/phpunit tests/BusProNet/ # API integration tests
# Code quality
./vendor/bin/php-cs-fixer fix # Fix code style
# Cache & debugging
bin/console cache:clear # Clear cache
bin/console debug:router # Debug routes
bin/console debug:container # Debug services
# Custom commands
bin/console app:cleanup-xml-dumps # Clean XML dump files
bin/console app:generate-keys # Generate OAuth2 keys
```
### Development Standards
#### Code Style
- PSR-12 compliance with `declare(strict_types=1)`
- PHP 8+ features (typed properties, constructor promotion, match expressions)
- Explicit comparisons and Yoda conditions
- Immutable DateTime objects (DateTimeImmutable, CarbonImmutable)
#### Architecture Patterns
- Service layer for business logic
- DTO pattern for type-safe form data
- Registry pattern for configurable components
- Field handler pattern for complex form processing
- Trait-based code reuse
## 🏛️ Technical Stack
### Backend
- **Framework**: Symfony 6.4 LTS
- **PHP**: 8.1+
- **Database**: MariaDB with Doctrine ORM
- **API Integration**: Custom XML client for BPN API
- **Authentication**: OAuth2 Server Bundle
### Frontend
- **JavaScript**: Stimulus (Hotwired) controllers
- **Dynamic Updates**: HTMX for seamless interactions
- **Styling**: TailwindCSS
- **Build Tool**: Webpack Encore
- **Templating**: Twig
### Development & Operations
- **Local Environment**: DDEV (PHP 8.2, MariaDB 10.11)
- **File Operations**: Flysystem with SFTP support
- **Date Handling**: Carbon for advanced date/time manipulation
- **Logging**: Monolog with multiple channels
- **Testing**: PHPUnit with Symfony bridge
## 📋 Form System Architecture
### Field Handler System
The application uses a sophisticated field handler system for processing complex participant forms:
```php
// Example field handler
class ParticipantTransportationOutboundFieldHandler extends AbstractParticipantFieldHandler
{
public function processField(/* ... */): void
{
// Complex field processing with conditional logic
}
public function getDependencies(): array
{
return ['assignedRoomId', 'dateOfBirth']; // Field dependencies
}
}
```
### Conditional Field States
Fields can have dynamic states based on conditions:
```php
// Age-based field state
$this->fieldStateConditions['advancedServices'] = [
'hidden' => new AgeRangeCondition(null, 15), // Hide for under 15
'required' => FieldValueCondition::equals('roomType', 'suite'),
];
```
### Available Field Handlers
- **Transportation**: Outbound/inbound transport selection
- **Pickup Services**: Location-based pickup options
- **Parking**: Self-organized transport parking
- **Accommodation**: Board, room assignment
- **Activities**: Ski passes, courses, rentals
- **Personal Data**: Age-aware field processing
## 💰 Pricing System
### Real-time Pricing Display
- **Inline Pricing**: Costs shown in form options
- **Live Updates**: HTMX-powered real-time calculations
- **Smart Formatting**: Zero-price services handled elegantly
- **Unified Summary**: Integrated pricing in booking summary
### Service Integration
```php
// Pricing calculation example
$serviceTotal = $this->bookingService->calculateServiceTotal($bookingDto);
$roomTotal = $this->bookingService->calculateRoomTotal($bookingDto);
$grandTotal = $serviceTotal + $roomTotal;
```
## 🔄 BusProNet API Integration
### XML Communication
- **Request Building**: Dynamic XML generation for BPN API
- **Response Parsing**: Structured XML parsing with validation
- **Data Caching**: Intelligent caching for performance
- **Error Handling**: Comprehensive error management
### Data Flow
1. Form submission triggers API request building
2. XML sent to BPN API endpoints
3. Response parsed and validated
4. Data transformed for application use
5. Results cached for performance
## 🧪 Testing Strategy
### Test Coverage
- **Unit Tests**: Service layer and business logic
- **Integration Tests**: API communication and data processing
- **Form Tests**: Field handler and validation logic
- **XML Tests**: API response parsing with fixtures
### Running Tests
```bash
# All tests
./vendor/bin/phpunit
# Specific test suites
./vendor/bin/phpunit tests/BusProNet/ # API integration
./vendor/bin/phpunit tests/Service/ # Service layer
./vendor/bin/phpunit tests/Form/ # Form processing
```
## 📚 Documentation
### Available Documentation
- **[PROJECT_OVERVIEW.md](PROJECT_OVERVIEW.md)**: Comprehensive architecture and implementation guide
- **[CLAUDE.md](../CLAUDE.md)**: Development guidelines for AI assistance (root level)
### Architecture Documentation
Each major system component has detailed documentation covering:
- Implementation patterns
- Usage examples
- Extension guidelines
- Testing strategies
## 🔒 Security & Configuration
### Environment Setup
- BPN API credentials in `.env.local`
- OAuth2 encryption keys via custom command
- SFTP configuration for deployment
- Logging channels for monitoring
### Security Features
- XSS protection in form processing
- OAuth2 authentication
- Secure API communication
- Input validation and sanitization
## 🚀 Deployment
### Production Requirements
- PHP 8.1+ with required extensions
- MariaDB 10.11+
- Web server (Apache/Nginx)
- SFTP access for file operations
- BPN API credentials
### Deployment Steps
1. Install dependencies (`composer install --no-dev`)
2. Generate OAuth2 keys (`bin/console app:generate-keys`)
3. Run database migrations (`bin/console doctrine:migrations:migrate`)
4. Build production assets (`npm run build`)
5. Configure environment variables
6. Set up SFTP access for XML exports
## 🤝 Contributing
### Development Workflow
1. Follow PSR-12 coding standards
2. Use type declarations consistently
3. Write comprehensive tests for new features
4. Update documentation for architectural changes
5. Use the field handler pattern for form extensions
### Key Patterns
- **Service Layer**: Business logic separation
- **DTO Pattern**: Type-safe data transfer
- **Registry Pattern**: Component discovery
- **Field Handlers**: Modular form processing
## 📞 Support
### Logging Channels
- **app**: General application logs
- **bpn**: BusProNet API interactions
- **security**: Authentication/authorization
- **db**: Database-related logs
### Debugging
- Use `bin/console debug:router` for route inspection
- Use `bin/console debug:container` for service inspection
- Check logs in `var/log/` for troubleshooting
- Use DDEV for consistent development environment
---
**Version**: 2.0
**Symfony**: 6.4 LTS
**PHP**: 8.1+
**Status**: Production Ready
**Last Updated**: 2025-01-XX