浏览代码

refactor: currency exchange logic

3.x
wallo 2 年前
父节点
当前提交
46c0053ca0

+ 5
- 5
README.md 查看文件

96
 ],
96
 ],
97
 ```
97
 ```
98
 
98
 
99
-Additionally, you may update the following method in the `app/Services/CurrencyService.php` file which is responsible for retrieving the exchange rate:
99
+Additionally, you may update the following method in the `app/Services/CurrencyService.php` file which is responsible for retrieving the exchange rates:
100
 
100
 
101
 ```php
101
 ```php
102
-public function getExchangeRate($from, $to)
102
+public function getExchangeRates($base)
103
 {
103
 {
104
     $api_key = config('services.currency_api.key');
104
     $api_key = config('services.currency_api.key');
105
     $base_url = config('services.currency_api.base_url');
105
     $base_url = config('services.currency_api.base_url');
106
 
106
 
107
-    $req_url = "{$base_url}/{$api_key}/pair/{$from}/{$to}";
107
+    $req_url = "{$base_url}/{$api_key}/latest/{$base}";
108
 
108
 
109
     $response = Http::get($req_url);
109
     $response = Http::get($req_url);
110
 
110
 
111
     if ($response->successful()) {
111
     if ($response->successful()) {
112
         $responseData = $response->json();
112
         $responseData = $response->json();
113
-        if (isset($responseData['conversion_rate'])) {
114
-            return $responseData['conversion_rate'];
113
+        if (isset($responseData['conversion_rates'])) {
114
+            return $responseData['conversion_rates'];
115
         }
115
         }
116
     }
116
     }
117
 
117
 

+ 23
- 0
app/Events/DefaultCurrencyChanged.php 查看文件

1
+<?php
2
+
3
+namespace App\Events;
4
+
5
+use App\Models\Setting\Currency;
6
+use Illuminate\Broadcasting\InteractsWithSockets;
7
+use Illuminate\Foundation\Events\Dispatchable;
8
+use Illuminate\Queue\SerializesModels;
9
+
10
+class DefaultCurrencyChanged
11
+{
12
+    use Dispatchable, InteractsWithSockets, SerializesModels;
13
+
14
+    public Currency $currency;
15
+
16
+    /**
17
+     * Create a new event instance.
18
+     */
19
+    public function __construct(Currency $currency)
20
+    {
21
+        $this->currency = $currency;
22
+    }
23
+}

+ 35
- 28
app/Filament/Company/Resources/Setting/CurrencyResource.php 查看文件

7
 use App\Models\Setting\Currency;
7
 use App\Models\Setting\Currency;
8
 use App\Services\CurrencyService;
8
 use App\Services\CurrencyService;
9
 use App\Traits\ChecksForeignKeyConstraints;
9
 use App\Traits\ChecksForeignKeyConstraints;
10
+use Closure;
10
 use Filament\Forms\Form;
11
 use Filament\Forms\Form;
11
 use Filament\Notifications\Notification;
12
 use Filament\Notifications\Notification;
12
 use Filament\Resources\Resource;
13
 use Filament\Resources\Resource;
13
-use Filament\Support\Colors\Color;
14
 use Filament\Tables\Table;
14
 use Filament\Tables\Table;
15
 use Filament\{Forms, Tables};
15
 use Filament\{Forms, Tables};
16
 use Illuminate\Database\Eloquent\Collection;
16
 use Illuminate\Database\Eloquent\Collection;
41
                             ->placeholder('Select a currency code...')
41
                             ->placeholder('Select a currency code...')
42
                             ->live()
42
                             ->live()
43
                             ->required()
43
                             ->required()
44
-                            ->hidden(static fn (Forms\Get $get): bool => $get('enabled'))
44
+                            ->hidden(static fn (Forms\Get $get, $state): bool => $get('enabled') && $state !== null)
45
                             ->afterStateUpdated(static function (Forms\Set $set, $state) {
45
                             ->afterStateUpdated(static function (Forms\Set $set, $state) {
46
                                 if ($state === null) {
46
                                 if ($state === null) {
47
                                     return;
47
                                     return;
48
                                 }
48
                                 }
49
 
49
 
50
-                                $code = $state;
50
+                                $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
51
+                                $currencyService = app(CurrencyService::class);
51
 
52
 
53
+                                $code = $state;
52
                                 $allCurrencies = Currency::getAllCurrencies();
54
                                 $allCurrencies = Currency::getAllCurrencies();
53
-
54
                                 $selectedCurrencyCode = $allCurrencies[$code] ?? [];
55
                                 $selectedCurrencyCode = $allCurrencies[$code] ?? [];
55
 
56
 
56
-                                $currencyService = app(CurrencyService::class);
57
-                                $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
58
-                                $rate = 1;
59
-
60
-                                if ($defaultCurrencyCode !== null) {
61
-                                    $rate = $currencyService->getCachedExchangeRate($defaultCurrencyCode, $code);
62
-                                }
57
+                                $rate = $defaultCurrencyCode ? $currencyService->getCachedExchangeRate($defaultCurrencyCode, $code) : 1;
63
 
58
 
64
                                 $set('name', $selectedCurrencyCode['name'] ?? '');
59
                                 $set('name', $selectedCurrencyCode['name'] ?? '');
65
-                                $set('rate', $rate);
60
+                                $set('rate', $rate ?? '');
66
                                 $set('precision', $selectedCurrencyCode['precision'] ?? '');
61
                                 $set('precision', $selectedCurrencyCode['precision'] ?? '');
67
                                 $set('symbol', $selectedCurrencyCode['symbol'] ?? '');
62
                                 $set('symbol', $selectedCurrencyCode['symbol'] ?? '');
68
                                 $set('symbol_first', $selectedCurrencyCode['symbol_first'] ?? '');
63
                                 $set('symbol_first', $selectedCurrencyCode['symbol_first'] ?? '');
71
                             }),
66
                             }),
72
                         Forms\Components\TextInput::make('code')
67
                         Forms\Components\TextInput::make('code')
73
                             ->label('Code')
68
                             ->label('Code')
74
-                            ->hidden(static fn (Forms\Get $get): bool => ! $get('enabled'))
69
+                            ->hidden(static fn (Forms\Get $get): bool => !($get('enabled') && $get('code') !== null))
75
                             ->disabled(static fn (Forms\Get $get): bool => $get('enabled'))
70
                             ->disabled(static fn (Forms\Get $get): bool => $get('enabled'))
76
                             ->required(),
71
                             ->required(),
77
                         Forms\Components\TextInput::make('name')
72
                         Forms\Components\TextInput::make('name')
80
                             ->required(),
75
                             ->required(),
81
                         Forms\Components\TextInput::make('rate')
76
                         Forms\Components\TextInput::make('rate')
82
                             ->label('Rate')
77
                             ->label('Rate')
83
-                            ->dehydrateStateUsing(static fn (Forms\Get $get, $state): float => $get('enabled') ? '1.0' : (float) $state)
84
                             ->numeric()
78
                             ->numeric()
79
+                            ->rule('gt:0')
85
                             ->live()
80
                             ->live()
86
-                            ->disabled(static fn (Forms\Get $get): bool => $get('enabled'))
87
                             ->required(),
81
                             ->required(),
88
                         Forms\Components\Select::make('precision')
82
                         Forms\Components\Select::make('precision')
89
                             ->label('Precision')
83
                             ->label('Precision')
90
-                            ->searchable()
84
+                            ->native(false)
85
+                            ->selectablePlaceholder(false)
91
                             ->placeholder('Select a precision...')
86
                             ->placeholder('Select a precision...')
92
                             ->options(['0', '1', '2', '3', '4'])
87
                             ->options(['0', '1', '2', '3', '4'])
93
                             ->required(),
88
                             ->required(),
97
                             ->required(),
92
                             ->required(),
98
                         Forms\Components\Select::make('symbol_first')
93
                         Forms\Components\Select::make('symbol_first')
99
                             ->label('Symbol Position')
94
                             ->label('Symbol Position')
100
-                            ->searchable()
101
-                            ->boolean('Before Amount', 'After Amount', 'Select the currency symbol position...')
95
+                            ->native(false)
96
+                            ->selectablePlaceholder(false)
97
+                            ->formatStateUsing(static fn($state) => isset($state) ? (int) $state : null)
98
+                            ->boolean('Before Amount', 'After Amount', 'Select a symbol position...')
102
                             ->required(),
99
                             ->required(),
103
                         Forms\Components\TextInput::make('decimal_mark')
100
                         Forms\Components\TextInput::make('decimal_mark')
104
                             ->label('Decimal Separator')
101
                             ->label('Decimal Separator')
107
                         Forms\Components\TextInput::make('thousands_separator')
104
                         Forms\Components\TextInput::make('thousands_separator')
108
                             ->label('Thousands Separator')
105
                             ->label('Thousands Separator')
109
                             ->maxLength(1)
106
                             ->maxLength(1)
110
-                            ->required(),
107
+                            ->rule(static function (Forms\Get $get): Closure {
108
+                                return static function ($attribute, $value, Closure $fail) use ($get) {
109
+                                    $decimalMark = $get('decimal_mark');
110
+
111
+                                    if ($value === $decimalMark) {
112
+                                        $fail('The thousands separator and decimal separator must be different.');
113
+                                    }
114
+                                };
115
+                            })
116
+                            ->nullable(),
111
                         ToggleButton::make('enabled')
117
                         ToggleButton::make('enabled')
112
                             ->label('Default Currency')
118
                             ->label('Default Currency')
113
                             ->live()
119
                             ->live()
114
-                            ->offColor(Color::Red)
115
-                            ->onColor(Color::Indigo)
120
+                            ->offColor('danger')
121
+                            ->onColor('primary')
116
                             ->afterStateUpdated(static function (Forms\Set $set, Forms\Get $get, $state) {
122
                             ->afterStateUpdated(static function (Forms\Set $set, Forms\Get $get, $state) {
117
-                                $enabled = $state;
123
+                                $enabledState = (bool)$state;
118
                                 $code = $get('code');
124
                                 $code = $get('code');
125
+
126
+                                $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
119
                                 $currencyService = app(CurrencyService::class);
127
                                 $currencyService = app(CurrencyService::class);
120
 
128
 
121
-                                if ($enabled) {
122
-                                    $rate = 1;
129
+                                if ($enabledState) {
130
+                                    $set('rate', 1);
123
                                 } else {
131
                                 } else {
124
                                     if ($code === null) {
132
                                     if ($code === null) {
125
                                         return;
133
                                         return;
126
                                     }
134
                                     }
127
 
135
 
128
-                                    $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
129
-                                    $rate = $defaultCurrencyCode ? $currencyService->getCachedExchangeRate($defaultCurrencyCode, $code) : 1;
130
-                                }
136
+                                    $rate = $currencyService->getCachedExchangeRate($defaultCurrencyCode, $code);
131
 
137
 
132
-                                $set('rate', $rate);
138
+                                    $set('rate', $rate ?? '');
139
+                                }
133
                             }),
140
                             }),
134
                     ])->columns(),
