# Booking Submission Implementation Guide
## Overview
This document describes the complete implementation of the two-phase booking submission system for the CREATE booking flow in MyEP Next Booking.
**Implementation Date:** 2025-10-06
**Status:** ✅ TESTED SUCCESSFULLY (2025-10-06)
**Related Files:** See "Files Modified/Created" section below
## Architecture
### Two-Phase Submission Flow
The booking submission uses a two-phase commit pattern for safety and validation:
1. **Phase 1: Inquiry (Anfrage) - Step 3**
- Executed at the end of payment method selection (Step 3)
- Validates all booking data with BusProNet API
- Returns pricing information for validation
- Compares API total price with calculated price (exact match required)
- No permanent changes made
- Request payload: `buchungsart => 'Anfrage'`
- Response status: `möglich` indicates valid
- Blocks progression to Step 4 if validation fails or prices don't match
2. **Phase 2: Booking (Buchung) - Step 4**
- Executed when user confirms booking on Step 4
- Creates actual booking in BPN system (already validated in Step 3)
- Returns transaction number (Vorgangsnummer)
- Request payload: `buchungsart => 'Buchung'`
- Response status: `erfolgt` indicates success
- Fast execution (no re-validation needed)
### Request Payload Structure
The payload generation follows the participant-centric data structure established in the CREATE flow:
**Key Characteristics:**
- Services grouped by ID with participant assignments
- 1-based participant indexing (API requirement)
- Comma-separated participant lists in `zuordnung` attribute
- Includes ALL service types: transportation, rooms, additional services, pickups, insurances, parking
- Participant wishes (room remarks, license plate) included in `` section
- Agency ID resolution with fallback to default agency (code '0001')
- Price validation: API total must match calculated total exactly (1:1)
**Service Grouping Example:**
```xml
```
**Full XML Structure:**
```xml
USERNAME
HASH
Anfrage|Buchung
F
12345
Herr
Max
Mustermann
Musterstraße 1
12345
Musterstadt
max@example.com
+49123456789
Herr
Max
Mustermann
1990-01-01
2|5
Max Mustermann
DE89370400440532013000
```
### Response Structure
**Success Response:**
```xml
möglich|erfolgt
321530
1500.00
```
**Error Response:**
```xml
Error message here
```
## Implementation Details
### 1. Response Models
**File:** `src/BusProNet/Model/BookingResponse.php`
```php
status;
}
public function isBookingSuccessful(): bool
{
return 'erfolgt' === $this->status;
}
}
```
**File:** `src/BusProNet/Model/PriceItem.php`
Individual price item from response for validation against calculated prices.
```php
` node
- Transaction number from `` node
- All price items from `` nodes
- Total price from `` node
- Payment terms from `` node
```php
getTextOrNull($node, 'buchung') ?? '';
$transactionNumber = $this->getTextOrNull($node, 'vorgang');
$priceItems = $this->parsePriceItems($node);
$totalPrice = $this->getFloatOrNull($node, 'gesamtpreis');
$paymentTerms = $this->parsePaymentTerms($node);
return new BookingResponse(
status: $status,
transactionNumber: $transactionNumber,
priceItems: $priceItems,
totalPrice: $totalPrice,
paymentTerms: $paymentTerms
);
}
private function parsePriceItems(Crawler $node): array
{
$priceItems = [];
$node->filterXPath('//preise/preis')->each(function (Crawler $priceNode) use (&$priceItems): void {
$priceItems[] = new PriceItem(
position: (int) $priceNode->attr('position'),
type: $priceNode->attr('art'),
subType: $priceNode->attr('unterart'),
label: $priceNode->attr('bezeichnung'),
dateFrom: $this->parseDate($priceNode->attr('datumvon')),
dateTo: $this->parseDate($priceNode->attr('datumbis')),
quantity: (int) $priceNode->attr('anzahl'),
assignment: $priceNode->attr('zuordnung'),
unitPrice: (float) $priceNode->attr('einzelpreis'),
totalPrice: (float) $priceNode->attr('gesamtpreis'),
id: $priceNode->attr('id') ? (int) $priceNode->attr('id') : null
);
});
return $priceItems;
}
private function parsePaymentTerms(Crawler $node): ?PaymentTerms
{
$termsNode = $node->filterXPath('//zahlungsbedingungen');
if (0 === $termsNode->count()) {
return null;
}
$depositAmount = (float) $termsNode->filterXPath('//anzahlung')->attr('betrag');
$depositDate = $this->parseDate($termsNode->filterXPath('//anzahlung')->attr('datum'));
$finalAmount = (float) $termsNode->filterXPath('//restzahlung')->attr('betrag');
$finalDate = $this->parseDate($termsNode->filterXPath('//restzahlung')->attr('datum'));
if (null === $depositDate || null === $finalDate) {
return null;
}
return new PaymentTerms(
depositAmount: $depositAmount,
depositDate: $depositDate,
finalPaymentAmount: $finalAmount,
finalPaymentDate: $finalDate
);
}
}
```
### 3. Payload Generation
**File:** `src/BusProNet/DataProcessor/BookingDataProcessor.php`
**Main Method:**
```php
public function createBookingRequestPayload(
BookingCreateDto $bookingDto,
string $bookingType
): array
```
**Helper Methods:**
- `collectServiceMappings()` - Groups services by ID
- `collectTransportationMappings()` - Groups transportation services
- `collectRoomMappings()` - Groups room assignments
- `collectPickupMappings()` - Groups pickup locations
- `collectInsuranceMappings()` - Groups insurance selections
- `addServicesFromMap()` - Generic XML structure builder
**Critical Implementation Details:**
- Participant indexing is 1-based (API requirement)
- Services grouped by ID with comma-separated participant assignments
- Insurance included in CREATE flow (unlike UPDATE flow)
- Payment type IDs: 2 for transfer, 5 for debit
### 4. API Client Methods
**File:** `src/BusProNet/ApiClient.php`
**Constants Added:**
```php
public const TYPE_BOOKING = 'BUCHUNG';
```
**Payment Type Constants (in Constants.php):**
```php
public const PAYMENT_TYPE_ID_TRANSFER = 2;
public const PAYMENT_TYPE_ID_DEBIT = 5;
```
**Methods Added:**
```php
public function createBookingInquiry(
BookingCreateDto $bookingDto,
bool $debug = false
): Notification|BookingResponse
{
$payload = (new BookingDataProcessor())->createBookingRequestPayload($bookingDto, 'Anfrage');
$data = [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING),
'satz' => ['@typ' => static::TYPE_BOOKING],
...$payload,
];
return $this->sendRequest(static::TYPE_BOOKING, $data, [], $debug);
}
public function createBooking(
BookingCreateDto $bookingDto,
bool $debug = false
): Notification|BookingResponse
{
$payload = (new BookingDataProcessor())->createBookingRequestPayload($bookingDto, 'Buchung');
$data = [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_BOOKING),
'satz' => ['@typ' => static::TYPE_BOOKING],
...$payload,
];
return $this->sendRequest(static::TYPE_BOOKING, $data, [], $debug);
}
```
Both methods:
- Use `BookingDataProcessor::createBookingRequestPayload()`
- Return either `Notification` (error) or `BookingResponse` (success)
- Support debug mode for XML dumping
### 5. Response Routing
**File:** `src/BusProNet/XmlParser/ApiResponseParser.php`
Added routing for `TYPE_BOOKING` responses:
```php
case ApiClient::TYPE_BOOKING:
return (new BookingResponseParser())->parse($resultNode);
```
Error responses still return `Notification` objects via existing error handling.
### 6. Controller Logic
#### Step 3: Validation with Price Check
**File:** `src/Controller/Booking/CreateStep3Controller.php`
**Dependencies Injected:**
- `BookingService` - Session management
- `ApiClient` - API communication
- `BookingPriceCalculatorService` - Price calculation
- `LoggerInterface` - Error logging
**Form Submission Flow:**
```php
if ($form->isSubmitted() && $form->isValid()) {
// Call inquiry API to validate booking
$inquiryResponse = $this->apiClient->createBookingInquiry($bookingCreateDto);
if ($inquiryResponse instanceof Notification || !$inquiryResponse->isInquiryValid()) {
// Handle validation failure
}
// Compare API price with calculated price (exact match required)
$apiTotal = $inquiryResponse->totalPrice ?? 0.0;
$calculatedTotal = $this->priceCalculator->calculateGrandTotal($bookingCreateDto);
if ($apiTotal !== $calculatedTotal) {
$this->logger->error('Price mismatch detected - payload incomplete', [
'apiTotal' => $apiTotal,
'calculatedTotal' => $calculatedTotal,
'difference' => abs($apiTotal - $calculatedTotal),
]);
$this->addFlash('error', 'Ein technischer Fehler ist aufgetreten.');
return; // Block progression to Step 4
}
// Proceed to Step 4 (confirmation)
$bookingCreateDto->currentStep = 4;
return $this->redirectToRoute('app_booking_create_step_4');
}
```
**Price Validation Logic:**
- Exact match required: `$apiTotal !== $calculatedTotal`
- No tolerance for rounding differences
- Mismatch indicates missing service in payload
- Logs full context for debugging
#### Step 4: Final Booking Submission
**File:** `src/Controller/Booking/CreateStep4Controller.php`
**Dependencies Injected:**
- `BookingService` - Session management
- `ApiClient` - API communication
- `LoggerInterface` - Error logging
**Form Submission Flow:**
```php
if ($form->isSubmitted() && $form->isValid()) {
try {
// Submit final booking (already validated in Step 3)
$bookingResponse = $this->apiClient->createBooking($bookingCreateDto);
if ($bookingResponse instanceof Notification) {
$this->addFlash('error', $bookingResponse->message);
return $this->render('booking/create_step_4.html.twig', [...]);
}
if (false === $bookingResponse->isBookingSuccessful()) {
$this->addFlash('error', 'Buchung konnte nicht erstellt werden.');
return $this->render('booking/create_step_4.html.twig', [...]);
}
// Success: Store booking number in flash and clear session
$this->addFlash('booking_number', $bookingResponse->transactionNumber);
$this->bookingService->clearBookingCreateDto($request);
return $this->redirectToRoute('app_booking_success');
} catch (\Exception $e) {
$this->logger->error('Booking creation failed', [...]);
$this->addFlash('error', 'Ein technischer Fehler ist aufgetreten.');
return $this->render('booking/create_step_4.html.twig', [...]);
}
}
```
**Benefits:**
- Step 3: Validates early, catches payload errors before confirmation
- Step 4: Fast execution, no validation delay
- User experience: Reduced wait time on final submission
**Error Handling:**
- API errors: Display `Notification::message` to user
- Validation failures: Display generic error message
- Price mismatch: Log detailed context, block with generic error
- Unexpected exceptions: Log full trace and display generic error
### 7. Service Layer
**File:** `src/Service/BookingService.php`
**Method Added:**
```php
/**
* Clears the booking creation DTO from the session.
*
* This method removes only the booking DTO while preserving other session data.
* Used after successful booking submission to clear the booking flow state.
*/
public function clearBookingCreateDto(Request $request): void
{
$request->getSession()->remove(self::BOOKING_CREATE_KEY);
}
```
Clears only the booking DTO (not baseline snapshot) after successful submission.
### 8. Success Page
**Controller:** `src/Controller/Booking/BookingSuccessController.php`
```php
getSession()->getFlashBag()->get('booking_number')[0] ?? null;
// Redirect to homepage if no booking number (direct access or refresh)
if (null === $bookingNumber) {
return $this->redirectToRoute('app_home');
}
return $this->render('booking/success.html.twig', [
'bookingNumber' => $bookingNumber,
]);
}
}
```
**Implementation Details:**
- Booking number passed via flash message (not URL parameter)
- Flash message automatically cleared after first display
- Direct access or page refresh redirects to homepage
- Clean URL: `/bookings/success` (no sensitive data in URL)
- No persistent browser history with booking numbers
**Template:** `templates/booking/success.html.twig`
Displays:
- Success icon (green checkmark)
- Confirmation message
- Booking number (monospace font for easy copying)
- Information about email confirmation
- Link back to homepage
## Testing Strategy
### Integration Testing
**Test Scenarios:**
1. **Successful Booking:**
- Complete all 4 steps
- Submit confirmation form
- Verify inquiry call made
- Verify booking call made
- Verify redirect to success page
- Verify session cleared
2. **Inquiry Validation Failure:**
- Submit invalid data
- Verify inquiry returns error
- Verify booking NOT called
- Verify user sees error message
- Verify session NOT cleared
3. **Booking Commit Failure:**
- Inquiry succeeds but booking fails
- Verify appropriate error handling
- Verify session NOT cleared
4. **API Error Response:**
- API returns Notification
- Verify error message displayed
- Verify session NOT cleared
### Sandbox Testing
**Prerequisites:**
- DDEV environment running
- BPN sandbox credentials configured in `.env.local`
- Valid travel data available
**Test Checklist:**
- [x] Single participant booking - ✅ PASSED
- [x] Multiple participants booking - ✅ PASSED
- [x] All service types selected - ✅ PASSED (transportation, rooms, services, pickups, insurances)
- [x] Insurance selection - ✅ PASSED
- [x] Both payment methods - ✅ TRANSFER TESTED (debit not tested)
- [x] Applicant address mandatory - ✅ PASSED
- [x] Dependent participant address optional - ✅ PASSED
- [x] Email mandatory for all - ✅ PASSED
- [x] Mobile mandatory for applicant - ✅ PASSED
- [x] Room quantity matches step 1 - ✅ PASSED
- [x] Pickup location included - ✅ PASSED
- [x] Two-phase submission - ✅ PASSED (inquiry → booking)
- [x] Session cleared on success - ✅ PASSED
- [x] Success page with booking number - ✅ PASSED
## Files Modified/Created
### Created Files:
- `src/BusProNet/Model/BookingResponse.php`
- `src/BusProNet/Model/PriceItem.php`
- `src/BusProNet/Model/PaymentTerms.php`
- `src/BusProNet/Model/Agency.php`
- `src/BusProNet/XmlParser/BookingResponseParser.php`
- `src/BusProNet/XmlParser/AgencyParser.php`
- `src/BusProNet/XmlLoader/AgencyLoader.php`
- `src/Controller/Booking/BookingSuccessController.php`
- `templates/booking/success.html.twig`
- `tests/BusProNet/XmlParser/AgencyParserTest.php`
- `docs/BOOKING_SUBMISSION_IMPLEMENTATION.md` (this file)
- `docs/BOOKING_SUBMISSION_STATUS.md`
- `docs/REFACTORING_BOOKING_DATA_PROCESSOR.md`
### Modified Files:
- `src/BusProNet/Constants.php` - Added payment type ID constants
- `src/BusProNet/DataProcessor/BookingDataProcessor.php` - Added `createBookingRequestPayload()` with parking and wishes section
- `src/BusProNet/ApiClient.php` - Added `TYPE_BOOKING`, `TYPE_AGENCIES`, `createBookingInquiry()`, `createBooking()`, `getAgencies()`
- `src/BusProNet/XmlParser/ApiResponseParser.php` - Added BUCHUNG and AGENTUREN routing
- `src/Controller/Booking/CreateStep3Controller.php` - Added inquiry API call with price validation
- `src/Controller/Booking/CreateStep4Controller.php` - Simplified to direct booking submission (validation moved to Step 3)
- `src/Controller/Booking/CreateInitController.php` - Added agency resolution with optional query parameter
- `src/Service/BookingService.php` - Added `clearBookingCreateDto()`, agency ID parameter in `startFreshBooking()`
- `src/Form/Model/BookingCreateDto.php` - Added `agencyId` property
## Known Limitations
1. **Price Validation:**
- ~~Pricing data is parsed but not automatically validated against calculated prices~~ ✅ IMPLEMENTED
- Exact price match validation implemented in Step 3
2. **Email/Password Fields:**
- Response contains `` and `` fields that are not currently parsed
- Can be added if needed for confirmation emails
3. **Update Flow Refactoring:**
- UPDATE flow still uses different payload structure
- Future refactoring documented in `REFACTORING_BOOKING_DATA_PROCESSOR.md`
4. **Contact Information:**
- Phone number (mobile) is mandatory for the applicant only
- Email is mandatory for all participants
## Future Enhancements
1. **Price Validation:**
- ~~Compare `$bookingResponse->totalPrice` with `BookingPriceCalculatorService` result~~ ✅ IMPLEMENTED
- ~~Warn if discrepancy detected~~ ✅ BLOCKS PROGRESSION
2. **Email Confirmation:**
- Parse email/password fields from response
- Send custom confirmation email
- Include PDF password in email
3. **Transaction Logging:**
- Log all inquiry/booking requests with responses
- Facilitate debugging and audit trail
4. **Retry Logic:**
- Handle transient API failures
- Implement exponential backoff
5. **Price Item Validation:**
- Compare individual price items with selections
- Detect unexpected charges
## Troubleshooting
### Issue: Inquiry succeeds but booking fails
**Symptoms:** User sees error after successful validation
**Debugging:**
1. Check application logs for exception details
2. Enable API debug mode to dump XML
3. Verify data hasn't changed between calls
4. Check BPN API logs in admin panel
### Issue: Session cleared prematurely
**Symptoms:** User redirected to init page
**Debugging:**
1. Verify `clearBookingCreateDto()` only called after successful booking
2. Check for duplicate form submissions
3. Verify error handling re-renders without clearing session
## References
- BusProNet API Documentation: `docs/Beschreibung XMLAnfrage.pdf`
- Example Request Payload: `scratch_113.xml`
- Example Response: `scratch_111.xml` (with pricing), `scratch_112.xml` (minimal)
- Payment Step Implementation: `docs/BOOKING_PAYMENT_STEP.md`
- Refactoring Plan: `REFACTORING_BOOKING_DATA_PROCESSOR.md`
- Implementation Status: `BOOKING_SUBMISSION_STATUS.md`
---
**Implementation Status:** ✅ TESTED SUCCESSFULLY
**Code Quality:** ✅ PHP-CS-Fixer validated, syntax checked
**Test Date:** 2025-10-06
**Next Step:** Improvements and UPDATE flow refactoring (see REFACTORING_BOOKING_DATA_PROCESSOR.md)
## Test Results Summary
**Test Date:** 2025-10-06
**Environment:** DDEV sandbox with BusProNet API
**Successful Test Booking:**
- 2 participants with complete data
- All service types: transportation, rooms, additional services, pickups, insurance, parking
- Address validation working (mandatory for applicant, optional for others)
- Contact info validation working (email for all, mobile for applicant)
- Room quantity correctly using step 1 selections
- Two-phase submission successful (inquiry → booking)
- Session cleared after success
- Success page displaying booking number
- Agency ID resolved from optional query parameter with fallback to default (code '0001')
- Participant wishes (room remarks, license plate) included in payload
**Bugs Fixed During Testing:**
1. Room quantity using participant count → Fixed to use roomSelections[].quantity
2. Insurance selection reset on dependent participants → Fixed bulk handler clearing logic
3. Pickup quantity issues → Simplified to use only outbound pickups
4. Missing contact info for non-applicants → Removed applicant-only restriction
5. Parking service not included in payload → Added to collectServiceMappings()
6. License plate and room remarks not submitted → Added wünsche section to participant payload
**Result:** ✅ All critical features working correctly