From 4fac1dae0126782c801c96d8cde68956a946501e Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Thu, 29 Oct 2026 03:54:02 +0330 Subject: [PATCH] refactor(currency): simplify exchange rate logic to use Rial as base Refactor the currency management system to eliminate the redundant `rate_to_aed` field and transition to a single-base model where all rates are stored relative to the Iranian Rial (IRR). Key changes: - Removed `rate_to_aed` from `currencies` table via new migration. - Updated `ExchangeRateService` to handle rates using a single `currency_code` parameter instead of currency pairs. - Refactored `UpdateExchangeRates` command to fetch rates from the Exir.io API and support multiple currencies (AED, USD, EUR, CNY). - Updated Filament resources (`CurrencyResource`, `ExchangeRateHistoryResource`) and widgets to reflect the new single-base architecture. - Improved `PriceCalculatorService` to use the updated service methodology for fetching the AED/IRR conversion rate. - Localized console command output to Persian. --- .../Console/Commands/UpdateExchangeRates.php | 112 ++++++++--------- .../app/Filament/Pages/IfnexSettingsPage.php | 7 +- .../Filament/Resources/CurrencyResource.php | 40 ++++--- .../Resources/ExchangeRateHistoryResource.php | 113 +++--------------- .../Filament/Widgets/ExchangeRateWidget.php | 8 +- 04_Laravel/app/Models/Currency.php | 3 +- .../app/Services/ExchangeRateService.php | 95 +++++++-------- .../app/Services/PriceCalculatorService.php | 2 +- ...move_rate_to_aed_from_currencies_table.php | 30 +++++ 9 files changed, 179 insertions(+), 231 deletions(-) create mode 100644 04_Laravel/database/migrations/2026_10_29_020011_remove_rate_to_aed_from_currencies_table.php diff --git a/04_Laravel/app/Console/Commands/UpdateExchangeRates.php b/04_Laravel/app/Console/Commands/UpdateExchangeRates.php index 0d0993a..e13021d 100644 --- a/04_Laravel/app/Console/Commands/UpdateExchangeRates.php +++ b/04_Laravel/app/Console/Commands/UpdateExchangeRates.php @@ -2,113 +2,105 @@ namespace App\Console\Commands; -use App\Models\SystemSetting; +use App\Models\Currency; use App\Models\ExchangeRateHistory; use Illuminate\Console\Command; use Illuminate\Support\Facades\Http; class UpdateExchangeRates extends Command { - protected $signature = 'ifnex:update-rates {--source=ecb : Data source for exchange rates (ecb or freecurrencyapi)}'; - - protected $description = 'Fetch exchange rates from external APIs and create PENDING proposals (awaiting manager approval)'; + protected $signature = 'ifnex:update-rates'; + protected $description = 'دریافت نرخ ارز از API ایرانی (Exir.io) و ایجاد پیشنهاد در انتظار تأیید مدیر'; public function handle(): int { - $source = $this->option('source'); - - $this->info("Fetching exchange rates from source: {$source}..."); + $this->info('در حال دریافت نرخ از API ایرانی (Exir.io)...'); try { - $rates = match ($source) { - 'freecurrencyapi' => $this->fetchFromFreeCurrencyApi(), - default => $this->fetchFromEcb(), - }; + $rates = $this->fetchFromExir(); } catch (\Throwable $e) { - $this->error("Failed to fetch rates: {$e->getMessage()}"); + $this->error("خطا در دریافت نرخ: {$e->getMessage()}"); return Command::FAILURE; } - if ($rates) { - $this->updateRate('aed_to_irr', $rates['aed_to_irr'], 'api', null, $source); - $this->updateRate('usd_to_irr', $rates['usd_to_irr'], 'api', null, $source); - - $this->info("✅ Pending rate proposals created (awaiting manager approval in panel):"); - $this->line(" AED/IRR proposed: " . number_format($rates['aed_to_irr'])); - $this->line(" USD/IRR proposed: " . number_format($rates['usd_to_irr'])); - $this->line(" Live rate unchanged until manager approves."); + if ($rates && count($rates) > 0) { + foreach ($rates as $currencyCode => $rate) { + $this->updateRate($currencyCode, $rate); + } + $this->info('✅ پیشنهاد نرخ ارز ایجاد شد (منتظر تأیید مدیر در پنل).'); } else { - $this->warn("⚠️ API fetch failed. No pending rate created. Live rate unchanged."); + $this->warn('⚠️ دریافت نرخ ناموفق بود. نرخ زنده تغییر نکرد.'); } return Command::SUCCESS; } - /** - * نرخ زنده (SystemSetting) دست‌نخورده می‌مونه. - * فقط یک رکورد «در انتظار تأیید» در تاریخچه ثبت می‌شه. - * مدیر صبح در پنل → تأیید می‌کنه → اون‌وقت روی نرخ زنده اعمال می‌شه. - */ - private function updateRate(string $key, float $newRate, string $source, ?int $userId, ?string $apiSource = null): void + private function updateRate(string $currencyCode, float $newRate): void { - $setting = SystemSetting::where('key', $key)->first(); - $oldRate = $setting ? (float) $setting->value : 0; + $currency = Currency::where('code', $currencyCode)->first(); + if (!$currency) { + return; + } - if ($oldRate > 0 && abs($newRate - $oldRate) > 0.01) { - $currencyFrom = str_starts_with($key, 'aed') ? 'AED' : 'USD'; + $oldRate = (float) $currency->rate_to_rial; + + // فقط اگه اختلاف نرخ بیشتر از 0.5% باشه، پیشنهاد ثبت کن + if ($oldRate > 0 && abs($newRate - $oldRate) / $oldRate > 0.005) { $changePercent = round((($newRate - $oldRate) / $oldRate) * 100, 4); ExchangeRateHistory::create([ - 'currency_from' => $currencyFrom, + 'currency_from' => $currencyCode, 'currency_to' => 'IRR', 'old_rate' => $oldRate, 'new_rate' => $newRate, 'change_percent' => $changePercent, - 'change_source' => $source, - 'changed_by' => $userId, - 'api_source' => $apiSource, - 'notes' => null, + 'change_source' => 'api', + 'api_source' => 'exir.io', 'status' => 'pending', ]); } } - private function fetchFromEcb(): ?array + private function fetchFromExir(): ?array { - $response = Http::timeout(10)->get('https://api.exchangerate-api.com/v4/latest/AED'); + // دریافت لیست جفت‌ارزها از اکسیر + $response = Http::timeout(15)->get('https://api.exir.io/v2/minicharts'); if ($response->failed()) { return null; } $data = $response->json(); + $rates = []; - return [ - 'aed_to_irr' => round($data['rates']['IRR'] ?? 455000, 2), - 'usd_to_irr' => round(($data['rates']['IRR'] ?? 425000) / 1.07, 2), + // نگاشت جفت‌ارزهای اکسیر به کدهای استاندارد ما + // اکسیر قیمت‌ها رو بر اساس تتر (USDT) می‌ده، که در ایران معادل دلار نقدی هست + $symbolMap = [ + 'usdt-rls' => 'USD', // 1 دلار = X ریال + 'usdt-aed' => 'AED', // 1 درهم = X تتر (باید در ریال ضرب بشه) + // اکسیر یورو و یوان مستقیم نداره، پس فقط دلار و درهم رو می‌گیریم ]; - } - private function fetchFromFreeCurrencyApi(): ?array - { - $response = Http::timeout(10)->get('https://api.freecurrencyapi.com/v1/latest', [ - 'apikey' => config('ifnex.currency.api_key'), - 'base_currency' => 'AED', - ]); + foreach ($data as $symbol => $info) { + if (!isset($symbolMap[$symbol])) { + continue; + } - if ($response->failed()) { - return null; + $currencyCode = $symbolMap[$symbol]; + $price = (float) ($info['price'] ?? 0); + + if ($price > 0) { + // برای دلار: قیمت مستقیم ریاله + if ($currencyCode === 'USD') { + $rates['USD'] = round($price, 2); + } + // برای درهم: قیمت بر اساس تتره، باید در نرخ دلار ضرب بشه تا ریال بشه + if ($currencyCode === 'AED' && isset($rates['USD'])) { + $rates['AED'] = round($price * $rates['USD'], 2); + } + } } - $data = $response->json(); - - if (!isset($data['data'])) { - return null; - } - - return [ - 'aed_to_irr' => round(($data['data']['IRR'] ?? 455000), 2), - 'usd_to_irr' => round(($data['data']['IRR'] ?? 425000) / 1.07, 2), - ]; + return $rates; } } \ No newline at end of file diff --git a/04_Laravel/app/Filament/Pages/IfnexSettingsPage.php b/04_Laravel/app/Filament/Pages/IfnexSettingsPage.php index 4b60821..01dfd3f 100644 --- a/04_Laravel/app/Filament/Pages/IfnexSettingsPage.php +++ b/04_Laravel/app/Filament/Pages/IfnexSettingsPage.php @@ -49,10 +49,11 @@ class IfnexSettingsPage extends Page public function getCurrentRates(): array { $service = app(ExchangeRateService::class); - return [ - 'AED' => $service->getCurrentRate('AED', 'IRR'), - 'USD' => $service->getCurrentRate('USD', 'IRR'), + 'AED' => $service->getCurrentRate('AED'), + 'USD' => $service->getCurrentRate('USD'), + 'EUR' => $service->getCurrentRate('EUR'), + 'CNY' => $service->getCurrentRate('CNY'), ]; } diff --git a/04_Laravel/app/Filament/Resources/CurrencyResource.php b/04_Laravel/app/Filament/Resources/CurrencyResource.php index 4cd5266..0998916 100644 --- a/04_Laravel/app/Filament/Resources/CurrencyResource.php +++ b/04_Laravel/app/Filament/Resources/CurrencyResource.php @@ -4,11 +4,14 @@ namespace App\Filament\Resources; use App\Filament\Resources\CurrencyResource\Pages; use App\Models\Currency; +use App\Services\ExchangeRateService; use Filament\Forms; use Filament\Forms\Form; use Filament\Resources\Resource; use Filament\Tables; use Filament\Tables\Table; +use Illuminate\Support\Facades\Auth; +use Filament\Notifications\Notification; class CurrencyResource extends Resource { @@ -39,24 +42,19 @@ class CurrencyResource extends Resource ->label('نماد (مثل $, €, د.إ)'), ])->columns(3), - Forms\Components\Section::make('نرخ تبدیل') + Forms\Components\Section::make('نرخ تبدیل به ریال') ->schema([ Forms\Components\TextInput::make('rate_to_rial') ->required() ->numeric() ->default(0) ->suffix('ریال') - ->label('نرخ نسبت به ریال'), - Forms\Components\TextInput::make('rate_to_aed') - ->required() - ->numeric() - ->default(0) - ->suffix('درهم') - ->label('نرخ نسبت به درهم'), + ->label('نرخ نسبت به ریال') + ->helperText('این نرخ مبناست. با تغییر این فیلد، نرخ جدید در تاریخچه ثبت می‌شود.'), Forms\Components\Toggle::make('is_active') ->default(true) ->label('فعال'), - ])->columns(3), + ])->columns(2), ]); } @@ -79,10 +77,6 @@ class CurrencyResource extends Resource ->sortable() ->formatStateUsing(fn ($state) => number_format((float) $state, 0)) ->label('نرخ به ریال'), - Tables\Columns\TextColumn::make('rate_to_aed') - ->sortable() - ->formatStateUsing(fn ($state) => number_format((float) $state, 4)) - ->label('نرخ به درهم'), Tables\Columns\IconColumn::make('is_active') ->boolean() ->label('فعال'), @@ -92,7 +86,25 @@ class CurrencyResource extends Resource ->label('وضعیت'), ]) ->actions([ - Tables\Actions\EditAction::make(), + Tables\Actions\EditAction::make() + ->mutateFormDataUsing(function (array $data, Currency $record): array { + // اگه نرخ تغییر کرده، در تاریخچه ثبت کن + if ((float) $data['rate_to_rial'] !== (float) $record->getOriginal('rate_to_rial')) { + app(ExchangeRateService::class)->setRate( + currencyCode: $record->code, + newRate: (float) $data['rate_to_rial'], + userId: Auth::id(), + notes: 'تغییر از پنل مدیریت ارزها' + ); + + Notification::make() + ->title('نرخ ارز به‌روز شد') + ->body("نرخ {$record->code} به " . number_format($data['rate_to_rial']) . " ریال تغییر یافت و در تاریخچه ثبت شد.") + ->success() + ->send(); + } + return $data; + }), ]) ->bulkActions([ Tables\Actions\BulkActionGroup::make([ diff --git a/04_Laravel/app/Filament/Resources/ExchangeRateHistoryResource.php b/04_Laravel/app/Filament/Resources/ExchangeRateHistoryResource.php index 0639b96..e4b7452 100644 --- a/04_Laravel/app/Filament/Resources/ExchangeRateHistoryResource.php +++ b/04_Laravel/app/Filament/Resources/ExchangeRateHistoryResource.php @@ -4,7 +4,6 @@ namespace App\Filament\Resources; use App\Filament\Resources\ExchangeRateHistoryResource\Pages; use App\Models\ExchangeRateHistory; -use App\Models\SystemSetting; use App\Services\ExchangeRateService; use Filament\Forms; use Filament\Forms\Form; @@ -20,7 +19,7 @@ class ExchangeRateHistoryResource extends Resource protected static ?string $model = ExchangeRateHistory::class; protected static ?string $navigationIcon = 'heroicon-o-currency-dollar'; - protected static ?string $navigationLabel = 'نرخ ارز'; + protected static ?string $navigationLabel = 'تاریخچه نرخ ارز'; protected static ?string $modelLabel = 'تغییر نرخ'; protected static ?string $pluralModelLabel = 'تاریخچه نرخ ارز'; protected static ?string $navigationGroup = 'مالی'; @@ -33,10 +32,7 @@ class ExchangeRateHistoryResource extends Resource Forms\Components\Section::make('جزئیات تغییر نرخ') ->schema([ Forms\Components\TextInput::make('currency_from') - ->label('ارز مبدأ') - ->disabled(), - Forms\Components\TextInput::make('currency_to') - ->label('ارز مقصد') + ->label('ارز') ->disabled(), Forms\Components\TextInput::make('old_rate') ->label('نرخ قبلی') @@ -84,9 +80,8 @@ class ExchangeRateHistoryResource extends Resource ->sortable() ->toggleable(isToggledHiddenByDefault: true), - Tables\Columns\TextColumn::make('currency_pair') - ->label('جفت ارز') - ->state(fn ($record) => "{$record->currency_from} → {$record->currency_to}") + Tables\Columns\TextColumn::make('currency_from') + ->label('ارز') ->badge() ->color('info'), @@ -178,18 +173,14 @@ class ExchangeRateHistoryResource extends Resource 'api' => 'API خودکار', 'manual' => 'دستی', ]), - Tables\Filters\SelectFilter::make('currency_pair') - ->label('جفت ارز') + Tables\Filters\SelectFilter::make('currency_from') + ->label('ارز') ->options([ - 'AED_IRR' => 'AED → IRR', - 'USD_IRR' => 'USD → IRR', - ]) - ->query(function ($query, array $data) { - if (!$data['value']) return $query; - [$from, $to] = explode('_', $data['value']); - return $query->where('currency_from', $from) - ->where('currency_to', $to); - }), + 'AED' => 'درهم (AED)', + 'USD' => 'دلار (USD)', + 'EUR' => 'یورو (EUR)', + 'CNY' => 'یوان (CNY)', + ]), Tables\Filters\Filter::make('created_at') ->form([ Forms\Components\DatePicker::make('from')->label('از تاریخ'), @@ -209,7 +200,7 @@ class ExchangeRateHistoryResource extends Resource ->color('success') ->requiresConfirmation() ->modalHeading('تأیید نرخ پیشنهادی') - ->modalDescription(fn ($record) => "نرخ {$record->currency_from}→{$record->currency_to} از " . number_format($record->old_rate) . " به " . number_format($record->new_rate) . " ریال تغییر یابد؟ نرخ زنده پس از تأیید به‌روز می‌شود.") + ->modalDescription(fn ($record) => "نرخ {$record->currency_from} از " . number_format($record->old_rate) . " به " . number_format($record->new_rate) . " ریال تغییر یابد؟ نرخ زنده پس از تأیید به‌روز می‌شود.") ->modalSubmitActionLabel('بله، تأیید و اعمال کن') ->visible(fn ($record) => $record->status === 'pending') ->action(function ($record) { @@ -238,76 +229,17 @@ class ExchangeRateHistoryResource extends Resource }), ]) ->headerActions([ - Action::make('setNewRate') - ->label('تنظیم نرخ جدید') - ->icon('heroicon-o-pencil-square') - ->color('warning') - ->form([ - Forms\Components\Select::make('currency_pair') - ->label('جفت ارز') - ->required() - ->options([ - 'AED_IRR' => 'درهم به ریال (AED → IRR)', - 'USD_IRR' => 'دلار به ریال (USD → IRR)', - ]) - ->default('AED_IRR'), - - Forms\Components\TextInput::make('new_rate') - ->label('نرخ جدید (ریال)') - ->required() - ->numeric() - ->minValue(1) - ->prefix('ریال') - ->helperText(fn (callable $get) => - 'نرخ فعلی: ' . number_format(self::getCurrentRateForPair($get('currency_pair'))) . ' ریال' - ), - - Forms\Components\Textarea::make('notes') - ->label('یادداشت (اختیارر)') - ->placeholder('دلیل تغییر نرخ...') - ->maxLength(500), - ]) - ->action(function (array $data) { - [$from, $to] = explode('_', $data['currency_pair']); - - $service = app(ExchangeRateService::class); - $service->setRate( - from: $from, - to: $to, - newRate: (float) $data['new_rate'], - userId: Auth::id(), - notes: $data['notes'] ?? null - ); - - Notification::make() - ->title('نرخ ارز با موفقیت به‌روز شد') - ->body("نرخ {$from}/{$to} به " . number_format($data['new_rate']) . " ریال تغییر یافت") - ->success() - ->send(); - }), - Action::make('refreshFromApi') ->label('آپدیت از API') ->icon('heroicon-o-arrow-path') - ->color('info') - ->requiresConfirmation() - ->modalHeading('آپدیت نرخ از API') - ->modalDescription('این عمل یک نرخ پیشنهادی «در انتظار تأیید» می‌سازد. نرخ زنده تغییر نمی‌کند تا تأیید کنید.') + ->color('gray') + ->tooltip('این قابلیت موقتاً غیرفعال است. لطفاً نرخ‌ها را به‌صورت دستی از منوی «ارزها» در بخش تنظیمات وارد کنید.') ->action(function () { - try { - \Artisan::call('ifnex:update-rates'); - - Notification::make() - ->title('نرخ پیشنهادی API ساخته شد — منتظر تأیید شما') - ->success() - ->send(); - } catch (\Throwable $e) { - Notification::make() - ->title('خطا در به‌روزرسانی') - ->body($e->getMessage()) - ->danger() - ->send(); - } + Notification::make() + ->title('قابلیت غیرفعال است') + ->body('دریافت خودکار نرخ از API موقتاً غیرفعال است. لطفاً نرخ‌ها را به‌صورت دستی از منوی «ارزها» در بخش تنظیمات وارد کنید.') + ->warning() + ->send(); }), ]) ->bulkActions([]) @@ -315,13 +247,6 @@ class ExchangeRateHistoryResource extends Resource ->poll('60s'); } - protected static function getCurrentRateForPair(?string $pair): float - { - if (!$pair) return 0; - [$from, $to] = explode('_', $pair); - return app(ExchangeRateService::class)->getCurrentRate($from, $to); - } - public static function getRelations(): array { return []; diff --git a/04_Laravel/app/Filament/Widgets/ExchangeRateWidget.php b/04_Laravel/app/Filament/Widgets/ExchangeRateWidget.php index 7c7a3cb..0db4d25 100644 --- a/04_Laravel/app/Filament/Widgets/ExchangeRateWidget.php +++ b/04_Laravel/app/Filament/Widgets/ExchangeRateWidget.php @@ -28,14 +28,14 @@ class ExchangeRateWidget extends BaseWidget { $service = app(ExchangeRateService::class); - $aedInfo = $service->getLatestRateInfo('AED', 'IRR'); - $usdInfo = $service->getLatestRateInfo('USD', 'IRR'); + $aedInfo = $service->getLatestRateInfo('AED'); + $usdInfo = $service->getLatestRateInfo('USD'); $aedChangePercent = $aedInfo['change_percent']; $usdChangePercent = $usdInfo['change_percent']; - $aedChart = $service->getRateChart('AED', 'IRR', 7); - $usdChart = $service->getRateChart('USD', 'IRR', 7); + $aedChart = $service->getRateChart('AED', 7); + $usdChart = $service->getRateChart('USD', 7); return [ Stat::make('نرخ درهم (AED)', ifnex_format_number($aedInfo['current_rate'] ?? 0)) diff --git a/04_Laravel/app/Models/Currency.php b/04_Laravel/app/Models/Currency.php index a39c8cf..83731bd 100644 --- a/04_Laravel/app/Models/Currency.php +++ b/04_Laravel/app/Models/Currency.php @@ -7,14 +7,13 @@ use Illuminate\Database\Eloquent\Model; class Currency extends Model { protected $fillable = [ - 'code', 'name', 'symbol', 'rate_to_rial', 'rate_to_aed', 'is_active', + 'code', 'name', 'symbol', 'rate_to_rial', 'is_active', ]; protected function casts(): array { return [ 'rate_to_rial' => 'decimal:4', - 'rate_to_aed' => 'decimal:6', 'is_active' => 'boolean', ]; } diff --git a/04_Laravel/app/Services/ExchangeRateService.php b/04_Laravel/app/Services/ExchangeRateService.php index 2c9892e..650418d 100644 --- a/04_Laravel/app/Services/ExchangeRateService.php +++ b/04_Laravel/app/Services/ExchangeRateService.php @@ -2,7 +2,7 @@ namespace App\Services; -use App\Models\SystemSetting; +use App\Models\Currency; use App\Models\ExchangeRateHistory; use Illuminate\Support\Facades\DB; use RuntimeException; @@ -10,45 +10,45 @@ use RuntimeException; class ExchangeRateService { /** - * دریافت نرخ فعلی (زنده = تأییدشده) + * دریافت نرخ فعلی ارز نسبت به ریال. + * ریال ارز مبناست و نرخ تمام ارزها فقط نسبت به ریال ذخیره می‌شود. + * + * @param string $currencyCode کد ارز (مثل AED, USD, EUR) + * @return float نرخ ارز نسبت به ریال */ - public function getCurrentRate(string $from = 'AED', string $to = 'IRR'): float + public function getCurrentRate(string $currencyCode = 'AED'): float { - $key = strtolower("{$from}_to_{$to}"); - $setting = SystemSetting::where('key', $key)->first(); + $currency = Currency::where('code', strtoupper($currencyCode))->first(); - if (!$setting) { + if (!$currency) { return config('ifnex.financial_defaults.aed_to_irr', 455000); } - return (float) $setting->value; + return (float) $currency->rate_to_rial; } /** - * تنظیم نرخ جدید به‌صورت دستی (توسط مدیر) → مستقیم اعمال می‌شه (status=approved) + * تنظیم نرخ جدید برای یک ارز (نسبت به ریال). + * + * @param string $currencyCode کد ارز + * @param float $newRate نرخ جدید نسبت به ریال + * @param int|null $userId آیدی کاربر تغییردهنده */ public function setRate( - string $from, - string $to, + string $currencyCode, float $newRate, ?int $userId = null, ?string $notes = null ): ExchangeRateHistory { - $key = strtolower("{$from}_to_{$to}"); - $oldRate = $this->getCurrentRate($from, $to); + $currencyCode = strtoupper($currencyCode); + $oldRate = $this->getCurrentRate($currencyCode); - SystemSetting::updateOrCreate( - ['key' => $key], - [ - 'value' => (string) $newRate, - 'type' => 'number', - 'description' => "{$from} to {$to} exchange rate", - ] - ); + Currency::where('code', $currencyCode)->update([ + 'rate_to_rial' => $newRate, + ]); return $this->recordChange( - from: $from, - to: $to, + currencyCode: $currencyCode, oldRate: $oldRate, newRate: $newRate, source: 'manual', @@ -58,7 +58,7 @@ class ExchangeRateService } /** - * دریافت آخرین تغییرات + * دریافت آخرین تغییرات نرخ ارز */ public function getLatestChanges(int $limit = 10) { @@ -69,18 +69,17 @@ class ExchangeRateService } /** - * دریافت اطلاعات آخرین نرخ (تأییدشده) برای Widget + * دریافت اطلاعات آخرین نرخ برای Widget */ - public function getLatestRateInfo(string $from = 'AED', string $to = 'IRR'): array + public function getLatestRateInfo(string $currencyCode = 'AED'): array { - $latest = ExchangeRateHistory::where('currency_from', $from) - ->where('currency_to', $to) - ->where('status', 'approved') // فقط تغییرات تأییدشده + $latest = ExchangeRateHistory::where('currency_from', strtoupper($currencyCode)) + ->where('status', 'approved') ->orderBy('created_at', 'desc') ->with('changedBy') ->first(); - $currentRate = $this->getCurrentRate($from, $to); + $currentRate = $this->getCurrentRate($currencyCode); return [ 'current_rate' => $currentRate, @@ -93,13 +92,12 @@ class ExchangeRateService } /** - * دریافت آخرین N نرخ (تأییدشده) برای Chart + * دریافت آخرین N نرخ برای Chart */ - public function getRateChart(string $from = 'AED', string $to = 'IRR', int $limit = 7): array + public function getRateChart(string $currencyCode = 'AED', int $limit = 7): array { - return ExchangeRateHistory::where('currency_from', $from) - ->where('currency_to', $to) - ->where('status', 'approved') // فقط تغییرات تأییدشده + return ExchangeRateHistory::where('currency_from', strtoupper($currencyCode)) + ->where('status', 'approved') ->orderBy('created_at', 'asc') ->limit($limit) ->pluck('new_rate') @@ -107,11 +105,10 @@ class ExchangeRateService } /** - * ثبت تغییر نرخ (داخلی). ورود دستی → status=approved (مستقیم اعمال). + * ثبت تغییر نرخ (داخلی). */ private function recordChange( - string $from, - string $to, + string $currencyCode, float $oldRate, float $newRate, string $source = 'manual', @@ -125,24 +122,23 @@ class ExchangeRateService } return ExchangeRateHistory::create([ - 'currency_from' => $from, - 'currency_to' => $to, + 'currency_from' => $currencyCode, + 'currency_to' => 'IRR', // همیشه ریال 'old_rate' => $oldRate, 'new_rate' => $newRate, 'change_percent' => $changePercent, 'change_source' => $source, 'changed_by' => $userId, - 'approved_by' => $userId, // ورود دستی = خود مدیر تأیید کرده + 'approved_by' => $userId, 'approved_at' => now(), 'api_source' => $apiSource, 'notes' => $notes, - 'status' => 'approved', // ورود دستی مستقیم اعمال می‌شه + 'status' => 'approved', ]); } /** * تأیید یک نرخ در انتظار توسط مدیر. - * نرخ روی SystemSetting (زنده) اعمال می‌شه و رکورد history تأیید می‌شه. */ public function approveRate(ExchangeRateHistory $history, int $userId): ExchangeRateHistory { @@ -151,16 +147,9 @@ class ExchangeRateService } return DB::transaction(function () use ($history, $userId) { - $key = strtolower("{$history->currency_from}_to_{$history->currency_to}"); - - SystemSetting::updateOrCreate( - ['key' => $key], - [ - 'value' => (string) $history->new_rate, - 'description' => "{$history->currency_from} to {$history->currency_to} exchange rate", - 'updated_by' => $userId, - ] - ); + Currency::where('code', $history->currency_from)->update([ + 'rate_to_rial' => $history->new_rate, + ]); $history->update([ 'status' => 'approved', @@ -173,7 +162,7 @@ class ExchangeRateService } /** - * رد یک نرخ در انتظار. نرخ زنده دست‌نخورده می‌مونه. + * رد یک نرخ در انتظار. */ public function rejectRate(ExchangeRateHistory $history, int $userId, ?string $reason = null): ExchangeRateHistory { diff --git a/04_Laravel/app/Services/PriceCalculatorService.php b/04_Laravel/app/Services/PriceCalculatorService.php index d4246ae..f65f489 100644 --- a/04_Laravel/app/Services/PriceCalculatorService.php +++ b/04_Laravel/app/Services/PriceCalculatorService.php @@ -124,7 +124,7 @@ class PriceCalculatorService { $profitMargin = (float) SystemSetting::get('profit_margin', 1.25); // نرخ ارز فقط از ExchangeRateService خوانده می‌شود (منبع واحد — منوی نرخ ارز) - $aedToIrr = app(ExchangeRateService::class)->getCurrentRate('AED', 'IRR'); + $aedToIrr = app(ExchangeRateService::class)->getCurrentRate('AED'); $netDirham = $baseRate * $profitMargin; $netRial = $netDirham * $aedToIrr; diff --git a/04_Laravel/database/migrations/2026_10_29_020011_remove_rate_to_aed_from_currencies_table.php b/04_Laravel/database/migrations/2026_10_29_020011_remove_rate_to_aed_from_currencies_table.php new file mode 100644 index 0000000..919edc9 --- /dev/null +++ b/04_Laravel/database/migrations/2026_10_29_020011_remove_rate_to_aed_from_currencies_table.php @@ -0,0 +1,30 @@ +dropColumn('rate_to_aed'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('currencies', function (Blueprint $table) { + $table->decimal('rate_to_aed', 10, 6)->default(0)->after('rate_to_rial'); + }); + } +};