141
                     ])->columns(),
135
             ]);
142
             ]);

+ 7
- 1
app/Filament/Company/Resources/Setting/CurrencyResource/Pages/EditCurrency.php 查看文件

2
 
2
 
3
 namespace App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
3
 namespace App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
4
 
4
 
5
+use App\Events\DefaultCurrencyChanged;
5
 use App\Filament\Company\Resources\Setting\CurrencyResource;
6
 use App\Filament\Company\Resources\Setting\CurrencyResource;
7
+use App\Models\Setting\Currency;
6
 use App\Traits\HandlesResourceRecordUpdate;
8
 use App\Traits\HandlesResourceRecordUpdate;
7
 use Filament\Actions;
9
 use Filament\Actions;
8
 use Filament\Resources\Pages\EditRecord;
10
 use Filament\Resources\Pages\EditRecord;
38
     /**
40
     /**
39
      * @throws Halt
41
      * @throws Halt
40
      */
42
      */
41
-    protected function handleRecordUpdate(Model $record, array $data): Model
43
+    protected function handleRecordUpdate(Model|Currency $record, array $data): Model|Currency
42
     {
44
     {
43
         $user = Auth::user();
45
         $user = Auth::user();
44
 
46
 
46
             throw new Halt('No authenticated user found');
48
             throw new Halt('No authenticated user found');
47
         }
49
         }
