From c90b93556a1809814bc318ce0f84868446cc1a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Wed, 14 Jan 2026 16:57:17 +0100 Subject: [PATCH] feat: cli command to download travel xml data from buspro api --- src/BusProNet/ApiClient.php | 72 ++++++++++++++++- src/Command/BpnFetchTravelCommand.php | 110 ++++++++++++++++++++++++++ 2 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 src/Command/BpnFetchTravelCommand.php diff --git a/src/BusProNet/ApiClient.php b/src/BusProNet/ApiClient.php index 14821c8..446603d 100644 --- a/src/BusProNet/ApiClient.php +++ b/src/BusProNet/ApiClient.php @@ -401,6 +401,30 @@ class ApiClient return $this->sendRequest(static::TYPE_PRODUCT_DATA, $data, ['hotelId' => $hotelId]); } + /** + * Fetches raw XML travel data from the BusProNet API. + * + * Returns the unprocessed XML response for direct storage in the XML export directory. + * The response format matches the XML export structure from BusPro. + * + * @param int $travelId The travel product ID to fetch + * + * @return string The raw XML response + * + * @throws ApiClientException If the API request fails + */ + public function getTravelDataXml(int $travelId): string + { + $data = [ + 'user' => $this->config['bpn_username'], + 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], static::TYPE_PRODUCT_DATA), + 'satz' => ['@typ' => static::TYPE_PRODUCT_DATA], + 'idprodukt' => $travelId, + ]; + + return $this->sendRequestRaw($data); + } + /** * @throws ApiClientException */ @@ -582,6 +606,14 @@ class ApiClient return $this->executeWithRetry(fn () => $this->doSendRequest($type, $data, $additionalArgs, $debug), $type); } + /** + * @throws ApiClientException + */ + private function sendRequestRaw(array $data): string + { + return $this->executeWithRetry(fn () => $this->doSendRequestRaw($data)); + } + /** * Executes an operation with automatic retry on immediate connection close. * @@ -680,6 +712,44 @@ class ApiClient throw new ApiClientException('Unexpected response received from API'); } + /** + * @throws ApiClientException + * @throws ImmediateConnectionCloseException + */ + private function doSendRequestRaw(array $data): string + { + $requestId = $this->getRequestId(); + + $body = $this->serializer->serialize($data, 'xml', [ + XmlEncoder::ROOT_NODE_NAME => 'anfrage', + XmlEncoder::ENCODING => 'UTF-8', + ]); + + if (true === $this->config['debug']) { + $this->dumpXmlToFile('request', $requestId, $body); + } + + $socket = $this->connect(); + + $this->logger->info('Sending raw request to BPN API', [ + 'requestId' => $requestId, + 'type' => $data['satz']['@typ'], + 'port' => $this->selectedPort, + ]); + + $this->send($socket, $body); + $response = $this->receive($socket); + $this->disconnect($socket); + + $xml = substr($response, 10); + + if (true === $this->config['debug']) { + $this->dumpXmlToFile('response', $requestId, $xml); + } + + return $xml; + } + private function dumpXmlToFile(string $type, string $requestId, string $body): void { try { @@ -693,7 +763,7 @@ class ApiClient $baseId = $this->requestStack->getMainRequest()?->attributes->get('request_id') ?? date(DATE_ATOM); - return $baseId . '_' . ++$this->requestCounter; + return $baseId.'_'.++$this->requestCounter; } private function createKey(string $username, string $password, string $type): string diff --git a/src/Command/BpnFetchTravelCommand.php b/src/Command/BpnFetchTravelCommand.php new file mode 100644 index 0000000..adcaeb5 --- /dev/null +++ b/src/Command/BpnFetchTravelCommand.php @@ -0,0 +1,110 @@ +addArgument('travel-id', InputArgument::REQUIRED, 'The travel product ID to fetch') + ->addOption('filename', 'f', InputOption::VALUE_REQUIRED, 'Output filename (default: Ziel_{travel-id}.xml)') + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Fetch and display info without saving'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $travelId = (int) $input->getArgument('travel-id'); + $filename = $input->getOption('filename') ?? sprintf('Ziel_%d.xml', $travelId); + $dryRun = $input->getOption('dry-run'); + + $io->text(sprintf('Fetching travel data for product ID %d...', $travelId)); + + try { + $xml = $this->apiClient->getTravelDataXml($travelId); + } catch (ApiClientException $e) { + $io->error(sprintf('API request failed: %s', $e->getMessage())); + $this->logger->error('Failed to fetch travel data from API', [ + 'travelId' => $travelId, + 'error' => $e->getMessage(), + ]); + + return Command::FAILURE; + } + + $io->text(sprintf('Received %d bytes of XML data', strlen($xml))); + + if ($dryRun) { + $io->note('Dry-run mode: not saving to file'); + $io->text($xml); + + return Command::SUCCESS; + } + + try { + $this->xmlExport->write($filename, $xml); + } catch (FilesystemException $e) { + $io->error(sprintf('Failed to write file: %s', $e->getMessage())); + $this->logger->error('Failed to write travel XML to file', [ + 'travelId' => $travelId, + 'filename' => $filename, + 'error' => $e->getMessage(), + ]); + + return Command::FAILURE; + } + + $this->invalidateCaches(); + + $io->success(sprintf('Saved to %s and invalidated travel mapping cache', $filename)); + $this->logger->info('Travel data fetched and saved', [ + 'travelId' => $travelId, + 'filename' => $filename, + 'bytes' => strlen($xml), + ]); + + return Command::SUCCESS; + } + + private function invalidateCaches(): void + { + foreach (self::CACHE_KEYS_TO_INVALIDATE as $key) { + $this->cache->delete($key); + } + } +}