243 lines
8.2 KiB
PHP
243 lines
8.2 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Currency;
|
|
use App\Models\ExchangeRateHistory;
|
|
use Illuminate\Support\Facades\DB;
|
|
use RuntimeException;
|
|
|
|
class ExchangeRateService
|
|
{
|
|
/**
|
|
* دریافت نرخ فعلی ارز نسبت به ریال.
|
|
*
|
|
* ارز مبنا در سیستم IFNEX = ریال ایران (IRR).
|
|
* همه نرخها فقط نسبت به ریال در جدول currencies ذخیره میشوند.
|
|
*
|
|
* نرخها از دو مسیر بهروز میشوند:
|
|
* 1. مدیر بهصورت دستی در پنل (Currencies) وارد میکند
|
|
* 2. API نرخ ارز (مثلاً Exir.io) نرخ را دریافت و با وضعیت 'pending' ذخیره میکند
|
|
* و مدیر باید آن را تأیید کند تا بهعنوان نرخ فعال اعمال شود
|
|
* (در حال حاضر دکمه API موقتاً غیرفعال است).
|
|
*
|
|
* @param string $currencyCode کد ارز (مثل AED, USD, EUR)
|
|
* @return float نرخ ارز نسبت به ریال
|
|
* @throws \RuntimeException اگر ارز در دیتابیس یافت نشود (fallback حذف شد تا سیستم نرخ غلط استفاده نکند)
|
|
*/
|
|
public function getCurrentRate(string $currencyCode = 'AED'): float
|
|
{
|
|
$currency = Currency::where('code', strtoupper($currencyCode))->first();
|
|
|
|
if (!$currency) {
|
|
throw new \RuntimeException(
|
|
"نرخ ارز برای '{$currencyCode}' در دیتابیس تنظیم نشده است. "
|
|
. "لطفاً در پنل ادمین → ارزی → این ارز را اضافه و نرخ آن را وارد کنید."
|
|
);
|
|
}
|
|
|
|
$rate = (float) $currency->rate_to_rial;
|
|
if ($rate <= 0) {
|
|
throw new \RuntimeException(
|
|
"نرخ ارز برای '{$currencyCode}' صفر یا منفی است. "
|
|
. "لطفاً در پنل ادمین → ارزی → نرخ معتبر وارد کنید."
|
|
);
|
|
}
|
|
|
|
return $rate;
|
|
}
|
|
|
|
/**
|
|
* تبدیل مبلغ یک ارز به ریال ایران با نرخ روز.
|
|
* ریال ارز مبناست و نرخ تمام ارزها فقط نسبت به ریال ذخیره میشود.
|
|
*
|
|
* @param float $amount مبلغ به ارز دادهشده
|
|
* @param string $currencyCode کد ارز مبدأ (مثل AED, USD, EUR)
|
|
* @return float مبلغ معادل به ریال
|
|
*/
|
|
public function convertToIrr(float $amount, string $currencyCode = 'AED'): float
|
|
{
|
|
if ($amount == 0) {
|
|
return 0.0;
|
|
}
|
|
|
|
// ریال → ریال: تبدیل لازم نیست
|
|
if (strtoupper($currencyCode) === 'IRR') {
|
|
return $amount;
|
|
}
|
|
|
|
return $amount * $this->getCurrentRate($currencyCode);
|
|
}
|
|
|
|
/**
|
|
* تنظیم نرخ جدید برای یک ارز (نسبت به ریال).
|
|
*
|
|
* @param string $currencyCode کد ارز
|
|
* @param float $newRate نرخ جدید نسبت به ریال
|
|
* @param int|null $userId آیدی کاربر تغییردهنده
|
|
*/
|
|
public function setRate(
|
|
string $currencyCode,
|
|
float $newRate,
|
|
?int $userId = null,
|
|
?string $notes = null
|
|
): ExchangeRateHistory {
|
|
$currencyCode = strtoupper($currencyCode);
|
|
$oldRate = $this->getCurrentRate($currencyCode);
|
|
|
|
Currency::where('code', $currencyCode)->update([
|
|
'rate_to_rial' => $newRate,
|
|
]);
|
|
|
|
return $this->recordChange(
|
|
currencyCode: $currencyCode,
|
|
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 $currencyCode = 'AED'): array
|
|
{
|
|
$latest = ExchangeRateHistory::where('currency_from', strtoupper($currencyCode))
|
|
->where('status', 'approved')
|
|
->orderBy('created_at', 'desc')
|
|
->with('changedBy')
|
|
->first();
|
|
|
|
$currentRate = $this->getCurrentRate($currencyCode);
|
|
|
|
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,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* دریافت آخرین N نرخ برای Chart
|
|
*/
|
|
public function getRateChart(string $currencyCode = 'AED', int $limit = 7): array
|
|
{
|
|
return ExchangeRateHistory::where('currency_from', strtoupper($currencyCode))
|
|
->where('status', 'approved')
|
|
->orderBy('created_at', 'asc')
|
|
->limit($limit)
|
|
->pluck('new_rate')
|
|
->toArray();
|
|
}
|
|
|
|
/**
|
|
* ثبت تغییر نرخ (داخلی).
|
|
*/
|
|
private function recordChange(
|
|
string $currencyCode,
|
|
float $oldRate,
|
|
float $newRate,
|
|
string $source = 'manual',
|
|
?int $userId = null,
|
|
?string $apiSource = null,
|
|
?string $notes = null
|
|
): ExchangeRateHistory {
|
|
$changePercent = null;
|
|
if ($oldRate > 0) {
|
|
$changePercent = round((($newRate - $oldRate) / $oldRate) * 100, 4);
|
|
}
|
|
|
|
return ExchangeRateHistory::create([
|
|
'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_at' => now(),
|
|
'api_source' => $apiSource,
|
|
'notes' => $notes,
|
|
'status' => 'approved',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* تأیید یک نرخ در انتظار توسط مدیر.
|
|
*/
|
|
public function approveRate(ExchangeRateHistory $history, int $userId): ExchangeRateHistory
|
|
{
|
|
if (! $history->isPending()) {
|
|
throw new RuntimeException('این رکورد در وضعیت قابل تأیید نیست.');
|
|
}
|
|
|
|
return DB::transaction(function () use ($history, $userId) {
|
|
Currency::where('code', $history->currency_from)->update([
|
|
'rate_to_rial' => $history->new_rate,
|
|
]);
|
|
|
|
$history->update([
|
|
'status' => 'approved',
|
|
'approved_by' => $userId,
|
|
'approved_at' => now(),
|
|
]);
|
|
|
|
return $history->fresh();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* رد یک نرخ در انتظار.
|
|
*/
|
|
public function rejectRate(ExchangeRateHistory $history, int $userId, ?string $reason = null): ExchangeRateHistory
|
|
{
|
|
if (! $history->isPending()) {
|
|
throw new RuntimeException('این رکورد در وضعیت قابل رد نیست.');
|
|
}
|
|
|
|
$history->update([
|
|
'status' => 'rejected',
|
|
'approved_by' => $userId,
|
|
'approved_at' => now(),
|
|
'notes' => $reason ?: $history->notes,
|
|
]);
|
|
|
|
return $history->fresh();
|
|
}
|
|
|
|
/**
|
|
* لیست نرخهای در انتظار تأیید.
|
|
*/
|
|
public function getPendingRates(int $limit = 20)
|
|
{
|
|
return ExchangeRateHistory::where('status', 'pending')
|
|
->orderBy('created_at', 'desc')
|
|
->limit($limit)
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* تعداد نرخهای در انتظار تأیید (برای badge).
|
|
*/
|
|
public function getPendingCount(): int
|
|
{
|
|
return ExchangeRateHistory::where('status', 'pending')->count();
|
|
}
|
|
} |