48
 
50
 
51
+        if ($data['enabled'] && ! $record->enabled) {
52
+            event(new DefaultCurrencyChanged($record));
53
+        }
54
+
49
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
55
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
50
     }
56
     }
51
 }
57
 }

+ 36
- 0
app/Listeners/UpdateCurrencyRates.php 查看文件

1
+<?php
2
+
3
+namespace App\Listeners;
4
+
5
+use App\Events\DefaultCurrencyChanged;
6
+use App\Models\Setting\Currency;
7
+use App\Services\CurrencyService;
8
+
9
+class UpdateCurrencyRates
10
+{
11
+    /**
12
+     * Create the event listener.
13
+     */
14
+    public function __construct()
15
+    {
16
+        //
17
+    }
18
+
19
+    /**
20
+     * Handle the event.
21
+     */
22
+    public function handle(DefaultCurrencyChanged $event): void
23
+    {
24
+        $currencyService = app(CurrencyService::class);
25
+
26
+        $currencies = Currency::where('code', '!=', $event->currency->code)->get();
27
+
28
+        foreach ($currencies as $currency) {
29
+            $newRate = $currencyService->getCachedExchangeRate($event->currency->code, $currency->code);
30
+
31
+            if ($newRate !== null) {
32
+                $currency->update(['rate' => $newRate]);
33
+            }
34
+        }
35
+    }
36
+}

