Files
myep/docs/PROJECT_OVERVIEW.md
T

17 KiB

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

// 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

// 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:

// 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
  • 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

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

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 (especially new ones like InsuranceLoader, InsuranceTypeFilterService)
  • Insurance mutability: In edit mode, insurances are always readonly (API limitation)

File Locations

Key Design Patterns

Service Availability in Edit Mode: Edit mode requires special handling of unavailable services to prevent fingerprint false positives:

// 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

./vendor/bin/phpunit                              # All tests (182 tests, 465 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
  • Service layer (pricing, insurance matching, room assignment)
  • Conditional field system
  • Utility classes

Development Environment

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 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
  • Participant naming convention: Index 0 is "Anmelder:in", others are "Teilnehmer:in N" (1-based, not 0-based)

References

  • Project conventions: ../CLAUDE.md (root level)
  • User preferences: ~/.claude/CLAUDE.md
  • Documentation index: README.md (this directory)