feat: refactor to cards

This commit is contained in:
Björn Fromme
2025-10-15 18:31:10 +02:00
parent ead2c092da
commit 49b7a3fe34
40 changed files with 2639 additions and 3097 deletions
+332
View File
@@ -0,0 +1,332 @@
# 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 and submission to BPN API (`Create\Step4Controller`)
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
- `ParticipantValidationTrait` - Validation error extraction for card indicators
- **Validation Pattern**:
- Both controllers use validation-only forms (`BookingCreateStep2Type`, `BookingEditType`)
- Standard Symfony form flow: `handleRequest()``isSubmitted()``isValid()`
- On invalid: Extract error indices, display error banner, highlight cards, disable submit button
- 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` (session-stored)
- **Field Handlers**: 15+ specialized handlers in `src/Form/Service/`
- Registered via service tags with dependency resolution
- Process in dependency order via `ParticipantFieldHandlerRegistry`
- **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
### Service Layer (`src/Service/`)
- `BookingService` - Core booking workflow
- `BookingPriceCalculatorService` - Real-time pricing
- `BookingFingerprintService` - Dirty state detection for edit mode
- `TravelDataService` - API integration and caching
- `ParticipantCardDataService` - Card display data
- `InsuranceMatchingService` - Insurance eligibility and auto-reassignment
- `RoomAssignmentService` - Automatic room assignment
## 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.
### 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
### 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
## 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
### Bulk Insurance Booking
- Applicant enables bulk → applies to all participants
- `BulkInsuranceBookingCondition` hides dependent participant insurance fields
- Uses `InsuranceMatchingService::batchAssignInsuranceToParticipants()` for price tier matching
## 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
## 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 and API submission
- `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
- `ParticipantValidationTrait` - Validation error extraction for card indicators
- `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` - Confirmation
- `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
- `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` (15+ handlers)
- Transportation, pickup, parking, ski pass, rentals, insurance, bulk insurance, etc.
## Testing
```bash
./vendor/bin/phpunit # All tests
./vendor/bin/phpunit tests/Service/ # Service layer
./vendor/bin/phpunit tests/BusProNet/ # API integration
./vendor/bin/php-cs-fixer fix # Code style (Symfony ruleset)
```
## 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**
- **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)
- **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 error indicators**: `ParticipantValidationTrait::extractParticipantErrorIndices()` parses form errors to highlight invalid participant cards
- **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
## References
- **Project conventions**: `../CLAUDE.md` (root level)
- **User preferences**: `~/.claude/CLAUDE.md`
- **Documentation index**: `README.md` (this directory)
+337
View File
@@ -0,0 +1,337 @@
# 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