507 lines
21 KiB
PHP
507 lines
21 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\BusProNet\DataProcessor;
|
|
|
|
use App\BusProNet\Constants;
|
|
use App\BusProNet\Model\Booking;
|
|
use App\BusProNet\Service\ParticipantStatusRuleRegistry;
|
|
use App\Form\Model\BookingDto;
|
|
|
|
/**
|
|
* Builds API payload structures for BusProNet booking requests.
|
|
*
|
|
* Transforms booking data into the structured array format expected by the
|
|
* BusProNet XML API for both create and update operations.
|
|
*/
|
|
class BookingPayloadBuilder
|
|
{
|
|
public function __construct(
|
|
private readonly ServiceMappingCollector $mappingCollector,
|
|
private readonly ParticipantStatusRuleRegistry $statusRuleRegistry,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* Builds the base API payload structure with booking information.
|
|
*
|
|
* Creates the main structure that will be populated with detailed data sections.
|
|
*
|
|
* @param Booking $bookingData The booking data object
|
|
*
|
|
* @return array The base payload structure
|
|
*/
|
|
public function buildBasePayload(Booking $bookingData): array
|
|
{
|
|
return [
|
|
'idbuchung' => $bookingData->id,
|
|
'status' => $bookingData->status,
|
|
'idagentur' => $bookingData->agencyId,
|
|
'idreise' => $bookingData->dateId,
|
|
'idpartner' => $bookingData->hotelId,
|
|
'anmelder' => $bookingData->applicant->toPayload(),
|
|
'zahlung' => [
|
|
'@idzahlungsart' => $bookingData->paymentId,
|
|
'@bezeichnung' => $bookingData->paymentLabel,
|
|
'@art' => $bookingData->paymentType,
|
|
],
|
|
'teilnehmerliste' => [
|
|
'teilnehmer' => [],
|
|
],
|
|
'zusatzleistungen' => [
|
|
'zusatzleistung' => [],
|
|
],
|
|
'beförderungen' => [
|
|
'beförderung' => [],
|
|
],
|
|
'ferienzielunterbringungen' => [
|
|
'ferienzielunterbringung' => [],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Adds bank account information to the payload if present.
|
|
*
|
|
* Bank account information is required for direct debit payments.
|
|
*
|
|
* @param array $payload The payload array to modify
|
|
* @param Booking $bookingData The booking data object
|
|
*/
|
|
public function addBankAccountToPayload(array &$payload, Booking $bookingData): void
|
|
{
|
|
if (null !== $bookingData->bankAccount) {
|
|
$payload['zahlung']['bankverbindung'] = [
|
|
'@kreditinstitut' => $bookingData->bankAccount->bankName,
|
|
'@iban' => $bookingData->bankAccount->iban,
|
|
'@bic' => $bookingData->bankAccount->bic,
|
|
'@kontoinhaber' => $bookingData->bankAccount->holder,
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the participant list section of the payload.
|
|
*
|
|
* Includes status, personal data, and wishes (room remarks, license plate) for each participant.
|
|
*
|
|
* @param array $payload The payload array to modify
|
|
* @param Booking $bookingData The booking data object
|
|
* @param array $participantDtos The participant DTOs from the form (for wishes data)
|
|
*/
|
|
public function buildParticipantPayload(array &$payload, Booking $bookingData, array $participantDtos): void
|
|
{
|
|
foreach ($bookingData->participants as $index => $participant) {
|
|
$participantPayload = [
|
|
'@id' => $index + 1,
|
|
'status' => $bookingData->participantsStatus[$index],
|
|
...$participant->toPayload(),
|
|
];
|
|
|
|
// Add wishes (room remarks, license plate) from form DTO
|
|
if (isset($participantDtos[$index])) {
|
|
$dto = $participantDtos[$index];
|
|
if (null !== $dto->remarksRoom || null !== $dto->licensePlate) {
|
|
$wishes = [];
|
|
|
|
if (null !== $dto->remarksRoom && '' !== trim($dto->remarksRoom)) {
|
|
$wishes['unterbringungswunsch'] = $dto->remarksRoom;
|
|
}
|
|
|
|
if (null !== $dto->licensePlate && '' !== trim($dto->licensePlate)) {
|
|
$wishes['beförderungswunsch'] = $dto->licensePlate;
|
|
}
|
|
|
|
if (false === empty($wishes)) {
|
|
$participantPayload['wünsche'] = $wishes;
|
|
}
|
|
}
|
|
|
|
// Add promotional voucher as promotionalCode
|
|
$promotionalCode = $this->mappingCollector->getPromoVoucherCodeForParticipant($dto);
|
|
if (null !== $promotionalCode) {
|
|
$participantPayload['aktionscode'] = $promotionalCode;
|
|
}
|
|
|
|
// Add goodwill voucher as participant-level einloesecode
|
|
$goodwillCode = $this->mappingCollector->getGoodwillVoucherCodeForParticipant($dto);
|
|
if (null !== $goodwillCode) {
|
|
$participantPayload['einloesecode'] = $goodwillCode;
|
|
}
|
|
}
|
|
|
|
$payload['teilnehmerliste']['teilnehmer'][] = $participantPayload;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the services sections of the payload.
|
|
*
|
|
* Includes additional services, transportation services, and accommodation details
|
|
* with participant mappings and quantities.
|
|
*
|
|
* @param array $payload The payload array to modify
|
|
* @param Booking $bookingData The booking data object
|
|
*/
|
|
public function buildServicesPayload(array &$payload, Booking $bookingData): void
|
|
{
|
|
foreach ($bookingData->additionalServices as $service) {
|
|
$uniqueMapping = array_unique($service->mapping);
|
|
$payload['zusatzleistungen']['zusatzleistung'][] = [
|
|
'@idleistung' => $service->id,
|
|
'@anzahl' => count($uniqueMapping),
|
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
|
];
|
|
}
|
|
|
|
foreach ($bookingData->transportationServices as $service) {
|
|
$uniqueMapping = array_unique($service->mapping);
|
|
$payload['beförderungen']['beförderung'][] = [
|
|
'@idleistung' => $service->id,
|
|
'@anzahl' => count($uniqueMapping),
|
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
|
];
|
|
}
|
|
|
|
foreach ($bookingData->rooms as $room) {
|
|
$uniqueMapping = array_unique($room->mapping);
|
|
$payload['ferienzielunterbringungen']['ferienzielunterbringung'][] = [
|
|
'@idzimmer' => $room->id,
|
|
'@kategorie' => $room->category,
|
|
'@idverpflegung' => $room->boardId,
|
|
'@anreise' => $room->dateFrom ? $room->dateFrom->format('d.m.Y') : null,
|
|
'@abreise' => $room->dateTo ? $room->dateTo->format('d.m.Y') : null,
|
|
'@anzahl' => $room->totalCount,
|
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
|
];
|
|
}
|
|
|
|
// Add insurances with participant mappings
|
|
if (false === empty($bookingData->insurances)) {
|
|
$payload['versicherungen']['versicherung'] = [];
|
|
foreach ($bookingData->insurances as $insurance) {
|
|
$uniqueMapping = array_unique($insurance->mapping);
|
|
if (count($uniqueMapping) > 0) {
|
|
$payload['versicherungen']['versicherung'][] = [
|
|
'@idversicherung' => $insurance->id,
|
|
'@anzahl' => count($uniqueMapping),
|
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds the pickup locations section of the payload.
|
|
*
|
|
* Only included in payload if there are actual pickup assignments for bus transportation.
|
|
*
|
|
* @param array $payload The payload array to modify
|
|
* @param Booking $bookingData The booking data object
|
|
*/
|
|
public function buildPickupPayload(array &$payload, Booking $bookingData): void
|
|
{
|
|
if (0 < count($bookingData->pickupsOutbound)) {
|
|
$payload['zustiege']['zustieg'] = [];
|
|
foreach ($bookingData->pickupsOutbound as $pickup) {
|
|
$uniqueMapping = array_unique($pickup->mapping);
|
|
$payload['zustiege']['zustieg'][] = [
|
|
'@idzustieg' => $pickup->id,
|
|
'@anzahl' => count($uniqueMapping),
|
|
'@zuordnung' => implode(',', array_map(fn ($index) => $index + 1, $uniqueMapping)),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Builds a create booking request payload.
|
|
*
|
|
* Generates the array payload structure for creating new bookings through the BusProNet API.
|
|
*
|
|
* @param BookingDto $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 buildCreatePayload(BookingDto $bookingDto, string $bookingType): array
|
|
{
|
|
$firstParticipant = $bookingDto->participants[0];
|
|
|
|
$payload = [
|
|
'buchungsart' => $bookingType,
|
|
'status' => $bookingDto->bookingStatus,
|
|
'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 ?? '',
|
|
];
|
|
|
|
// DO NOT include personId or addressId in create mode
|
|
// BPN will automatically match existing customers by exact personal data (name, DOB, address)
|
|
// Including IDs would prevent automatic matching and could cause data inconsistencies
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Determine if this is an inquiry booking for voucher handling
|
|
$isInquiryBooking = 'A' === $bookingDto->bookingStatus;
|
|
|
|
// Add participants
|
|
$payload['teilnehmerliste']['teilnehmer'] = [];
|
|
foreach ($bookingDto->participants as $index => $participant) {
|
|
$participantData = [
|
|
'@id' => $index + 1,
|
|
'status' => $this->statusRuleRegistry->evaluateStatus($participant),
|
|
'name' => $participant->lastName,
|
|
'vorname' => $participant->firstName,
|
|
'geschlecht' => $participant->gender ?? '',
|
|
'nationalitaet' => $participant->nationality ?? '',
|
|
];
|
|
|
|
// DO NOT include personId or addressId in create mode
|
|
// BPN will automatically match existing customers by exact personal data (name, DOB, address)
|
|
// Including IDs would prevent automatic matching and could cause data inconsistencies
|
|
|
|
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 body dimensions
|
|
if (null !== $participant->height) {
|
|
$participantData['sonstiges1'] = $participant->height;
|
|
}
|
|
if (null !== $participant->weight) {
|
|
$participantData['sonstiges2'] = $participant->weight;
|
|
}
|
|
if (null !== $participant->shoeSize) {
|
|
$participantData['sonstiges3'] = $participant->shoeSize;
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
// Add promotional voucher as aktionscode (excluded from inquiry bookings)
|
|
if (false === $isInquiryBooking) {
|
|
$promotionalCode = $this->mappingCollector->getPromoVoucherCodeForParticipant($participant);
|
|
if (null !== $promotionalCode) {
|
|
$participantData['aktionscode'] = $promotionalCode;
|
|
}
|
|
}
|
|
|
|
// Add goodwill voucher as participant-level einloesecode
|
|
// Goodwill vouchers are excluded from inquiry bookings (status='A') but included in final bookings (status='F')
|
|
// They can be sent in both validation requests (buchungsart=Anfrage) and commit requests (buchungsart=Buchung)
|
|
if (false === $isInquiryBooking) {
|
|
$goodwillCode = $this->mappingCollector->getGoodwillVoucherCodeForParticipant($participant);
|
|
if (null !== $goodwillCode) {
|
|
$participantData['einloesecode'] = $goodwillCode;
|
|
}
|
|
}
|
|
|
|
$payload['teilnehmerliste']['teilnehmer'][] = $participantData;
|
|
}
|
|
|
|
// Collect and group all services by ID with participant mappings
|
|
$serviceMap = $this->mappingCollector->collectServiceMappings($bookingDto);
|
|
$transportationMap = $this->mappingCollector->collectTransportationMappings($bookingDto);
|
|
$roomMap = $this->mappingCollector->collectRoomMappings($bookingDto);
|
|
$pickupMap = $this->mappingCollector->collectPickupMappings($bookingDto);
|
|
$insuranceMap = $this->mappingCollector->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 purchase vouchers (regular purchase vouchers, excluding goodwill vouchers)
|
|
// All vouchers (purchase, promotional, goodwill) are excluded from inquiry bookings
|
|
if (false === $isInquiryBooking) {
|
|
$purchaseVouchers = $this->mappingCollector->collectPurchaseVouchers($bookingDto);
|
|
if (false === empty($purchaseVouchers)) {
|
|
$payload['gutscheine']['gutschein'] = [];
|
|
foreach ($purchaseVouchers as $code) {
|
|
$payload['gutscheine']['gutschein'][] = [
|
|
'@einloesecode' => $code,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
*/
|
|
public 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) {
|
|
$uniqueParticipantIds = array_unique($participantIds);
|
|
$payload[$sectionKey][$itemKey][] = [
|
|
$idAttributeName => $serviceId,
|
|
'@anzahl' => count($uniqueParticipantIds),
|
|
'@zuordnung' => implode(',', $uniqueParticipantIds),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 BookingDto $bookingDto The booking data for accessing room details and quantities
|
|
*/
|
|
public function addRoomMappingsToPayload(array &$payload, array $roomMap, BookingDto $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->id] = $selection->quantity;
|
|
}
|
|
}
|
|
|
|
foreach ($roomMap as $roomId => $participantIds) {
|
|
$room = $availableRooms[$roomId] ?? null;
|
|
if (null === $room) {
|
|
continue;
|
|
}
|
|
|
|
$quantity = $roomQuantities[$roomId] ?? 1;
|
|
$uniqueParticipantIds = array_unique($participantIds);
|
|
|
|
$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(',', $uniqueParticipantIds),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds purchase vouchers to the payload.
|
|
*
|
|
* @param array $payload The payload array to modify
|
|
* @param BookingDto $bookingDto The booking data
|
|
*/
|
|
public function addPurchaseVouchersToPayload(array &$payload, BookingDto $bookingDto): void
|
|
{
|
|
$purchaseVouchers = $this->mappingCollector->collectPurchaseVouchers($bookingDto);
|
|
if (false === empty($purchaseVouchers)) {
|
|
$payload['gutscheine']['gutschein'] = [];
|
|
foreach ($purchaseVouchers as $code) {
|
|
$payload['gutscheine']['gutschein'][] = [
|
|
'@einloesecode' => $code,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|