timeout = (int) config('accounting.fx_api.timeout', 15); } /** * @param array $currencies * @return array currency => rate (1 unit of currency = X base) */ public function fetchRatesToBase(string $baseCurrency, array $currencies, ?string $provider = null, ?string $apiKey = null): array { $base = strtoupper($baseCurrency); $payload = $this->fetchPayload($base, $provider, $apiKey); if (empty($payload['rates']) || empty($payload['base'])) { return []; } $apiBase = strtoupper((string) $payload['base']); $apiRates = $payload['rates']; $result = []; foreach ($currencies as $currency) { $currency = strtoupper($currency); if ($currency === $base) { continue; } $rate = $this->convertToBaseRate($currency, $base, $apiBase, $apiRates); if ($rate !== null && $rate > 0) { $result[$currency] = $rate; } } return $result; } protected function fetchPayload(string $baseCurrency, ?string $provider, ?string $apiKey): array { $provider = $provider ?: config('accounting.fx_api.default_provider', 'open_er_api'); try { if ($provider === 'exchangerate_api_v6') { if (empty($apiKey)) { return []; } $url = 'https://v6.exchangerate-api.com/v6/'.$apiKey.'/latest/'.$baseCurrency; $response = Http::timeout($this->timeout)->get($url); if (! $response->successful()) { Log::warning('FX API v6 failed', ['status' => $response->status(), 'body' => $response->body()]); return []; } $json = $response->json(); return [ 'base' => data_get($json, 'base_code', $baseCurrency), 'rates' => (array) data_get($json, 'conversion_rates', []), ]; } if ($provider === 'iran_market' || $provider === 'cbi_ir') { return $this->fetchIranMarketPayload(); } if ($provider === 'exchangerate_api_v4') { $url = 'https://api.exchangerate-api.com/v4/latest/'.$baseCurrency; $response = Http::timeout($this->timeout)->get($url); if (! $response->successful()) { Log::warning('FX API v4 failed', ['status' => $response->status()]); return []; } $json = $response->json(); return [ 'base' => data_get($json, 'base', $baseCurrency), 'rates' => (array) data_get($json, 'rates', []), ]; } $url = 'https://open.er-api.com/v6/latest/'.$baseCurrency; $response = Http::timeout($this->timeout)->get($url); if (! $response->successful()) { Log::warning('open.er-api failed', ['status' => $response->status()]); return []; } $json = $response->json(); if (data_get($json, 'result') !== 'success') { return []; } return [ 'base' => data_get($json, 'base_code', $baseCurrency), 'rates' => (array) data_get($json, 'rates', []), ]; } catch (\Throwable $e) { Log::error('FX API request failed: '.$e->getMessage()); return []; } } /** * @param array $apiRates */ protected function convertToBaseRate(string $currency, string $targetBase, string $apiBase, array $apiRates): ?float { if ($apiBase === $targetBase) { if (empty($apiRates[$currency])) { return null; } return 1 / (float) $apiRates[$currency]; } if (empty($apiRates[$currency]) || empty($apiRates[$targetBase])) { return null; } return (float) $apiRates[$targetBase] / (float) $apiRates[$currency]; } protected function fetchIranMarketPayload(): array { $response = Http::timeout($this->timeout) ->withHeaders(['User-Agent' => 'UltimatePOS-Accounting/1.0']) ->get('https://call2.tgju.org/ajax.json'); if (! $response->successful()) { Log::warning('Iran market FX API failed', ['status' => $response->status()]); return []; } $current = (array) data_get($response->json(), 'current', []); $map = [ 'USD' => 'price_dollar_rl', 'EUR' => 'price_euro', 'GBP' => 'price_gbp', 'AED' => 'price_derham', ]; $rates = []; foreach ($map as $code => $key) { $raw = data_get($current, $key.'.p'); if (empty($raw)) { continue; } $priceInIrr = (float) str_replace([',', ' '], '', (string) $raw); if ($priceInIrr > 0) { $rates[$code] = 1 / $priceInIrr; } } if (empty($rates)) { return []; } return [ 'base' => 'IRR', 'rates' => $rates, ]; } /** * @return array{market: array, official: array} */ public function fetchIranDualRates(): array { $response = Http::timeout($this->timeout) ->withHeaders(['User-Agent' => 'UltimatePOS-Accounting/1.0']) ->get('https://call2.tgju.org/ajax.json'); if (! $response->successful()) { return ['market' => [], 'official' => []]; } $current = (array) data_get($response->json(), 'current', []); $marketMap = [ 'USD' => 'price_dollar_rl', 'EUR' => 'price_euro', 'GBP' => 'price_gbp', 'AED' => 'price_derham', ]; $officialMap = [ 'USD' => 'sana_dollar_rl', 'EUR' => 'sana_euro', 'GBP' => 'sana_gbp', 'AED' => 'sana_derham', ]; return [ 'market' => $this->parseIranPriceMap($current, $marketMap), 'official' => $this->parseIranPriceMap($current, $officialMap), ]; } /** * @param array $map * @return array */ protected function parseIranPriceMap(array $current, array $map): array { $rates = []; foreach ($map as $code => $key) { $raw = data_get($current, $key.'.p'); if (empty($raw)) { continue; } $priceInIrr = (float) str_replace([',', ' '], '', (string) $raw); if ($priceInIrr > 0) { $rates[$code] = $priceInIrr; } } return $rates; } }