ultimatepos/Modules/Accounting/Services/ExternalExchangeRateProvider.php

242 lines
7.1 KiB
PHP

<?php
namespace Modules\Accounting\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class ExternalExchangeRateProvider
{
protected int $timeout;
public function __construct()
{
$this->timeout = (int) config('accounting.fx_api.timeout', 15);
}
/**
* @param array<int, string> $currencies
* @return array<string, float> 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<string, float> $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<string, float>, official: array<string, float>}
*/
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<string, string> $map
* @return array<string, float>
*/
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;
}
}