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:
-
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_DEBITbankAccount- BankAccountDto instance (nullable)
- Payment method constants in
Constants.php(PAYMENT_METHOD_TRANSFER, PAYMENT_METHOD_DEBIT)
-
Form Type:
BookingCreateStep3Type- Main step 3 form with payment method and conditional bank account fields- Removed
PaymentTypewrapper (unnecessary with proper event handling) - Dynamic field management using POST_SET_DATA and PRE_SUBMIT events
- Helper method
addBankAccountField()to avoid duplication
-
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
-
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
-
Validation:
- Conditional validation via
validateBankAccount()callback inBookingCreateDto - 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
- Conditional validation via
-
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-selectextracts only#form-paymentfrom 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:
- Direct Transfer (Überweisung) - Requires bank account details
- 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 displaygetIbanWithoutSpaces()- 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 selectedPRE_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
BookingCreateDtofrom 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(): ?BookingCreateDtosaveBookingDto(BookingCreateDto $dto): voidclearBookingSession(): voidvalidateStepAccess(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\Ibanis 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
- IBAN (
- Payment method (
- 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_mandatestable - 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
- HTTPS Required: Bank details must be transmitted over HTTPS only
- Session Security: Ensure session data encrypted
- Input Sanitization: Strip/validate all bank account inputs
- CSRF Protection: Ensure form has CSRF token
- Rate Limiting: Prevent brute force on payment form
- Audit Trail: Log payment method changes
UX Considerations
- Clear Labels: "Überweisung" and "Lastschrift" with explanations
- Visual Feedback: Show selected payment method prominently
- Inline Validation: Real-time IBAN format checking
- Error Messages: Clear, actionable German error messages
- Help Text: Examples of valid IBAN formats
- Progress Indicator: Show user is on Step 3 of 4
- 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/ibanfor enhanced IBAN validation (optional)
JavaScript Libraries
- Stimulus (already installed)
- Consider: Custom IBAN formatter or library
No new major dependencies required
Migration Path
- Implement in order: Phases 1 → 2 → 3 → 4 → 5 → 6
- Test each phase before moving to next
- Phases 7-11 can be done in parallel after Phase 6
- Deploy behind feature flag initially (optional)
Open Questions
- SEPA Creditor ID: What is the company's SEPA Gläubiger-ID?
- Payment Method Codes: What are the exact BPN API codes for transfer/debit?
- Mandate Reference Format: Any specific format requirements?
- Invoice Generation: Does invoice need to show SEPA mandate reference?
- Default Payment Method: Should we pre-select one method?
- 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