From cea88963daf166926b848f2df8c6d9120176ef29 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Wed, 29 Apr 2026 01:14:29 +0200 Subject: [PATCH] adasd --- modules/fx-rates/module.json | 6 +- .../fx-rates/src/Domain/FxRatesService.php | 268 ++++++++++++++---- 2 files changed, 217 insertions(+), 57 deletions(-) diff --git a/modules/fx-rates/module.json b/modules/fx-rates/module.json index a1ac54a..22b82da 100644 --- a/modules/fx-rates/module.json +++ b/modules/fx-rates/module.json @@ -13,9 +13,9 @@ { "name": "db.schema", "label": "DB Schema", "type": "text", "required": false }, { "name": "db.user", "label": "DB User", "type": "text", "required": false }, { "name": "db.password", "label": "DB Passwort", "type": "password", "required": false }, - { "name": "provider", "label": "FX Provider", "type": "text", "required": false, "help": "Aktuell getestet mit currencyapi." }, - { "name": "api_url", "label": "FX API URL", "type": "text", "required": false }, - { "name": "currencies_url", "label": "FX Currencies URL", "type": "text", "required": false }, + { "name": "provider", "label": "FX Provider", "type": "text", "required": false, "help": "Unterstuetzt legacy currencyapi.net und currencyapi.com v3." }, + { "name": "api_url", "label": "FX API URL", "type": "text", "required": false, "help": "Nur die Basis-URL eintragen, z.B. https://api.currencyapi.com oder https://currencyapi.net." }, + { "name": "currencies_url", "label": "FX Currencies URL", "type": "text", "required": false, "help": "Nur die Basis-URL fuer den Waehrungskatalog, z.B. https://api.currencyapi.com." }, { "name": "api_key", "label": "FX API Key", "type": "password", "required": false }, { "name": "timeout_sec", "label": "Timeout (Sek.)", "type": "number", "required": false }, { "name": "cache_ttl_sec", "label": "Datei-Cache TTL (Sek.)", "type": "number", "required": false }, diff --git a/modules/fx-rates/src/Domain/FxRatesService.php b/modules/fx-rates/src/Domain/FxRatesService.php index c57e727..e020198 100644 --- a/modules/fx-rates/src/Domain/FxRatesService.php +++ b/modules/fx-rates/src/Domain/FxRatesService.php @@ -342,35 +342,15 @@ final class FxRatesService private function fetchLatestPayload(string $baseCurrency, ?array $currencies = null): array { - if (!function_exists('curl_init')) { - throw new \RuntimeException('curl_init ist nicht verfuegbar.'); - } - - $url = $this->buildLatestUrl($baseCurrency); - if ($url === null) { + $request = $this->buildLatestRequest($baseCurrency, $currencies); + if ($request === null) { throw new \RuntimeException('FX-URL oder API-Key fehlt.'); } - $ch = curl_init(); - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_TIMEOUT => $this->timeoutSeconds(), - CURLOPT_HTTPHEADER => ['Accept: application/json'], - ]); - $response = curl_exec($ch); - $httpStatus = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); - $curlError = curl_error($ch); - curl_close($ch); + $payload = $this->requestJson($request['url'], $request['headers'] ?? [], 'FX-Kurse konnten nicht geladen werden.'); - if ($response === false || $curlError !== '' || $httpStatus >= 400) { - throw new \RuntimeException('FX-Kurse konnten nicht geladen werden.'); - } - - $payload = json_decode((string) $response, true); - if (!is_array($payload)) { - throw new \RuntimeException('FX-Antwort ist kein gueltiges JSON.'); + if ($this->isCurrencyApiCom()) { + return $this->normalizeCurrencyApiComLatestPayload($payload, $baseCurrency, $currencies); } $rates = []; @@ -416,20 +396,107 @@ final class FxRatesService private function fetchCurrenciesPayload(): array { - if (!function_exists('curl_init')) { - throw new \RuntimeException('curl_init ist nicht verfuegbar.'); - } - - $apiKey = $this->apiKey(); - if ($apiKey === '') { + $request = $this->buildCurrenciesRequest(); + if ($request === null) { throw new \RuntimeException('FX-API-Key fehlt.'); } - $url = sprintf( - '%s/api/v2/currencies?output=json&key=%s', - $this->currenciesApiUrl(), - rawurlencode($apiKey) - ); + $payload = $this->requestJson($request['url'], $request['headers'] ?? [], 'Waehrungskatalog konnte nicht geladen werden.'); + + if ($this->isCurrencyApiCom()) { + return $this->normalizeCurrencyApiComCurrenciesPayload($payload); + } + + if (($payload['valid'] ?? false) !== true || !is_array($payload['currencies'] ?? null)) { + throw new \RuntimeException($this->extractProviderError($payload, 'Waehrungskatalog konnte nicht geladen werden.')); + } + + return $payload; + } + + private function buildLatestRequest(string $baseCurrency, ?array $currencies = null): ?array + { + $apiKey = $this->apiKey(); + if ($this->isCurrencyApiCom()) { + if ($apiKey === '') { + return null; + } + + $query = ['base_currency=' . rawurlencode($baseCurrency)]; + $normalizedCurrencies = []; + foreach ($currencies ?? [] as $currency) { + $currency = $this->normalizeCurrency((string) $currency); + if ($currency !== '' && $currency !== $baseCurrency) { + $normalizedCurrencies[] = $currency; + } + } + if ($normalizedCurrencies !== []) { + $query[] = 'currencies=' . rawurlencode(implode(',', array_values(array_unique($normalizedCurrencies)))); + } + + return [ + 'url' => $this->apiUrl() . '/v3/latest?' . implode('&', $query), + 'headers' => [ + 'Accept: application/json', + 'apikey: ' . $apiKey, + ], + ]; + } + + if ($this->provider() === 'currencyapi') { + if ($apiKey === '') { + return null; + } + + return [ + 'url' => sprintf( + '%s/api/v2/rates?base=%s&output=json&key=%s', + $this->apiUrl(), + rawurlencode($baseCurrency), + rawurlencode($apiKey) + ), + 'headers' => ['Accept: application/json'], + ]; + } + + return [ + 'url' => sprintf('%s/latest?base=%s', $this->apiUrl(), rawurlencode($baseCurrency)), + 'headers' => ['Accept: application/json'], + ]; + } + + private function buildCurrenciesRequest(): ?array + { + $apiKey = $this->apiKey(); + if ($apiKey === '') { + return null; + } + + if ($this->isCurrencyApiCom()) { + return [ + 'url' => $this->currenciesApiUrl() . '/v3/currencies', + 'headers' => [ + 'Accept: application/json', + 'apikey: ' . $apiKey, + ], + ]; + } + + return [ + 'url' => sprintf( + '%s/api/v2/currencies?output=json&key=%s', + $this->currenciesApiUrl(), + rawurlencode($apiKey) + ), + 'headers' => ['Accept: application/json'], + ]; + } + + private function requestJson(string $url, array $headers, string $fallbackError): array + { + if (!function_exists('curl_init')) { + throw new \RuntimeException('curl_init ist nicht verfuegbar.'); + } $ch = curl_init(); curl_setopt_array($ch, [ @@ -437,7 +504,7 @@ final class FxRatesService CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => $this->timeoutSeconds(), - CURLOPT_HTTPHEADER => ['Accept: application/json'], + CURLOPT_HTTPHEADER => $headers !== [] ? $headers : ['Accept: application/json'], ]); $response = curl_exec($ch); $httpStatus = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); @@ -445,38 +512,119 @@ final class FxRatesService curl_close($ch); if ($response === false || $curlError !== '' || $httpStatus >= 400) { - throw new \RuntimeException('Waehrungskatalog konnte nicht geladen werden.'); + $payload = is_string($response) ? json_decode($response, true) : null; + throw new \RuntimeException($this->extractProviderError(is_array($payload) ? $payload : [], $fallbackError)); } $payload = json_decode((string) $response, true); - if (!is_array($payload) || ($payload['valid'] ?? false) !== true) { - throw new \RuntimeException($this->extractProviderError(is_array($payload) ? $payload : [], 'Waehrungskatalog konnte nicht geladen werden.')); + if (!is_array($payload)) { + throw new \RuntimeException('FX-Antwort ist kein gueltiges JSON.'); } return $payload; } - private function buildLatestUrl(string $baseCurrency): ?string + private function normalizeCurrencyApiComLatestPayload(array $payload, string $baseCurrency, ?array $currencies = null): array { - $apiKey = $this->apiKey(); - if ($this->provider() === 'currencyapi') { - if ($apiKey === '') { - return null; - } - - return sprintf( - '%s/api/v2/rates?base=%s&output=json&key=%s', - $this->apiUrl(), - rawurlencode($baseCurrency), - rawurlencode($apiKey) - ); + $rawRates = is_array($payload['data'] ?? null) ? $payload['data'] : null; + if ($rawRates === null) { + throw new \RuntimeException($this->extractProviderError($payload, 'FX-Kurse konnten nicht geladen werden.')); } - return sprintf('%s/latest?base=%s', $this->apiUrl(), rawurlencode($baseCurrency)); + $filter = []; + foreach ($currencies ?? [] as $currency) { + $currency = $this->normalizeCurrency((string) $currency); + if ($currency !== '' && $currency !== $baseCurrency) { + $filter[$currency] = true; + } + } + + $rates = []; + foreach ($rawRates as $code => $rateData) { + $code = $this->normalizeCurrency((string) $code); + if ($code === '' || $code === $baseCurrency) { + continue; + } + if ($filter !== [] && !isset($filter[$code])) { + continue; + } + + $value = is_array($rateData) ? ($rateData['value'] ?? null) : null; + if (!is_numeric($value)) { + continue; + } + + $rates[$code] = (float) $value; + } + + return [ + 'base' => $baseCurrency, + 'date' => $payload['meta']['last_updated_at'] ?? null, + 'rates' => $rates, + ]; + } + + private function normalizeCurrencyApiComCurrenciesPayload(array $payload): array + { + $rawCurrencies = is_array($payload['data'] ?? null) ? $payload['data'] : null; + if ($rawCurrencies === null) { + throw new \RuntimeException($this->extractProviderError($payload, 'Waehrungskatalog konnte nicht geladen werden.')); + } + + $currencies = []; + foreach ($rawCurrencies as $code => $currencyData) { + $normalizedCode = $this->normalizeCurrency((string) $code); + if ($normalizedCode === '') { + continue; + } + + $name = ''; + if (is_array($currencyData)) { + $name = trim((string) ($currencyData['name'] ?? $currencyData['name_plural'] ?? $currencyData['code'] ?? '')); + } + + if ($name === '') { + continue; + } + + $currencies[$normalizedCode] = $name; + } + + return [ + 'valid' => true, + 'currencies' => $currencies, + ]; } private function extractProviderError(array $payload, string $fallback): string { + $error = $payload['error'] ?? null; + if (is_array($error)) { + foreach (['message', 'info', 'code'] as $field) { + $value = $error[$field] ?? null; + if (is_string($value) && trim($value) !== '') { + return trim($value); + } + } + } + + $errors = $payload['errors'] ?? null; + if (is_array($errors)) { + foreach ($errors as $entry) { + if (is_string($entry) && trim($entry) !== '') { + return trim($entry); + } + if (is_array($entry)) { + foreach (['message', 'detail', 'title'] as $field) { + $value = $entry[$field] ?? null; + if (is_string($value) && trim($value) !== '') { + return trim($value); + } + } + } + } + } + foreach (['error', 'message', 'msg'] as $field) { $value = $payload[$field] ?? null; if (is_string($value) && trim($value) !== '') { @@ -539,6 +687,18 @@ final class FxRatesService return rtrim((string) ($this->settings['currencies_url'] ?? $this->apiUrl()), '/'); } + private function isCurrencyApiCom(): bool + { + foreach ([$this->apiUrl(), $this->currenciesApiUrl()] as $url) { + $host = strtolower((string) parse_url($url, PHP_URL_HOST)); + if ($host !== '' && str_contains($host, 'currencyapi.com')) { + return true; + } + } + + return false; + } + private function apiKey(): string { return trim((string) ($this->settings['api_key'] ?? ''));