feat: implement payment step

This commit is contained in:
Björn Fromme
2025-10-05 13:29:19 +02:00
parent 3edf5a80aa
commit 798beae597
12 changed files with 1273 additions and 30 deletions
+726
View File
@@ -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
<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
- 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**