wip: submit booking to api

This commit is contained in:
Björn Fromme
2025-10-06 11:22:02 +02:00
parent 139ec3c1db
commit 7f1ef4059d
34 changed files with 3316 additions and 31 deletions
+79
View File
@@ -7,6 +7,7 @@ use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Exception\ResponseParserException;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Booking;
use App\BusProNet\Model\BookingResponse;
use App\BusProNet\Model\BookingUpdate;
use App\BusProNet\Model\CrmAttributes;
use App\BusProNet\Model\Notification;
@@ -14,6 +15,7 @@ use App\BusProNet\Model\PersonalData;
use App\BusProNet\Model\Travel;
use App\BusProNet\Traits\ApiClientTrait;
use App\BusProNet\XmlParser\ApiResponseParser;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingEditDto;
use App\Form\Model\RegistrationDto;
use League\Flysystem\FilesystemException;
@@ -34,8 +36,10 @@ class ApiClient
public const TYPE_AVAILABILITY = 'VERFUEGBARKEIT';
public const TYPE_AVAILABILITY_HOTEL = 'VERFUEGBARKEITHOTEL';
public const TYPE_BOOKING_UPDATE = 'BUCHUNGAENDERUNG';
public const TYPE_BOOKING = 'BUCHUNG';
public const TYPE_PRODUCTS = 'PRODUKTE';
public const TYPE_PRODUCT_DATA = 'PRODUKTDATEN';
public const TYPE_AGENCIES = 'AGENTUREN';
private array $config;
@@ -203,6 +207,60 @@ class ApiClient
return $this->sendRequest(static::TYPE_BOOKING_UPDATE, $data, [], $debug);
}
/**
* Submits a booking inquiry for validation.
*
* First phase of the two-phase booking process. Validates all booking data
* and returns pricing information without creating an actual booking.
*
* @param BookingCreateDto $bookingDto The booking creation form data
* @param bool $debug Enable debug mode (XML dumps)
*
* @return Notification|BookingResponse Notification on error, BookingResponse on success
*
* @throws ApiClientException If the API request fails
*/
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);
}
/**
* Submits the final booking request.
*
* Second phase of the two-phase booking process. Creates the actual booking
* after successful inquiry validation.
*
* @param BookingCreateDto $bookingDto The booking creation form data
* @param bool $debug Enable debug mode (XML dumps)
*
* @return Notification|BookingResponse Notification on error, BookingResponse with booking number on success
*
* @throws ApiClientException If the API request fails
*/
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);
}
/**
* @throws ApiClientException
*/
@@ -328,6 +386,27 @@ class ApiClient
return $this->sendRequest(static::TYPE_PRODUCTS, $data);
}
/**
* Fetches all available agencies from the BusProNet API.
*
* Returns a list of all agencies with their contact information.
* This data is typically cached for long periods as it changes infrequently.
*
* @return Agency[]|Notification Array of Agency objects on success, Notification on error
*
* @throws ApiClientException If the API request fails
*/
public function getAgencies(): array|Notification
{
$data = [
'user' => $this->config['bpn_username'],
'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_AGENCIES),
'satz' => ['@typ' => static::TYPE_AGENCIES],
];
return $this->sendRequest(static::TYPE_AGENCIES, $data);
}
/**
* @throws ApiClientException
*/
+4
View File
@@ -51,4 +51,8 @@ final class Constants
// Payment methods
public const PAYMENT_METHOD_TRANSFER = 'transfer';
public const PAYMENT_METHOD_DEBIT = 'debit';
// Payment type IDs for API
public const PAYMENT_TYPE_ID_TRANSFER = 2;
public const PAYMENT_TYPE_ID_DEBIT = 5;
}
@@ -4,7 +4,9 @@ declare(strict_types=1);
namespace App\BusProNet\DataProcessor;
use App\BusProNet\Constants;
use App\BusProNet\Model\Communication;
use App\Form\Model\BookingCreateDto;
use App\Form\Model\BookingEditDto;
/**
@@ -408,4 +410,365 @@ class BookingDataProcessor
}
}
}
/**
* Creates a booking request payload for new bookings (inquiry or final booking).
*
* Generates the array payload structure for creating new bookings through the BusProNet API.
* This includes all participant data, room selections, services (including insurance), and
* payment information. The booking type determines whether this is an inquiry validation
* ('Anfrage') or a final booking commit ('Buchung').
*
* @param BookingCreateDto $bookingDto The booking creation form data
* @param string $bookingType Either 'Anfrage' (inquiry) or 'Buchung' (final booking)
*
* @return array The structured payload array for BusProNet API submission
*/
public function createBookingRequestPayload(BookingCreateDto $bookingDto, string $bookingType): array
{
$firstParticipant = $bookingDto->participants[0];
$payload = [
'buchungsart' => $bookingType,
'status' => 'F',
'idreise' => $bookingDto->travel->id,
'idpartner' => $bookingDto->travel->hotelId,
'idagentur' => $bookingDto->agencyId,
];
// Add applicant (first participant data)
$payload['anmelder'] = [
'name' => $firstParticipant->lastName,
'vorname' => $firstParticipant->firstName,
'geschlecht' => $firstParticipant->gender ?? '',
'nationalitaet' => $firstParticipant->nationality ?? '',
];
if (null !== $firstParticipant->dateOfBirth) {
$payload['anmelder']['geburtsdatum'] = $firstParticipant->dateOfBirth->format('d.m.Y');
}
// Add address for applicant
if (null !== $firstParticipant->address) {
$payload['anmelder']['anschrift'] = $firstParticipant->address->toPayload();
}
if (null !== $firstParticipant->email || null !== $firstParticipant->mobile) {
$payload['anmelder']['kommunikation'] = [];
if (null !== $firstParticipant->email) {
$payload['anmelder']['kommunikation']['email'] = $firstParticipant->email;
}
if (null !== $firstParticipant->mobile) {
$payload['anmelder']['kommunikation']['telefonmobil'] = $firstParticipant->mobile;
}
}
// Add participants
$payload['teilnehmerliste']['teilnehmer'] = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantData = [
'@id' => $index + 1,
'name' => $participant->lastName,
'vorname' => $participant->firstName,
'geschlecht' => $participant->gender ?? '',
'nationalitaet' => $participant->nationality ?? '',
];
if (null !== $participant->dateOfBirth) {
$participantData['geburtsdatum'] = $participant->dateOfBirth->format('d.m.Y');
}
// Add address (always include structure, even if empty)
$participantData['anschrift'] = $participant->address?->toPayload() ?? [
'strasse' => null,
'plz' => null,
'ort' => null,
'ortsteil' => null,
'land' => null,
];
// Add contact info for all participants
if (null !== $participant->email || null !== $participant->mobile) {
$participantData['kommunikation'] = [];
if (null !== $participant->email) {
$participantData['kommunikation']['email'] = $participant->email;
}
if (null !== $participant->mobile) {
$participantData['kommunikation']['telefonmobil'] = $participant->mobile;
}
}
// Add wishes (room remarks and license plate)
if (null !== $participant->remarksRoom || null !== $participant->licensePlate) {
$participantData['wünsche'] = [];
if (null !== $participant->remarksRoom && '' !== trim($participant->remarksRoom)) {
$participantData['wünsche']['unterbringungswunsch'] = $participant->remarksRoom;
}
if (null !== $participant->licensePlate && '' !== trim($participant->licensePlate)) {
$participantData['wünsche']['beförderungswunsch'] = $participant->licensePlate;
}
}
$payload['teilnehmerliste']['teilnehmer'][] = $participantData;
}
// Collect and group all services by ID with participant mappings
$serviceMap = $this->collectServiceMappings($bookingDto);
$transportationMap = $this->collectTransportationMappings($bookingDto);
$roomMap = $this->collectRoomMappings($bookingDto);
$pickupMap = $this->collectPickupMappings($bookingDto);
$insuranceMap = $this->collectInsuranceMappings($bookingDto);
// Add services using reusable helper methods
$this->addServicesFromMap($payload, 'beförderungen', 'beförderung', '@idleistung', $transportationMap);
$this->addRoomMappingsToPayload($payload, $roomMap, $bookingDto);
$this->addServicesFromMap($payload, 'zusatzleistungen', 'zusatzleistung', '@idleistung', $serviceMap);
$this->addServicesFromMap($payload, 'zustiege', 'zustieg', '@idzustieg', $pickupMap);
$this->addServicesFromMap($payload, 'versicherungen', 'versicherung', '@idversicherung', $insuranceMap);
// Add payment information
$payload['zahlung'] = [
'@idzahlungsart' => Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod
? Constants::PAYMENT_TYPE_ID_DEBIT
: Constants::PAYMENT_TYPE_ID_TRANSFER,
'@art' => Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod ? 'EINZUG' : 'UEBERWEISUNG',
];
if (Constants::PAYMENT_METHOD_DEBIT === $bookingDto->paymentMethod && null !== $bookingDto->bankAccount) {
$payload['zahlung']['bankverbindung'] = [
'@iban' => $bookingDto->bankAccount->iban,
'@kontoinhaber' => $bookingDto->bankAccount->accountHolder,
];
}
return $payload;
}
/**
* Adds services from a mapping to the payload.
*
* Generic helper method that converts service ID => participant IDs mappings
* into XML payload structure.
*
* @param array $payload The payload array to modify
* @param string $sectionKey The section key (e.g., 'beförderungen', 'versicherungen')
* @param string $itemKey The item key (e.g., 'beförderung', 'versicherung')
* @param string $idAttributeName The ID attribute name (e.g., '@idleistung', '@idversicherung')
* @param array $serviceMap Map of service ID to participant IDs
*/
private function addServicesFromMap(
array &$payload,
string $sectionKey,
string $itemKey,
string $idAttributeName,
array $serviceMap,
): void {
if (false === empty($serviceMap)) {
$payload[$sectionKey][$itemKey] = [];
foreach ($serviceMap as $serviceId => $participantIds) {
$payload[$sectionKey][$itemKey][] = [
$idAttributeName => $serviceId,
'@anzahl' => count($participantIds),
'@zuordnung' => implode(',', $participantIds),
];
}
}
}
/**
* Adds room mappings with detailed attributes to the payload.
*
* Rooms require special attributes beyond simple service mapping:
* - kategorie (room category code)
* - idverpflegung (board type ID)
* - anreise (arrival date)
* - abreise (departure date)
* - anzahl (number of rooms of this type booked)
*
* @param array $payload The payload array to modify
* @param array $roomMap Map of room ID to participant IDs
* @param BookingCreateDto $bookingDto The booking data for accessing room details and quantities
*/
private function addRoomMappingsToPayload(array &$payload, array $roomMap, BookingCreateDto $bookingDto): void
{
if (empty($roomMap)) {
return;
}
$availableRooms = $bookingDto->travel->getAvailableRooms();
$payload['ferienzielunterbringungen']['ferienzielunterbringung'] = [];
// Build room selection quantity lookup
$roomQuantities = [];
foreach ($bookingDto->roomSelections as $selection) {
if ($selection->quantity > 0) {
$roomQuantities[$selection->roomId] = $selection->quantity;
}
}
foreach ($roomMap as $roomId => $participantIds) {
$room = $availableRooms[$roomId] ?? null;
if (null === $room) {
continue;
}
$quantity = $roomQuantities[$roomId] ?? 1;
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
'@idzimmer' => $room->id,
'@kategorie' => $room->category,
'@idverpflegung' => $room->boardId,
'@anreise' => $bookingDto->travel->dateFrom->format('d.m.Y'),
'@abreise' => $bookingDto->travel->dateTo->format('d.m.Y'),
'@anzahl' => $quantity,
'@zuordnung' => implode(',', $participantIds),
];
}
}
/**
* Collects room mappings.
*
* Groups participants by their assigned room ID.
*
* @return array<string, array<int>> Map of room ID to participant IDs
*/
private function collectRoomMappings(BookingCreateDto $bookingDto): array
{
$roomMap = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantId = $index + 1;
if (null !== $participant->assignedRoomId) {
$roomMap[$participant->assignedRoomId][] = $participantId;
}
}
return $roomMap;
}
/**
* Collects service mappings for the booking request.
*
* Groups board, ski passes, rentals, rental insurance, courses, parking, and additional services
* by service ID with their participant assignments (1-based).
*
* @return array<string, array<int>> Map of service ID to participant IDs
*/
private function collectServiceMappings(BookingCreateDto $bookingDto): array
{
$serviceMap = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantId = $index + 1;
// Board services
foreach ($participant->board as $board) {
$serviceMap[$board->id][] = $participantId;
}
// Ski pass
if (null !== $participant->skiPass) {
$serviceMap[$participant->skiPass->id][] = $participantId;
}
// Rentals
foreach ($participant->rentals as $rental) {
$serviceMap[$rental->id][] = $participantId;
}
// Rental insurance
if (null !== $participant->rentalInsurance) {
$serviceMap[$participant->rentalInsurance->id][] = $participantId;
}
// Courses
foreach ($participant->courses as $course) {
$serviceMap[$course->id][] = $participantId;
}
// Parking service (for self-organized transportation)
if (true === $participant->parking && null !== $participant->parkingService) {
$serviceMap[$participant->parkingService->id][] = $participantId;
}
// Additional services
foreach ($participant->additionalServices as $service) {
$serviceMap[$service->id][] = $participantId;
}
}
return $serviceMap;
}
/**
* Collects transportation service mappings.
*
* @return array<string, array<int>> Map of transportation service ID to participant IDs
*/
private function collectTransportationMappings(BookingCreateDto $bookingDto): array
{
$transportationMap = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantId = $index + 1;
if (null !== $participant->transportationOutbound) {
$transportationMap[$participant->transportationOutbound->id][] = $participantId;
}
if (null !== $participant->transportationInbound) {
$transportationMap[$participant->transportationInbound->id][] = $participantId;
}
}
return $transportationMap;
}
/**
* Collects pickup location mappings.
*
* Only collects outbound pickups as the API doesn't support different pickups
* for inbound direction. Both directions use the same pickup location.
*
* @return array<string, array<int>> Map of pickup ID to participant IDs
*/
private function collectPickupMappings(BookingCreateDto $bookingDto): array
{
$pickupMap = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantId = $index + 1;
// Only use outbound pickups (inbound uses same location)
if (null !== $participant->pickupOutbound) {
$pickupMap[$participant->pickupOutbound->id][] = $participantId;
}
}
return $pickupMap;
}
/**
* Collects insurance mappings.
*
* CRITICAL: Insurance data is only included in CREATE flow, not in UPDATE flow.
*
* @return array<string, array<int>> Map of insurance ID to participant IDs
*/
private function collectInsuranceMappings(BookingCreateDto $bookingDto): array
{
$insuranceMap = [];
foreach ($bookingDto->participants as $index => $participant) {
$participantId = $index + 1;
if (null !== $participant->insurance) {
$insuranceMap[$participant->insurance->id][] = $participantId;
}
}
return $insuranceMap;
}
}
+4 -4
View File
@@ -15,18 +15,18 @@ use Symfony\Component\Validator\Constraints as Assert;
*/
class Address
{
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data', 'applicant_address'])]
public ?string $street = null;
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data', 'applicant_address'])]
public ?string $postCode = null;
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data', 'applicant_address'])]
public ?string $city = null;
public ?string $district = null;
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data'])]
#[Assert\NotBlank(message: 'Bitte angeben', groups: ['personal_data', 'applicant_address'])]
public ?string $country = null;
/**
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
final readonly class Agency
{
public function __construct(
public int $id,
public string $name,
public string $code,
public ?string $street = null,
public ?string $postCode = null,
public ?string $city = null,
public ?string $phone = null,
) {
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
/**
* Represents a successful API response from a booking request.
*
* Response structure for successful requests:
* - <buchung>möglich</buchung> = inquiry validation successful
* - <buchung>erfolgt</buchung> = booking creation successful
*
* Error responses return Notification objects instead (typ="HINWEIS").
*/
class BookingResponse
{
/**
* @param string $status Booking status (möglich|erfolgt)
* @param string|null $transactionNumber Transaction number (vorgang)
* @param array<int, PriceItem> $priceItems Individual price items from response
* @param float|null $totalPrice Total price (gesamtpreis)
* @param PaymentTerms|null $paymentTerms Payment terms (anzahlung/restzahlung)
*/
public function __construct(
public readonly string $status,
public readonly ?string $transactionNumber = null,
public readonly array $priceItems = [],
public readonly ?float $totalPrice = null,
public readonly ?PaymentTerms $paymentTerms = null,
) {
}
/**
* Returns true if the inquiry validation was successful.
*/
public function isInquiryValid(): bool
{
return 'möglich' === $this->status;
}
/**
* Returns true if the booking was successfully created.
*/
public function isBookingSuccessful(): bool
{
return 'erfolgt' === $this->status;
}
}
+1 -1
View File
@@ -175,4 +175,4 @@ class Insurance
return array_values(array_unique($urls));
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
/**
* Represents payment terms from the booking response.
*/
class PaymentTerms
{
public function __construct(
public readonly ?float $depositAmount = null,
public readonly ?string $depositDate = null,
public readonly ?float $finalPaymentAmount = null,
public readonly ?string $finalPaymentDate = null,
) {
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\Model;
/**
* Represents a single price item from the booking response.
*/
class PriceItem
{
public function __construct(
public readonly int $position,
public readonly string $type,
public readonly ?string $subType,
public readonly string $label,
public readonly ?string $dateFrom,
public readonly ?string $dateTo,
public readonly int $quantity,
public readonly ?string $assignment,
public readonly float $unitPrice,
public readonly float $totalPrice,
public readonly ?string $id,
) {
}
}
+87
View File
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\XmlLoader;
use App\BusProNet\ApiClient;
use App\BusProNet\Model\Agency;
use App\BusProNet\Model\Notification;
use Psr\Cache\InvalidArgumentException;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
class AgencyLoader
{
public const DEFAULT_AGENCY_CODE = '0001';
public function __construct(
private readonly CacheInterface $cache,
private readonly ApiClient $apiClient,
private readonly LoggerInterface $logger,
) {
}
/**
* @return Agency[]
*/
public function loadAll(): array
{
try {
return $this->cache->get('bpn_agencies', function (ItemInterface $item) {
// Cache for 24 hours (agencies change infrequently)
$item->expiresAfter(24 * 60 * 60);
$result = $this->apiClient->getAgencies();
if ($result instanceof Notification) {
$this->logger->error('Failed to fetch agencies from BPN API', [
'message' => $result->message,
]);
return [];
}
return $result;
});
} catch (InvalidArgumentException $e) {
$this->logger->error('Cache error while loading agencies', [
'exception' => $e->getMessage(),
]);
return [];
}
}
public function loadById(int $id): ?Agency
{
$agencies = $this->loadAll();
foreach ($agencies as $agency) {
if ($agency->id === $id) {
return $agency;
}
}
return null;
}
public function loadByCode(string $code): ?Agency
{
$agencies = $this->loadAll();
foreach ($agencies as $agency) {
if ($agency->code === $code) {
return $agency;
}
}
return null;
}
public function loadDefault(): ?Agency
{
return $this->loadByCode(self::DEFAULT_AGENCY_CODE);
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\BusProNet\XmlParser;
use App\BusProNet\Model\Agency;
use Symfony\Component\DomCrawler\Crawler;
final class AgencyParser extends AbstractParser
{
/**
* @return Agency[]
*/
public function parse(Crawler $node): array
{
$agencies = [];
$node->filterXPath('//agenturen/agentur')->each(function (Crawler $agencyNode) use (&$agencies): void {
$id = (int) $agencyNode->attr('id');
$name = $this->getStringOrNullValue($agencyNode->filterXPath('//name')) ?? '';
$code = $this->getStringOrNullValue($agencyNode->filterXPath('//code')) ?? '';
$street = $this->getStringOrNullValue($agencyNode->filterXPath('//strasse'));
$postCode = $this->getStringOrNullValue($agencyNode->filterXPath('//plz'));
$city = $this->getStringOrNullValue($agencyNode->filterXPath('//ort'));
$phone = $this->getStringOrNullValue($agencyNode->filterXPath('//telefon'));
$agencies[] = new Agency(
id: $id,
name: $name,
code: $code,
street: $street,
postCode: $postCode,
city: $city,
phone: $phone
);
});
return $agencies;
}
}
@@ -59,12 +59,16 @@ class ApiResponseParser extends AbstractParser
return (new AvailabilitiesParser())->parseRooms($resultNode);
case ApiClient::TYPE_BOOKING_UPDATE:
return (new BookingUpdateParser())->parse($resultNode);
case ApiClient::TYPE_BOOKING:
return (new BookingResponseParser())->parse($resultNode);
case ApiClient::TYPE_PRODUCTS:
return (new ProductsParser())->parse($resultNode);
case ApiClient::TYPE_PRODUCT_DATA:
$travelNode = $crawler->filterXPath('//reise/termin');
return (new TravelParser())->parse($travelNode->first(), ...$additionalArgs);
case ApiClient::TYPE_AGENCIES:
return (new AgencyParser())->parse($resultNode);
}
throw new ResponseParserException('Unable to parse XML response');
@@ -0,0 +1,113 @@
<?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;
/**
* Parses XML responses from booking requests (inquiry and final booking).
*
* Expected XML structure:
* <ergebnis>
* <satz typ="BUCHUNG" />
* <buchung>möglich|erfolgt</buchung>
* <vorgang>321530</vorgang>
* <preise>
* <preis position="1" art="BEF" unterart="BUS" bezeichnung="..." ... />
* </preise>
* <gesamtpreis>671,78</gesamtpreis>
* <zahlungsbedingungen>
* <anzahlung betrag="128,00" termin="21.03.2017" />
* <restzahlung betrag="543,78" termin="12.11.2017" />
* </zahlungsbedingungen>
* </ergebnis>
*/
class BookingResponseParser extends AbstractParser
{
public function parse(Crawler $node): BookingResponse
{
$status = $node->filterXPath('//buchung')->text();
$transactionNumber = $this->getStringOrNullValue($node->filterXPath('//vorgang'));
$totalPrice = $this->getFloatOrNullValue($node->filterXPath('//gesamtpreis'));
$priceItems = $this->parsePriceItems($node);
$paymentTerms = $this->parsePaymentTerms($node);
return new BookingResponse(
status: $status,
transactionNumber: $transactionNumber,
priceItems: $priceItems,
totalPrice: $totalPrice,
paymentTerms: $paymentTerms
);
}
/**
* Parses individual price items from the response.
*
* @return array<int, PriceItem>
*/
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: $priceNode->attr('terminvon'),
dateTo: $priceNode->attr('terminbis'),
quantity: (int) $priceNode->attr('anzahl'),
assignment: $priceNode->attr('zuordnung'),
unitPrice: $this->stringToFloat($priceNode->attr('preis')),
totalPrice: $this->stringToFloat($priceNode->attr('gesamtpreis')),
id: $priceNode->attr('id')
);
});
return $priceItems;
}
/**
* Parses payment terms from the response.
*/
private function parsePaymentTerms(Crawler $node): ?PaymentTerms
{
$paymentNode = $node->filterXPath('//zahlungsbedingungen');
if (0 === $paymentNode->count()) {
return null;
}
$depositAmount = null;
$depositDate = null;
$finalPaymentAmount = null;
$finalPaymentDate = null;
$depositNode = $paymentNode->filterXPath('//anzahlung');
if ($depositNode->count() > 0) {
$depositAmount = $this->stringToFloat($depositNode->attr('betrag'));
$depositDate = $depositNode->attr('termin');
}
$finalPaymentNode = $paymentNode->filterXPath('//restzahlung');
if ($finalPaymentNode->count() > 0) {
$finalPaymentAmount = $this->stringToFloat($finalPaymentNode->attr('betrag'));
$finalPaymentDate = $finalPaymentNode->attr('termin');
}
return new PaymentTerms(
depositAmount: $depositAmount,
depositDate: $depositDate,
finalPaymentAmount: $finalPaymentAmount,
finalPaymentDate: $finalPaymentDate
);
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ class InsuranceParser extends AbstractParser
// Second pass: Parse individual insurances with conditional filtering
$individualInsurances = [];
$xmlContent->filterXPath('//versicherungen/versicherung')
->each(function (Crawler $node) use (&$individualInsurances, &$insurances, $referencedIds) {
->each(function (Crawler $node) use (&$individualInsurances, &$insurances) {
$id = (int) $node->attr('idbuspro'); // Individual insurances have int IDs
$isComplementary = $this->getBoolAttributeValue($node->attr('zusatzversicherung'));