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()); +});