diff --git a/docs/BOOKING_PAYMENT_STEP.md b/docs/BOOKING_PAYMENT_STEP.md
new file mode 100644
index 0000000..84ca83e
--- /dev/null
+++ b/docs/BOOKING_PAYMENT_STEP.md
@@ -0,0 +1,726 @@
+# Booking Payment Step Implementation Plan
+
+**Status:** ✅ COMPLETED
+**Last Updated:** 2025-01-04
+**Goal:** Add payment method selection step (Step 3) to booking creation flow
+
+## Implementation Summary
+
+✅ **Completed Components:**
+
+1. **DTOs Created:**
+ - `BankAccountDto` - IBAN validation, account holder, bank name, SEPA mandate
+ - Payment properties added directly to `BookingCreateDto`:
+ - `paymentMethod` - PAYMENT_METHOD_TRANSFER (default) or PAYMENT_METHOD_DEBIT
+ - `bankAccount` - BankAccountDto instance (nullable)
+ - Payment method constants in `Constants.php` (PAYMENT_METHOD_TRANSFER, PAYMENT_METHOD_DEBIT)
+
+2. **Form Type:**
+ - `BookingCreateStep3Type` - Main step 3 form with payment method and conditional bank account fields
+ - **Removed** `PaymentType` wrapper (unnecessary with proper event handling)
+ - Dynamic field management using POST_SET_DATA and PRE_SUBMIT events
+ - Helper method `addBankAccountField()` to avoid duplication
+
+3. **Controller:**
+ - `CreateStep3Controller` - Main step 3 action and HTMX refresh endpoint
+ - Uses `getSummaryVariables()` trait method for booking summary data
+ - Conditional field rendering handled entirely by form events
+
+4. **Template:**
+ - `create_step_3.html.twig` - Grid layout matching steps 1 & 2
+ - Payment method selection with HTMX for dynamic bank account fields
+ - HTMX attributes on outer wrapper div for proper field replacement
+ - Booking summary sidebar visible on all steps
+
+5. **Validation:**
+ - Conditional validation via `validateBankAccount()` callback in `BookingCreateDto`
+ - Bank account fields only validated when payment method is DEBIT
+ - IBAN format validation using Symfony's built-in `@Assert\Iban`
+ - SEPA mandate acceptance required for direct debit
+
+6. **Business Rules:**
+ - **14-day rule**: Direct debit only available if travel starts ≥14 days from now
+ - Debit option becomes readonly with tooltip when not available
+ - Authorization text from existing booking flow integrated into choice label
+
+## Key Implementation Details
+
+### Form Event Pattern
+```php
+// BookingCreateStep3Type.php - Conditional field rendering
+$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event): void {
+ $bookingDto = $event->getData();
+
+ if (Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod) {
+ $this->addBankAccountField($event->getForm());
+ }
+});
+
+$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event): void {
+ $data = $event->getData();
+ $form = $event->getForm();
+ $paymentMethod = $data['paymentMethod'] ?? null;
+
+ if ($form->has('bankAccount')) {
+ $form->remove('bankAccount');
+ }
+
+ if (Constants::PAYMENT_METHOD_DEBIT === $paymentMethod) {
+ $this->addBankAccountField($form);
+ }
+});
+```
+
+**Key Points:**
+- POST_SET_DATA: Adds field when rendering if paymentMethod is DEBIT
+- PRE_SUBMIT: Removes existing field first, then adds only if DEBIT selected
+- Helper method avoids duplicate field configuration
+- No need to manually unset data - Symfony ignores unmapped fields
+
+### Conditional Validation Pattern
+```php
+// BookingCreateDto.php - Conditional bank account validation
+#[Assert\Callback]
+public function validateBankAccount(ExecutionContextInterface $context): void
+{
+ if (Constants::PAYMENT_METHOD_DEBIT !== $this->paymentMethod) {
+ return; // Skip validation for transfer
+ }
+
+ // Validate only when debit is selected
+ if (null === $this->bankAccount) {
+ $context->buildViolation('Bitte geben Sie Ihre Bankverbindung an.')
+ ->atPath('bankAccount')
+ ->addViolation();
+ }
+ // ... additional field validations
+}
+```
+
+### 14-Day Availability Rule
+```php
+private function isDebitAvailable(BookingCreateDto $bookingDto): bool
+{
+ $travelStartDate = CarbonImmutable::instance($bookingDto->travel->dateFrom);
+ $now = CarbonImmutable::now();
+ $daysUntilTravel = $now->diffInDays($travelStartDate, false);
+
+ return $daysUntilTravel >= 14;
+}
+```
+
+Applied via `choice_attr` callback to make debit option readonly when unavailable.
+
+### HTMX Pattern for Expanded Forms
+For radio buttons (expanded choice types), HTMX attributes must be on a wrapper element:
+
+```twig
+
+ {{ form_row(form.paymentMethod) }}
+
+ {# Conditionally rendered bank account section #}
+ {% if form.bankAccount is defined %}
+ ...
+ {% endif %}
+
+```
+
+**Why this pattern:**
+- Wrapper div captures change events from child radio buttons
+- Entire section (payment method + bank account fields) gets replaced
+- `hx-select` extracts only `#form-payment` from full page response
+
+---
+
+## Executive Summary
+
+Currently, the booking creation flow ends at Step 2 (participant details). We need to add Step 3 to collect payment method information before final submission. Users can choose between:
+1. **Direct Transfer (Überweisung)** - Requires bank account details
+2. **Direct Debit (Lastschrift)** - Requires bank account details + SEPA mandate
+
+---
+
+## Requirements Analysis
+
+### Payment Methods
+
+#### 1. Direct Transfer (Überweisung)
+- User will transfer money manually
+- Required fields:
+ - Payment method selection (radio/select)
+- No bank details needed (invoice will show our account for transfer)
+
+#### 2. Direct Debit (Lastschrift)
+- Automatic withdrawal from user's account
+- Required fields:
+ - IBAN (validated format)
+ - Account holder name (text)
+ - SEPA mandate checkbox (required agreement)
+- Optional fields:
+ - Bank name (text)
+
+### Validation Requirements
+
+**IBAN Validation:**
+- Format: Country code (2 letters) + Check digits (2 digits) + BBAN (up to 30 alphanumeric)
+- German IBAN: DE + 2 digits + 18 digits (total 22 characters)
+- International: Support common EU countries
+- Checksum validation (mod 97 algorithm) via Symfony's `@Assert\Iban`
+- Display formatted with spaces (DE12 3456 7890 1234 5678 90)
+
+**Account Holder Name:**
+- Must match participant/applicant name or be explicitly confirmed
+- Min length: 2 characters
+- Max length: 70 characters (SEPA standard)
+- Allowed: Letters, spaces, hyphens, apostrophes
+
+**Bank Name:**
+- Optional but recommended
+- Max length: 70 characters
+
+### Data Flow
+
+```
+Step 1 (Room Selection)
+ ↓
+Step 2 (Participant Details)
+ ↓
+Step 3 (Payment Method) ← NEW
+ ↓
+Final Submission to API
+```
+
+---
+
+## Implementation Plan
+
+### Phase 1: Create Bank Account DTO
+
+#### Task 1.1: Create BankAccountDto
+**Status:** ✅ Simplified - BIC omitted
+**Files:**
+- `src/Form/Model/BankAccountDto.php` (new)
+
+**Actions:**
+- [ ] Create DTO class with properties:
+ - `?string $iban` - IBAN with spaces stripped for storage
+ - `?string $accountHolder` - Account holder name
+ - `?string $bankName` - Name of the bank (optional)
+ - `bool $sepaMandateAccepted = false` - SEPA mandate checkbox
+- [ ] Add Symfony validation constraints:
+ - `@Assert\Iban()` for IBAN
+ - `@Assert\NotBlank()` for IBAN and account holder
+ - `@Assert\Length(min: 2, max: 70)` for account holder
+ - `@Assert\Length(max: 70)` for bank name
+ - `@Assert\IsTrue()` for SEPA mandate
+- [ ] Add methods:
+ - `getFormattedIban()` - Returns IBAN with spaces for display
+ - `getIbanWithoutSpaces()` - Returns IBAN without spaces for storage/API
+
+**Notes:**
+- Public properties (no constructor promotion needed for DTOs)
+- BIC omitted (not required for German IBANs since 2016)
+- Follow project DTO patterns
+
+---
+
+#### Task 1.2: Create PaymentDto
+**Files:**
+- `src/Form/Model/PaymentDto.php` (new)
+
+**Actions:**
+- [ ] Create DTO class with properties:
+ - `?string $paymentMethod` - 'transfer' or 'debit'
+ - `?BankAccountDto $bankAccount` - Bank account details (nullable)
+- [ ] Add validation:
+ - `@Assert\Choice(choices: ['transfer', 'debit'])`
+ - `@Assert\Valid()` for bankAccount when debit selected
+- [ ] Add method `requiresBankAccount(): bool` - returns true if debit
+- [ ] Add getter/setter methods
+
+**Notes:**
+- Bank account only required for direct debit
+- Validation must be conditional based on payment method
+
+---
+
+### Phase 2: Extend BookingCreateDto
+
+#### Task 2.1: Add Payment Property to BookingCreateDto
+**Files:**
+- `src/Form/Model/BookingCreateDto.php`
+
+**Actions:**
+- [ ] Add property: `public PaymentDto $payment`
+- [ ] Initialize in constructor: `$this->payment = new PaymentDto()`
+- [ ] Ensure serialization works for session storage
+
+**Notes:**
+- Payment data stored in session alongside participant data
+- Must survive page navigation
+
+---
+
+### Phase 3: Create Step 3 Form Types
+
+#### Task 3.1: Create BankAccountType
+**Status:** ✅ Simplified - BIC omitted
+**Files:**
+- `src/Form/BankAccountType.php` (new)
+
+**Actions:**
+- [ ] Create form type for `BankAccountDto`
+- [ ] Add fields:
+ - `iban` - TextType with formatting help text (monospace font)
+ - `accountHolder` - TextType (required)
+ - `bankName` - TextType (optional)
+ - `sepaMandateAccepted` - CheckboxType with rich label (SEPA text)
+- [ ] Add data transformer for IBAN:
+ - Strip spaces from IBAN on submit
+ - Format IBAN with spaces for display
+- [ ] Add help text with IBAN format example: "DE12 3456 7890 1234 5678 90"
+- [ ] Style SEPA mandate as prominent checkbox with simple legal text
+
+**Notes:**
+- Use monospace font for IBAN field
+- Add inline validation feedback
+- SEPA text: Simple generic authorization text (no Creditor ID needed)
+
+---
+
+#### Task 3.2: Create PaymentType
+**Files:**
+- `src/Form/PaymentType.php` (new)
+
+**Actions:**
+- [ ] Create form type for `PaymentDto`
+- [ ] Add field: `paymentMethod` - ChoiceType (expanded radios)
+ - Choice 1: 'transfer' → "Überweisung"
+ - Choice 2: 'debit' → "Lastschrift"
+- [ ] Add field: `bankAccount` - BankAccountType (conditional)
+- [ ] Add form events to show/hide bank account fields:
+ - `PRE_SET_DATA` - Add bank account if debit selected
+ - `PRE_SUBMIT` - Add bank account if debit submitted
+- [ ] Add JavaScript/HTMX to toggle bank account section visibility
+
+**Notes:**
+- Bank account section hidden when transfer selected
+- Use Stimulus controller for toggle behavior
+- Smooth transition when switching payment methods
+
+---
+
+#### Task 3.3: Create BookingCreateStep3Type
+**Files:**
+- `src/Form/BookingCreateStep3Type.php` (new)
+
+**Actions:**
+- [ ] Create form type for `BookingCreateDto` (uses payment property)
+- [ ] Add field: `payment` - PaymentType
+- [ ] Set data_class to `BookingCreateDto::class`
+- [ ] Add validation groups: `['payment']`
+
+**Notes:**
+- Follows same pattern as Step1Type and Step2Type
+- Validates only payment-related fields
+
+---
+
+### Phase 4: Create Step 3 Controller
+
+#### Task 4.1: Create CreateStep3Controller
+**Files:**
+- `src/Controller/Booking/CreateStep3Controller.php` (new)
+
+**Actions:**
+- [ ] Create controller with two actions:
+ - `step3()` - Display form (GET)
+ - `processStep3()` - Process form (POST)
+- [ ] Load `BookingCreateDto` from session
+- [ ] Create form with `BookingCreateStep3Type`
+- [ ] On valid submission:
+ - Update session with payment data
+ - Redirect to confirmation/summary page (or direct to API submission)
+- [ ] Add "Back to Step 2" button
+- [ ] Add route: `/booking/create/step-3`
+- [ ] Add access control: `@IsGranted('IS_AUTHENTICATED_ANONYMOUSLY')`
+
+**Notes:**
+- Session key: same as Step 1/2 (`booking_create_dto`)
+- Validate that Steps 1 & 2 are complete before showing Step 3
+- Clear session on final submission
+
+---
+
+### Phase 5: Create Step 3 Template
+
+#### Task 5.1: Create step_3.html.twig
+**Files:**
+- `templates/booking/create_step_3.html.twig` (new)
+
+**Actions:**
+- [ ] Create template extending base layout
+- [ ] Add progress indicator (Step 1 → Step 2 → **Step 3** → Confirmation)
+- [ ] Add payment method selection with clear labels
+- [ ] Add conditional bank account section:
+ - Hidden by default
+ - Shows when direct debit selected
+ - Smooth CSS transition
+- [ ] Add form with:
+ - Payment method radios (large, clear)
+ - Bank account fields (conditional)
+ - SEPA mandate checkbox with legal text
+ - "Back" and "Continue" buttons
+- [ ] Add Stimulus controller for payment method toggle
+- [ ] Add inline validation feedback
+- [ ] Add help text for IBAN format examples
+
+**Notes:**
+- Clear visual hierarchy: payment method choice prominent
+- Bank account section visually grouped
+- SEPA text: "Ich ermächtige [Company] Zahlungen von meinem Konto mittels Lastschrift einzuziehen..."
+
+---
+
+### Phase 6: Update Navigation Flow
+
+#### Task 6.1: Update CreateStep2Controller
+**Files:**
+- `src/Controller/Booking/CreateStep2Controller.php`
+
+**Actions:**
+- [ ] Change successful submission redirect:
+ - FROM: Confirmation page
+ - TO: `app_booking_create_step_3`
+- [ ] Keep session data intact (don't clear)
+
+**Notes:**
+- Simple redirect change
+- Session persistence already in place
+
+---
+
+#### Task 6.2: Add Navigation Helpers
+**Files:**
+- `src/Service/BookingSessionService.php` (new, optional)
+
+**Actions:**
+- [ ] Create service to manage booking session state (optional)
+- [ ] Add methods:
+ - `getBookingDto(): ?BookingCreateDto`
+ - `saveBookingDto(BookingCreateDto $dto): void`
+ - `clearBookingSession(): void`
+ - `validateStepAccess(int $step): bool` - Check prerequisite steps
+- [ ] Inject into controllers
+
+**Notes:**
+- Optional improvement for cleaner controller code
+- Can be deferred if time-constrained
+
+---
+
+### Phase 7: Add Validation & Error Handling
+
+**Status:** ✅ Simplified - Using Symfony's built-in validators only
+
+**Notes:**
+- Symfony's `@Assert\Iban` is sufficient for IBAN validation
+- No custom validators needed
+- German error messages configured in translation files
+
+---
+
+### Phase 8: Add SEPA Mandate Text Management
+
+#### Task 8.1: Create SEPA Mandate Template
+**Files:**
+- `templates/booking/_sepa_mandate_text.html.twig` (new)
+
+**Actions:**
+- [ ] Create reusable template partial with SEPA mandate text
+- [ ] Include variables:
+ - Company name
+ - Creditor ID (SEPA Gläubiger-ID)
+ - Mandate reference (generated per booking)
+- [ ] Format as legal text with proper line breaks
+- [ ] Add checkbox label referencing this text
+
+**Notes:**
+- Text must be legally compliant
+- Consult legal team for exact wording
+- Consider making mandate reference visible to user
+
+---
+
+### Phase 9: Frontend Enhancements
+
+#### Task 9.1: Create Payment Toggle Stimulus Controller
+**Files:**
+- `assets/controllers/payment_toggle_controller.js` (new)
+
+**Actions:**
+- [ ] Create Stimulus controller to toggle bank account visibility
+- [ ] Listen to payment method radio change
+- [ ] Show/hide bank account section with smooth transition
+- [ ] Clear bank account fields when switching to transfer
+- [ ] Add/remove required attributes dynamically
+
+**Notes:**
+- Use CSS transitions for smooth UX
+- Ensure accessibility (aria attributes)
+- Works without JavaScript (progressive enhancement)
+
+---
+
+#### Task 9.2: Add IBAN Formatting Helper
+**Files:**
+- `assets/controllers/iban_formatter_controller.js` (new)
+
+**Actions:**
+- [ ] Create Stimulus controller for IBAN input
+- [ ] Format IBAN with spaces as user types: `DE12 3456 7890 1234 5678 90`
+- [ ] Strip spaces on submit
+- [ ] Show character count/validation status
+- [ ] Use monospace font for input
+
+**Notes:**
+- Real-time formatting improves UX
+- Visual feedback for correct format
+- Consider using library: `iban-formatter`
+
+---
+
+### Phase 10: Testing
+
+#### Task 10.1: Create Unit Tests
+**Files:**
+- `tests/Form/Model/BankAccountDtoTest.php` (new)
+- `tests/Form/Model/PaymentDtoTest.php` (new)
+
+**Actions:**
+- [ ] Test IBAN validation (valid/invalid formats)
+- [ ] Test conditional validation (bank account required for debit)
+- [ ] Test SEPA mandate validation
+- [ ] Test account holder name validation
+
+**Notes:**
+- Cover edge cases: empty, malformed, international IBANs
+- Test both valid and invalid inputs
+
+---
+
+#### Task 10.2: Create Integration Tests
+**Files:**
+- `tests/Controller/Booking/CreateStep3ControllerTest.php` (new)
+
+**Actions:**
+- [ ] Test form display
+- [ ] Test transfer selection (no bank account)
+- [ ] Test debit selection (requires bank account)
+- [ ] Test validation errors
+- [ ] Test session persistence
+- [ ] Test navigation (back to Step 2)
+
+**Notes:**
+- Use test client to simulate user interaction
+- Verify session state after each action
+
+---
+
+### Phase 11: Update API Submission
+
+#### Task 11.1: Update BookingDataProcessor
+**Status:** ✅ Simplified - BIC omitted from API payload
+**Files:**
+- `src/BusProNet/DataProcessor/BookingDataProcessor.php`
+
+**Actions:**
+- [ ] Update `createBookingRequestPayload()` method (when implemented in Phase 5)
+- [ ] Add payment data to API payload:
+ - Payment method (`zahlart`)
+ - Bank account details (if debit):
+ - IBAN (`iban`)
+ - Account holder (`kontoinhaber`)
+ - Bank name (`bankname`) - optional
+- [ ] Map payment method to API codes:
+ - 'transfer' → 'UE' (or appropriate API code)
+ - 'debit' → 'LS' (or appropriate API code)
+
+**Notes:**
+- BIC omitted (not required for SEPA since 2016)
+- Check BPN API documentation for exact field names
+- SEPA mandate reference may need to be generated
+
+---
+
+#### Task 11.2: Store SEPA Mandate Reference
+**Files:**
+- `src/Service/SepaMandateService.php` (new)
+
+**Actions:**
+- [ ] Create service to generate unique SEPA mandate references
+- [ ] Format: `[PREFIX]-[BOOKING_ID]-[TIMESTAMP]`
+- [ ] Store reference in booking data
+- [ ] Make available for PDF invoice generation
+
+**Notes:**
+- Mandate reference must be unique and traceable
+- Consider using UUID or sequential ID
+
+---
+
+## Database Considerations
+
+### SEPA Mandate Storage
+
+**Option 1: Store in Booking API Data**
+- Mandate reference sent to BPN API
+- Stored in BPN system
+- Retrieved with booking data
+
+**Option 2: Local Database Table**
+- Create `sepa_mandates` table
+- Store: mandate_id, booking_id, iban, date_signed, status
+- Allows local tracking and reporting
+
+**Recommendation:** Start with Option 1 (API storage), add Option 2 if needed for compliance/reporting
+
+---
+
+## Security Considerations
+
+1. **HTTPS Required:** Bank details must be transmitted over HTTPS only
+2. **Session Security:** Ensure session data encrypted
+3. **Input Sanitization:** Strip/validate all bank account inputs
+4. **CSRF Protection:** Ensure form has CSRF token
+5. **Rate Limiting:** Prevent brute force on payment form
+6. **Audit Trail:** Log payment method changes
+
+---
+
+## UX Considerations
+
+1. **Clear Labels:** "Überweisung" and "Lastschrift" with explanations
+2. **Visual Feedback:** Show selected payment method prominently
+3. **Inline Validation:** Real-time IBAN format checking
+4. **Error Messages:** Clear, actionable German error messages
+5. **Help Text:** Examples of valid IBAN formats
+6. **Progress Indicator:** Show user is on Step 3 of 4
+7. **Mobile Friendly:** Large touch targets for payment method selection
+
+---
+
+## Error Handling
+
+### Validation Errors
+- Show inline next to field
+- Highlight invalid fields in red
+- Provide clear guidance on how to fix
+
+### Session Errors
+- If session expired, redirect to Step 1 with message
+- Preserve as much data as possible
+
+### API Errors
+- If payment method not accepted by API, show clear error
+- Suggest alternative payment method
+
+---
+
+## Localization
+
+All text in German:
+- **Überweisung:** Direct transfer
+- **Lastschrift:** Direct debit
+- **IBAN:** Internationale Bankkontonummer
+- **BIC:** Bank Identifier Code
+- **Kontoinhaber:** Account holder
+- **SEPA-Mandat:** SEPA mandate
+- **Bankname:** Bank name
+
+Error messages in German with clear instructions.
+
+---
+
+## Dependencies
+
+### PHP Libraries
+- `symfony/validator` (already installed)
+- `symfony/form` (already installed)
+- Consider: `iban-validation/iban` for enhanced IBAN validation (optional)
+
+### JavaScript Libraries
+- Stimulus (already installed)
+- Consider: Custom IBAN formatter or library
+
+### No new major dependencies required
+
+---
+
+## Migration Path
+
+1. Implement in order: Phases 1 → 2 → 3 → 4 → 5 → 6
+2. Test each phase before moving to next
+3. Phases 7-11 can be done in parallel after Phase 6
+4. Deploy behind feature flag initially (optional)
+
+---
+
+## Open Questions
+
+1. **SEPA Creditor ID:** What is the company's SEPA Gläubiger-ID?
+2. **Payment Method Codes:** What are the exact BPN API codes for transfer/debit?
+3. **Mandate Reference Format:** Any specific format requirements?
+4. **Invoice Generation:** Does invoice need to show SEPA mandate reference?
+5. **Default Payment Method:** Should we pre-select one method?
+6. **International IBANs:** Support only German IBANs or EU-wide?
+
+---
+
+## Success Criteria
+
+- [ ] User can select payment method
+- [ ] Bank account fields shown only for direct debit
+- [ ] IBAN validated correctly
+- [ ] SEPA mandate text displayed and accepted
+- [ ] Payment data stored in session
+- [ ] Payment data submitted to API
+- [ ] Session cleared after successful booking
+- [ ] All validation works (unit + integration tests)
+- [ ] Mobile-friendly UI
+- [ ] Accessible (keyboard navigation, screen readers)
+
+---
+
+## Timeline Estimate
+
+- **Phase 1-2:** DTOs & Validation - 2 hours
+- **Phase 3:** Form Types - 2 hours
+- **Phase 4:** Controller - 1 hour
+- **Phase 5:** Template - 2 hours
+- **Phase 6:** Navigation - 0.5 hour
+- **Phase 7:** Custom Validators - 1 hour (optional)
+- **Phase 8:** SEPA Text - 0.5 hour
+- **Phase 9:** Frontend - 2 hours
+- **Phase 10:** Testing - 2 hours
+- **Phase 11:** API Integration - 1 hour
+
+**Total:** ~14 hours (can be reduced if some phases skipped/simplified)
+
+---
+
+## References
+
+- SEPA Direct Debit Scheme: https://www.europeanpaymentscouncil.eu/
+- IBAN Validation: https://en.wikipedia.org/wiki/International_Bank_Account_Number
+- Symfony Form Events: https://symfony.com/doc/current/form/events.html
+- Symfony Validation: https://symfony.com/doc/current/validation.html
+
+---
+
+**End of Document**
\ No newline at end of file
diff --git a/src/Controller/Booking/BookingCreateTrait.php b/src/Controller/Booking/BookingCreateTrait.php
index abf9f44..0df8d33 100644
--- a/src/Controller/Booking/BookingCreateTrait.php
+++ b/src/Controller/Booking/BookingCreateTrait.php
@@ -46,4 +46,35 @@ trait BookingCreateTrait
return $this->redirectToRoute($route);
}
+
+ /**
+ * Returns the total number of participants based on room selections.
+ */
+ private function getParticipantsCount(BookingCreateDto $bookingCreateDto): int
+ {
+ return $this
+ ->bookingService
+ ->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travel);
+ }
+
+ /**
+ * Prepares template variables for the booking summary sidebar.
+ *
+ * @return array Array containing all variables needed for the summary partial
+ */
+ private function getSummaryVariables(BookingCreateDto $bookingCreateDto): array
+ {
+ $participantsCount = $this->getParticipantsCount($bookingCreateDto);
+ $summary = $this->bookingService->getRoomSummaryAndParticipantCount($bookingCreateDto);
+ $roomAssignmentCounts = $this->bookingService->getRoomAssignmentCounts($bookingCreateDto);
+ $availableRooms = $bookingCreateDto->travel->getAvailableRooms();
+ $groupedSelectedRooms = $this->bookingService->groupRoomSelectionsByType($bookingCreateDto->getSelectedRooms(), $availableRooms);
+
+ return [
+ 'participantsCount' => $participantsCount,
+ 'pricingData' => $summary['pricing'],
+ 'assignmentCounts' => $roomAssignmentCounts,
+ 'groupedSelectedRooms' => $groupedSelectedRooms,
+ ];
+ }
}
diff --git a/src/Controller/Booking/CreateStep2Controller.php b/src/Controller/Booking/CreateStep2Controller.php
index 0a34216..2f194b8 100644
--- a/src/Controller/Booking/CreateStep2Controller.php
+++ b/src/Controller/Booking/CreateStep2Controller.php
@@ -166,20 +166,6 @@ class CreateStep2Controller extends AbstractController
return $response;
}
- /**
- * Calculates the total number of participants based on room selections.
- *
- * @param BookingCreateDto $bookingCreateDto The booking DTO containing room selections
- *
- * @return int Total number of participants required
- */
- private function getParticipantsCount(BookingCreateDto $bookingCreateDto): int
- {
- return $this
- ->bookingService
- ->getParticipantsCount($bookingCreateDto->roomSelections, $bookingCreateDto->travel);
- }
-
/**
* Ensures the booking DTO has the correct number of participant objects.
*
diff --git a/src/Controller/Booking/CreateStep3Controller.php b/src/Controller/Booking/CreateStep3Controller.php
index 6b31450..13cad92 100644
--- a/src/Controller/Booking/CreateStep3Controller.php
+++ b/src/Controller/Booking/CreateStep3Controller.php
@@ -4,35 +4,37 @@ declare(strict_types=1);
namespace App\Controller\Booking;
+use App\BusProNet\Constants;
+use App\Controller\Traits\HtmxControllerTrait;
+use App\Form\BookingCreateStep3Type;
use App\Service\BookingService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
+use Symfony\Component\Security\Http\Attribute\IsGranted;
/**
- * Handles the third step of the booking creation process.
- *
- * This controller manages the booking confirmation and final review
- * before completing the booking process.
+ * Handles the third step of the booking creation process (payment method selection).
*/
class CreateStep3Controller extends AbstractController
{
use BookingCreateTrait;
use BookingExceptionHandlerTrait;
+ use HtmxControllerTrait;
public function __construct(
- private readonly BookingService $bookingCreateService,
+ private readonly BookingService $bookingService,
) {
}
/**
- * Displays the booking confirmation page.
+ * Displays and processes the payment method form.
*/
- #[Route('/bookings/create/confirm', name: 'app_booking_create_step_3')]
- public function confirm(Request $request): Response
+ #[Route('/bookings/create/payment', name: 'app_booking_create_step_3')]
+ public function step3(Request $request): Response
{
- $result = $this->getOrCreateBookingCreateDto($this->bookingCreateService, $request);
+ $result = $this->getOrCreateBookingCreateDto($this->bookingService, $request);
if ($result instanceof Response) {
return $result;
}
@@ -43,8 +45,48 @@ class CreateStep3Controller extends AbstractController
return $redirect;
}
+ $form = $this->createForm(BookingCreateStep3Type::class, $bookingCreateDto);
+ $form->handleRequest($request);
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
+
+ // TODO: Redirect to confirmation page or direct API submission
+ $this->addFlash('success', 'Zahlungsart erfolgreich ausgewählt.');
+
+ return $this->redirectToRoute('app_booking_create_step_3');
+ }
+
return $this->render('booking/create_step_3.html.twig', [
'bookingCreateDto' => $bookingCreateDto,
+ 'form' => $form->createView(),
+ ...$this->getSummaryVariables($bookingCreateDto),
+ ]);
+ }
+
+ /**
+ * Handles HTMX refresh when payment method changes.
+ */
+ #[Route('/bookings/create/payment/refresh', name: 'app_booking_create_step_3_refresh')]
+ public function refresh(Request $request): Response
+ {
+ $result = $this->getOrCreateBookingCreateDto($this->bookingService, $request);
+ if ($result instanceof Response) {
+ return $result;
+ }
+ $bookingCreateDto = $result;
+
+ $form = $this->createForm(BookingCreateStep3Type::class, $bookingCreateDto, [
+ 'validation_groups' => false,
+ ]);
+ $form->handleRequest($request);
+
+ $this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
+
+ return $this->render('booking/create_step_3.html.twig', [
+ 'bookingCreateDto' => $bookingCreateDto,
+ 'form' => $form->createView(),
+ ...$this->getSummaryVariables($bookingCreateDto),
]);
}
}
diff --git a/src/Form/BankAccountType.php b/src/Form/BankAccountType.php
new file mode 100644
index 0000000..8a60431
--- /dev/null
+++ b/src/Form/BankAccountType.php
@@ -0,0 +1,53 @@
+add('iban', TextType::class, [
+ 'label' => 'IBAN',
+ 'required' => true,
+ 'attr' => [
+ 'class' => 'font-mono',
+ 'placeholder' => 'DE12 3456 7890 1234 5678 90',
+ ],
+ ])
+ ->add('accountHolder', TextType::class, [
+ 'label' => 'Kontoinhaber',
+ 'required' => true,
+ ])
+ ->add('bankName', TextType::class, [
+ 'label' => 'Bankname',
+ 'required' => false,
+ 'attr' => [
+ 'placeholder' => 'optional',
+ ],
+ ])
+ ->add('sepaMandateAccepted', CheckboxType::class, [
+ 'label' => 'Hiermit ermächtige/n ich/wir Sie widerruflich, die von mir/uns zu entrichtende Zahlung bei Fälligkeit zu Lasten meines/unseres Girokontos durch Lastschrift einzuziehen.
' .
+ 'Bei kurzfristigen Buchungen (ab 2 Wochen vor Reisebeginn) ist Lastschrift NICHT mehr möglich.',
+ 'required' => true,
+ 'label_html' => true,
+ ])
+ ;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => BankAccountDto::class,
+ ]);
+ }
+}
diff --git a/src/Form/BookingCreateStep3Type.php b/src/Form/BookingCreateStep3Type.php
new file mode 100644
index 0000000..12a8c92
--- /dev/null
+++ b/src/Form/BookingCreateStep3Type.php
@@ -0,0 +1,99 @@
+add('paymentMethod', ChoiceType::class, [
+ 'label' => 'Bitte wähle hier die gewünschte Zahlungsart:',
+ 'choices' => [
+ 'Überweisung' => Constants::PAYMENT_METHOD_TRANSFER,
+ 'Lastschrift (Einzugsermächtigungsverfahren)' => Constants::PAYMENT_METHOD_DEBIT,
+ ],
+ 'expanded' => true,
+ 'required' => true,
+ 'placeholder' => false,
+ 'label_html' => true,
+ 'choice_attr' => function (string $choice) use ($options): array {
+ if (Constants::PAYMENT_METHOD_DEBIT === $choice && false === $this->isDebitAvailable($options['data'])) {
+ return [
+ 'readonly' => true,
+ 'data-tooltip' => 'nicht mehr verfügbar (weniger als 14 Tage vor Reisebeginn)',
+ ];
+ }
+
+ return [];
+ },
+ ])
+ ;
+
+ // Add bank account fields conditionally based on payment method
+ $builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event): void {
+ $bookingDto = $event->getData();
+
+ if (Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod) {
+ $this->addBankAccountField($event->getForm());
+ }
+ });
+
+ $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event): void {
+ $data = $event->getData();
+ $form = $event->getForm();
+ $paymentMethod = $data['paymentMethod'] ?? null;
+
+ if ($form->has('bankAccount')) {
+ $form->remove('bankAccount');
+ }
+
+ if (Constants::PAYMENT_METHOD_DEBIT === $paymentMethod) {
+ $this->addBankAccountField($form);
+ }
+ });
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => BookingCreateDto::class,
+ ]);
+ }
+
+ /**
+ * Adds the bank account field to the form.
+ */
+ private function addBankAccountField($form): void
+ {
+ $form->add('bankAccount', BankAccountType::class, [
+ 'label' => false,
+ ]);
+ }
+
+ /**
+ * Checks if direct debit payment is available based on travel date.
+ *
+ * Direct debit is only available if the travel starts at least 14 days from now.
+ */
+ private function isDebitAvailable(BookingCreateDto $bookingDto): bool
+ {
+ $travelStartDate = CarbonImmutable::instance($bookingDto->travel->dateFrom);
+ $now = CarbonImmutable::now();
+ $daysUntilTravel = $now->diffInDays($travelStartDate, false);
+
+ return $daysUntilTravel >= 14;
+ }
+}
diff --git a/src/Form/Model/BankAccountDto.php b/src/Form/Model/BankAccountDto.php
new file mode 100644
index 0000000..fa46110
--- /dev/null
+++ b/src/Form/Model/BankAccountDto.php
@@ -0,0 +1,61 @@
+iban) {
+ return null;
+ }
+
+ $cleanIban = $this->getIbanWithoutSpaces();
+
+ return chunk_split($cleanIban, 4, ' ');
+ }
+
+ /**
+ * Returns IBAN without spaces for storage and API submission.
+ */
+ public function getIbanWithoutSpaces(): ?string
+ {
+ if (null === $this->iban) {
+ return null;
+ }
+
+ return (string) preg_replace('/\s+/', '', $this->iban);
+ }
+}
diff --git a/src/Form/Model/BookingCreateDto.php b/src/Form/Model/BookingCreateDto.php
index 3194500..d3b3827 100644
--- a/src/Form/Model/BookingCreateDto.php
+++ b/src/Form/Model/BookingCreateDto.php
@@ -2,6 +2,7 @@
namespace App\Form\Model;
+use App\BusProNet\Constants;
use App\BusProNet\Model\Travel;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -22,6 +23,14 @@ class BookingCreateDto implements BookingDtoInterface
#[Assert\Valid]
public array $participants = [];
+ #[Assert\Choice(
+ choices: [Constants::PAYMENT_METHOD_TRANSFER, Constants::PAYMENT_METHOD_DEBIT],
+ message: 'Bitte wählen Sie eine gültige Zahlungsart.'
+ )]
+ public ?string $paymentMethod = Constants::PAYMENT_METHOD_TRANSFER;
+
+ public ?BankAccountDto $bankAccount = null;
+
public function __construct(public Travel $travel, public int $hotelId)
{
}
@@ -95,4 +104,41 @@ class BookingCreateDto implements BookingDtoInterface
->addViolation();
}
}
+
+ #[Assert\Callback]
+ public function validateBankAccount(ExecutionContextInterface $context): void
+ {
+ if (Constants::PAYMENT_METHOD_DEBIT !== $this->paymentMethod) {
+ return;
+ }
+
+ if (null === $this->bankAccount) {
+ $context->buildViolation('Bitte geben Sie Ihre Bankverbindung an.')
+ ->atPath('bankAccount')
+ ->addViolation();
+
+ return;
+ }
+
+ // Validate IBAN
+ if (null === $this->bankAccount->iban || '' === trim($this->bankAccount->iban)) {
+ $context->buildViolation('Bitte geben Sie Ihre IBAN ein.')
+ ->atPath('bankAccount.iban')
+ ->addViolation();
+ }
+
+ // Validate account holder
+ if (null === $this->bankAccount->accountHolder || '' === trim($this->bankAccount->accountHolder)) {
+ $context->buildViolation('Bitte geben Sie den Kontoinhaber ein.')
+ ->atPath('bankAccount.accountHolder')
+ ->addViolation();
+ }
+
+ // Validate SEPA mandate
+ if (false === $this->bankAccount->sepaMandateAccepted) {
+ $context->buildViolation('Bitte akzeptieren Sie das SEPA-Mandat.')
+ ->atPath('bankAccount.sepaMandateAccepted')
+ ->addViolation();
+ }
+ }
}
diff --git a/src/Form/Model/ParticipantDto.php b/src/Form/Model/ParticipantDto.php
index cfe7c4e..35e59a2 100644
--- a/src/Form/Model/ParticipantDto.php
+++ b/src/Form/Model/ParticipantDto.php
@@ -12,6 +12,28 @@ use Symfony\Component\Validator\Constraints as Assert;
#[AppAssert\Participant(groups: ['booking_edit', 'booking_create_step_2'])]
class ParticipantDto
{
+ /**
+ * List of dynamic participant fields that can be conditionally hidden/shown.
+ */
+ public const DYNAMIC_FIELDS = [
+ 'assignedRoomId',
+ 'remarksRoom',
+ 'courses',
+ 'additionalServices',
+ 'board',
+ 'rentals',
+ 'rentalInsurance',
+ 'skiPass',
+ 'transportationOutbound',
+ 'transportationInbound',
+ 'pickupOutbound',
+ 'pickupInbound',
+ 'parking',
+ 'licensePlate',
+ 'bulkInsuranceBooking',
+ 'insurance',
+ ];
+
public ?int $index = null;
public ?int $addressId = null;
public ?int $personId = null;
diff --git a/src/Form/PaymentType.php b/src/Form/PaymentType.php
new file mode 100644
index 0000000..4f1e6af
--- /dev/null
+++ b/src/Form/PaymentType.php
@@ -0,0 +1,106 @@
+add('paymentMethod', ChoiceType::class, [
+ 'label' => 'Zahlungsart',
+ 'help' => 'Lastschrift ist nur bis 14 Tage vor Reisebeginn möglich',
+ 'choices' => [
+ 'Überweisung' => Constants::PAYMENT_METHOD_TRANSFER,
+ 'Lastschrift' => Constants::PAYMENT_METHOD_DEBIT,
+ ],
+ 'expanded' => true,
+ 'required' => true,
+ 'placeholder' => false,
+ ])
+ ;
+
+ // Add bank account fields conditionally based on payment method
+ $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void {
+ $bookingDto = $event->getData();
+
+ // Add choice_attr dynamically based on travel date
+ $paymentMethodField = $event->getForm()->get('paymentMethod');
+ $paymentMethodConfig = $paymentMethodField->getConfig();
+
+ // Rebuild the field with dynamic choice_attr
+ $event->getForm()->add('paymentMethod', ChoiceType::class, [
+ 'label' => 'Zahlungsart',
+ 'help' => 'Lastschrift ist nur bis 14 Tage vor Reisebeginn möglich',
+ 'choices' => [
+ 'Überweisung' => Constants::PAYMENT_METHOD_TRANSFER,
+ 'Lastschrift' => Constants::PAYMENT_METHOD_DEBIT,
+ ],
+ 'expanded' => true,
+ 'required' => true,
+ 'placeholder' => false,
+ 'choice_attr' => function (string $choice) use ($bookingDto): array {
+ if (Constants::PAYMENT_METHOD_DEBIT === $choice && false === $this->isDebitAvailable($bookingDto)) {
+ return [
+ 'readonly' => true,
+ 'data-tooltip' => 'nicht mehr verfügbar (weniger als 14 Tage vor Reisebeginn)',
+ ];
+ }
+
+ return [];
+ },
+ ]);
+
+ // Add bank account fields if debit is selected
+ if (Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod) {
+ $event->getForm()->add('bankAccount', BankAccountType::class, [
+ 'label' => false,
+ ]);
+ }
+ });
+
+ $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event): void {
+ $data = $event->getData();
+ $paymentMethod = $data['paymentMethod'] ?? null;
+
+ if (Constants::PAYMENT_METHOD_DEBIT === $paymentMethod) {
+ $event->getForm()->add('bankAccount', BankAccountType::class, [
+ 'label' => false,
+ ]);
+ }
+ });
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'inherit_data' => true,
+ ]);
+ }
+
+ /**
+ * Checks if direct debit payment is available based on travel date.
+ *
+ * Direct debit is only available if the travel starts at least 14 days from now.
+ */
+ private function isDebitAvailable(BookingCreateDto $bookingDto): bool
+ {
+ $travelStartDate = CarbonImmutable::instance($bookingDto->travel->dateFrom);
+ $now = CarbonImmutable::now();
+ $daysUntilTravel = $now->diffInDays($travelStartDate, false);
+
+ return $daysUntilTravel >= 14;
+ }
+}
diff --git a/templates/booking/create_step_2.html.twig b/templates/booking/create_step_2.html.twig
index 36d3d25..c97d737 100644
--- a/templates/booking/create_step_2.html.twig
+++ b/templates/booking/create_step_2.html.twig
@@ -281,13 +281,7 @@
{% endif %}
{% if participant.licensePlate is defined %}
- {{ form_row(participant.licensePlate, {
- 'attr': {
- 'hx-trigger': 'change',
- 'hx-post': path('app_booking_create_step_2_refresh'),
- 'hx-swap': 'none'
- }
- }) }}
+ {{ form_row(participant.licensePlate) }}
{% endif %}
diff --git a/templates/booking/create_step_3.html.twig b/templates/booking/create_step_3.html.twig
index 50e3410..6e75580 100644
--- a/templates/booking/create_step_3.html.twig
+++ b/templates/booking/create_step_3.html.twig
@@ -1,4 +1,81 @@
{% extends 'layout.html.twig' %}
+{# Local form theme override for consistent fieldset rendering #}
+{% use 'forms.html.twig' %}
+
+{% block form_row %}
+ {%- if form.vars.expanded is defined and form.vars.expanded -%}
+ {# Expanded forms get fieldset wrapper #}
+
+ {%- else -%}
+ {{- parent() -}}
+ {%- endif -%}
+{% endblock %}
+
{% block content %}
+ {% include '_partials/_flashes.html.twig' %}
+
+ Neue Buchung
+
+
+
+
Zahlungsart
+
+ {{ form_start(form, {'attr': {'novalidate': 'novalidate'}}) }}
+
+ {# Payment method selection with HTMX #}
+
+
+
+
+ {{ form_end(form) }}
+
+
+
+ {% include 'booking/_summary.html.twig' with {
+ 'bookingCreateDto': bookingCreateDto,
+ 'participantCount': participantsCount,
+ 'groupedSelectedRooms': groupedSelectedRooms,
+ 'assignmentCounts': assignmentCounts
+ } %}
+
+
{% endblock %}