feat(pricing): centralize exchange rate management via ExchangeRateService

Migrate exchange rate handling from static system settings to a
centralized `ExchangeRateService`. This change ensures that currency
rates are managed through a single source of truth that supports
history tracking, approval workflows, and API updates.

Laravel:
- Remove manual exchange rate fields (`aed_to_irr`, `usd_to_irr`, etc.)
  from `IfnexSettingsPage` and the `SystemSetting` persistence logic.
- Update `PricingPageController` and `PriceCalculatorService` to
  retrieve live rates from `ExchangeRateService` instead of `SystemSetting`.
- Refactor `IfnexSettingsPage` UI to display current rates as
  read-only information with a link to the dedicated exchange rate
  management resource.

Documentation:
- Update `CLIENT_DELIVERY.md` to reflect the change in exchange rate
  management workflow.
This commit is contained in:
Kazem Alghasi 2026-10-03 07:20:32 +03:30
parent d170007a8f
commit 499e0731b1
5 changed files with 41 additions and 43 deletions

View File

@ -3,6 +3,7 @@
namespace App\Filament\Pages;
use App\Models\SystemSetting;
use App\Services\ExchangeRateService;
use Filament\Pages\Page;
class IfnexSettingsPage extends Page
@ -17,10 +18,6 @@ class IfnexSettingsPage extends Page
public float $vat_rate = 0.09;
public float $packing_cost_default = 100000;
public float $profit_margin = 1.25;
public float $aed_to_irr = 455000;
public float $usd_to_irr = 0;
public float $cny_to_irr = 0;
public float $eur_to_irr = 0;
public string $kavenegar_api_key = '';
public string $kavenegar_sender = '10008566';
@ -36,10 +33,6 @@ class IfnexSettingsPage extends Page
if (isset($settings['vat_rate'])) $this->vat_rate = (float) $settings['vat_rate'];
if (isset($settings['packing_cost_default'])) $this->packing_cost_default = (float) $settings['packing_cost_default'];
if (isset($settings['profit_margin'])) $this->profit_margin = (float) $settings['profit_margin'];
if (isset($settings['aed_to_irr'])) $this->aed_to_irr = (float) $settings['aed_to_irr'];
if (isset($settings['usd_to_irr'])) $this->usd_to_irr = (float) $settings['usd_to_irr'];
if (isset($settings['cny_to_irr'])) $this->cny_to_irr = (float) $settings['cny_to_irr'];
if (isset($settings['eur_to_irr'])) $this->eur_to_irr = (float) $settings['eur_to_irr'];
if (isset($settings['kavenegar_api_key'])) $this->kavenegar_api_key = (string) $settings['kavenegar_api_key'];
if (isset($settings['kavenegar_sender'])) $this->kavenegar_sender = (string) $settings['kavenegar_sender'];
@ -49,6 +42,20 @@ class IfnexSettingsPage extends Page
if (isset($settings['kavenegar_send_tracking_update'])) $this->kavenegar_send_tracking_update = (bool) $settings['kavenegar_send_tracking_update'];
}
/**
* نرخ‌های ارز زنده — فقط برای نمایش.
* تغییر نرخ فقط از منوی «نرخ ارز» (با تاریخچه و گردش تأیید) انجام می‌شود.
*/
public function getCurrentRates(): array
{
$service = app(ExchangeRateService::class);
return [
'AED' => $service->getCurrentRate('AED', 'IRR'),
'USD' => $service->getCurrentRate('USD', 'IRR'),
];
}
public function save(): void
{
$userId = auth()->id();
@ -65,22 +72,6 @@ class IfnexSettingsPage extends Page
['key' => 'profit_margin'],
['value' => $this->profit_margin, 'updated_by' => $userId]
);
SystemSetting::updateOrCreate(
['key' => 'aed_to_irr'],
['value' => $this->aed_to_irr, 'updated_by' => $userId]
);
SystemSetting::updateOrCreate(
['key' => 'usd_to_irr'],
['value' => $this->usd_to_irr, 'updated_by' => $userId]
);
SystemSetting::updateOrCreate(
['key' => 'cny_to_irr'],
['value' => $this->cny_to_irr, 'updated_by' => $userId]
);
SystemSetting::updateOrCreate(
['key' => 'eur_to_irr'],
['value' => $this->eur_to_irr, 'updated_by' => $userId]
);
SystemSetting::updateOrCreate(
['key' => 'kavenegar_api_key'],

View File

@ -12,7 +12,7 @@ class PricingPageController extends Controller
{
$countries = Country::orderBy('name')->get(['id', 'name', 'iso_code']);
$settings = [
'aed_to_irr' => (float) SystemSetting::get('aed_to_irr', 455000),
'aed_to_irr' => app(\App\Services\ExchangeRateService::class)->getCurrentRate('AED', 'IRR'),
'profit_margin' => (float) SystemSetting::get('profit_margin', 1.25),
'vat_rate' => (float) SystemSetting::get('vat_rate', 0.09),
];

View File

@ -123,7 +123,8 @@ class PriceCalculatorService
private function calculateFinancials(float $baseRate): array
{
$profitMargin = (float) SystemSetting::get('profit_margin', 1.25);
$aedToIrr = (float) SystemSetting::get('aed_to_irr', 455000);
// نرخ ارز فقط از ExchangeRateService خوانده می‌شود (منبع واحد — منوی نرخ ارز)
$aedToIrr = app(ExchangeRateService::class)->getCurrentRate('AED', 'IRR');
$netDirham = $baseRate * $profitMargin;
$netRial = $netDirham * $aedToIrr;

View File

@ -19,25 +19,29 @@
<input type="number" step="0.01" wire:model="profit_margin" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
<p class="mt-1 text-sm text-gray-500">Example: 1.25 for 25% margin</p>
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">AED to IRR</label>
<input type="number" wire:model="aed_to_irr" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
{{-- نرخ‌های ارز: فقط نمایش — تغییر فقط از منوی «نرخ ارز» (تاریخچه + گردش تأیید) --}}
<div class="mt-6 rounded-lg border border-amber-200 bg-amber-50 dark:border-gray-700 dark:bg-gray-800 p-4">
<div class="flex items-center justify-between flex-wrap gap-3">
<div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">💱 نرخ‌های ارز (فقط نمایش)</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
تغییر نرخ فقط از منوی «نرخ ارز» انجام می‌شود — همیشه با تاریخچه، امکان آپدیت از API و گردش تأیید.
</p>
</div>
<a href="{{ \App\Filament\Resources\ExchangeRateHistoryResource::getUrl() }}"
class="text-xs font-semibold text-amber-600 hover:text-amber-700 dark:text-amber-400">
مدیریت نرخ‌ها در منوی نرخ ارز ←
</a>
</div>
<div>
<label class="block text-sm font-medium text-gray-700">USD to IRR</label>
<input type="number" wire:model="usd_to_irr" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">CNY to IRR</label>
<input type="number" wire:model="cny_to_irr" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
</div>
<div>
<label class="block text-sm font-medium text-gray-700">EUR to IRR</label>
<input type="number" wire:model="eur_to_irr" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm">
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
@foreach ($this->getCurrentRates() as $currency => $rate)
<div class="flex items-center justify-between rounded-md bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 px-3 py-2">
<span class="text-sm text-gray-600 dark:text-gray-300">{{ $currency }} → IRR</span>
<span class="text-sm font-bold text-gray-900 dark:text-white">{{ number_format($rate) }} ریال</span>
</div>
@endforeach
</div>
</div>
</x-filament::card>

View File

@ -396,6 +396,8 @@ CSV / approved import format
* [ ] Money / currency precision
* [ ] Multi-currency debt integrity
> ✅ **انجام شد (2026-10-03):** تغییر نرخ ارز فقط از منوی «نرخ ارز» (تاریخچه + گردش تأیید + آپدیت از API) — فیلدهای مستقیم نرخ در «تنظیمات سیستم» حذف و به نمایش فقط‌خواندنی تبدیل شدند.
## Application
* [ ] Legacy Order Flow review