feat: refactor to cards
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user