From 42ce99eebafc952c227a678c2a4c806601007869 Mon Sep 17 00:00:00 2001 From: Darko Gjorgjijoski Date: Fri, 10 Apr 2026 15:55:46 +0200 Subject: [PATCH] Show common currencies first in dropdowns and default to USD in install wizard Currency dropdowns now display the most-traded currencies (USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, INR, BRL) at the top, followed by the rest alphabetically. The install wizard defaults to USD instead of EUR and formats currency names as "USD - US Dollar" for consistency with the rest of the app. --- .../Admin/CurrenciesController.php | 17 +++-- app/Models/Currency.php | 5 ++ app/Services/CurrencyService.php | 29 ++++++++ .../installation/views/PreferencesView.vue | 19 ++++- tests/Feature/Admin/CurrencyTest.php | 70 +++++++++++++++++++ 5 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 app/Services/CurrencyService.php create mode 100644 tests/Feature/Admin/CurrencyTest.php diff --git a/app/Http/Controllers/Admin/CurrenciesController.php b/app/Http/Controllers/Admin/CurrenciesController.php index cecf41e1..61f39fbb 100644 --- a/app/Http/Controllers/Admin/CurrenciesController.php +++ b/app/Http/Controllers/Admin/CurrenciesController.php @@ -4,20 +4,19 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Http\Resources\CurrencyResource; -use App\Models\Currency; +use App\Services\CurrencyService; use Illuminate\Http\Request; -use Illuminate\Http\Response; +use Illuminate\Http\Resources\Json\AnonymousResourceCollection; class CurrenciesController extends Controller { - /** - * Handle the incoming request. - * - * @return Response - */ - public function __invoke(Request $request) + public function __construct( + private readonly CurrencyService $currencyService, + ) {} + + public function __invoke(Request $request): AnonymousResourceCollection { - $currencies = Currency::latest()->get(); + $currencies = $this->currencyService->getAllWithCommonFirst(); return CurrencyResource::collection($currencies); } diff --git a/app/Models/Currency.php b/app/Models/Currency.php index 326aaf8b..3bd7581c 100644 --- a/app/Models/Currency.php +++ b/app/Models/Currency.php @@ -9,6 +9,11 @@ class Currency extends Model { use HasFactory; + public const COMMON_CURRENCY_CODES = [ + 'USD', 'EUR', 'GBP', 'JPY', 'CAD', + 'AUD', 'CHF', 'CNY', 'INR', 'BRL', + ]; + protected $guarded = [ 'id', ]; diff --git a/app/Services/CurrencyService.php b/app/Services/CurrencyService.php new file mode 100644 index 00000000..c3a800b5 --- /dev/null +++ b/app/Services/CurrencyService.php @@ -0,0 +1,29 @@ + + */ + public function getAllWithCommonFirst(): Collection + { + $currencies = Currency::query()->get(); + + $commonCodes = Currency::COMMON_CURRENCY_CODES; + + $common = $currencies->filter(fn (Currency $c): bool => in_array($c->code, $commonCodes, true)) + ->sortBy(fn (Currency $c): int => array_search($c->code, $commonCodes)); + + $rest = $currencies->reject(fn (Currency $c): bool => in_array($c->code, $commonCodes, true)) + ->sortBy('name'); + + return $common->concat($rest)->values(); + } +} diff --git a/resources/scripts/features/installation/views/PreferencesView.vue b/resources/scripts/features/installation/views/PreferencesView.vue index ceb6ac86..eaf7e7e8 100644 --- a/resources/scripts/features/installation/views/PreferencesView.vue +++ b/resources/scripts/features/installation/views/PreferencesView.vue @@ -137,7 +137,7 @@ import { clearInstallWizardAuth } from '../install-auth' import { useInstallationFeedback } from '../use-installation-feedback' interface PreferencesData { - currency: number + currency: number | null language: string carbon_date_format: string time_zone: string @@ -157,6 +157,7 @@ interface DateFormatOption { interface CurrencyOption { id: number name: string + code: string } interface LanguageOption { @@ -179,7 +180,7 @@ const timeZones = ref([]) const fiscalYears = ref([]) const currentPreferences = reactive({ - currency: 3, + currency: null, language: 'en', carbon_date_format: 'd M Y', time_zone: 'UTC', @@ -223,7 +224,19 @@ onMounted(async () => { installClient.get(`${API.CONFIG}?key=fiscal_years`), installClient.get(`${API.CONFIG}?key=languages`), ]) - currencies.value = currRes.data.data ?? currRes.data + const rawCurrencies: CurrencyOption[] = currRes.data.data ?? currRes.data + currencies.value = rawCurrencies.map((c) => ({ + ...c, + name: `${c.code} - ${c.name}`, + })) + + if (!currentPreferences.currency) { + const usd = currencies.value.find((c) => c.code === 'USD') + if (usd) { + currentPreferences.currency = usd.id + } + } + dateFormats.value = dateRes.data.data ?? dateRes.data timeZones.value = tzRes.data.data ?? tzRes.data fiscalYears.value = fyRes.data.data ?? fyRes.data ?? [] diff --git a/tests/Feature/Admin/CurrencyTest.php b/tests/Feature/Admin/CurrencyTest.php new file mode 100644 index 00000000..93e7a110 --- /dev/null +++ b/tests/Feature/Admin/CurrencyTest.php @@ -0,0 +1,70 @@ + 'DatabaseSeeder', '--force' => true]); + Artisan::call('db:seed', ['--class' => 'DemoSeeder', '--force' => true]); + + $user = User::find(1); + $this->withHeaders([ + 'company' => $user->companies()->first()->id, + ]); + Sanctum::actingAs( + $user, + ['*'] + ); +}); + +test('currencies endpoint returns common currencies first', function () { + $response = getJson('/api/v1/currencies')->assertOk(); + + $currencies = collect($response->json('data')); + $commonCodes = Currency::COMMON_CURRENCY_CODES; + $commonCount = count($commonCodes); + + $firstCodes = $currencies->take($commonCount)->pluck('code')->toArray(); + + foreach ($commonCodes as $code) { + expect($firstCodes)->toContain($code); + } +}); + +test('common currencies follow the defined priority order', function () { + $response = getJson('/api/v1/currencies')->assertOk(); + + $currencies = collect($response->json('data')); + $commonCodes = Currency::COMMON_CURRENCY_CODES; + $commonCount = count($commonCodes); + + $firstCodes = $currencies->take($commonCount)->pluck('code')->toArray(); + + expect($firstCodes)->toBe($commonCodes); +}); + +test('non-common currencies are sorted by name after common currencies', function () { + $response = getJson('/api/v1/currencies')->assertOk(); + + $currencies = collect($response->json('data')); + $commonCodes = Currency::COMMON_CURRENCY_CODES; + $commonCount = count($commonCodes); + + $restNames = $currencies->skip($commonCount)->pluck('name')->toArray(); + $sorted = $restNames; + sort($sorted, SORT_STRING | SORT_FLAG_CASE); + + expect($restNames)->toBe($sorted); +}); + +test('all seeded currencies are returned', function () { + $response = getJson('/api/v1/currencies')->assertOk(); + + $currencies = collect($response->json('data')); + + expect($currencies->count())->toBe(Currency::count()); +});