option('source'); $autoApprove = (bool) $this->option('auto-approve'); $threshold = (float) $this->option('threshold'); if (! in_array($source, ['exir', 'freecurrencyapi', 'all'], true)) { $this->error("مقدار --source نامعتبر است. مجاز: exir, freecurrencyapi, all"); return Command::INVALID; } $this->warnIfCurrenciesMissing(); $rates = []; if ($source === 'exir' || $source === 'all') { $exirRates = $this->fetchFromExir(); $rates = array_merge($rates, $exirRates); // ثبت کدهایی که از Exir آمدند تا FreeCurrencyAPI آنها را overwrite نکند foreach (array_keys($exirRates) as $code) { $this->ratesFromExir[$code] = true; } } if ($source === 'freecurrencyapi' || $source === 'all') { $rates = array_merge($rates, $this->fetchFromFreeCurrencyApi()); } if (empty($rates)) { $this->error('❌ هیچ نرخی دریافت نشد. بررسی لاگ برای جزئیات.'); Log::critical('ifnex:import-fx-rates — هیچ نرخی دریافت نشد.', [ 'source' => $source, ]); return Command::FAILURE; } $created = 0; $approved = 0; foreach ($rates as $code => $rate) { [$c, $a] = $this->upsertRate($code, $rate, $autoApprove, $threshold); $created += $c; $approved += $a; } $this->newLine(); $this->info("✅ دریافت نرخ انجام شد. کل ارزها: " . count($rates)); $this->line(" رکورد جدید در exchange_rate_history: {$created}"); $this->line(" نرخ‌های تأییدشده در currencies: {$approved}"); if (! $autoApprove) { $this->newLine(); $this->warn('ℹ️ نرخ‌ها به‌صورت pending ثبت شدند. برای اعمال فوری:'); $this->line(' php artisan ifnex:import-fx-rates --source=all --auto-approve'); $this->line(' یا از پنل ادمین ← تاریخچه نرخ ارز ← تأیید.'); } return Command::SUCCESS; } // ─── Exir.io ──────────────────────────────────────────── private function fetchFromExir(): array { $url = (string) (config('ifnex.exir.api_url') ?? 'https://api.exir.io/v2/minicharts'); $timeout = (int) (config('ifnex.exir.timeout') ?? 15); $this->line("→ در حال دریافت از Exir.io: {$url}"); try { $response = Http::timeout($timeout)->get($url); } catch (Throwable $e) { $this->error("خطا در اتصال به Exir.io: {$e->getMessage()}"); Log::critical('ifnex:import-fx-rates — Exir.io unreachable', [ 'url' => $url, 'error' => $e->getMessage(), ]); return []; } if ($response->failed()) { $this->error("Exir.io پاسخ ناموفق داد: HTTP {$response->status()}"); Log::critical('ifnex:import-fx-rates — Exir.io HTTP failure', [ 'url' => $url, 'status' => $response->status(), ]); return []; } $data = $response->json(); $rates = []; // نگاشت جفت‌ارزهای اکسیر به کدهای استاندارد ما $symbolMap = [ 'usdt-rls' => 'USD', // 1 دلار = X ریال (قیمت مستقیم ریالی) 'usdt-aed' => 'AED', // 1 درهم = X تتر → باید در نرخ دلار ضرب شود ]; foreach ($data as $symbol => $info) { if (! isset($symbolMap[$symbol])) { continue; } $code = $symbolMap[$symbol]; $price = (float) ($info['price'] ?? 0); if ($price <= 0) { continue; } if ($code === 'USD') { $rates['USD'] = round($price, 2); } // برای درهم: قیمت بر اساس تتر است؛ باید در نرخ دلار ضرب شود تا ریالی شود if ($code === 'AED' && isset($rates['USD'])) { $rates['AED'] = round($price * $rates['USD'], 2); } } $this->line(' Exir.io دریافت شد: ' . json_encode($rates, JSON_UNESCAPED_UNICODE)); return $rates; } // ─── FreeCurrencyAPI (EUR/CNY) ────────────────────────── private function fetchFromFreeCurrencyApi(): array { $apiKey = (string) config('ifnex.currency.api_key'); if ($apiKey === '' || $apiKey === 'your_api_key_here') { $this->warn(' CURRENCY_API_KEY تنظیم نشده — پرش از منبع freecurrencyapi.'); $this->line(' برای دریافت EUR/CNY لازم است: https://app.freecurrencyapi.com/'); return []; } $url = (string) config('ifnex.currency.api_url'); $base = (string) config('ifnex.currency.base_currency', 'USD'); $fullUrl = "{$url}?apikey={$apiKey}&base_currency={$base}¤cies=IRR,EUR,CNY"; $this->line("→ در حال دریافت از FreeCurrencyAPI: {$url}"); try { $response = Http::timeout(15)->get($fullUrl); } catch (Throwable $e) { $this->error("خطا در اتصال به FreeCurrencyAPI: {$e->getMessage()}"); Log::critical('ifnex:import-fx-rates — FreeCurrencyAPI unreachable', [ 'error' => $e->getMessage(), ]); return []; } if ($response->failed()) { $this->error("FreeCurrencyAPI پاسخ ناموفق داد: HTTP {$response->status()}"); Log::critical('ifnex:import-fx-rates — FreeCurrencyAPI HTTP failure', [ 'status' => $response->status(), ]); return []; } $data = $response->json('data', []); $rates = []; // تبدیل نسبت دلار به ریال: free_currency_api نرخ 1USD = X IRR می‌دهد if (isset($data['IRR']) && $data['IRR'] > 0) { $usdToIrr = (float) $data['IRR']; // USD را overwrite نکن اگر از Exir آمده — Exir در ایران دقیق‌تر است. // فقط اگه Exir نداشتیم، از اینجا پر کن. if (! isset($this->ratesFromExir['USD'])) { $rates['USD'] = round($usdToIrr, 2); } // EUR = usd_to_eur × usd_to_irr if (isset($data['EUR']) && $data['EUR'] > 0) { $rates['EUR'] = round(((float) $data['EUR']) * $usdToIrr, 2); } // CNY = usd_to_cny × usd_to_irr if (isset($data['CNY']) && $data['CNY'] > 0) { $rates['CNY'] = round(((float) $data['CNY']) * $usdToIrr, 2); } } $this->line(' FreeCurrencyAPI دریافت شد: ' . json_encode($rates, JSON_UNESCAPED_UNICODE)); return $rates; } // ─── Upsert نرخ ───────────────────────────────────────── /** * @return array{0:int,1:int} [records_created_in_history, records_approved_in_currencies] */ private function upsertRate(string $code, float $newRate, bool $autoApprove, float $threshold): array { $code = strtoupper($code); $currency = Currency::where('code', $code)->first(); if (! $currency) { $this->warn(" ارز {$code} در جدول currencies وجود ندارد — پرش."); Log::warning("ifnex:import-fx-rates — currency {$code} not found in table."); return [0, 0]; } $oldRate = (float) $currency->rate_to_rial; // اگر نرخ صفر است (اولین بار)، بدون بررسی threshold بنویس if ($oldRate <= 0) { return $this->applyApproved($currency, $oldRate, $newRate, $code, source: $this->currentSource($code)); } $changePct = abs($newRate - $oldRate) / $oldRate * 100; // اگر تغییر قابل‌توجه نیست (>0.5٪)، نادیده بگیر if ($changePct < self::CHANGE_THRESHOLD_PCT) { $this->line(" {$code}: تغییر {$changePct}٪ < آستانه — پرش."); return [0, 0]; } // اگر تغییر بیش از threshold auto-approve است، حتی با auto-approve فقط pending بساز if ($autoApprove && $changePct <= $threshold) { return $this->applyApproved($currency, $oldRate, $newRate, $code, source: $this->currentSource($code)); } // در غیر این صورت، فقط رکورد pending بساز ExchangeRateHistory::create([ 'currency_from' => $code, 'currency_to' => 'IRR', 'old_rate' => $oldRate, 'new_rate' => $newRate, 'change_percent' => round((($newRate - $oldRate) / $oldRate) * 100, 4), 'change_source' => 'api', 'api_source' => $this->currentSource($code), 'status' => 'pending', ]); $this->line(" {$code}: رکورد pending ساخته شد (تغییر {$changePct}٪)."); return [1, 0]; } private function applyApproved(Currency $currency, float $oldRate, float $newRate, string $code, string $source): array { $currency->update(['rate_to_rial' => $newRate]); ExchangeRateHistory::create([ 'currency_from' => $code, 'currency_to' => 'IRR', 'old_rate' => $oldRate, 'new_rate' => $newRate, 'change_percent' => $oldRate > 0 ? round((($newRate - $oldRate) / $oldRate) * 100, 4) : null, 'change_source' => 'api', 'api_source' => $source, 'approved_at' => now(), 'status' => 'approved', ]); $this->line(" {$code}: نرخ در currencies نوشته شد (auto-approved، منبع {$source})."); return [1, 1]; } private function currentSource(string $code): string { return in_array($code, ['USD', 'AED'], true) ? 'exir.io' : 'freecurrencyapi.com'; } // ─── هشدار در صورت خالی بودن جدول currencies ──────────── private function warnIfCurrenciesMissing(): void { $count = Currency::count(); if ($count === 0) { $this->warn('⚠️ جدول currencies خالی است! ابتدا اجرا کنید:'); $this->line(' php artisan db:seed --class=CurrencySeeder --force'); $this->newLine(); } } }