Преглед на файлове

refactor: currency exchange logic

3.x
wallo преди 2 години
родител
ревизия
46c0053ca0

+ 5
- 5
README.md Целия файл

@@ -96,22 +96,22 @@ Of course, you may use any service you wish to retrieve currency exchange rates.
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 101
 ```php
102
-public function getExchangeRate($from, $to)
102
+public function getExchangeRates($base)
103 103
 {
104 104
     $api_key = config('services.currency_api.key');
105 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 109
     $response = Http::get($req_url);
110 110
 
111 111
     if ($response->successful()) {
112 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 Целия файл

@@ -0,0 +1,23 @@
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,10 +7,10 @@ use App\Models\Banking\Account;
7 7
 use App\Models\Setting\Currency;
8 8
 use App\Services\CurrencyService;
9 9
 use App\Traits\ChecksForeignKeyConstraints;
10
+use Closure;
10 11
 use Filament\Forms\Form;
11 12
 use Filament\Notifications\Notification;
12 13
 use Filament\Resources\Resource;
13
-use Filament\Support\Colors\Color;
14 14
 use Filament\Tables\Table;
15 15
 use Filament\{Forms, Tables};
16 16
 use Illuminate\Database\Eloquent\Collection;
@@ -41,28 +41,23 @@ class CurrencyResource extends Resource
41 41
                             ->placeholder('Select a currency code...')
42 42
                             ->live()
43 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 45
                             ->afterStateUpdated(static function (Forms\Set $set, $state) {
46 46
                                 if ($state === null) {
47 47
                                     return;
48 48
                                 }
49 49
 
50
-                                $code = $state;
50
+                                $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
51
+                                $currencyService = app(CurrencyService::class);
51 52
 
53
+                                $code = $state;
52 54
                                 $allCurrencies = Currency::getAllCurrencies();
53
-
54 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 59
                                 $set('name', $selectedCurrencyCode['name'] ?? '');
65
-                                $set('rate', $rate);
60
+                                $set('rate', $rate ?? '');
66 61
                                 $set('precision', $selectedCurrencyCode['precision'] ?? '');
67 62
                                 $set('symbol', $selectedCurrencyCode['symbol'] ?? '');
68 63
                                 $set('symbol_first', $selectedCurrencyCode['symbol_first'] ?? '');
@@ -71,7 +66,7 @@ class CurrencyResource extends Resource
71 66
                             }),
72 67
                         Forms\Components\TextInput::make('code')
73 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 70
                             ->disabled(static fn (Forms\Get $get): bool => $get('enabled'))
76 71
                             ->required(),
77 72
                         Forms\Components\TextInput::make('name')
@@ -80,14 +75,14 @@ class CurrencyResource extends Resource
80 75
                             ->required(),
81 76
                         Forms\Components\TextInput::make('rate')
82 77
                             ->label('Rate')
83
-                            ->dehydrateStateUsing(static fn (Forms\Get $get, $state): float => $get('enabled') ? '1.0' : (float) $state)
84 78
                             ->numeric()
79
+                            ->rule('gt:0')
85 80
                             ->live()
86
-                            ->disabled(static fn (Forms\Get $get): bool => $get('enabled'))
87 81
                             ->required(),
88 82
                         Forms\Components\Select::make('precision')
89 83
                             ->label('Precision')
90
-                            ->searchable()
84
+                            ->native(false)
85
+                            ->selectablePlaceholder(false)
91 86
                             ->placeholder('Select a precision...')
92 87
                             ->options(['0', '1', '2', '3', '4'])
93 88
                             ->required(),
@@ -97,8 +92,10 @@ class CurrencyResource extends Resource
97 92
                             ->required(),
98 93
                         Forms\Components\Select::make('symbol_first')
99 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 99
                             ->required(),
103 100
                         Forms\Components\TextInput::make('decimal_mark')
104 101
                             ->label('Decimal Separator')
@@ -107,29 +104,39 @@ class CurrencyResource extends Resource
107 104
                         Forms\Components\TextInput::make('thousands_separator')
108 105
                             ->label('Thousands Separator')
109 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 117
                         ToggleButton::make('enabled')
112 118
                             ->label('Default Currency')
113 119
                             ->live()
114
-                            ->offColor(Color::Red)
115
-                            ->onColor(Color::Indigo)
120
+                            ->offColor('danger')
121
+                            ->onColor('primary')
116 122
                             ->afterStateUpdated(static function (Forms\Set $set, Forms\Get $get, $state) {
117
-                                $enabled = $state;
123
+                                $enabledState = (bool)$state;
118 124
                                 $code = $get('code');
125
+
126
+                                $defaultCurrencyCode = Currency::getDefaultCurrencyCode();
119 127
                                 $currencyService = app(CurrencyService::class);
120 128
 
121
-                                if ($enabled) {
122
-                                    $rate = 1;
129
+                                if ($enabledState) {
130
+                                    $set('rate', 1);
123 131
                                 } else {
124 132
                                     if ($code === null) {
125 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 141
                     ])->columns(),
135 142
             ]);

+ 7
- 1
app/Filament/Company/Resources/Setting/CurrencyResource/Pages/EditCurrency.php Целия файл

@@ -2,7 +2,9 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Setting\CurrencyResource\Pages;
4 4
 
5
+use App\Events\DefaultCurrencyChanged;
5 6
 use App\Filament\Company\Resources\Setting\CurrencyResource;
7
+use App\Models\Setting\Currency;
6 8
 use App\Traits\HandlesResourceRecordUpdate;
7 9
 use Filament\Actions;
8 10
 use Filament\Resources\Pages\EditRecord;
@@ -38,7 +40,7 @@ class EditCurrency extends EditRecord
38 40
     /**
39 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 45
         $user = Auth::user();
44 46
 
@@ -46,6 +48,10 @@ class EditCurrency extends EditRecord
46 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 55
         return $this->handleRecordUpdateWithUniqueField($record, $data, $user);
50 56
     }
51 57
 }

+ 36
- 0
app/Listeners/UpdateCurrencyRates.php Целия файл

@@ -0,0 +1,36 @@
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,7 +10,6 @@ use Database\Factories\Setting\CurrencyFactory;
10 10
 use Illuminate\Database\Eloquent\Factories\{Factory, HasFactory};
11 11
 use Illuminate\Database\Eloquent\Model;
12 12
 use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany, HasOne};
13
-use Illuminate\Support\Facades\DB;
14 13
 use Wallo\FilamentCompanies\FilamentCompanies;
15 14
 
16 15
 class Currency extends Model
@@ -102,21 +101,14 @@ class Currency extends Model
102 101
         $oldCurrency = $currencies->firstWhere('code', $oldCurrency);
103 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 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 114
     protected static function newFactory(): Factory

+ 9
- 2
app/Providers/EventServiceProvider.php Целия файл

@@ -2,8 +2,12 @@
2 2
 
3 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 11
 use Filament\Events\TenantSet;
8 12
 use Illuminate\Auth\Events\Registered;
9 13
 use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
@@ -32,6 +36,9 @@ class EventServiceProvider extends ServiceProvider
32 36
         CompanyGenerated::class => [
33 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,43 +6,50 @@ use Illuminate\Support\Facades\{Cache, Http};
6 6
 
7 7
 class CurrencyService
8 8
 {
9
-    public function getExchangeRate($from, $to)
9
+    public function getExchangeRates($base)
10 10
     {
11 11
         $api_key = config('services.currency_api.key');
12 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 16
         $response = Http::get($req_url);
17 17
 
18 18
         if ($response->successful()) {
19 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 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,6 +1464,17 @@ return [
1464 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 1478
         'SVC' => [
1468 1479
             'name' => 'El Salvador Colon',
1469 1480
             'code' => 222,

+ 1
- 1
database/migrations/2023_09_03_032820_create_currencies_table.php Целия файл

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

Loading…
Отказ
Запис