+ 5
- 13
app/Models/Setting/Currency.php 查看文件

10
 use Illuminate\Database\Eloquent\Factories\{Factory, HasFactory};
10
 use Illuminate\Database\Eloquent\Factories\{Factory, HasFactory};
11
 use Illuminate\Database\Eloquent\Model;
11
 use Illuminate\Database\Eloquent\Model;
12
 use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany, HasOne};
12
 use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany, HasOne};
13
-use Illuminate\Support\Facades\DB;
14
 use Wallo\FilamentCompanies\FilamentCompanies;
13
 use Wallo\FilamentCompanies\FilamentCompanies;
15
 
14
 
16
 class Currency extends Model
15
 class Currency extends Model
102
         $oldCurrency = $currencies->firstWhere('code', $oldCurrency);
101
         $oldCurrency = $currencies->firstWhere('code', $oldCurrency);
103
         $newCurrency = $currencies->firstWhere('code', $newCurrency);
102
         $newCurrency = $currencies->firstWhere('code', $newCurrency);
104
 
103
 
105
-        $oldRate = DB::table('currencies')
106
-            ->where('code', $oldCurrency->code)
107
-            ->value('rate');
108
-
109
-        $newRate = DB::table('currencies')
110
-            ->where('code', $newCurrency->code)
111
-            ->value('rate');
112
-
113
-        $precision = max($oldCurrency->precision, $newCurrency->precision);
114
-
115
-        $scale = 10 ** $precision;
104
+        $oldRate = $oldCurrency->rate;
105
+        $newRate = $newCurrency->rate;
116
 
106
 
117
         $cleanBalance = (int) filter_var($balance, FILTER_SANITIZE_NUMBER_INT);
107
         $cleanBalance = (int) filter_var($balance, FILTER_SANITIZE_NUMBER_INT);
118
 
108
 
119
-        return round(($cleanBalance * $newRate * $scale) / ($oldRate * $scale));
109
+        $convertedBalance = ($cleanBalance / $oldRate) * $newRate;
110
+
111
+        return round($convertedBalance);
120
     }
112
     }
121
 
113
 
122
     protected static function newFactory(): Factory
114
     protected static function newFactory(): Factory

+ 9
- 2
app/Providers/EventServiceProvider.php 查看文件

2
 
2
 
3
 namespace App\Providers;
3
 namespace App\Providers;
4
 
4
 
5
-use App\Events\{CompanyDefaultEvent, CompanyDefaultUpdated, CompanyGenerated};
6
-use App\Listeners\{ConfigureCompanyDefault, CreateCompanyDefaults, SyncAssociatedModels, SyncWithCompanyDefaults};
5
+use App\Events\{CompanyDefaultEvent, CompanyDefaultUpdated, CompanyGenerated, DefaultCurrencyChanged};
6
+use App\Listeners\{ConfigureCompanyDefault,
7
+    CreateCompanyDefaults,
8
+    SyncAssociatedModels,
9
+    SyncWithCompanyDefaults,
10
+    UpdateCurrencyRates};
7
 use Filament\Events\TenantSet;
11
 use Filament\Events\TenantSet;
8
 use Illuminate\Auth\Events\Registered;
12
 use Illuminate\Auth\Events\Registered;
9
 use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
13
 use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
32
         CompanyGenerated::class => [
36
         CompanyGenerated::class => [
33
             CreateCompanyDefaults::class,
37
             CreateCompanyDefaults::class,
34
         ],
38
         ],
39
+        DefaultCurrencyChanged::class => [
40
+            UpdateCurrencyRates::class,
41
+        ],
35
     ];
42
     ];
36
 
43
 
