wip: submit booking to api
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
# Address Fields Implementation
|
||||
|
||||
**Date:** 2025-10-06
|
||||
**Status:** ✅ TESTED SUCCESSFULLY (2025-10-06)
|
||||
|
||||
## Overview
|
||||
|
||||
Added participant address collection to the booking create flow. Address is mandatory for the applicant (first participant) and optional for others.
|
||||
|
||||
## Implementation
|
||||
|
||||
### 1. Models
|
||||
|
||||
**ParticipantDto** (`src/Form/Model/ParticipantDto.php`):
|
||||
- Added `Address $address` property
|
||||
- Made email field required (`@Assert\NotBlank`)
|
||||
- Added constructor to initialize `Address` object
|
||||
- Removed `@Assert\NotNull` constraint (validation handled by ParticipantValidator)
|
||||
|
||||
**Address** (`src/BusProNet/Model/Address.php`):
|
||||
- Already had `toPayload()` method for XML generation
|
||||
- Properties: street, postCode, city, district, country
|
||||
- No changes needed (existing model worked perfectly)
|
||||
|
||||
### 2. Forms
|
||||
|
||||
**AddressType** (`src/Form/AddressType.php`) - NEW:
|
||||
```php
|
||||
class AddressType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('street', TextType::class, [...])
|
||||
->add('postCode', TextType::class, [...])
|
||||
->add('city', TextType::class, [...])
|
||||
->add('country', CountryType::class, [
|
||||
'property' => 'country',
|
||||
'preferred_choices' => ['DE', 'AT', 'CH'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**BookingParticipantType** (`src/Form/BookingParticipantType.php`):
|
||||
- Added address field after mobile field
|
||||
- `required => 0 === $participantIndex` (mandatory for applicant)
|
||||
- Added 'address' to `$baseFields` array for rebuild handling
|
||||
|
||||
### 3. Validation
|
||||
|
||||
**ParticipantValidator** (`src/Validator/Constraints/ParticipantValidator.php`):
|
||||
- Added `assertApplicantAddressValid()` method
|
||||
- Validates street, postCode, city, country for applicant only
|
||||
- Uses `$participant->isApplicant()` to check if validation should run
|
||||
- Error messages: "Bitte angeben" for each missing field
|
||||
|
||||
### 4. Payload Generation
|
||||
|
||||
**BookingDataProcessor** (`src/BusProNet/DataProcessor/BookingDataProcessor.php`):
|
||||
|
||||
**Applicant section** (lines 451-454):
|
||||
```php
|
||||
// Add address for applicant
|
||||
if (null !== $firstParticipant->address) {
|
||||
$payload['anmelder']['anschrift'] = $firstParticipant->address->toPayload();
|
||||
}
|
||||
```
|
||||
|
||||
**Participant list** (lines 481-488):
|
||||
```php
|
||||
// Add address (always include structure, even if empty)
|
||||
$participantData['anschrift'] = $participant->address?->toPayload() ?? [
|
||||
'strasse' => null,
|
||||
'plz' => null,
|
||||
'ort' => null,
|
||||
'ortsteil' => null,
|
||||
'land' => null,
|
||||
];
|
||||
```
|
||||
|
||||
**XML Output:**
|
||||
```xml
|
||||
<anmelder>
|
||||
<anschrift>
|
||||
<strasse>Gablonzer Straße 32</strasse>
|
||||
<plz>53359</plz>
|
||||
<ort>Rheinbach</ort>
|
||||
<ortsteil />
|
||||
<land>D</land>
|
||||
</anschrift>
|
||||
</anmelder>
|
||||
|
||||
<teilnehmerliste>
|
||||
<teilnehmer id="1">
|
||||
<anschrift>
|
||||
<strasse>Gablonzer Straße 32</strasse>
|
||||
<plz>53359</plz>
|
||||
<ort>Rheinbach</ort>
|
||||
<ortsteil />
|
||||
<land>D</land>
|
||||
</anschrift>
|
||||
</teilnehmer>
|
||||
</teilnehmerliste>
|
||||
```
|
||||
|
||||
## Bug Fixes (Same Session)
|
||||
|
||||
### 1. Room Quantity Fix
|
||||
**Issue:** Room `@anzahl` was set to participant count instead of room quantity
|
||||
**Fix:** Use `roomSelections[].quantity` from step 1 (lines 576-603)
|
||||
```php
|
||||
$roomQuantities = [];
|
||||
foreach ($bookingDto->roomSelections as $selection) {
|
||||
if ($selection->quantity > 0) {
|
||||
$roomQuantities[$selection->roomId] = $selection->quantity;
|
||||
}
|
||||
}
|
||||
$quantity = $roomQuantities[$roomId] ?? 1;
|
||||
```
|
||||
|
||||
### 2. Insurance Selection for Dependent Participants
|
||||
**Issue:** `ParticipantBulkInsuranceFieldHandler` cleared independent selections on every form submit
|
||||
**Fix:** Added `wasBulkInsurancePreviouslyEnabled()` check (lines 90, 144-176)
|
||||
```php
|
||||
if (false === $isBulkEnabled && $this->wasBulkInsurancePreviouslyEnabled($bookingDto)) {
|
||||
$this->clearDependentParticipantsInsurance($bookingDto);
|
||||
}
|
||||
```
|
||||
|
||||
## Files Modified
|
||||
|
||||
1. `src/Form/Model/ParticipantDto.php` - Added address property, constructor, required email
|
||||
2. `src/Form/AddressType.php` - NEW form type
|
||||
3. `src/Form/BookingParticipantType.php` - Added address field
|
||||
4. `src/Validator/Constraints/ParticipantValidator.php` - Added address validation
|
||||
5. `src/BusProNet/DataProcessor/BookingDataProcessor.php` - Address in payload, room quantity fix, `isApplicant()` usage
|
||||
6. `src/Form/Service/ParticipantBulkInsuranceFieldHandler.php` - Insurance clearing fix
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Testing Checklist:
|
||||
- [x] Applicant address required (all fields) - ✅ PASSED
|
||||
- [x] Dependent participant address optional - ✅ PASSED
|
||||
- [x] Country dropdown shows DE, AT, CH first - ✅ PASSED
|
||||
- [x] Address appears in XML for applicant - ✅ PASSED
|
||||
- [x] Address appears in XML for all participants (empty if not provided) - ✅ PASSED
|
||||
- [x] Email is mandatory for all participants - ✅ PASSED
|
||||
- [x] Insurance selection works for dependent participants - ✅ PASSED
|
||||
- [x] Room quantity matches selection from step 1 - ✅ PASSED
|
||||
|
||||
### Expected XML:
|
||||
- Applicant: Full address in `<anmelder><anschrift>`
|
||||
- All participants: Address structure in `<teilnehmer><anschrift>` (may be empty)
|
||||
- Room quantity: Matches quantity selected in step 1, not participant count
|
||||
|
||||
## Notes
|
||||
|
||||
- Address object initialized in ParticipantDto constructor
|
||||
- Uses existing `Address::toPayload()` method for XML generation
|
||||
- CountryType provides nationality dropdown with German country codes
|
||||
- Validation runs via existing ParticipantValidator constraint
|
||||
- Template integration happens automatically via Symfony form system
|
||||
|
||||
---
|
||||
|
||||
**Implementation Status:** ✅ TESTED SUCCESSFULLY
|
||||
**Code Quality:** ✅ PHP-CS-Fixer validated
|
||||
**Test Date:** 2025-10-06
|
||||
|
||||
## Test Results
|
||||
|
||||
**Test Date:** 2025-10-06
|
||||
**Environment:** DDEV sandbox with BusProNet API
|
||||
|
||||
**Validated Features:**
|
||||
- ✅ Applicant address validation (all fields mandatory)
|
||||
- ✅ Dependent participant address optional
|
||||
- ✅ Country dropdown with preferred choices (DE, AT, CH)
|
||||
- ✅ Address correctly included in XML payload for applicant
|
||||
- ✅ Address structure included for all participants (empty nodes for optional)
|
||||
- ✅ Email mandatory validation working for all participants
|
||||
- ✅ Mobile mandatory validation working for applicant only
|
||||
|
||||
**Validated Bug Fixes:**
|
||||
- ✅ Room quantity using correct value from step 1 selections
|
||||
- ✅ Insurance selection working correctly for dependent participants
|
||||
- ✅ Pickup locations included with correct quantities
|
||||
|
||||
**Result:** All address field requirements working correctly in production-like environment.
|
||||
@@ -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
|
||||
@@ -0,0 +1,559 @@
|
||||
# Booking Submission Implementation Status
|
||||
|
||||
## Overview
|
||||
|
||||
Implementation of two-phase booking submission for the booking creation flow. This allows users to create new bookings through inquiry validation followed by final booking commit.
|
||||
|
||||
**Status:** ✅ 100% Complete - TESTED SUCCESSFULLY
|
||||
**Last Updated:** 2025-10-06
|
||||
**First Successful Test Booking:** 2025-10-06
|
||||
**Related Documentation:**
|
||||
- `docs/BOOKING_PAYMENT_STEP.md` - Payment step implementation
|
||||
- `docs/REFACTORING_BOOKING_DATA_PROCESSOR.md` - Future refactoring plan
|
||||
|
||||
## Architecture Decision
|
||||
|
||||
**Participant-Centric Structure:** The CREATE flow uses a cleaner participant-centric data structure where services are attached directly to participants in the DTO, not centralized with mapping arrays. This is the new standard.
|
||||
|
||||
**UPDATE Flow:** Currently uses a different structure (centralized services with mappings). Future refactoring will align it with the CREATE flow's participant-centric approach.
|
||||
|
||||
## Completed Work (80%)
|
||||
|
||||
### 1. Response Models ✅
|
||||
|
||||
**Created Files:**
|
||||
- `src/BusProNet/Model/BookingResponse.php`
|
||||
- `src/BusProNet/Model/PriceItem.php`
|
||||
- `src/BusProNet/Model/PaymentTerms.php`
|
||||
|
||||
**BookingResponse:**
|
||||
- Represents API response from booking requests (inquiry or final)
|
||||
- Properties: `status`, `transactionNumber`, `priceItems`, `totalPrice`, `paymentTerms`
|
||||
- Methods: `isInquiryValid()`, `isBookingSuccessful()`
|
||||
- Handles both `möglich` (inquiry valid) and `erfolgt` (booking created) statuses
|
||||
|
||||
**PriceItem:**
|
||||
- Represents individual price items from response
|
||||
- Properties: `position`, `type`, `subType`, `label`, `dateFrom`, `dateTo`, `quantity`, `assignment`, `unitPrice`, `totalPrice`, `id`
|
||||
- Used for price validation against calculated prices
|
||||
|
||||
**PaymentTerms:**
|
||||
- Represents payment schedule from response
|
||||
- Properties: `depositAmount`, `depositDate`, `finalPaymentAmount`, `finalPaymentDate`
|
||||
|
||||
### 2. Response Parser ✅
|
||||
|
||||
**File:** `src/BusProNet/XmlParser/BookingResponseParser.php`
|
||||
|
||||
**Functionality:**
|
||||
- Extends `AbstractParser`
|
||||
- Parses BUCHUNG type responses
|
||||
- Extracts booking status from `<buchung>` node
|
||||
- Extracts transaction number from `<vorgang>` node
|
||||
- Parses all price items from `<preise><preis>` nodes
|
||||
- Parses total price from `<gesamtpreis>` node
|
||||
- Parses payment terms from `<zahlungsbedingungen>` node
|
||||
|
||||
**XML Structure Handled:**
|
||||
```xml
|
||||
<ergebnis>
|
||||
<satz typ="BUCHUNG" />
|
||||
<buchung>möglich|erfolgt</buchung>
|
||||
<vorgang>321530</vorgang>
|
||||
<preise>
|
||||
<preis position="1" art="BEF" unterart="BUS" bezeichnung="..."
|
||||
terminvon="..." terminbis="..." anzahl="2" zuordnung="1,2"
|
||||
preis="50.00" gesamtpreis="100.00" id="123" />
|
||||
</preise>
|
||||
<gesamtpreis>671,78</gesamtpreis>
|
||||
<zahlungsbedingungen>
|
||||
<anzahlung betrag="128,00" termin="21.03.2017" />
|
||||
<restzahlung betrag="543,78" termin="12.11.2017" />
|
||||
</zahlungsbedingungen>
|
||||
</ergebnis>
|
||||
```
|
||||
|
||||
### 3. Constants ✅
|
||||
|
||||
**File:** `src/BusProNet/Constants.php`
|
||||
|
||||
**Added:**
|
||||
- `PAYMENT_TYPE_ID_TRANSFER = 2` - Payment type ID for bank transfer
|
||||
- `PAYMENT_TYPE_ID_DEBIT = 5` - Payment type ID for direct debit
|
||||
|
||||
### 4. Payload Generation ✅
|
||||
|
||||
**File:** `src/BusProNet/DataProcessor/BookingDataProcessor.php`
|
||||
|
||||
**New Method:** `createBookingRequestPayload(BookingCreateDto $bookingDto, string $bookingType): array`
|
||||
|
||||
**Functionality:**
|
||||
- Generates XML payload for new bookings (inquiry or final)
|
||||
- Supports both 'Anfrage' (inquiry) and 'Buchung' (final booking) modes
|
||||
- Participant-centric structure (services attached to participants)
|
||||
- Includes ALL service types: board, ski passes, rentals, courses, additional services, transportation, pickups
|
||||
- **INCLUDES INSURANCE** (critical difference from update flow)
|
||||
- Proper room mapping via `assignedRoomId`
|
||||
- Payment information with correct type IDs
|
||||
|
||||
**Helper Methods:**
|
||||
- `addServicesFromMap()` - Reusable helper for converting service maps to XML structure
|
||||
- `collectServiceMappings()` - Groups all participant services by ID
|
||||
- `collectTransportationMappings()` - Groups transportation services
|
||||
- `collectRoomMappings()` - Groups room assignments
|
||||
- `collectPickupMappings()` - Groups pickup selections
|
||||
- `collectInsuranceMappings()` - Groups insurance selections (CREATE only!)
|
||||
|
||||
**Payload Structure:**
|
||||
```php
|
||||
[
|
||||
'buchungsart' => 'Anfrage|Buchung',
|
||||
'status' => 'F',
|
||||
'idreise' => $travelId,
|
||||
'anmelder' => [
|
||||
'name' => '...',
|
||||
'vorname' => '...',
|
||||
'geschlecht' => '...',
|
||||
'nationalitaet' => '...',
|
||||
'geburtsdatum' => '...',
|
||||
'kommunikation' => ['email' => '...', 'telefonmobil' => '...'],
|
||||
],
|
||||
'teilnehmerliste' => ['teilnehmer' => [...]],
|
||||
'beförderungen' => ['beförderung' => [...]],
|
||||
'unterbringungen' => ['unterbringung' => [...]],
|
||||
'zusatzleistungen' => ['zusatzleistung' => [...]],
|
||||
'zustiege' => ['zustieg' => [...]],
|
||||
'versicherungen' => ['versicherung' => [...]], // CREATE only!
|
||||
'zahlung' => [
|
||||
'@idzahlungsart' => 2|5,
|
||||
'@art' => 'EINZUG|UEBERWEISUNG',
|
||||
'bankverbindung' => [...], // if debit
|
||||
],
|
||||
]
|
||||
```
|
||||
|
||||
### 5. API Client Methods ✅
|
||||
|
||||
**File:** `src/BusProNet/ApiClient.php`
|
||||
|
||||
**New Constant:**
|
||||
- `TYPE_BOOKING = 'BUCHUNG'`
|
||||
|
||||
**New Methods:**
|
||||
|
||||
```php
|
||||
public function createBookingInquiry(
|
||||
BookingCreateDto $bookingDto,
|
||||
bool $debug = false
|
||||
): Notification|BookingResponse
|
||||
```
|
||||
- First phase: validates booking data
|
||||
- Returns pricing information
|
||||
- Does not create actual booking
|
||||
|
||||
```php
|
||||
public function createBooking(
|
||||
BookingCreateDto $bookingDto,
|
||||
bool $debug = false
|
||||
): Notification|BookingResponse
|
||||
```
|
||||
- Second phase: creates actual booking
|
||||
- Returns booking number (transaction number)
|
||||
- Only called after successful inquiry
|
||||
|
||||
**Both methods:**
|
||||
- Use `BookingDataProcessor::createBookingRequestPayload()`
|
||||
- Send request to BUCHUNG type endpoint
|
||||
- Return `Notification` on error or `BookingResponse` on success
|
||||
- Support debug mode for XML dumps
|
||||
|
||||
### 6. Response Parser Integration ✅
|
||||
|
||||
**File:** `src/BusProNet/XmlParser/ApiResponseParser.php`
|
||||
|
||||
**Updated:**
|
||||
- Added case for `ApiClient::TYPE_BOOKING`
|
||||
- Routes to `BookingResponseParser`
|
||||
- Handles both inquiry and final booking responses
|
||||
|
||||
## Remaining Work (0%)
|
||||
|
||||
### 1. Controller Implementation ✅
|
||||
|
||||
**File:** `src/Controller/Booking/CreateStep4Controller.php`
|
||||
|
||||
**Completed:**
|
||||
- Imported `ApiClient` and injected via constructor
|
||||
- Imported `LoggerInterface` and injected via constructor
|
||||
- Implemented two-phase submission in form handler
|
||||
|
||||
```php
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
// Phase 1: Inquiry (Validation)
|
||||
$inquiryResponse = $this->apiClient->createBookingInquiry($bookingCreateDto);
|
||||
|
||||
if ($inquiryResponse instanceof Notification) {
|
||||
// API returned error notification
|
||||
$this->addFlash('error', $inquiryResponse->message);
|
||||
return $this->render(...);
|
||||
}
|
||||
|
||||
if (false === $inquiryResponse->isInquiryValid()) {
|
||||
// Inquiry validation failed
|
||||
$this->addFlash('error', 'Buchungsvalidierung fehlgeschlagen.');
|
||||
return $this->render(...);
|
||||
}
|
||||
|
||||
// Optional: Validate prices match our calculations
|
||||
// Compare $inquiryResponse->totalPrice with calculated total
|
||||
|
||||
// Phase 2: Booking (Commit)
|
||||
$bookingResponse = $this->apiClient->createBooking($bookingCreateDto);
|
||||
|
||||
if ($bookingResponse instanceof Notification) {
|
||||
// API returned error notification
|
||||
$this->addFlash('error', $bookingResponse->message);
|
||||
return $this->render(...);
|
||||
}
|
||||
|
||||
if (false === $bookingResponse->isBookingSuccessful()) {
|
||||
// Booking creation failed
|
||||
$this->addFlash('error', 'Buchung konnte nicht erstellt werden.');
|
||||
return $this->render(...);
|
||||
}
|
||||
|
||||
// Success: Clear session and redirect
|
||||
$this->bookingService->clearBookingCreateDto($request);
|
||||
|
||||
return $this->redirectToRoute('app_booking_success', [
|
||||
'bookingNumber' => $bookingResponse->transactionNumber,
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error('Booking creation failed', [
|
||||
'exception' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
]);
|
||||
|
||||
$this->addFlash('error', 'Ein technischer Fehler ist aufgetreten.');
|
||||
return $this->render(...);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Constructor Update:**
|
||||
```php
|
||||
public function __construct(
|
||||
private readonly BookingService $bookingService,
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Service Layer ✅
|
||||
|
||||
**File:** `src/Service/BookingService.php`
|
||||
|
||||
**Completed:**
|
||||
```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);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Success Page ✅
|
||||
|
||||
**New Controller:** `src/Controller/Booking/BookingSuccessController.php`
|
||||
|
||||
**Completed:**
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller\Booking;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class BookingSuccessController extends AbstractController
|
||||
{
|
||||
#[Route('/bookings/success/{bookingNumber}', name: 'app_booking_success')]
|
||||
public function success(string $bookingNumber): Response
|
||||
{
|
||||
return $this->render('booking/success.html.twig', [
|
||||
'bookingNumber' => $bookingNumber,
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**New Template:** `templates/booking/success.html.twig`
|
||||
|
||||
```twig
|
||||
{% extends 'layout.html.twig' %}
|
||||
|
||||
{% block title %}Buchung erfolgreich{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-2xl mx-auto text-center py-12">
|
||||
<div class="mb-8">
|
||||
<svg class="w-24 h-24 text-green-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl font-bold mb-4">Buchung erfolgreich abgeschlossen</h1>
|
||||
|
||||
<p class="text-xl mb-8">
|
||||
Ihre Buchungsnummer: <strong class="font-mono">{{ bookingNumber }}</strong>
|
||||
</p>
|
||||
|
||||
<div class="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-8">
|
||||
<p class="text-gray-700">
|
||||
Sie erhalten in Kürze eine Bestätigungs-E-Mail mit allen Details zu Ihrer Buchung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a href="{{ path('app_home') }}" class="button bg-button bg-button--primary">
|
||||
Zurück zur Startseite
|
||||
</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
```
|
||||
|
||||
### 4. Testing ⏳
|
||||
|
||||
**Sandbox Testing Checklist:**
|
||||
- [ ] Test inquiry phase with valid data
|
||||
- [ ] Test inquiry phase with invalid data (validation errors)
|
||||
- [ ] Test booking phase after successful inquiry
|
||||
- [ ] Test booking phase failure scenarios
|
||||
- [ ] Verify pricing data matches calculations
|
||||
- [ ] Test with different service combinations:
|
||||
- [ ] With insurance
|
||||
- [ ] Without insurance
|
||||
- [ ] With transportation services
|
||||
- [ ] With pickup locations
|
||||
- [ ] With all service types
|
||||
- [ ] Minimum services only
|
||||
- [ ] Test payment methods:
|
||||
- [ ] Direct debit (14+ days before travel)
|
||||
- [ ] Bank transfer
|
||||
- [ ] Direct debit blocked (<14 days)
|
||||
- [ ] Test session clearing
|
||||
- [ ] Verify booking number display
|
||||
- [ ] Test error handling
|
||||
- [ ] Verify XML dumps in debug mode
|
||||
|
||||
## Key Implementation Notes
|
||||
|
||||
### Two-Phase Process
|
||||
|
||||
1. **Phase 1: Inquiry (`buchungsart => 'Anfrage'`)**
|
||||
- Validates all booking data
|
||||
- Returns pricing information
|
||||
- Response: `<buchung>möglich</buchung>`
|
||||
- No actual booking created
|
||||
|
||||
2. **Phase 2: Booking (`buchungsart => 'Buchung'`)**
|
||||
- Creates actual booking
|
||||
- Returns booking number
|
||||
- Response: `<buchung>erfolgt</buchung>`
|
||||
- Only proceed if Phase 1 succeeded
|
||||
|
||||
### Error Handling
|
||||
|
||||
**API Errors:**
|
||||
- API may return `Notification` object instead of `BookingResponse`
|
||||
- Check instanceof before accessing BookingResponse methods
|
||||
- Display error message from notification
|
||||
|
||||
**Validation Errors:**
|
||||
- Check `isInquiryValid()` on inquiry response
|
||||
- Check `isBookingSuccessful()` on booking response
|
||||
- Display appropriate error messages
|
||||
|
||||
**Network/System Errors:**
|
||||
- Catch all exceptions
|
||||
- Log with full trace
|
||||
- Display generic error message to user
|
||||
- Do NOT clear session on error (allow retry)
|
||||
|
||||
### Price Validation (Optional)
|
||||
|
||||
**Inquiry response includes:**
|
||||
- Individual price items with quantities and assignments
|
||||
- Total price from API
|
||||
- Payment terms (deposit/final payment)
|
||||
|
||||
**Can compare:**
|
||||
- API total vs calculated total
|
||||
- Individual service prices
|
||||
- Participant assignments
|
||||
|
||||
**Implementation:**
|
||||
```php
|
||||
if (abs($inquiryResponse->totalPrice - $calculatedTotal) > 0.01) {
|
||||
$this->logger->warning('Price mismatch', [
|
||||
'api_price' => $inquiryResponse->totalPrice,
|
||||
'calculated_price' => $calculatedTotal,
|
||||
]);
|
||||
// Decide: continue or abort
|
||||
}
|
||||
```
|
||||
|
||||
### Session Management
|
||||
|
||||
**Important:**
|
||||
- Only clear session on successful booking
|
||||
- Keep session on errors (allows retry)
|
||||
- Clear using `BookingService::clearBookingCreateDto()`
|
||||
|
||||
### Logging
|
||||
|
||||
**Log events:**
|
||||
- Inquiry submission (info level)
|
||||
- Inquiry success/failure (info/error)
|
||||
- Booking submission (info level)
|
||||
- Booking success/failure (info/error)
|
||||
- Price mismatches (warning)
|
||||
- Exceptions (error with full trace)
|
||||
|
||||
**Context to include:**
|
||||
- Travel ID
|
||||
- Participant count
|
||||
- Total price
|
||||
- Payment method
|
||||
- Error messages
|
||||
- Exception traces
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests (Future)
|
||||
|
||||
**BookingResponseParser:**
|
||||
- Test parsing successful inquiry response
|
||||
- Test parsing successful booking response
|
||||
- Test parsing price items
|
||||
- Test parsing payment terms
|
||||
- Test handling missing optional fields
|
||||
|
||||
**BookingDataProcessor:**
|
||||
- Test payload generation with all services
|
||||
- Test payload generation with minimum services
|
||||
- Test insurance inclusion
|
||||
- Test payment methods
|
||||
- Test participant mappings
|
||||
|
||||
### Integration Tests (Future)
|
||||
|
||||
**ApiClient:**
|
||||
- Mock socket communication
|
||||
- Test inquiry request format
|
||||
- Test booking request format
|
||||
- Test response parsing
|
||||
- Test error handling
|
||||
|
||||
**Controller:**
|
||||
- Test two-phase submission flow
|
||||
- Test error scenarios
|
||||
- Test session clearing
|
||||
- Test redirects
|
||||
|
||||
### Manual Testing (Immediate)
|
||||
|
||||
**Use sandbox environment:**
|
||||
- Current ddev setup points to sandbox
|
||||
- XML dumps enabled for debugging
|
||||
- Test with real travel data
|
||||
- Verify all email notifications
|
||||
|
||||
## File Locations Summary
|
||||
|
||||
**Models:**
|
||||
- `src/BusProNet/Model/BookingResponse.php`
|
||||
- `src/BusProNet/Model/PriceItem.php`
|
||||
- `src/BusProNet/Model/PaymentTerms.php`
|
||||
|
||||
**Parsers:**
|
||||
- `src/BusProNet/XmlParser/BookingResponseParser.php`
|
||||
- `src/BusProNet/XmlParser/ApiResponseParser.php` (updated)
|
||||
|
||||
**Data Processing:**
|
||||
- `src/BusProNet/DataProcessor/BookingDataProcessor.php` (enhanced)
|
||||
|
||||
**API:**
|
||||
- `src/BusProNet/ApiClient.php` (enhanced)
|
||||
- `src/BusProNet/Constants.php` (enhanced)
|
||||
|
||||
**Controllers (to be updated/created):**
|
||||
- `src/Controller/Booking/CreateStep4Controller.php` (update)
|
||||
- `src/Controller/Booking/BookingSuccessController.php` (create)
|
||||
|
||||
**Services (to be updated):**
|
||||
- `src/Service/BookingService.php` (add method)
|
||||
|
||||
**Templates (to be created):**
|
||||
- `templates/booking/success.html.twig`
|
||||
|
||||
**Documentation:**
|
||||
- `docs/BOOKING_SUBMISSION_STATUS.md` (this file)
|
||||
- `docs/REFACTORING_BOOKING_DATA_PROCESSOR.md`
|
||||
- `docs/BOOKING_PAYMENT_STEP.md`
|
||||
- `docs/Beschreibung XMLAnfrage.pdf` (API documentation)
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Immediate:**
|
||||
- Implement controller logic (20 minutes)
|
||||
- Add session clearing method (5 minutes)
|
||||
- Create success page (10 minutes)
|
||||
- Test with sandbox (30 minutes)
|
||||
|
||||
2. **Short-term:**
|
||||
- Price validation logic (optional)
|
||||
- Enhanced error messages
|
||||
- Email confirmation integration
|
||||
- PDF generation
|
||||
|
||||
3. **Long-term:**
|
||||
- Refactor UPDATE flow to use participant-centric structure
|
||||
- Comprehensive test suite
|
||||
- Performance optimization
|
||||
- Enhanced logging and monitoring
|
||||
|
||||
---
|
||||
|
||||
**Status:** ✅ Implementation Complete - Ready for Sandbox Testing
|
||||
**Estimated Time to Test:** 30-60 minutes
|
||||
**Blockers:** None
|
||||
**Dependencies:** All completed
|
||||
|
||||
## Implementation Summary
|
||||
|
||||
All coding tasks have been completed:
|
||||
|
||||
1. ✅ **Response Models** - BookingResponse, PriceItem, PaymentTerms created with full pricing support
|
||||
2. ✅ **Response Parser** - BookingResponseParser parses all XML response data including prices
|
||||
3. ✅ **Payload Generation** - createBookingRequestPayload() with participant-centric structure and all service types
|
||||
4. ✅ **API Client Methods** - createBookingInquiry() and createBooking() methods implemented
|
||||
5. ✅ **Response Routing** - ApiResponseParser updated to handle BUCHUNG type
|
||||
6. ✅ **Controller Logic** - Two-phase submission with comprehensive error handling in CreateStep4Controller
|
||||
7. ✅ **Service Method** - clearBookingCreateDto() added to BookingService
|
||||
8. ✅ **Success Page** - BookingSuccessController and success.html.twig template created
|
||||
9. ✅ **Code Quality** - All files validated with PHP-CS-Fixer and syntax checking
|
||||
|
||||
**Next Step:** Sandbox testing with real API calls
|
||||
@@ -0,0 +1,285 @@
|
||||
# Loading Indicators Implementation Plan
|
||||
|
||||
## Overview
|
||||
|
||||
Add loading indicators to Step 3 and Step 4 of the booking flow to provide visual feedback during slow API calls.
|
||||
|
||||
**Date:** 2025-10-06
|
||||
**Status:** 📋 Planned (not yet implemented)
|
||||
|
||||
## Problem Statement
|
||||
|
||||
**Current User Experience:**
|
||||
- Step 3: User clicks "Weiter" → 2-3 second wait (inquiry API) → No visual feedback
|
||||
- Step 4: User clicks "Verbindlich buchen" → 1-2 second wait (booking API) → No visual feedback
|
||||
- Users may click multiple times thinking the form didn't submit
|
||||
- No indication that processing is happening
|
||||
|
||||
## Solution
|
||||
|
||||
Use HTMX for form submissions with built-in loading indicators.
|
||||
|
||||
### Why HTMX?
|
||||
|
||||
1. ✅ Already extensively used in the project (Step 2 form refreshes)
|
||||
2. ✅ Built-in loading state management via `hx-indicator`
|
||||
3. ✅ Better error handling (no page reload on validation errors)
|
||||
4. ✅ Progressive enhancement (graceful degradation)
|
||||
5. ✅ Consistent with existing architecture
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### Step 1: Add HTMX Indicator Styles
|
||||
|
||||
**File:** `assets/styles/app.css`
|
||||
|
||||
Add global styles for HTMX loading indicators:
|
||||
|
||||
```css
|
||||
/* HTMX Loading Indicator */
|
||||
.htmx-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.htmx-request .htmx-indicator {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.htmx-request.htmx-indicator {
|
||||
display: flex;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Update Step 3 Form
|
||||
|
||||
**File:** `templates/booking/create_step_3.html.twig`
|
||||
|
||||
**Changes:**
|
||||
|
||||
1. Add HTMX attributes to form:
|
||||
```twig
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'novalidate': 'novalidate',
|
||||
'hx-post': path('app_booking_create_step_3'),
|
||||
'hx-swap': 'none',
|
||||
'hx-indicator': '#step3-loading'
|
||||
}
|
||||
}) }}
|
||||
```
|
||||
|
||||
2. Add loading overlay before form close:
|
||||
```twig
|
||||
{# Loading indicator #}
|
||||
<div id="step3-loading" class="htmx-indicator fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-8 shadow-xl">
|
||||
<div class="flex items-center space-x-4">
|
||||
<svg class="animate-spin h-8 w-8 text-primary-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span class="text-lg font-medium">Buchung wird validiert...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
```
|
||||
|
||||
**File:** `src/Controller/Booking/CreateStep3Controller.php`
|
||||
|
||||
**Changes:**
|
||||
|
||||
Add HTMX detection and response handling:
|
||||
|
||||
```php
|
||||
public function step3(Request $request): Response
|
||||
{
|
||||
// ... existing validation logic ...
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
// ... existing inquiry + price validation logic ...
|
||||
|
||||
// Validation successful - proceed to confirmation step
|
||||
$bookingCreateDto->currentStep = 4;
|
||||
$this->bookingService->saveBookingCreateDto($request, $bookingCreateDto);
|
||||
|
||||
// Handle HTMX requests
|
||||
if ($request->headers->get('HX-Request')) {
|
||||
return new Response('', 200, [
|
||||
'HX-Redirect' => $this->generateUrl('app_booking_create_step_4')
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_booking_create_step_4');
|
||||
} catch (\Exception $e) {
|
||||
// ... existing error handling ...
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('booking/create_step_3.html.twig', [
|
||||
'bookingCreateDto' => $bookingCreateDto,
|
||||
'form' => $form->createView(),
|
||||
...$this->getSummaryVariables($bookingCreateDto),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Update Step 4 Form
|
||||
|
||||
**File:** `templates/booking/create_step_4.html.twig`
|
||||
|
||||
**Changes:**
|
||||
|
||||
1. Add HTMX attributes to form (find `form_start`):
|
||||
```twig
|
||||
{{ form_start(form, {
|
||||
'attr': {
|
||||
'novalidate': 'novalidate',
|
||||
'hx-post': path('app_booking_create_step_4'),
|
||||
'hx-swap': 'none',
|
||||
'hx-indicator': '#step4-loading'
|
||||
}
|
||||
}) }}
|
||||
```
|
||||
|
||||
2. Add loading overlay before submit button:
|
||||
```twig
|
||||
{# Loading indicator #}
|
||||
<div id="step4-loading" class="htmx-indicator fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-8 shadow-xl">
|
||||
<div class="flex items-center space-x-4">
|
||||
<svg class="animate-spin h-8 w-8 text-primary-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span class="text-lg font-medium">Buchung wird durchgeführt...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
```
|
||||
|
||||
**File:** `src/Controller/Booking/CreateStep4Controller.php`
|
||||
|
||||
**Changes:**
|
||||
|
||||
Add HTMX response handling:
|
||||
|
||||
```php
|
||||
public function step4(Request $request): Response
|
||||
{
|
||||
// ... existing code ...
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
// Submit final booking (already validated in Step 3)
|
||||
$bookingResponse = $this->apiClient->createBooking($bookingCreateDto);
|
||||
|
||||
// ... existing error handling ...
|
||||
|
||||
// Success: Store booking number in flash and clear session
|
||||
$this->addFlash('booking_number', $bookingResponse->transactionNumber);
|
||||
$this->bookingService->clearBookingCreateDto($request);
|
||||
|
||||
// Handle HTMX requests
|
||||
if ($request->headers->get('HX-Request')) {
|
||||
return new Response('', 200, [
|
||||
'HX-Redirect' => $this->generateUrl('app_booking_success')
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_booking_success');
|
||||
} catch (\Exception $e) {
|
||||
// ... existing error handling ...
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('booking/create_step_4.html.twig', [
|
||||
'bookingCreateDto' => $bookingCreateDto,
|
||||
'form' => $form->createView(),
|
||||
...$this->getSummaryVariables($bookingCreateDto),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
## Alternative: Stimulus-Only Approach
|
||||
|
||||
If HTMX is not desired, use the existing `loading_controller.js`:
|
||||
|
||||
**Template:**
|
||||
```twig
|
||||
<div data-controller="loading" data-loading-hidden-class="hidden">
|
||||
{{ form_start(form, {'attr': {'data-action': 'submit->loading#toggle'}}) }}
|
||||
|
||||
<div data-loading-target="indicator" class="hidden fixed inset-0 bg-gray-900 bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-8 shadow-xl">
|
||||
<!-- Loading spinner -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form fields -->
|
||||
|
||||
<button type="submit">Weiter</button>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Pros:** Simpler, no controller changes
|
||||
**Cons:**
|
||||
- Loading indicator persists if server returns error
|
||||
- Page reload happens anyway
|
||||
- No error handling improvement
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### Templates:
|
||||
1. `templates/booking/create_step_3.html.twig` - Add HTMX attributes + loading indicator
|
||||
2. `templates/booking/create_step_4.html.twig` - Add HTMX attributes + loading indicator
|
||||
|
||||
### Controllers:
|
||||
3. `src/Controller/Booking/CreateStep3Controller.php` - Add HTMX response handling
|
||||
4. `src/Controller/Booking/CreateStep4Controller.php` - Add HTMX response handling
|
||||
|
||||
### Styles:
|
||||
5. `assets/styles/app.css` - Add `.htmx-indicator` styles (if not already present)
|
||||
|
||||
## Benefits
|
||||
|
||||
**User Experience:**
|
||||
- ✅ Clear visual feedback during API calls
|
||||
- ✅ Prevents duplicate submissions (button disabled during request)
|
||||
- ✅ Professional loading experience
|
||||
- ✅ Reduced user confusion and frustration
|
||||
|
||||
**Technical:**
|
||||
- ✅ Better error handling (no page reload on validation errors)
|
||||
- ✅ Consistent with existing HTMX usage in Step 2
|
||||
- ✅ Progressive enhancement (works without JavaScript)
|
||||
- ✅ Flash messages still work via HX-Redirect
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Step 3: Loading indicator shows during inquiry API call
|
||||
- [ ] Step 3: Form disabled during submission
|
||||
- [ ] Step 3: Success redirects to Step 4
|
||||
- [ ] Step 3: Validation errors show inline without reload
|
||||
- [ ] Step 4: Loading indicator shows during booking API call
|
||||
- [ ] Step 4: Form disabled during submission
|
||||
- [ ] Step 4: Success redirects to success page with flash message
|
||||
- [ ] Step 4: Errors show inline without reload
|
||||
- [ ] Works without JavaScript (graceful degradation)
|
||||
- [ ] No duplicate submissions possible
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
**High Priority** - Significantly improves UX during slow API operations
|
||||
|
||||
## Notes
|
||||
|
||||
- HTMX already included in project dependencies
|
||||
- Loading indicators match existing design system
|
||||
- Compatible with all existing validation logic
|
||||
- No changes to backend business logic required
|
||||
@@ -0,0 +1,183 @@
|
||||
# Booking Data Processor Refactoring Plan
|
||||
|
||||
## Current Status (2025-10-05)
|
||||
|
||||
We discovered a structural inconsistency between the UPDATE and CREATE booking flows while implementing the booking submission feature.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The UPDATE and CREATE flows use fundamentally different data structures, leading to code duplication and complexity:
|
||||
|
||||
### UPDATE Flow (Current)
|
||||
- `BookingEditDto` contains a `Booking` object
|
||||
- `Booking` has centralized service arrays with participant mappings:
|
||||
- `booking.additionalServices` - array of Service objects with `mapping` property (0-based indices)
|
||||
- `booking.transportationServices` - array of Service objects with `mapping` property
|
||||
- `booking.pickupsOutbound` - array of Pickup objects with `mapping` property
|
||||
- `BookingDataProcessor.createUpdateRequestPayload()`:
|
||||
- Resets all service mappings
|
||||
- Iterates through participants
|
||||
- Rebuilds service mappings by looking up services in booking data
|
||||
- Removes unused services
|
||||
- Converts 0-based indices to 1-based for API
|
||||
|
||||
### CREATE Flow (Current)
|
||||
- `BookingCreateDto` contains only `participants` array
|
||||
- Each `ParticipantDto` has direct service references:
|
||||
- `courses`, `skiPass`, `additionalServices`, `board`, `rentals`
|
||||
- `transportationOutbound`, `transportationInbound`
|
||||
- `pickupOutbound`, `pickupInbound`
|
||||
- `insurance`
|
||||
- `BookingDataProcessor.createBookingRequestPayload()`:
|
||||
- Collects services directly from participants
|
||||
- Groups by service ID
|
||||
- Converts to 1-based participant IDs for API
|
||||
|
||||
## Root Cause
|
||||
|
||||
The UPDATE flow was designed to work with API-sourced `Booking` objects that already have centralized service mappings. The CREATE flow was designed from scratch with a simpler participant-centric approach.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
**Align both flows to use the participant-centric structure:**
|
||||
|
||||
1. **Both DTOs work the same way:**
|
||||
- Both have `participants` array
|
||||
- Services are attached directly to participants
|
||||
- No centralized service objects with mappings
|
||||
|
||||
2. **Unified payload generation:**
|
||||
- Use same `collect*Mappings()` methods for both flows
|
||||
- Use same `addServicesFromMap()` helper
|
||||
- Remove complex service manipulation in update flow
|
||||
|
||||
3. **Benefits:**
|
||||
- Single source of truth for service mappings
|
||||
- Less code duplication
|
||||
- Easier to understand and maintain
|
||||
- Consistent patterns across all booking operations
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Refactor BookingEditDto.fromBooking()
|
||||
- [x] Already populates participant services correctly
|
||||
- [x] Services are already attached to participants
|
||||
- [ ] Verify all service types are covered
|
||||
|
||||
### Phase 2: Refactor BookingDataProcessor.createUpdateRequestPayload()
|
||||
- [ ] Remove `resetServiceMappings()`
|
||||
- [ ] Remove `processParticipantServices()` (complex service lookup)
|
||||
- [ ] Remove `processAdditionalServices()`
|
||||
- [ ] Remove `processTransportationServices()`
|
||||
- [ ] Remove `processPickupLocations()`
|
||||
- [ ] Remove `removeUnusedServices()`
|
||||
- [ ] Use `collect*Mappings()` methods instead (same as create flow)
|
||||
- [ ] Update `buildServicesPayload()` to use collected maps
|
||||
- [ ] Update `buildPickupPayload()` to use collected maps
|
||||
|
||||
### Phase 3: Add convertServicesToMap() Helper
|
||||
- [ ] Create helper to convert service objects with mapping to ID => participant IDs map
|
||||
- [ ] This bridges the gap between old structure (if needed) and new structure
|
||||
|
||||
### Phase 4: Testing
|
||||
- [ ] Test update flow with all service types
|
||||
- [ ] Test create flow (should remain unchanged)
|
||||
- [ ] Verify API payloads are identical before/after refactoring
|
||||
- [ ] Test edge cases (no services, all services, mixed scenarios)
|
||||
|
||||
### Phase 5: Cleanup
|
||||
- [ ] Remove unused methods from BookingDataProcessor
|
||||
- [ ] Remove unused properties from Booking model (if any)
|
||||
- [ ] Update documentation
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
**MEDIUM RISK** - This refactoring touches critical booking update functionality that is already working in production.
|
||||
|
||||
### Risks:
|
||||
1. Breaking existing update flow
|
||||
2. Subtle bugs in service mapping
|
||||
3. Data loss if participant service references are incorrect
|
||||
4. Payment/bank account handling might break
|
||||
|
||||
### Mitigation:
|
||||
1. Comprehensive testing before deployment
|
||||
2. Keep git history clean with atomic commits
|
||||
3. Test with real booking data from sandbox
|
||||
4. Verify XML payloads match exactly (before/after)
|
||||
5. Have rollback plan ready
|
||||
|
||||
## Decision Point
|
||||
|
||||
**Should we refactor NOW or LATER?**
|
||||
|
||||
### Arguments for NOW:
|
||||
- We're already in BookingDataProcessor
|
||||
- Fresh understanding of both flows
|
||||
- Prevents further divergence
|
||||
- Makes current task (booking submission) cleaner
|
||||
|
||||
### Arguments for LATER:
|
||||
- Current task (booking submission) is incomplete
|
||||
- Refactoring is significant and risky
|
||||
- Could introduce bugs in working update flow
|
||||
- Should be separate PR with focused testing
|
||||
- Current booking submission is more urgent
|
||||
|
||||
## Decision
|
||||
|
||||
**REFACTOR LATER** - Complete the booking submission task first, then do this refactoring as a separate focused effort.
|
||||
|
||||
**AGREED:** The CREATE flow's participant-centric structure is the new standard. The UPDATE flow should adopt this architecture in the future refactoring.
|
||||
|
||||
### Reasoning:
|
||||
1. Booking submission is nearly complete and is the immediate business need
|
||||
2. Update flow is working and tested - don't break what works
|
||||
3. Refactoring deserves dedicated focus and testing
|
||||
4. Can create comprehensive tests for both flows first
|
||||
5. Allows for proper code review and QA
|
||||
|
||||
### Short-term Solution:
|
||||
- Keep both flows separate for now
|
||||
- Add the `convertServicesToMap()` helper to bridge structures
|
||||
- Complete booking submission with current architecture
|
||||
- Document this technical debt
|
||||
|
||||
### Long-term Plan:
|
||||
- Create separate refactoring task/issue
|
||||
- Write comprehensive tests for update flow first
|
||||
- Perform refactoring in dedicated branch
|
||||
- Extensive testing with sandbox data
|
||||
- Separate PR with focused review
|
||||
|
||||
## Current Task: Booking Submission
|
||||
|
||||
We are 60% complete with booking submission implementation:
|
||||
|
||||
### Completed:
|
||||
- [x] BookingResponse, PriceItem, PaymentTerms models
|
||||
- [x] BookingResponseParser with pricing data
|
||||
- [x] createBookingRequestPayload() in BookingDataProcessor
|
||||
- [x] Payment type ID constants
|
||||
- [x] Helper method addServicesFromMap()
|
||||
|
||||
### Remaining:
|
||||
- [ ] Add TYPE_BOOKING constant to ApiClient
|
||||
- [ ] Add createBookingInquiry() and createBooking() to ApiClient
|
||||
- [ ] Update ApiResponseParser to handle BUCHUNG response type
|
||||
- [ ] Implement two-phase submission in CreateStep4Controller
|
||||
- [ ] Add clearBookingCreateDto() to BookingService
|
||||
- [ ] Create BookingSuccessController and template
|
||||
- [ ] Test with sandbox API
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **IMMEDIATE:** Continue with booking submission task
|
||||
2. **AFTER COMPLETION:** Create refactoring issue/task
|
||||
3. **FUTURE:** Dedicated refactoring effort with proper testing
|
||||
|
||||
---
|
||||
|
||||
**Document Created:** 2025-10-05
|
||||
**Status:** Deferred - Continue with booking submission
|
||||
**Related:** Booking submission implementation (in progress)
|
||||
Reference in New Issue
Block a user