$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' => count($uniqueMapping), '@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'] = []; foreach ($roomMap as $roomId => $participantIds) { $room = $availableRooms[$roomId] ?? null; if (null === $room) { continue; } $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' => count($uniqueParticipantIds), '@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, ]; } } } }