37
     /**
44
     /**

+ 22
- 15
app/Services/CurrencyService.php 查看文件

6
 
6
 
7
 class CurrencyService
7
 class CurrencyService
8
 {
8
 {
9
-    public function getExchangeRate($from, $to)
9
+    public function getExchangeRates($base)
10
     {
10
     {
11
         $api_key = config('services.currency_api.key');
11
         $api_key = config('services.currency_api.key');
12
         $base_url = config('services.currency_api.base_url');
12
         $base_url = config('services.currency_api.base_url');
13
 
13
 
14
-        $req_url = "{$base_url}/{$api_key}/pair/{$from}/{$to}";
14
+        $req_url = "{$base_url}/{$api_key}/latest/{$base}";
15
 
15
 
16
         $response = Http::get($req_url);
16
         $response = Http::get($req_url);
17
 
17
 
18
         if ($response->successful()) {
18
         if ($response->successful()) {
19
             $responseData = $response->json();
19
             $responseData = $response->json();
20
-            if (isset($responseData['conversion_rate'])) {
21
-                return $responseData['conversion_rate'];
20
+            if (isset($responseData['conversion_rates'])) {
21
+                return $responseData['conversion_rates'];
22
             }
22
             }
23
         }
23
         }
24
 
24
 
25
         return null;
25
         return null;
26
     }
26
     }
27
 
27
 
28
-    public function getCachedExchangeRate(string $defaultCurrencyCode, string $code): ?float
28
+    public function updateCachedExchangeRates(string $base): void
29
     {
29
     {
30
-        $cacheKey = 'currency_data_' . $defaultCurrencyCode . '_' . $code;
30
+        $rates = $this->getExchangeRates($base);
31
 
31
 
32
-        $cachedData = Cache::get($cacheKey);
32
+        if ($rates !== null) {
33
+            $expirationTimeInSeconds = 60 * 60 * 24; // 1 day (24 hours)
33
 
34
 
34
-        if ($cachedData !== null) {
35
-            return $cachedData['rate'];
35
+            foreach ($rates as $code => $rate) {
36
+                $cacheKey = 'currency_data_' . $base . '_' . $code;
37
+                Cache::put($cacheKey, $rate, $expirationTimeInSeconds);
38
+            }
36
         }
39
         }
40
+    }
41
+
42
+    public function getCachedExchangeRate(string $defaultCurrencyCode, string $code): ?float
43
+    {
44
+        $cacheKey = 'currency_data_' . $defaultCurrencyCode . '_' . $code;
37
 
45
 
38
-        $rate = $this->getExchangeRate($defaultCurrencyCode, $code);
46
+        $cachedRate = Cache::get($cacheKey);
39
 
47
 
40
-        if ($rate !== null) {
41
-            $dataToCache = compact('rate');
42
-            $expirationTimeInSeconds = 60 * 60 * 24; // 24 hours
43
-            Cache::put($cacheKey, $dataToCache, $expirationTimeInSeconds);
48
+        if ($cachedRate === null) {
49
+            $this->updateCachedExchangeRates($defaultCurrencyCode);
50
+            $cachedRate = Cache::get($cacheKey);
44
         }
51
         }
45
 
52
 
46
-        return $rate;
53
+        return $cachedRate;
47
     }
54
     }
48
 }
55
 }

+ 11
- 0
config/money.php 查看文件

1464
             'thousands_separator' => ',',
1464
             'thousands_separator' => ',',
1465
         ],
1465
         ],
1466
 
1466
 
1467
+        'STN' => [
1468
+            'name' => 'Dobra',
1469
+            'code' => 930,
1470
+            'precision' => 2,
1471
+            'subunit' => 100,
1472
+            'symbol' => 'Db',
1473
+            'symbol_first' => false,
1474
+            'decimal_mark' => '.',
1475
+            'thousands_separator' => ',',
1476
+        ],
1477
+
1467
         'SVC' => [
1478
         'SVC' => [
1468
             'name' => 'El Salvador Colon',
1479
             'name' => 'El Salvador Colon',
1469
             'code' => 222,
1480
             'code' => 222,

+ 1
- 1
database/migrations/2023_09_03_032820_create_currencies_table.php 查看文件

21
             $table->string('symbol')->default('$');
21
             $table->string('symbol')->default('$');
22
             $table->boolean('symbol_first')->default(true);
22
             $table->boolean('symbol_first')->default(true);
23
             $table->string('decimal_mark')->default('.');
23
             $table->string('decimal_mark')->default('.');
24
-            $table->string('thousands_separator')->default(',');
24
+            $table->string('thousands_separator')->nullable();
25
             $table->boolean('enabled')->default(true);
25
             $table->boolean('enabled')->default(true);
26
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
26
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
27
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
27
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();

正在加载...
取消
保存