diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 935a1716..013213d9 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -2,6 +2,7 @@ namespace App\Actions\Fortify; +use App\Enums\Locale; use App\Models\User; use App\Services\LandingAuthOverrideService; use Illuminate\Support\Facades\Validator; @@ -42,7 +43,7 @@ class CreateNewUser implements CreatesNewUsers 'name' => $input['name'], 'email' => $input['email'], 'password' => $input['password'], - 'locale' => $this->detectLocaleFromRequest(), + 'locale' => Locale::detectFromHeader(request()->header('Accept-Language'))->value, 'timezone' => $this->normalizeTimezone($input['timezone'] ?? null), ]); @@ -69,19 +70,4 @@ class CreateNewUser implements CreatesNewUsers return $timezone; } - - /** - * Detect locale from Accept-Language header. - */ - protected function detectLocaleFromRequest(): string - { - $acceptLanguage = request()->header('Accept-Language', ''); - - // Check if Spanish is preferred - if (preg_match('/^es(-|,|;)/i', $acceptLanguage) || $acceptLanguage === 'es') { - return 'es'; - } - - return 'en'; - } } diff --git a/app/Console/Commands/BackfillAccountIbans.php b/app/Console/Commands/BackfillAccountIbans.php index 0f962e72..1491d89b 100644 --- a/app/Console/Commands/BackfillAccountIbans.php +++ b/app/Console/Commands/BackfillAccountIbans.php @@ -3,6 +3,7 @@ namespace App\Console\Commands; use App\Contracts\BankingProviderInterface; +use App\Enums\BankingProvider; use App\Models\Account; use App\Models\User; use Illuminate\Console\Command; @@ -32,7 +33,7 @@ class BackfillAccountIbans extends Command ->whereNull('iban') ->whereNotNull('external_account_id') ->whereNotNull('banking_connection_id') - ->whereHas('bankingConnection', fn ($q) => $q->where('provider', 'enablebanking')); + ->whereHas('bankingConnection', fn ($q) => $q->where('provider', BankingProvider::EnableBanking)); if ($connectionId) { $query->where('banking_connection_id', $connectionId); diff --git a/app/Console/Commands/CancelFreeEnableBankingConnectionsCommand.php b/app/Console/Commands/CancelFreeEnableBankingConnectionsCommand.php index af238d5c..7e56f259 100644 --- a/app/Console/Commands/CancelFreeEnableBankingConnectionsCommand.php +++ b/app/Console/Commands/CancelFreeEnableBankingConnectionsCommand.php @@ -4,6 +4,7 @@ namespace App\Console\Commands; use App\Actions\OpenBanking\DisconnectBankingConnection; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use App\Mail\EnableBankingConnectionsCancelledEmail; use App\Models\BankingConnection; use Illuminate\Console\Command; @@ -22,7 +23,7 @@ class CancelFreeEnableBankingConnectionsCommand extends Command $connections = BankingConnection::query() ->with(['user', 'accounts']) ->whereHas('user') - ->where('provider', 'enablebanking') + ->where('provider', BankingProvider::EnableBanking) ->where('status', '!=', BankingConnectionStatus::Revoked) ->where('created_at', '<=', $cutoff) ->get(); diff --git a/app/Console/Commands/DeleteUserCommand.php b/app/Console/Commands/DeleteUserCommand.php index 56a88c43..7894454f 100644 --- a/app/Console/Commands/DeleteUserCommand.php +++ b/app/Console/Commands/DeleteUserCommand.php @@ -3,6 +3,7 @@ namespace App\Console\Commands; use App\Actions\OpenBanking\DisconnectBankingConnection; +use App\Enums\BankingProvider; use App\Models\User; use Illuminate\Console\Command; use Laravel\Cashier\Subscription; @@ -53,7 +54,7 @@ class DeleteUserCommand extends Command $subscription = $this->activeSubscription($user); $enableBankingConnections = $user->bankingConnections() ->with('accounts') - ->where('provider', 'enablebanking') + ->where('provider', BankingProvider::EnableBanking) ->get(); if ($subscription && ! $this->confirm("User '{$user->email}' has an active Stripe subscription. Cancel it before deleting the user?")) { diff --git a/app/Console/Commands/SyncBankingConnections.php b/app/Console/Commands/SyncBankingConnections.php index 32804d64..6233dd91 100644 --- a/app/Console/Commands/SyncBankingConnections.php +++ b/app/Console/Commands/SyncBankingConnections.php @@ -3,6 +3,7 @@ namespace App\Console\Commands; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use App\Jobs\SyncAllBankingConnectionsJob; use App\Jobs\SyncBankingConnectionJob; use App\Models\BankingConnection; @@ -55,7 +56,7 @@ class SyncBankingConnections extends Command ->orWhere('valid_until', '>', now()); }); })->orWhere(function ($query) { - $query->where('provider', 'enablebanking') + $query->where('provider', BankingProvider::EnableBanking) ->where('status', BankingConnectionStatus::Active) ->whereNotNull('valid_until') ->where('valid_until', '<=', now()); @@ -92,9 +93,9 @@ class SyncBankingConnections extends Command $connections->each(function (BankingConnection $connection) use ($sync, $fullSync) { if ($sync) { - $this->info("Syncing {$connection->provider} connection {$connection->id}..."); + $this->info("Syncing {$connection->provider->value} connection {$connection->id}..."); SyncBankingConnectionJob::dispatchSync($connection, $fullSync); - $this->info("Finished syncing {$connection->provider} connection {$connection->id}."); + $this->info("Finished syncing {$connection->provider->value} connection {$connection->id}."); } else { SyncBankingConnectionJob::dispatch($connection, $fullSync); } diff --git a/app/Enums/BankingProvider.php b/app/Enums/BankingProvider.php new file mode 100644 index 00000000..179181d6 --- /dev/null +++ b/app/Enums/BankingProvider.php @@ -0,0 +1,33 @@ + AccountType::Investment, + self::Wise, self::EnableBanking => AccountType::Checking, + }; + } +} diff --git a/app/Enums/Locale.php b/app/Enums/Locale.php new file mode 100644 index 00000000..b0661538 --- /dev/null +++ b/app/Enums/Locale.php @@ -0,0 +1,27 @@ +value || preg_match('/^'.$locale->value.'(-|,|;)/i', $acceptLanguage) === 1) { + return $locale; + } + } + + return self::English; + } +} diff --git a/app/Http/Controllers/OpenBanking/AccountMappingController.php b/app/Http/Controllers/OpenBanking/AccountMappingController.php index 1901336d..61aa1cb5 100644 --- a/app/Http/Controllers/OpenBanking/AccountMappingController.php +++ b/app/Http/Controllers/OpenBanking/AccountMappingController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers\OpenBanking; -use App\Enums\AccountType; use App\Enums\BankingConnectionStatus; use App\Http\Controllers\Controller; use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending; @@ -77,9 +76,7 @@ class AccountMappingController extends Controller $pendingAccounts = collect($connection->pending_accounts_data) ->keyBy('uid'); - $accountType = ($connection->isIndexaCapital() || $connection->isBinance() || $connection->isBitpanda() || $connection->isCoinbase()) - ? AccountType::Investment - : AccountType::Checking; + $accountType = $connection->provider->defaultAccountType(); foreach ($mappings as $mapping) { $uid = $mapping['bank_account_uid']; diff --git a/app/Http/Controllers/OpenBanking/AuthorizationController.php b/app/Http/Controllers/OpenBanking/AuthorizationController.php index a9420bf3..5d715d8d 100644 --- a/app/Http/Controllers/OpenBanking/AuthorizationController.php +++ b/app/Http/Controllers/OpenBanking/AuthorizationController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\OpenBanking; use App\Contracts\BankingProviderInterface; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use App\Http\Controllers\Controller; use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending; use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate; @@ -50,7 +51,7 @@ class AuthorizationController extends Controller ); $connection = $user->bankingConnections()->create([ - 'provider' => 'enablebanking', + 'provider' => BankingProvider::EnableBanking, 'authorization_id' => $result['authorization_id'], 'state_token' => $stateToken, 'aspsp_name' => $validated['aspsp_name'], diff --git a/app/Http/Controllers/OpenBanking/BinanceController.php b/app/Http/Controllers/OpenBanking/BinanceController.php index f3c8a911..6cf3fbbb 100644 --- a/app/Http/Controllers/OpenBanking/BinanceController.php +++ b/app/Http/Controllers/OpenBanking/BinanceController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\OpenBanking; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use App\Http\Controllers\Controller; use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending; use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate; @@ -49,7 +50,7 @@ class BinanceController extends Controller ); $connection = $user->bankingConnections()->create([ - 'provider' => 'binance', + 'provider' => BankingProvider::Binance, 'api_token' => $validated['api_key'], 'api_secret' => $validated['api_secret'], 'aspsp_name' => 'Binance', diff --git a/app/Http/Controllers/OpenBanking/BitpandaController.php b/app/Http/Controllers/OpenBanking/BitpandaController.php index 53654627..c9a5947b 100644 --- a/app/Http/Controllers/OpenBanking/BitpandaController.php +++ b/app/Http/Controllers/OpenBanking/BitpandaController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\OpenBanking; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use App\Http\Controllers\Controller; use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending; use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate; @@ -49,7 +50,7 @@ class BitpandaController extends Controller ); $connection = $user->bankingConnections()->create([ - 'provider' => 'bitpanda', + 'provider' => BankingProvider::Bitpanda, 'api_token' => $validated['api_key'], 'aspsp_name' => 'Bitpanda', 'aspsp_country' => $validated['country'], diff --git a/app/Http/Controllers/OpenBanking/CoinbaseController.php b/app/Http/Controllers/OpenBanking/CoinbaseController.php index 98ea72a4..b4284a63 100644 --- a/app/Http/Controllers/OpenBanking/CoinbaseController.php +++ b/app/Http/Controllers/OpenBanking/CoinbaseController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\OpenBanking; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use App\Http\Controllers\Controller; use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending; use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate; @@ -49,7 +50,7 @@ class CoinbaseController extends Controller ); $connection = $user->bankingConnections()->create([ - 'provider' => 'coinbase', + 'provider' => BankingProvider::Coinbase, 'api_token' => $validated['api_key_name'], 'api_secret' => $validated['private_key'], 'aspsp_name' => 'Coinbase', diff --git a/app/Http/Controllers/OpenBanking/Concerns/CreatesAccountsFromPending.php b/app/Http/Controllers/OpenBanking/Concerns/CreatesAccountsFromPending.php index 027d1f7c..95ef5fe5 100644 --- a/app/Http/Controllers/OpenBanking/Concerns/CreatesAccountsFromPending.php +++ b/app/Http/Controllers/OpenBanking/Concerns/CreatesAccountsFromPending.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers\OpenBanking\Concerns; -use App\Enums\AccountType; use App\Enums\BankingConnectionStatus; use App\Models\Bank; use App\Models\BankingConnection; @@ -29,9 +28,7 @@ trait CreatesAccountsFromPending $bank->update(['logo' => $connection->aspsp_logo]); } - $accountType = ($connection->isIndexaCapital() || $connection->isBinance() || $connection->isBitpanda() || $connection->isCoinbase()) - ? AccountType::Investment - : AccountType::Checking; + $accountType = $connection->provider->defaultAccountType(); foreach ($connection->pending_accounts_data ?? [] as $accountData) { $uid = $accountData['uid'] ?? null; diff --git a/app/Http/Controllers/OpenBanking/ConnectionController.php b/app/Http/Controllers/OpenBanking/ConnectionController.php index 9c2adb07..02d1410d 100644 --- a/app/Http/Controllers/OpenBanking/ConnectionController.php +++ b/app/Http/Controllers/OpenBanking/ConnectionController.php @@ -4,6 +4,7 @@ namespace App\Http\Controllers\OpenBanking; use App\Actions\OpenBanking\DisconnectBankingConnection; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use App\Http\Controllers\Controller; use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate; use App\Http\Requests\OpenBanking\DestroyConnectionRequest; @@ -94,10 +95,10 @@ class ConnectionController extends Controller } $updateData = match ($connection->provider) { - 'indexacapital' => ['api_token' => $validated['api_token']], - 'binance' => ['api_token' => $validated['api_key'], 'api_secret' => $validated['api_secret']], - 'bitpanda' => ['api_token' => $validated['api_key']], - 'coinbase' => ['api_token' => $validated['api_key_name'], 'api_secret' => $validated['private_key']], + BankingProvider::IndexaCapital => ['api_token' => $validated['api_token']], + BankingProvider::Binance => ['api_token' => $validated['api_key'], 'api_secret' => $validated['api_secret']], + BankingProvider::Bitpanda => ['api_token' => $validated['api_key']], + BankingProvider::Coinbase => ['api_token' => $validated['api_key_name'], 'api_secret' => $validated['private_key']], default => [], }; @@ -120,10 +121,10 @@ class ConnectionController extends Controller { try { match ($connection->provider) { - 'indexacapital' => (new IndexaCapitalClient($validated['api_token']))->getUser(), - 'binance' => (new BinanceClient($validated['api_key'], $validated['api_secret']))->getAccount(), - 'bitpanda' => (new BitpandaClient($validated['api_key']))->getCryptoWallets(), - 'coinbase' => (new CoinbaseClient($validated['api_key_name'], $validated['private_key']))->getAccounts(limit: 1), + BankingProvider::IndexaCapital => (new IndexaCapitalClient($validated['api_token']))->getUser(), + BankingProvider::Binance => (new BinanceClient($validated['api_key'], $validated['api_secret']))->getAccount(), + BankingProvider::Bitpanda => (new BitpandaClient($validated['api_key']))->getCryptoWallets(), + BankingProvider::Coinbase => (new CoinbaseClient($validated['api_key_name'], $validated['private_key']))->getAccounts(limit: 1), default => throw new \InvalidArgumentException('Unsupported provider for credential update.'), }; } catch (\InvalidArgumentException $e) { @@ -131,7 +132,7 @@ class ConnectionController extends Controller } catch (\Throwable $e) { Log::warning('Credential validation failed during update', [ 'connection_id' => $connection->id, - 'provider' => $connection->provider, + 'provider' => $connection->provider->value, 'error' => $e->getMessage(), ]); diff --git a/app/Http/Controllers/OpenBanking/IndexaCapitalController.php b/app/Http/Controllers/OpenBanking/IndexaCapitalController.php index 6968603b..76643d23 100644 --- a/app/Http/Controllers/OpenBanking/IndexaCapitalController.php +++ b/app/Http/Controllers/OpenBanking/IndexaCapitalController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\OpenBanking; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use App\Http\Controllers\Controller; use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending; use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate; @@ -49,7 +50,7 @@ class IndexaCapitalController extends Controller ); $connection = $user->bankingConnections()->create([ - 'provider' => 'indexacapital', + 'provider' => BankingProvider::IndexaCapital, 'api_token' => $validated['api_token'], 'aspsp_name' => 'Indexa Capital', 'aspsp_country' => 'ES', diff --git a/app/Http/Controllers/OpenBanking/WiseController.php b/app/Http/Controllers/OpenBanking/WiseController.php index 7ac7f6db..c10cda33 100644 --- a/app/Http/Controllers/OpenBanking/WiseController.php +++ b/app/Http/Controllers/OpenBanking/WiseController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\OpenBanking; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use App\Http\Controllers\Controller; use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending; use App\Http\Controllers\OpenBanking\Concerns\HandlesSubscriptionGate; @@ -58,7 +59,7 @@ class WiseController extends Controller ); $connection = $user->bankingConnections()->create([ - 'provider' => 'wise', + 'provider' => BankingProvider::Wise, 'api_token' => $request->api_token, 'api_secret' => null, 'aspsp_name' => 'Wise', diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index f26c94e8..faaf6fcd 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -3,6 +3,7 @@ namespace App\Http\Middleware; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use App\Features\CalculateBalancesOnImport; use App\Features\TransactionAnalysis; use App\Models\BankingConnection; @@ -103,7 +104,7 @@ class HandleInertiaRequests extends Middleware 'sidebarOpen' => ! $request->hasCookie('sidebar_state') || $request->cookie('sidebar_state') === 'true', 'features' => $this->resolveFeatureFlags(), 'expiredBankingConnections' => fn () => $user ? $user->bankingConnections() - ->where('provider', 'enablebanking') + ->where('provider', BankingProvider::EnableBanking) ->where(function ($query) { $query->where('status', BankingConnectionStatus::Expired) ->orWhere(function ($query) { @@ -117,7 +118,7 @@ class HandleInertiaRequests extends Middleware ->map(fn (BankingConnection $connection): array => [ 'id' => $connection->id, 'aspsp_name' => $connection->aspsp_name, - 'provider' => $connection->provider, + 'provider' => $connection->provider->value, 'valid_until' => $connection->valid_until?->toIso8601String(), 'reconnect_url' => route('open-banking.reconnect', $connection), ]) : [], diff --git a/app/Http/Middleware/SetLocale.php b/app/Http/Middleware/SetLocale.php index 212d00e4..546b4895 100644 --- a/app/Http/Middleware/SetLocale.php +++ b/app/Http/Middleware/SetLocale.php @@ -2,6 +2,7 @@ namespace App\Http\Middleware; +use App\Enums\Locale; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\App; @@ -29,12 +30,13 @@ class SetLocale protected function determineLocale(Request $request): string { // Priority 1: Check for lang query parameter (user override on welcome page) - if ($request->has('lang') && in_array($request->get('lang'), ['en', 'es', 'fr'])) { - $locale = $request->get('lang'); - // Store in session so subsequent requests remember this choice - $request->session()->put('locale', $locale); + $lang = $request->get('lang'); - return $locale; + if (is_string($lang) && Locale::tryFrom($lang) !== null) { + // Store in session so subsequent requests remember this choice + $request->session()->put('locale', $lang); + + return $lang; } // Priority 2: Check authenticated user's locale preference @@ -44,11 +46,11 @@ class SetLocale // Priority 2b: Authenticated user without locale — detect and persist if ($request->user()) { - $detected = $this->detectLocaleFromHeader($request); + $sessionLocale = $request->session()->get('locale'); - if (in_array($request->session()->get('locale'), ['en', 'es', 'fr'])) { - $detected = $request->session()->get('locale'); - } + $detected = is_string($sessionLocale) && Locale::tryFrom($sessionLocale) !== null + ? $sessionLocale + : Locale::detectFromHeader($request->header('Accept-Language'))->value; $request->user()->update(['locale' => $detected]); @@ -61,31 +63,11 @@ class SetLocale } // Priority 4: Detect from Accept-Language header - $detected = $this->detectLocaleFromHeader($request); + $detected = Locale::detectFromHeader($request->header('Accept-Language'))->value; // Store in session for subsequent requests $request->session()->put('locale', $detected); return $detected; } - - /** - * Detect locale from Accept-Language header. - */ - protected function detectLocaleFromHeader(Request $request): string - { - $acceptLanguage = $request->header('Accept-Language', ''); - - // Check if Spanish is preferred - if (preg_match('/^es(-|,|;)/i', $acceptLanguage) || $acceptLanguage === 'es') { - return 'es'; - } - - // Check if French is preferred - if (preg_match('/^fr(-|,|;)/i', $acceptLanguage) || $acceptLanguage === 'fr') { - return 'fr'; - } - - return 'en'; - } } diff --git a/app/Http/Requests/OpenBanking/UpdateConnectionCredentialsRequest.php b/app/Http/Requests/OpenBanking/UpdateConnectionCredentialsRequest.php index ce072fe5..121a9e03 100644 --- a/app/Http/Requests/OpenBanking/UpdateConnectionCredentialsRequest.php +++ b/app/Http/Requests/OpenBanking/UpdateConnectionCredentialsRequest.php @@ -2,6 +2,7 @@ namespace App\Http\Requests\OpenBanking; +use App\Enums\BankingProvider; use App\Models\BankingConnection; use Illuminate\Foundation\Http\FormRequest; @@ -27,17 +28,17 @@ class UpdateConnectionCredentialsRequest extends FormRequest } return match ($connection->provider) { - 'indexacapital' => [ + BankingProvider::IndexaCapital => [ 'api_token' => ['required', 'string', 'min:10'], ], - 'binance' => [ + BankingProvider::Binance => [ 'api_key' => ['required', 'string', 'min:10'], 'api_secret' => ['required', 'string', 'min:10'], ], - 'bitpanda' => [ + BankingProvider::Bitpanda => [ 'api_key' => ['required', 'string', 'min:10'], ], - 'coinbase' => [ + BankingProvider::Coinbase => [ 'api_key_name' => ['required', 'string', 'regex:/^(organizations\/[a-z0-9-]+\/apiKeys\/[a-z0-9-]+|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})$/i'], 'private_key' => ['required', 'string', 'min:40'], ], diff --git a/app/Http/Requests/Settings/ProfileUpdateRequest.php b/app/Http/Requests/Settings/ProfileUpdateRequest.php index 1c2af4fa..c7ee4776 100644 --- a/app/Http/Requests/Settings/ProfileUpdateRequest.php +++ b/app/Http/Requests/Settings/ProfileUpdateRequest.php @@ -2,6 +2,7 @@ namespace App\Http\Requests\Settings; +use App\Enums\Locale; use App\Models\User; use App\Services\CurrencyOptions; use Illuminate\Contracts\Validation\ValidationRule; @@ -31,7 +32,7 @@ class ProfileUpdateRequest extends FormRequest Rule::unique(User::class)->ignore($this->user()->id), ], 'currency_code' => ['required', 'string', 'max:3', Rule::in($currencyOptions->primaryCodes())], - 'locale' => ['nullable', 'string', Rule::in(['en', 'es', 'fr'])], + 'locale' => ['nullable', 'string', Rule::enum(Locale::class)], ]; } } diff --git a/app/Jobs/SyncAllBankingConnectionsJob.php b/app/Jobs/SyncAllBankingConnectionsJob.php index 0f3de9af..3a7807f9 100644 --- a/app/Jobs/SyncAllBankingConnectionsJob.php +++ b/app/Jobs/SyncAllBankingConnectionsJob.php @@ -3,6 +3,7 @@ namespace App\Jobs; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use App\Models\BankingConnection; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -35,7 +36,7 @@ class SyncAllBankingConnectionsJob implements ShouldQueue ->orWhere('valid_until', '>', now()); }); })->orWhere(function ($query) { - $query->where('provider', 'enablebanking') + $query->where('provider', BankingProvider::EnableBanking) ->where('status', BankingConnectionStatus::Active) ->whereNotNull('valid_until') ->where('valid_until', '<=', now()); diff --git a/app/Jobs/SyncBankingConnectionJob.php b/app/Jobs/SyncBankingConnectionJob.php index 09a90218..9307509a 100644 --- a/app/Jobs/SyncBankingConnectionJob.php +++ b/app/Jobs/SyncBankingConnectionJob.php @@ -221,7 +221,7 @@ class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue 'consecutive_sync_failures' => self::MAX_SCHEDULED_RETRIES + 1, ]); - if ($this->isApiKeyProvider($connection) && $connection->user?->canReceiveEmails()) { + if ($connection->usesApiKey() && $connection->user?->canReceiveEmails()) { Mail::to($connection->user)->send(new BankingConnectionAuthFailedEmail( $connection->user, $connection, @@ -269,7 +269,7 @@ class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue $scope->setTag('banking_connection_id', (string) $connection->id); $scope->setContext('banking_connection', [ 'id' => $connection->id, - 'provider' => $connection->provider, + 'provider' => $connection->provider->value, 'status' => $connection->status->value, ]); @@ -510,12 +510,4 @@ class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue return $e instanceof RequestException && in_array($e->response->status(), [401, 403]); } - - private function isApiKeyProvider(BankingConnection $connection): bool - { - return $connection->isIndexaCapital() - || $connection->isBinance() - || $connection->isBitpanda() - || $connection->isCoinbase(); - } } diff --git a/app/Models/BankingConnection.php b/app/Models/BankingConnection.php index 44f66d44..cb8051da 100644 --- a/app/Models/BankingConnection.php +++ b/app/Models/BankingConnection.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use Carbon\Carbon; use Database\Factories\BankingConnectionFactory; use Illuminate\Database\Eloquent\Concerns\HasUuids; @@ -15,6 +16,7 @@ use Illuminate\Database\Eloquent\SoftDeletes; /** * @property bool $has_pending_accounts + * @property BankingProvider $provider * @property BankingConnectionStatus $status * @property Carbon|null $valid_until * @property Carbon|null $last_synced_at @@ -61,6 +63,7 @@ class BankingConnection extends Model protected function casts(): array { return [ + 'provider' => BankingProvider::class, 'status' => BankingConnectionStatus::class, 'valid_until' => 'datetime', 'last_synced_at' => 'datetime', @@ -103,32 +106,37 @@ class BankingConnection extends Model public function isIndexaCapital(): bool { - return $this->provider === 'indexacapital'; + return $this->provider === BankingProvider::IndexaCapital; } public function isBinance(): bool { - return $this->provider === 'binance'; + return $this->provider === BankingProvider::Binance; } public function isBitpanda(): bool { - return $this->provider === 'bitpanda'; + return $this->provider === BankingProvider::Bitpanda; } public function isCoinbase(): bool { - return $this->provider === 'coinbase'; + return $this->provider === BankingProvider::Coinbase; } public function isEnableBanking(): bool { - return $this->provider === 'enablebanking'; + return $this->provider === BankingProvider::EnableBanking; + } + + public function usesApiKey(): bool + { + return $this->provider->usesApiKey(); } public function isWise(): bool { - return $this->provider === 'wise'; + return $this->provider === BankingProvider::Wise; } public function hasPendingAccounts(): bool diff --git a/database/factories/BankingConnectionFactory.php b/database/factories/BankingConnectionFactory.php index e6614624..5d87bc88 100644 --- a/database/factories/BankingConnectionFactory.php +++ b/database/factories/BankingConnectionFactory.php @@ -3,6 +3,7 @@ namespace Database\Factories; use App\Enums\BankingConnectionStatus; +use App\Enums\BankingProvider; use App\Models\BankingConnection; use App\Models\User; use Illuminate\Database\Eloquent\Factories\Factory; @@ -21,7 +22,7 @@ class BankingConnectionFactory extends Factory { return [ 'user_id' => User::factory(), - 'provider' => 'enablebanking', + 'provider' => BankingProvider::EnableBanking, 'authorization_id' => fake()->uuid(), 'session_id' => fake()->uuid(), 'aspsp_name' => fake()->company(), @@ -76,7 +77,7 @@ class BankingConnectionFactory extends Factory public function indexaCapital(): static { return $this->state(fn (array $attributes) => [ - 'provider' => 'indexacapital', + 'provider' => BankingProvider::IndexaCapital, 'authorization_id' => null, 'session_id' => null, 'api_token' => 'test-indexa-token-'.fake()->uuid(), @@ -90,7 +91,7 @@ class BankingConnectionFactory extends Factory public function binance(): static { return $this->state(fn (array $attributes) => [ - 'provider' => 'binance', + 'provider' => BankingProvider::Binance, 'authorization_id' => null, 'session_id' => null, 'api_token' => 'test-binance-api-key-'.fake()->uuid(), @@ -105,7 +106,7 @@ class BankingConnectionFactory extends Factory public function bitpanda(): static { return $this->state(fn (array $attributes) => [ - 'provider' => 'bitpanda', + 'provider' => BankingProvider::Bitpanda, 'authorization_id' => null, 'session_id' => null, 'api_token' => 'test-bitpanda-api-key-'.fake()->uuid(), @@ -120,7 +121,7 @@ class BankingConnectionFactory extends Factory public function coinbase(): static { return $this->state(fn (array $attributes) => [ - 'provider' => 'coinbase', + 'provider' => BankingProvider::Coinbase, 'authorization_id' => null, 'session_id' => null, 'api_token' => 'organizations/org-'.fake()->uuid().'/apiKeys/key-'.fake()->uuid(), @@ -135,7 +136,7 @@ class BankingConnectionFactory extends Factory public function wise(): static { return $this->state(fn (array $attributes) => [ - 'provider' => 'wise', + 'provider' => BankingProvider::Wise, 'authorization_id' => null, 'session_id' => null, 'api_token' => 'test-wise-api-token-'.fake()->uuid(), diff --git a/tests/Unit/Enums/BankingProviderTest.php b/tests/Unit/Enums/BankingProviderTest.php new file mode 100644 index 00000000..b8076d1b --- /dev/null +++ b/tests/Unit/Enums/BankingProviderTest.php @@ -0,0 +1,34 @@ +usesApiKey())->toBeTrue(); +})->with([ + 'indexa capital' => BankingProvider::IndexaCapital, + 'binance' => BankingProvider::Binance, + 'bitpanda' => BankingProvider::Bitpanda, + 'coinbase' => BankingProvider::Coinbase, + 'wise' => BankingProvider::Wise, +]); + +it('does not use an API key for EnableBanking', function () { + expect(BankingProvider::EnableBanking->usesApiKey())->toBeFalse(); +}); + +it('defaults investment providers to an investment account', function (BankingProvider $provider) { + expect($provider->defaultAccountType())->toBe(AccountType::Investment); +})->with([ + 'indexa capital' => BankingProvider::IndexaCapital, + 'binance' => BankingProvider::Binance, + 'bitpanda' => BankingProvider::Bitpanda, + 'coinbase' => BankingProvider::Coinbase, +]); + +it('defaults cash providers to a checking account', function (BankingProvider $provider) { + expect($provider->defaultAccountType())->toBe(AccountType::Checking); +})->with([ + 'wise' => BankingProvider::Wise, + 'enable banking' => BankingProvider::EnableBanking, +]); diff --git a/tests/Unit/Enums/LocaleTest.php b/tests/Unit/Enums/LocaleTest.php new file mode 100644 index 00000000..63840045 --- /dev/null +++ b/tests/Unit/Enums/LocaleTest.php @@ -0,0 +1,16 @@ +toBe($expected); +})->with([ + 'spanish with region' => ['es-ES,es;q=0.9', Locale::Spanish], + 'spanish bare' => ['es', Locale::Spanish], + 'french with region' => ['fr-FR,fr;q=0.9,en;q=0.8', Locale::French], + 'french bare' => ['fr', Locale::French], + 'english' => ['en-US,en;q=0.9', Locale::English], + 'unsupported falls back to english' => ['de-DE,de;q=0.9', Locale::English], + 'empty falls back to english' => ['', Locale::English], + 'null falls back to english' => [null, Locale::English], +]);