# 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