wip: submit booking to api
This commit is contained in:
@@ -0,0 +1,771 @@
|
||||
# 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: `<buchung>möglich</buchung>` 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: `<buchung>erfolgt</buchung>` 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 `<wünsche>` 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
|
||||
<versicherungen>
|
||||
<versicherung idversicherung="20040" anzahl="1" zuordnung="1" />
|
||||
<versicherung idversicherung="42" anzahl="2" zuordnung="2,3" />
|
||||
</versicherungen>
|
||||
```
|
||||
|
||||
**Full XML Structure:**
|
||||
```xml
|
||||
<xml>
|
||||
<user>USERNAME</user>
|
||||
<key>HASH</key>
|
||||
<satz typ="BUCHUNG" />
|
||||
<buchungsart>Anfrage|Buchung</buchungsart>
|
||||
<status>F</status>
|
||||
<idreise>12345</idreise>
|
||||
|
||||
<anmelder>
|
||||
<anrede>Herr</anrede>
|
||||
<vorname>Max</vorname>
|
||||
<name>Mustermann</name>
|
||||
<strasse>Musterstraße 1</strasse>
|
||||
<plz>12345</plz>
|
||||
<ort>Musterstadt</ort>
|
||||
<email>[email protected]</email>
|
||||
<telefon>+49123456789</telefon>
|
||||
</anmelder>
|
||||
|
||||
<teilnehmerliste>
|
||||
<teilnehmer position="1">
|
||||
<anrede>Herr</anrede>
|
||||
<vorname>Max</vorname>
|
||||
<name>Mustermann</name>
|
||||
<geburtsdatum>1990-01-01</geburtsdatum>
|
||||
</teilnehmer>
|
||||
<!-- Additional participants... -->
|
||||
</teilnehmerliste>
|
||||
|
||||
<beförderungen>
|
||||
<beförderung idbefoerderung="123" anzahl="1" zuordnung="1" />
|
||||
</beförderungen>
|
||||
|
||||
<unterbringungen>
|
||||
<unterbringung idunterbringung="456" anzahl="2" zuordnung="1,2" />
|
||||
</unterbringungen>
|
||||
|
||||
<zusatzleistungen>
|
||||
<zusatzleistung idzusatzleistung="789" anzahl="1" zuordnung="1" />
|
||||
</zusatzleistungen>
|
||||
|
||||
<zustiege>
|
||||
<zustieg idzustieg="101" anzahl="1" zuordnung="1" />
|
||||
</zustiege>
|
||||
|
||||
<versicherungen>
|
||||
<versicherung idversicherung="202" anzahl="1" zuordnung="1" />
|
||||
</versicherungen>
|
||||
|
||||
<zahlungsart>
|
||||
<art>2|5</art> <!-- 2=Überweisung, 5=Lastschrift -->
|
||||
<kontoinhaber>Max Mustermann</kontoinhaber>
|
||||
<iban>DE89370400440532013000</iban>
|
||||
</zahlungsart>
|
||||
</xml>
|
||||
```
|
||||
|
||||
### Response Structure
|
||||
|
||||
**Success Response:**
|
||||
```xml
|
||||
<ergebnis>
|
||||
<satz typ="BUCHUNG" />
|
||||
<buchung>möglich|erfolgt</buchung>
|
||||
<vorgang>321530</vorgang>
|
||||
<preise>
|
||||
<preis position="1" art="UNT" unterart="DZ" bezeichnung="Doppelzimmer"
|
||||
datumvon="2025-03-15" datumbis="2025-03-22" anzahl="1" zuordnung="1,2"
|
||||
einzelpreis="450.00" gesamtpreis="900.00" id="456" />
|
||||
<!-- More price items... -->
|
||||
</preise>
|
||||
<gesamtpreis>1500.00</gesamtpreis>
|
||||
<zahlungsbedingungen>
|
||||
<anzahlung betrag="500.00" datum="2025-02-01" />
|
||||
<restzahlung betrag="1000.00" datum="2025-03-01" />
|
||||
</zahlungsbedingungen>
|
||||
</ergebnis>
|
||||
```
|
||||
|
||||
**Error Response:**
|
||||
```xml
|
||||
<ergebnis>
|
||||
<satz typ="NOTIFICATION" />
|
||||
<nachricht typ="fehler">Error message here</nachricht>
|
||||
</ergebnis>
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Response Models
|
||||
|
||||
**File:** `src/BusProNet/Model/BookingResponse.php`
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
final readonly class BookingResponse
|
||||
{
|
||||
public function __construct(
|
||||
public string $status, // 'möglich' or 'erfolgt'
|
||||
public ?string $transactionNumber = null, // Vorgangsnummer
|
||||
public array $priceItems = [], // PriceItem[]
|
||||
public ?float $totalPrice = null, // Gesamtpreis
|
||||
public ?PaymentTerms $paymentTerms = null, // Payment schedule
|
||||
) {
|
||||
}
|
||||
|
||||
public function isInquiryValid(): bool
|
||||
{
|
||||
return 'möglich' === $this->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
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
final readonly class PriceItem
|
||||
{
|
||||
public function __construct(
|
||||
public int $position,
|
||||
public string $type,
|
||||
public ?string $subType,
|
||||
public string $label,
|
||||
public ?\DateTimeImmutable $dateFrom,
|
||||
public ?\DateTimeImmutable $dateTo,
|
||||
public int $quantity,
|
||||
public string $assignment,
|
||||
public float $unitPrice,
|
||||
public float $totalPrice,
|
||||
public ?int $id,
|
||||
) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**File:** `src/BusProNet/Model/PaymentTerms.php`
|
||||
|
||||
Payment schedule with deposit and final payment amounts/dates.
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Model;
|
||||
|
||||
final readonly class PaymentTerms
|
||||
{
|
||||
public function __construct(
|
||||
public float $depositAmount,
|
||||
public \DateTimeImmutable $depositDate,
|
||||
public float $finalPaymentAmount,
|
||||
public \DateTimeImmutable $finalPaymentDate,
|
||||
) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. XML Parser
|
||||
|
||||
**File:** `src/BusProNet/XmlParser/BookingResponseParser.php`
|
||||
|
||||
Extends `AbstractParser` and parses:
|
||||
- Booking status from `<buchung>` node
|
||||
- Transaction number from `<vorgang>` node
|
||||
- All price items from `<preise><preis>` nodes
|
||||
- Total price from `<gesamtpreis>` node
|
||||
- Payment terms from `<zahlungsbedingungen>` node
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\Model\BookingResponse;
|
||||
use App\BusProNet\Model\PaymentTerms;
|
||||
use App\BusProNet\Model\PriceItem;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
final class BookingResponseParser extends AbstractParser
|
||||
{
|
||||
public function parse(Crawler $node): BookingResponse
|
||||
{
|
||||
$status = $this->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
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Booking;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class BookingSuccessController extends AbstractController
|
||||
{
|
||||
#[Route('/bookings/success', name: 'app_booking_success')]
|
||||
public function success(Request $request): Response
|
||||
{
|
||||
$bookingNumber = $request->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 `<email>` and `<pdfpasswort>` 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
|
||||
Reference in New Issue
Block a user