242 lines
7.1 KiB
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;
|
|
}
|
|
}
|