# 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**