feat(exchange): implement exchange rate history tracking
Introduce a mechanism to track and audit changes in exchange rates. This includes: - Creating `ExchangeRateHistory` model and migration to store rate changes. - Implementing `ExchangeRateService` to encapsulate rate logic. - Updating `UpdateExchangeRates` command to record history when rates change. - Adding `ExchangeRateHistoryResource` to the Filament admin panel for monitoring.
This commit is contained in:
parent
a0bc871c8d
commit
ed5bae9997
@ -3,6 +3,7 @@
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\SystemSetting;
|
||||
use App\Models\ExchangeRateHistory;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
@ -10,7 +11,7 @@ class UpdateExchangeRates extends Command
|
||||
{
|
||||
protected $signature = 'ifnex:update-rates {--source=ecb : Data source for exchange rates (ecb or freecurrencyapi)}';
|
||||
|
||||
protected $description = 'Update exchange rates from external APIs';
|
||||
protected $description = 'Update exchange rates from external APIs and record history';
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
@ -25,28 +26,54 @@ class UpdateExchangeRates extends Command
|
||||
};
|
||||
} catch (\Throwable $e) {
|
||||
$this->error("Failed to fetch rates: {$e->getMessage()}");
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
if ($rates) {
|
||||
SystemSetting::where('key', 'aed_to_irr')->update(['value' => (string) $rates['aed_to_irr']]);
|
||||
SystemSetting::where('key', 'usd_to_irr')->update(['value' => (string) $rates['usd_to_irr']]);
|
||||
$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("Exchange rates updated successfully:");
|
||||
$this->line(" AED/IRR: {$rates['aed_to_irr']}");
|
||||
$this->line(" USD/IRR: {$rates['usd_to_irr']}");
|
||||
$this->info("✅ Exchange rates updated successfully:");
|
||||
$this->line(" AED/IRR: " . number_format($rates['aed_to_irr']));
|
||||
$this->line(" USD/IRR: " . number_format($rates['usd_to_irr']));
|
||||
} else {
|
||||
$this->warn("No rates were updated. Using fallback values.");
|
||||
$this->warn("⚠️ No rates were updated. Using fallback values.");
|
||||
$this->setFallbackRates();
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function updateRate(string $key, float $newRate, string $source, ?int $userId, ?string $apiSource = null): void
|
||||
{
|
||||
$setting = SystemSetting::where('key', $key)->first();
|
||||
$oldRate = $setting ? (float) $setting->value : 0;
|
||||
|
||||
// Update در system_settings
|
||||
SystemSetting::updateOrCreate(
|
||||
['key' => $key],
|
||||
['value' => (string) $newRate]
|
||||
);
|
||||
|
||||
// ثبت در تاریخچه فقط اگر تغییر کرده باشد
|
||||
if ($oldRate != $newRate && $oldRate > 0) {
|
||||
$currencyFrom = str_starts_with($key, 'aed') ? 'AED' : 'USD';
|
||||
|
||||
ExchangeRateHistory::recordChange(
|
||||
from: $currencyFrom,
|
||||
to: 'IRR',
|
||||
oldRate: $oldRate,
|
||||
newRate: $newRate,
|
||||
source: $source,
|
||||
userId: $userId,
|
||||
apiSource: $apiSource
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function fetchFromEcb(): ?array
|
||||
{
|
||||
$response = Http::get('https://api.exchangerate-api.com/v4/latest/AED');
|
||||
$response = Http::timeout(10)->get('https://api.exchangerate-api.com/v4/latest/AED');
|
||||
|
||||
if ($response->failed()) {
|
||||
return null;
|
||||
@ -56,14 +83,14 @@ class UpdateExchangeRates extends Command
|
||||
|
||||
return [
|
||||
'aed_to_irr' => round($data['rates']['IRR'] ?? 455000, 2),
|
||||
'usd_to_irr' => round($data['rates']['IRR'] ?? 425000, 2),
|
||||
'usd_to_irr' => round(($data['rates']['IRR'] ?? 425000) / 1.07, 2), // USD از AED محاسبه میشود
|
||||
];
|
||||
}
|
||||
|
||||
private function fetchFromFreeCurrencyApi(): ?array
|
||||
{
|
||||
$response = Http::get('https://free.currencyapi.com/api/v3/latest', [
|
||||
'apikey' => config('ifnex.currency_api_key'),
|
||||
$response = Http::timeout(10)->get('https://api.freecurrencyapi.com/v1/latest', [
|
||||
'apikey' => config('ifnex.currency.api_key'),
|
||||
'base_currency' => 'AED',
|
||||
]);
|
||||
|
||||
@ -78,21 +105,15 @@ class UpdateExchangeRates extends Command
|
||||
}
|
||||
|
||||
return [
|
||||
'aed_to_irr' => round(($data['data']['IRR']['value'] ?? 455000) * 100, 2),
|
||||
'usd_to_irr' => round(($data['data']['IRR']['value'] ?? 425000) * 100, 2),
|
||||
'aed_to_irr' => round(($data['data']['IRR'] ?? 455000), 2),
|
||||
'usd_to_irr' => round(($data['data']['IRR'] ?? 425000) / 1.07, 2),
|
||||
];
|
||||
}
|
||||
|
||||
private function setFallbackRates(): void
|
||||
{
|
||||
SystemSetting::updateOrCreate(
|
||||
['key' => 'aed_to_irr'],
|
||||
['value' => '455000', 'type' => 'number', 'description' => 'AED to IRR exchange rate']
|
||||
);
|
||||
SystemSetting::updateOrCreate(
|
||||
['key' => 'usd_to_irr'],
|
||||
['value' => '425000', 'type' => 'number', 'description' => 'USD to IRR exchange rate']
|
||||
);
|
||||
$this->updateRate('aed_to_irr', 455000, 'api', null, 'fallback');
|
||||
$this->updateRate('usd_to_irr', 425000, 'api', null, 'fallback');
|
||||
$this->info("Fallback rates set: AED/IRR=455000, USD/IRR=425000");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
use Filament\Resources\Resource;
|
||||
use Filament\Tables;
|
||||
use Filament\Tables\Table;
|
||||
use Filament\Tables\Actions\Action;
|
||||
use Filament\Notifications\Notification;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
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 $modelLabel = 'تغییر نرخ';
|
||||
protected static ?string $pluralModelLabel = 'تاریخچه نرخ ارز';
|
||||
protected static ?string $navigationGroup = 'مالی';
|
||||
protected static ?int $navigationSort = 3;
|
||||
|
||||
public static function form(Form $form): Form
|
||||
{
|
||||
return $form
|
||||
->schema([
|
||||
Forms\Components\Section::make('جزئیات تغییر نرخ')
|
||||
->schema([
|
||||
Forms\Components\TextInput::make('currency_from')
|
||||
->label('ارز مبدأ')
|
||||
->disabled(),
|
||||
Forms\Components\TextInput::make('currency_to')
|
||||
->label('ارز مقصد')
|
||||
->disabled(),
|
||||
Forms\Components\TextInput::make('old_rate')
|
||||
->label('نرخ قبلی')
|
||||
->disabled()
|
||||
->suffix('ریال'),
|
||||
Forms\Components\TextInput::make('new_rate')
|
||||
->label('نرخ جدید')
|
||||
->disabled()
|
||||
->suffix('ریال'),
|
||||
Forms\Components\TextInput::make('change_percent')
|
||||
->label('درصد تغییر')
|
||||
->disabled()
|
||||
->suffix('%'),
|
||||
Forms\Components\TextInput::make('change_source')
|
||||
->label('منبع تغییر')
|
||||
->disabled(),
|
||||
Forms\Components\Select::make('changed_by')
|
||||
->label('توسط')
|
||||
->relationship('changedBy', 'name')
|
||||
->disabled(),
|
||||
Forms\Components\Textarea::make('notes')
|
||||
->label('یادداشت')
|
||||
->disabled()
|
||||
->columnSpanFull(),
|
||||
])->columns(2),
|
||||
]);
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
return $table
|
||||
->columns([
|
||||
Tables\Columns\TextColumn::make('id')
|
||||
->label('شناسه')
|
||||
->sortable()
|
||||
->toggleable(isToggledHiddenByDefault: true),
|
||||
|
||||
Tables\Columns\TextColumn::make('currency_pair')
|
||||
->label('جفت ارز')
|
||||
->state(fn ($record) => "{$record->currency_from} → {$record->currency_to}")
|
||||
->badge()
|
||||
->color('info'),
|
||||
|
||||
Tables\Columns\TextColumn::make('old_rate')
|
||||
->label('نرخ قبلی')
|
||||
->numeric(0)
|
||||
->suffix(' ریال')
|
||||
->color('gray'),
|
||||
|
||||
Tables\Columns\TextColumn::make('new_rate')
|
||||
->label('نرخ جدید')
|
||||
->numeric(0)
|
||||
->suffix(' ریال')
|
||||
->weight('bold')
|
||||
->color('primary'),
|
||||
|
||||
Tables\Columns\TextColumn::make('change_percent')
|
||||
->label('تغییر')
|
||||
->state(fn ($record) => $record->change_percent !== null
|
||||
? number_format($record->change_percent, 2) . '%'
|
||||
: '—')
|
||||
->badge()
|
||||
->color(fn ($record) => match(true) {
|
||||
$record->change_percent > 0 => 'danger', // افزایش قیمت = قرمز
|
||||
$record->change_percent < 0 => 'success', // کاهش قیمت = سبز
|
||||
default => 'gray',
|
||||
})
|
||||
->icon(fn ($record) => match(true) {
|
||||
$record->change_percent > 0 => 'heroicon-m-arrow-trending-up',
|
||||
$record->change_percent < 0 => 'heroicon-m-arrow-trending-down',
|
||||
default => null,
|
||||
}),
|
||||
|
||||
Tables\Columns\TextColumn::make('change_source')
|
||||
->label('منبع')
|
||||
->badge()
|
||||
->formatStateUsing(fn ($state) => $state === 'api' ? 'API خودکار' : 'دستی')
|
||||
->color(fn ($state) => $state === 'api' ? 'info' : 'warning')
|
||||
->icon(fn ($state) => $state === 'api'
|
||||
? 'heroicon-m-cog-6-tooth'
|
||||
: 'heroicon-m-pencil-square'),
|
||||
|
||||
Tables\Columns\TextColumn::make('changedBy.name')
|
||||
->label('توسط')
|
||||
->state(fn ($record) => $record->changedBy?->name ?? ($record->change_source === 'api' ? 'سیستم' : '—'))
|
||||
->icon('heroicon-m-user'),
|
||||
|
||||
Tables\Columns\TextColumn::make('created_at')
|
||||
->label('تاریخ تغییر')
|
||||
->dateTime('Y/m/d H:i')
|
||||
->sortable(),
|
||||
])
|
||||
->filters([
|
||||
Tables\Filters\SelectFilter::make('change_source')
|
||||
->label('منبع')
|
||||
->options([
|
||||
'api' => 'API خودکار',
|
||||
'manual' => 'دستی',
|
||||
]),
|
||||
Tables\Filters\SelectFilter::make('currency_pair')
|
||||
->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);
|
||||
}),
|
||||
Tables\Filters\Filter::make('created_at')
|
||||
->form([
|
||||
Forms\Components\DatePicker::make('from')->label('از تاریخ'),
|
||||
Forms\Components\DatePicker::make('until')->label('تا تاریخ'),
|
||||
])
|
||||
->query(function ($query, array $data) {
|
||||
return $query
|
||||
->when($data['from'], fn ($q, $d) => $q->whereDate('created_at', '>=', $d))
|
||||
->when($data['until'], fn ($q, $d) => $q->whereDate('created_at', '<=', $d));
|
||||
}),
|
||||
])
|
||||
->actions([
|
||||
Tables\Actions\ViewAction::make()->label('جزئیات'),
|
||||
])
|
||||
->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();
|
||||
}),
|
||||
|
||||
// 🔄 دکمه آپدیت از API
|
||||
Action::make('refreshFromApi')
|
||||
->label('آپدیت از API')
|
||||
->icon('heroicon-o-arrow-path')
|
||||
->color('info')
|
||||
->requiresConfirmation()
|
||||
->modalHeading('آپدیت نرخ از API')
|
||||
->modalDescription('آیا مطمئن هستید که میخواهید نرخ ارز را از API بهروزرسانی کنید؟')
|
||||
->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();
|
||||
}
|
||||
}),
|
||||
])
|
||||
->bulkActions([])
|
||||
->defaultSort('created_at', 'desc')
|
||||
->poll('60s'); // هر ۶۰ ثانیه خودکار refresh میشود
|
||||
}
|
||||
|
||||
// متد کمکی برای نمایش نرخ فعلی در فرم
|
||||
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 [];
|
||||
}
|
||||
|
||||
public static function getPages(): array
|
||||
{
|
||||
return [
|
||||
'index' => Pages\ListExchangeRateHistories::route('/'),
|
||||
'view' => Pages\ViewExchangeRateHistory::route('/{record}'),
|
||||
];
|
||||
}
|
||||
|
||||
// فقط admin و super_admin دسترسی دارند
|
||||
public static function canViewAny(): bool
|
||||
{
|
||||
$user = Auth::user();
|
||||
return $user && $user->hasAnyRole(['super_admin', 'admin']);
|
||||
}
|
||||
|
||||
public static function canCreate(): bool
|
||||
{
|
||||
return false; // ایجاد دستی از فرم ممکن نیست، فقط از Action
|
||||
}
|
||||
|
||||
public static function canEdit($record): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function canDelete($record): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ExchangeRateHistoryResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ExchangeRateHistoryResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\CreateRecord;
|
||||
|
||||
class CreateExchangeRateHistory extends CreateRecord
|
||||
{
|
||||
protected static string $resource = ExchangeRateHistoryResource::class;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ExchangeRateHistoryResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ExchangeRateHistoryResource;
|
||||
use Filament\Actions;
|
||||
use Filament\Resources\Pages\EditRecord;
|
||||
|
||||
class EditExchangeRateHistory extends EditRecord
|
||||
{
|
||||
protected static string $resource = ExchangeRateHistoryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [
|
||||
Actions\DeleteAction::make(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ExchangeRateHistoryResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ExchangeRateHistoryResource;
|
||||
use Filament\Resources\Pages\ListRecords;
|
||||
|
||||
class ListExchangeRateHistories extends ListRecords
|
||||
{
|
||||
protected static string $resource = ExchangeRateHistoryResource::class;
|
||||
|
||||
protected function getHeaderActions(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Filament\Resources\ExchangeRateHistoryResource\Pages;
|
||||
|
||||
use App\Filament\Resources\ExchangeRateHistoryResource;
|
||||
use Filament\Resources\Pages\ViewRecord;
|
||||
|
||||
class ViewExchangeRateHistory extends ViewRecord
|
||||
{
|
||||
protected static string $resource = ExchangeRateHistoryResource::class;
|
||||
}
|
||||
73
04_Laravel/app/Models/ExchangeRateHistory.php
Normal file
73
04_Laravel/app/Models/ExchangeRateHistory.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ExchangeRateHistory extends Model
|
||||
{
|
||||
protected $table = 'exchange_rate_history';
|
||||
|
||||
protected $fillable = [
|
||||
'currency_from',
|
||||
'currency_to',
|
||||
'old_rate',
|
||||
'new_rate',
|
||||
'change_percent',
|
||||
'change_source',
|
||||
'changed_by',
|
||||
'api_source',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'old_rate' => 'decimal:2',
|
||||
'new_rate' => 'decimal:2',
|
||||
'change_percent' => 'decimal:4',
|
||||
];
|
||||
|
||||
public function changedBy(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'changed_by');
|
||||
}
|
||||
|
||||
// Helper برای محاسبه درصد تغییر
|
||||
public static function calculateChangePercent($old, $new): ?float
|
||||
{
|
||||
if (!$old || $old == 0) return null;
|
||||
return round((($new - $old) / $old) * 100, 4);
|
||||
}
|
||||
|
||||
// Helper برای ثبت تغییر
|
||||
public static function recordChange(
|
||||
string $from,
|
||||
string $to,
|
||||
float $oldRate,
|
||||
float $newRate,
|
||||
string $source = 'manual',
|
||||
?int $userId = null,
|
||||
?string $apiSource = null,
|
||||
?string $notes = null
|
||||
): self {
|
||||
return self::create([
|
||||
'currency_from' => $from,
|
||||
'currency_to' => $to,
|
||||
'old_rate' => $oldRate,
|
||||
'new_rate' => $newRate,
|
||||
'change_percent' => self::calculateChangePercent($oldRate, $newRate),
|
||||
'change_source' => $source,
|
||||
'changed_by' => $userId,
|
||||
'api_source' => $apiSource,
|
||||
'notes' => $notes,
|
||||
]);
|
||||
}
|
||||
|
||||
// Scope برای آخرین تغییر
|
||||
public function scopeLatest($query, string $from = 'AED', string $to = 'IRR')
|
||||
{
|
||||
return $query->where('currency_from', $from)
|
||||
->where('currency_to', $to)
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
}
|
||||
88
04_Laravel/app/Services/ExchangeRateService.php
Normal file
88
04_Laravel/app/Services/ExchangeRateService.php
Normal file
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\SystemSetting;
|
||||
use App\Models\ExchangeRateHistory;
|
||||
|
||||
class ExchangeRateService
|
||||
{
|
||||
/**
|
||||
* دریافت نرخ فعلی
|
||||
*/
|
||||
public function getCurrentRate(string $from = 'AED', string $to = 'IRR'): float
|
||||
{
|
||||
$key = strtolower("{$from}_to_{$to}");
|
||||
$setting = SystemSetting::where('key', $key)->first();
|
||||
|
||||
if (!$setting) {
|
||||
return config('ifnex.financial_defaults.aed_to_irr', 455000);
|
||||
}
|
||||
|
||||
return (float) $setting->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* تنظیم نرخ جدید (توسط کاربر)
|
||||
*/
|
||||
public function setRate(
|
||||
string $from,
|
||||
string $to,
|
||||
float $newRate,
|
||||
?int $userId = null,
|
||||
?string $notes = null
|
||||
): ExchangeRateHistory {
|
||||
$key = strtolower("{$from}_to_{$to}");
|
||||
$oldRate = $this->getCurrentRate($from, $to);
|
||||
|
||||
// Update system_settings
|
||||
SystemSetting::updateOrCreate(
|
||||
['key' => $key],
|
||||
[
|
||||
'value' => (string) $newRate,
|
||||
'type' => 'number',
|
||||
'description' => "{$from} to {$to} exchange rate",
|
||||
]
|
||||
);
|
||||
|
||||
// ثبت در تاریخچه
|
||||
return ExchangeRateHistory::recordChange(
|
||||
from: $from,
|
||||
to: $to,
|
||||
oldRate: $oldRate,
|
||||
newRate: $newRate,
|
||||
source: 'manual',
|
||||
userId: $userId,
|
||||
notes: $notes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* دریافت آخرین تغییرات
|
||||
*/
|
||||
public function getLatestChanges(int $limit = 10)
|
||||
{
|
||||
return ExchangeRateHistory::with('changedBy')
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* دریافت اطلاعات آخرین نرخ برای Widget
|
||||
*/
|
||||
public function getLatestRateInfo(string $from = 'AED', string $to = 'IRR'): array
|
||||
{
|
||||
$latest = ExchangeRateHistory::latest($from, $to)->with('changedBy')->first();
|
||||
$currentRate = $this->getCurrentRate($from, $to);
|
||||
|
||||
return [
|
||||
'current_rate' => $currentRate,
|
||||
'last_change' => $latest,
|
||||
'last_updated' => $latest?->created_at,
|
||||
'changed_by' => $latest?->changedBy?->name,
|
||||
'source' => $latest?->change_source,
|
||||
'change_percent' => $latest?->change_percent,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('exchange_rate_history', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('currency_from', 10)->default('AED');
|
||||
$table->string('currency_to', 10)->default('IRR');
|
||||
$table->decimal('old_rate', 15, 2)->nullable();
|
||||
$table->decimal('new_rate', 15, 2);
|
||||
$table->decimal('change_percent', 8, 4)->nullable();
|
||||
$table->enum('change_source', ['api', 'manual'])->default('manual');
|
||||
$table->foreignId('changed_by')->nullable()->constrained('users')->nullOnDelete();
|
||||
$table->string('api_source')->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index(['currency_from', 'currency_to', 'created_at']);
|
||||
$table->index('changed_by');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('exchange_rate_history');
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user