Files
myep/docs/BOOKING_PAYMENT_STEP.md
T

22 KiB

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

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

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

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:

<div id="form-payment"
     hx-post="{{ path('app_booking_create_step_3_refresh') }}"
     hx-trigger="change from:input[type='radio']"
     hx-target="#form-payment"
     hx-select="#form-payment"
     hx-swap="outerHTML">
    {{ form_row(form.paymentMethod) }}

    {# Conditionally rendered bank account section #}
    {% if form.bankAccount is defined %}
        ...
    {% endif %}
</div>

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


End of Document