ifnex/04_Laravel/app/Services/ExchangeRateService.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();
}
}