chore: Remove account-mapping feature flag (#252)
## Summary - Removes the `account-mapping` Pennant feature flag entirely, making the account mapping flow (pending accounts data + map-accounts page) the default and only code path - Removes the old direct-creation branches from all banking controllers (Authorization, Bitpanda, Binance, IndexaCapital) - Extracts a shared `CreatesAccountsFromPending` trait for the onboarding auto-create logic - Updates all controller tests to reflect the always-on mapping behavior ## Changes ### Backend - **AppServiceProvider** — removed `account-mapping` flag definition - **AuthorizationController** — removed flag check + `createAccountsFromSession()` dead code; always stores `pending_accounts_data` and redirects to mapping - **BitpandaController / BinanceController / IndexaCapitalController** — removed flag checks, old inline account creation, and unused imports - **HandleInertiaRequests / ActivateDevelopmentFeatures / ResolvesFeatures** — removed `account-mapping` from flag arrays - **New `CreatesAccountsFromPending` trait** — shared auto-create logic for onboarding path (to be consumed next) ### Frontend - Removed `'account-mapping'` from the TypeScript `Features` interface ### Tests - Merged flag-specific test variants into single always-on tests - Removed redundant tests that tested the old disabled-flag code path - Updated assertions to check `pending_accounts_data` instead of direct account creation
This commit is contained in:
parent
ca189a565b
commit
c42a48a952
|
|
@ -49,6 +49,6 @@ trait ResolvesFeatures
|
|||
|
||||
private function getStringBasedFeatures(): array
|
||||
{
|
||||
return ['open-banking', 'account-mapping', 'real-estate'];
|
||||
return ['open-banking', 'real-estate'];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,19 @@ 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;
|
||||
use App\Http\Requests\OpenBanking\MapAccountsRequest;
|
||||
use App\Jobs\SyncBankingConnectionJob;
|
||||
use App\Models\Bank;
|
||||
use App\Models\BankingConnection;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class AccountMappingController extends Controller
|
||||
{
|
||||
use CreatesAccountsFromPending;
|
||||
|
||||
public function show(BankingConnection $connection): Response|RedirectResponse
|
||||
{
|
||||
if ($connection->user_id !== auth()->id()) {
|
||||
|
|
@ -33,7 +35,7 @@ class AccountMappingController extends Controller
|
|||
|
||||
// During onboarding, skip the mapping UI — auto-create all accounts directly
|
||||
if (! $user->isOnboarded()) {
|
||||
$this->autoCreateAccounts($user, $connection);
|
||||
$this->createAccountsFromPending($user, $connection);
|
||||
SyncBankingConnectionJob::dispatch($connection);
|
||||
|
||||
return redirect()->route('onboarding', ['step' => 'create-account'])
|
||||
|
|
@ -74,7 +76,7 @@ class AccountMappingController extends Controller
|
|||
$pendingAccounts = collect($connection->pending_accounts_data)
|
||||
->keyBy('uid');
|
||||
|
||||
$accountType = ($connection->isIndexaCapital() || $connection->isBinance())
|
||||
$accountType = ($connection->isIndexaCapital() || $connection->isBinance() || $connection->isBitpanda())
|
||||
? AccountType::Investment
|
||||
: AccountType::Checking;
|
||||
|
||||
|
|
@ -102,6 +104,7 @@ class AccountMappingController extends Controller
|
|||
'type' => $accountType->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => $uid,
|
||||
'iban' => $accountData['account_id']['iban'] ?? null,
|
||||
]);
|
||||
} elseif ($action === 'link') {
|
||||
$existingAccount = $user->accounts()->find($mapping['existing_account_id']);
|
||||
|
|
@ -110,6 +113,7 @@ class AccountMappingController extends Controller
|
|||
$existingAccount->update([
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => $uid,
|
||||
'iban' => $accountData['account_id']['iban'] ?? $existingAccount->iban,
|
||||
'bank_id' => $bank->id,
|
||||
'linked_at' => now(),
|
||||
]);
|
||||
|
|
@ -130,52 +134,4 @@ class AccountMappingController extends Controller
|
|||
return redirect()->route($successRedirect, $redirectParams)
|
||||
->with('success', 'Bank account connected successfully.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-create all pending accounts without user interaction.
|
||||
*/
|
||||
private function autoCreateAccounts(User $user, BankingConnection $connection): void
|
||||
{
|
||||
$bank = Bank::firstOrCreate(
|
||||
['name' => $connection->aspsp_name, 'user_id' => null],
|
||||
['name' => $connection->aspsp_name, 'logo' => $connection->aspsp_logo],
|
||||
);
|
||||
|
||||
if (! $bank->logo && $connection->aspsp_logo) {
|
||||
$bank->update(['logo' => $connection->aspsp_logo]);
|
||||
}
|
||||
|
||||
$accountType = ($connection->isIndexaCapital() || $connection->isBinance())
|
||||
? AccountType::Investment
|
||||
: AccountType::Checking;
|
||||
|
||||
foreach ($connection->pending_accounts_data ?? [] as $accountData) {
|
||||
$uid = $accountData['uid'] ?? null;
|
||||
|
||||
if (! $uid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currency = $accountData['currency'] ?? 'EUR';
|
||||
$name = $accountData['name']
|
||||
?? $accountData['account_id']['iban']
|
||||
?? $connection->aspsp_name.' Account';
|
||||
|
||||
$user->accounts()->create([
|
||||
'name' => $name,
|
||||
'name_iv' => null,
|
||||
'encrypted' => false,
|
||||
'bank_id' => $bank->id,
|
||||
'currency_code' => $currency,
|
||||
'type' => $accountType->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => $uid,
|
||||
]);
|
||||
}
|
||||
|
||||
$connection->update([
|
||||
'status' => BankingConnectionStatus::Active,
|
||||
'pending_accounts_data' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,21 +3,21 @@
|
|||
namespace App\Http\Controllers\OpenBanking;
|
||||
|
||||
use App\Contracts\BankingProviderInterface;
|
||||
use App\Enums\AccountType;
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\OpenBanking\Concerns\CreatesAccountsFromPending;
|
||||
use App\Http\Requests\OpenBanking\StartAuthorizationRequest;
|
||||
use App\Jobs\SyncBankingConnectionJob;
|
||||
use App\Models\Bank;
|
||||
use App\Models\BankingConnection;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
class AuthorizationController extends Controller
|
||||
{
|
||||
use CreatesAccountsFromPending;
|
||||
|
||||
/**
|
||||
* Start the bank authorization flow.
|
||||
*/
|
||||
|
|
@ -159,40 +159,22 @@ class AuthorizationController extends Controller
|
|||
->with('success', __('Bank account reconnected successfully.'));
|
||||
}
|
||||
|
||||
if (Feature::for($user)->active('account-mapping')) {
|
||||
$connection->update([
|
||||
'session_id' => $sessionData['session_id'],
|
||||
'status' => BankingConnectionStatus::AwaitingMapping,
|
||||
'valid_until' => $sessionData['access']['valid_until'] ?? null,
|
||||
'pending_accounts_data' => $sessionData['accounts'],
|
||||
]);
|
||||
|
||||
if (! $user->isOnboarded()) {
|
||||
$this->createAccountsFromPending($user, $connection);
|
||||
SyncBankingConnectionJob::dispatch($connection);
|
||||
|
||||
return redirect()->route('onboarding', ['step' => 'create-account'])
|
||||
->with('success', 'Bank account connected successfully.');
|
||||
}
|
||||
|
||||
return redirect()->route('open-banking.map-accounts', $connection);
|
||||
}
|
||||
|
||||
$connection->update([
|
||||
'session_id' => $sessionData['session_id'],
|
||||
'status' => BankingConnectionStatus::Active,
|
||||
'status' => BankingConnectionStatus::AwaitingMapping,
|
||||
'valid_until' => $sessionData['access']['valid_until'] ?? null,
|
||||
'pending_accounts_data' => $sessionData['accounts'],
|
||||
]);
|
||||
|
||||
$this->createAccountsFromSession($user, $connection, $sessionData);
|
||||
if (! $user->isOnboarded()) {
|
||||
$this->createAccountsFromPending($user, $connection);
|
||||
SyncBankingConnectionJob::dispatch($connection);
|
||||
|
||||
SyncBankingConnectionJob::dispatch($connection);
|
||||
return redirect()->route('onboarding', ['step' => 'create-account'])
|
||||
->with('success', 'Bank account connected successfully.');
|
||||
}
|
||||
|
||||
$successRedirect = $user->isOnboarded() ? 'settings.connections.index' : 'onboarding';
|
||||
$redirectParams = $user->isOnboarded() ? [] : ['step' => 'create-account'];
|
||||
|
||||
return redirect()->route($successRedirect, $redirectParams)
|
||||
->with('success', 'Bank account connected successfully.');
|
||||
return redirect()->route('open-banking.map-accounts', $connection);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -249,95 +231,4 @@ class AuthorizationController extends Controller
|
|||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-create accounts from pending_accounts_data without user interaction.
|
||||
*/
|
||||
private function createAccountsFromPending($user, BankingConnection $connection): void
|
||||
{
|
||||
$bank = Bank::firstOrCreate(
|
||||
['name' => $connection->aspsp_name, 'user_id' => null],
|
||||
['name' => $connection->aspsp_name, 'logo' => $connection->aspsp_logo],
|
||||
);
|
||||
|
||||
if (! $bank->logo && $connection->aspsp_logo) {
|
||||
$bank->update(['logo' => $connection->aspsp_logo]);
|
||||
}
|
||||
|
||||
foreach ($connection->pending_accounts_data ?? [] as $accountData) {
|
||||
$uid = $accountData['uid'] ?? null;
|
||||
|
||||
if (! $uid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currency = $accountData['currency'] ?? 'EUR';
|
||||
$name = $accountData['name']
|
||||
?? $accountData['account_id']['iban']
|
||||
?? $connection->aspsp_name.' Account';
|
||||
|
||||
$user->accounts()->create([
|
||||
'name' => $name,
|
||||
'name_iv' => null,
|
||||
'encrypted' => false,
|
||||
'bank_id' => $bank->id,
|
||||
'currency_code' => $currency,
|
||||
'type' => AccountType::Checking->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => $uid,
|
||||
'iban' => $accountData['account_id']['iban'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create local accounts from the EnableBanking session data.
|
||||
*/
|
||||
private function createAccountsFromSession($user, BankingConnection $connection, array $sessionData): void
|
||||
{
|
||||
$bank = Bank::firstOrCreate(
|
||||
['name' => $connection->aspsp_name, 'user_id' => null],
|
||||
['name' => $connection->aspsp_name, 'logo' => $connection->aspsp_logo],
|
||||
);
|
||||
|
||||
if (! $bank->logo && $connection->aspsp_logo) {
|
||||
$bank->update(['logo' => $connection->aspsp_logo]);
|
||||
}
|
||||
|
||||
$accounts = $sessionData['accounts'] ?? [];
|
||||
|
||||
foreach ($accounts as $accountData) {
|
||||
$uid = $accountData['uid'] ?? null;
|
||||
|
||||
if (! $uid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingAccount = $user->accounts()
|
||||
->where('banking_connection_id', $connection->id)
|
||||
->where('external_account_id', $uid)
|
||||
->first();
|
||||
|
||||
if ($existingAccount) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currency = $accountData['currency'] ?? 'EUR';
|
||||
$name = $accountData['name']
|
||||
?? $accountData['account_id']['iban']
|
||||
?? $connection->aspsp_name.' Account';
|
||||
|
||||
$user->accounts()->create([
|
||||
'name' => $name,
|
||||
'name_iv' => null,
|
||||
'encrypted' => false,
|
||||
'bank_id' => $bank->id,
|
||||
'currency_code' => $currency,
|
||||
'type' => AccountType::Checking->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => $uid,
|
||||
'iban' => $accountData['account_id']['iban'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,20 @@
|
|||
|
||||
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;
|
||||
use App\Http\Requests\OpenBanking\ConnectBinanceRequest;
|
||||
use App\Jobs\SyncBankingConnectionJob;
|
||||
use App\Models\Bank;
|
||||
use App\Services\Banking\BinanceClient;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
class BinanceController extends Controller
|
||||
{
|
||||
use CreatesAccountsFromPending;
|
||||
|
||||
/**
|
||||
* Validate Binance API credentials and create a connection.
|
||||
*/
|
||||
|
|
@ -58,38 +59,23 @@ class BinanceController extends Controller
|
|||
],
|
||||
];
|
||||
|
||||
if (Feature::for($user)->active('account-mapping')) {
|
||||
$connection->update([
|
||||
'status' => BankingConnectionStatus::AwaitingMapping,
|
||||
'pending_accounts_data' => $pendingAccounts,
|
||||
]);
|
||||
$connection->update([
|
||||
'status' => BankingConnectionStatus::AwaitingMapping,
|
||||
'pending_accounts_data' => $pendingAccounts,
|
||||
]);
|
||||
|
||||
if (! $user->isOnboarded()) {
|
||||
$this->createAccountsFromPending($user, $connection);
|
||||
SyncBankingConnectionJob::dispatch($connection);
|
||||
|
||||
return response()->json([
|
||||
'redirect_url' => route('open-banking.map-accounts', $connection),
|
||||
'redirect_url' => route('onboarding', ['step' => 'create-account']),
|
||||
'connection_id' => $connection->id,
|
||||
]);
|
||||
}
|
||||
|
||||
$connection->update(['status' => BankingConnectionStatus::Active]);
|
||||
|
||||
$user->accounts()->create([
|
||||
'name' => 'Crypto Portfolio',
|
||||
'name_iv' => null,
|
||||
'encrypted' => false,
|
||||
'bank_id' => $bank->id,
|
||||
'currency_code' => $user->currency_code,
|
||||
'type' => AccountType::Investment->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => 'binance-portfolio',
|
||||
]);
|
||||
|
||||
SyncBankingConnectionJob::dispatch($connection);
|
||||
|
||||
$successRedirect = $user->isOnboarded() ? 'settings.connections.index' : 'onboarding';
|
||||
$redirectParams = $user->isOnboarded() ? [] : ['step' => 'create-account'];
|
||||
|
||||
return response()->json([
|
||||
'redirect_url' => route($successRedirect, $redirectParams),
|
||||
'redirect_url' => route('open-banking.map-accounts', $connection),
|
||||
'connection_id' => $connection->id,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,20 @@
|
|||
|
||||
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;
|
||||
use App\Http\Requests\OpenBanking\ConnectBitpandaRequest;
|
||||
use App\Jobs\SyncBankingConnectionJob;
|
||||
use App\Models\Bank;
|
||||
use App\Services\Banking\BitpandaClient;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
class BitpandaController extends Controller
|
||||
{
|
||||
use CreatesAccountsFromPending;
|
||||
|
||||
/**
|
||||
* Validate Bitpanda API key and create a connection.
|
||||
*/
|
||||
|
|
@ -57,38 +58,23 @@ class BitpandaController extends Controller
|
|||
],
|
||||
];
|
||||
|
||||
if (Feature::for($user)->active('account-mapping')) {
|
||||
$connection->update([
|
||||
'status' => BankingConnectionStatus::AwaitingMapping,
|
||||
'pending_accounts_data' => $pendingAccounts,
|
||||
]);
|
||||
$connection->update([
|
||||
'status' => BankingConnectionStatus::AwaitingMapping,
|
||||
'pending_accounts_data' => $pendingAccounts,
|
||||
]);
|
||||
|
||||
if (! $user->isOnboarded()) {
|
||||
$this->createAccountsFromPending($user, $connection);
|
||||
SyncBankingConnectionJob::dispatch($connection);
|
||||
|
||||
return response()->json([
|
||||
'redirect_url' => route('open-banking.map-accounts', $connection),
|
||||
'redirect_url' => route('onboarding', ['step' => 'create-account']),
|
||||
'connection_id' => $connection->id,
|
||||
]);
|
||||
}
|
||||
|
||||
$connection->update(['status' => BankingConnectionStatus::Active]);
|
||||
|
||||
$user->accounts()->create([
|
||||
'name' => 'Crypto Portfolio',
|
||||
'name_iv' => null,
|
||||
'encrypted' => false,
|
||||
'bank_id' => $bank->id,
|
||||
'currency_code' => $user->currency_code,
|
||||
'type' => AccountType::Investment->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => 'bitpanda-portfolio',
|
||||
]);
|
||||
|
||||
SyncBankingConnectionJob::dispatch($connection);
|
||||
|
||||
$successRedirect = $user->isOnboarded() ? 'settings.connections.index' : 'onboarding';
|
||||
$redirectParams = $user->isOnboarded() ? [] : ['step' => 'create-account'];
|
||||
|
||||
return response()->json([
|
||||
'redirect_url' => route($successRedirect, $redirectParams),
|
||||
'redirect_url' => route('open-banking.map-accounts', $connection),
|
||||
'connection_id' => $connection->id,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\OpenBanking\Concerns;
|
||||
|
||||
use App\Enums\AccountType;
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Models\Bank;
|
||||
use App\Models\BankingConnection;
|
||||
use App\Models\User;
|
||||
|
||||
trait CreatesAccountsFromPending
|
||||
{
|
||||
/**
|
||||
* Auto-create all pending accounts from a banking connection without user interaction.
|
||||
*
|
||||
* This resolves the correct account type based on the provider (Investment for
|
||||
* Indexa Capital, Binance, and Bitpanda; Checking for everything else) and clears the
|
||||
* pending data once accounts have been created.
|
||||
*/
|
||||
private function createAccountsFromPending(User $user, BankingConnection $connection): void
|
||||
{
|
||||
$bank = Bank::firstOrCreate(
|
||||
['name' => $connection->aspsp_name, 'user_id' => null],
|
||||
['name' => $connection->aspsp_name, 'logo' => $connection->aspsp_logo],
|
||||
);
|
||||
|
||||
if (! $bank->logo && $connection->aspsp_logo) {
|
||||
$bank->update(['logo' => $connection->aspsp_logo]);
|
||||
}
|
||||
|
||||
$accountType = ($connection->isIndexaCapital() || $connection->isBinance() || $connection->isBitpanda())
|
||||
? AccountType::Investment
|
||||
: AccountType::Checking;
|
||||
|
||||
foreach ($connection->pending_accounts_data ?? [] as $accountData) {
|
||||
$uid = $accountData['uid'] ?? null;
|
||||
|
||||
if (! $uid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$currency = $accountData['currency'] ?? 'EUR';
|
||||
$name = $accountData['name']
|
||||
?? $accountData['account_id']['iban']
|
||||
?? $connection->aspsp_name.' Account';
|
||||
|
||||
$user->accounts()->create([
|
||||
'name' => $name,
|
||||
'name_iv' => null,
|
||||
'encrypted' => false,
|
||||
'bank_id' => $bank->id,
|
||||
'currency_code' => $currency,
|
||||
'type' => $accountType->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => $uid,
|
||||
'iban' => $accountData['account_id']['iban'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
$connection->update([
|
||||
'status' => BankingConnectionStatus::Active,
|
||||
'pending_accounts_data' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,19 +2,20 @@
|
|||
|
||||
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;
|
||||
use App\Http\Requests\OpenBanking\ConnectIndexaCapitalRequest;
|
||||
use App\Jobs\SyncBankingConnectionJob;
|
||||
use App\Models\Bank;
|
||||
use App\Services\Banking\IndexaCapitalClient;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Laravel\Pennant\Feature;
|
||||
|
||||
class IndexaCapitalController extends Controller
|
||||
{
|
||||
use CreatesAccountsFromPending;
|
||||
|
||||
/**
|
||||
* Validate the Indexa Capital API token and create a connection.
|
||||
*/
|
||||
|
|
@ -51,40 +52,23 @@ class IndexaCapitalController extends Controller
|
|||
|
||||
$pendingAccounts = $this->buildPendingAccounts($userData);
|
||||
|
||||
if (Feature::for($user)->active('account-mapping')) {
|
||||
$connection->update([
|
||||
'status' => BankingConnectionStatus::AwaitingMapping,
|
||||
'pending_accounts_data' => $pendingAccounts,
|
||||
]);
|
||||
$connection->update([
|
||||
'status' => BankingConnectionStatus::AwaitingMapping,
|
||||
'pending_accounts_data' => $pendingAccounts,
|
||||
]);
|
||||
|
||||
if (! $user->isOnboarded()) {
|
||||
$this->createAccountsFromPending($user, $connection);
|
||||
SyncBankingConnectionJob::dispatch($connection);
|
||||
|
||||
return response()->json([
|
||||
'redirect_url' => route('open-banking.map-accounts', $connection),
|
||||
'redirect_url' => route('onboarding', ['step' => 'create-account']),
|
||||
'connection_id' => $connection->id,
|
||||
]);
|
||||
}
|
||||
|
||||
$connection->update(['status' => BankingConnectionStatus::Active]);
|
||||
|
||||
foreach ($pendingAccounts as $accountData) {
|
||||
$user->accounts()->create([
|
||||
'name' => $accountData['name'],
|
||||
'name_iv' => null,
|
||||
'encrypted' => false,
|
||||
'bank_id' => $bank->id,
|
||||
'currency_code' => $accountData['currency'],
|
||||
'type' => AccountType::Investment->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => $accountData['uid'],
|
||||
]);
|
||||
}
|
||||
|
||||
SyncBankingConnectionJob::dispatch($connection);
|
||||
|
||||
$successRedirect = $user->isOnboarded() ? 'settings.connections.index' : 'onboarding';
|
||||
$redirectParams = $user->isOnboarded() ? [] : ['step' => 'create-account'];
|
||||
|
||||
return response()->json([
|
||||
'redirect_url' => route($successRedirect, $redirectParams),
|
||||
'redirect_url' => route('open-banking.map-accounts', $connection),
|
||||
'connection_id' => $connection->id,
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ class ActivateDevelopmentFeatures
|
|||
if (app()->isLocal() && $request->user()) {
|
||||
Feature::for($request->user())->activate([
|
||||
'open-banking',
|
||||
'account-mapping',
|
||||
'real-estate',
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ class HandleInertiaRequests extends Middleware
|
|||
*/
|
||||
protected function resolveFeatureFlags(?object $user): array
|
||||
{
|
||||
$flags = ['open-banking', 'account-mapping', 'real-estate'];
|
||||
$flags = ['open-banking', 'real-estate'];
|
||||
|
||||
if (! $user) {
|
||||
return ['cashflow' => true, ...array_fill_keys($flags, false)];
|
||||
|
|
|
|||
|
|
@ -136,6 +136,8 @@ class SyncBankingConnectionJob implements ShouldBeUnique, ShouldQueue
|
|||
}
|
||||
|
||||
$this->fail($e);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ class AppServiceProvider extends ServiceProvider
|
|||
});
|
||||
|
||||
Feature::define('open-banking', fn (User $user) => false);
|
||||
Feature::define('account-mapping', fn (User $user) => false);
|
||||
Feature::define('real-estate', fn (User $user) => false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,6 @@ export interface NavDivider {
|
|||
export interface Features {
|
||||
cashflow: boolean;
|
||||
'open-banking': boolean;
|
||||
'account-mapping': boolean;
|
||||
'real-estate': boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,6 +105,7 @@ test('store with action create creates new accounts', function () {
|
|||
'external_account_id' => 'ext-1',
|
||||
'name' => 'Test Checking',
|
||||
'currency_code' => 'EUR',
|
||||
'iban' => 'ES1234567890',
|
||||
]);
|
||||
|
||||
$connection->refresh();
|
||||
|
|
@ -114,6 +115,49 @@ test('store with action create creates new accounts', function () {
|
|||
Queue::assertPushed(SyncBankingConnectionJob::class);
|
||||
});
|
||||
|
||||
test('store creates investment accounts for bitpanda connections', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create();
|
||||
Feature::for($user)->activate('open-banking');
|
||||
|
||||
$connection = BankingConnection::factory()->awaitingMapping()->create([
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'bitpanda',
|
||||
'aspsp_name' => 'Bitpanda',
|
||||
'pending_accounts_data' => [
|
||||
[
|
||||
'uid' => 'bitpanda-portfolio',
|
||||
'currency' => 'EUR',
|
||||
'name' => 'Crypto Portfolio',
|
||||
'account_id' => [],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->post(route('open-banking.map-accounts.store', $connection), [
|
||||
'mappings' => [
|
||||
[
|
||||
'bank_account_uid' => 'bitpanda-portfolio',
|
||||
'action' => 'create',
|
||||
'existing_account_id' => null,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('settings.connections.index'));
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => 'bitpanda-portfolio',
|
||||
'type' => 'investment',
|
||||
]);
|
||||
|
||||
Queue::assertPushed(SyncBankingConnectionJob::class);
|
||||
});
|
||||
|
||||
test('store with action link links existing account', function () {
|
||||
Queue::fake();
|
||||
|
||||
|
|
@ -158,6 +202,7 @@ test('store with action link links existing account', function () {
|
|||
expect($existingAccount->banking_connection_id)->toBe($connection->id);
|
||||
expect($existingAccount->external_account_id)->toBe('ext-1');
|
||||
expect($existingAccount->linked_at)->not->toBeNull();
|
||||
expect($existingAccount->iban)->toBe('ES1234567890');
|
||||
|
||||
Queue::assertPushed(SyncBankingConnectionJob::class);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ test('callback without code redirects with error', function () {
|
|||
$response->assertSessionHas('error');
|
||||
});
|
||||
|
||||
test('callback with valid code creates accounts directly when account-mapping is disabled', function () {
|
||||
test('callback with valid code stores pending accounts and redirects to mapping', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create();
|
||||
|
|
@ -148,57 +148,6 @@ test('callback with valid code creates accounts directly when account-mapping is
|
|||
'aspsp_country' => 'ES',
|
||||
]);
|
||||
|
||||
$mockProvider = Mockery::mock(BankingProviderInterface::class);
|
||||
$mockProvider->shouldReceive('createSession')
|
||||
->with('test-code')
|
||||
->once()
|
||||
->andReturn([
|
||||
'session_id' => 'session-123',
|
||||
'accounts' => [
|
||||
[
|
||||
'uid' => 'ext-account-1',
|
||||
'currency' => 'EUR',
|
||||
'name' => 'My Checking Account',
|
||||
'account_id' => ['iban' => 'ES1234567890123456789012'],
|
||||
],
|
||||
],
|
||||
'aspsp' => ['name' => 'Test Bank', 'country' => 'ES'],
|
||||
'access' => ['valid_until' => now()->addDays(90)->toIso8601String()],
|
||||
]);
|
||||
|
||||
$this->app->instance(BankingProviderInterface::class, $mockProvider);
|
||||
|
||||
$response = $this->actingAs($user)->get('/open-banking/callback?code=test-code');
|
||||
|
||||
$response->assertRedirect(route('settings.connections.index'));
|
||||
|
||||
$connection->refresh();
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::Active);
|
||||
expect($connection->session_id)->toBe('session-123');
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => 'ext-account-1',
|
||||
'encrypted' => false,
|
||||
]);
|
||||
|
||||
Queue::assertPushed(SyncBankingConnectionJob::class);
|
||||
});
|
||||
|
||||
test('callback with valid code stores pending accounts and redirects to mapping when account-mapping is enabled', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create();
|
||||
Feature::for($user)->activate('open-banking');
|
||||
Feature::for($user)->activate('account-mapping');
|
||||
|
||||
$connection = BankingConnection::factory()->pending()->create([
|
||||
'user_id' => $user->id,
|
||||
'aspsp_name' => 'Test Bank',
|
||||
'aspsp_country' => 'ES',
|
||||
]);
|
||||
|
||||
$accounts = [
|
||||
[
|
||||
'uid' => 'ext-account-1',
|
||||
|
|
@ -408,12 +357,11 @@ test('callback with existing accounts updates session without creating new accou
|
|||
Queue::assertPushed(SyncBankingConnectionJob::class);
|
||||
});
|
||||
|
||||
test('callback with existing accounts skips account-mapping even when feature flag is enabled', function () {
|
||||
test('callback with existing accounts skips mapping on reconnect', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create();
|
||||
Feature::for($user)->activate('open-banking');
|
||||
Feature::for($user)->activate('account-mapping');
|
||||
|
||||
$connection = BankingConnection::factory()->pending()->create([
|
||||
'user_id' => $user->id,
|
||||
|
|
@ -618,7 +566,7 @@ test('reconnect callback uses positional fallback for accounts without stored ib
|
|||
expect($accountB->refresh()->iban)->toBe('ES0000000000000000000002');
|
||||
});
|
||||
|
||||
test('callback stores iban when creating accounts for the first time', function () {
|
||||
test('callback stores iban in pending accounts data', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create();
|
||||
|
|
@ -651,9 +599,7 @@ test('callback stores iban when creating accounts for the first time', function
|
|||
|
||||
$this->actingAs($user)->get('/open-banking/callback?code=test-code');
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'external_account_id' => 'ext-account-1',
|
||||
'iban' => 'ES9999999999999999999999',
|
||||
]);
|
||||
$connection->refresh();
|
||||
expect($connection->pending_accounts_data)->toHaveCount(1);
|
||||
expect($connection->pending_accounts_data[0]['account_id']['iban'])->toBe('ES9999999999999999999999');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\AccountType;
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Jobs\SyncBankingConnectionJob;
|
||||
use App\Models\BankingConnection;
|
||||
|
|
@ -32,23 +31,19 @@ test('users can connect a binance account with valid credentials', function () {
|
|||
$response->assertOk();
|
||||
$response->assertJsonStructure(['redirect_url', 'connection_id']);
|
||||
|
||||
$this->assertDatabaseHas('banking_connections', [
|
||||
$connection = BankingConnection::where('user_id', $user->id)->where('provider', 'binance')->first();
|
||||
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::AwaitingMapping);
|
||||
expect($connection->pending_accounts_data)->toHaveCount(1);
|
||||
expect($connection->pending_accounts_data[0]['uid'])->toBe('binance-portfolio');
|
||||
expect($connection->pending_accounts_data[0]['name'])->toBe('Crypto Portfolio');
|
||||
|
||||
$this->assertDatabaseMissing('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'binance',
|
||||
'aspsp_name' => 'Binance',
|
||||
'aspsp_country' => 'ES',
|
||||
'status' => BankingConnectionStatus::Active->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'external_account_id' => 'binance-portfolio',
|
||||
'type' => AccountType::Investment->value,
|
||||
'currency_code' => 'EUR',
|
||||
'name' => 'Crypto Portfolio',
|
||||
]);
|
||||
|
||||
Queue::assertPushed(SyncBankingConnectionJob::class);
|
||||
Queue::assertNothingPushed();
|
||||
});
|
||||
|
||||
test('invalid binance credentials return 422', function () {
|
||||
|
|
@ -74,44 +69,6 @@ test('invalid binance credentials return 422', function () {
|
|||
]);
|
||||
});
|
||||
|
||||
test('binance connection with account-mapping flag returns mapping redirect', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create(['currency_code' => 'EUR']);
|
||||
Feature::for($user)->activate('open-banking');
|
||||
Feature::for($user)->activate('account-mapping');
|
||||
|
||||
Http::fake([
|
||||
'api.binance.com/api/v3/account*' => Http::response([
|
||||
'balances' => [
|
||||
['asset' => 'BTC', 'free' => '1.0', 'locked' => '0.0'],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->postJson('/open-banking/binance/connect', [
|
||||
'api_key' => 'valid-test-api-key-12345',
|
||||
'api_secret' => 'valid-test-api-secret-12345',
|
||||
'country' => 'ES',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$connection = BankingConnection::where('user_id', $user->id)->where('provider', 'binance')->first();
|
||||
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::AwaitingMapping);
|
||||
expect($connection->pending_accounts_data)->toHaveCount(1);
|
||||
expect($connection->pending_accounts_data[0]['uid'])->toBe('binance-portfolio');
|
||||
expect($connection->pending_accounts_data[0]['name'])->toBe('Crypto Portfolio');
|
||||
|
||||
$this->assertDatabaseMissing('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'banking_connection_id' => $connection->id,
|
||||
]);
|
||||
|
||||
Queue::assertNothingPushed();
|
||||
});
|
||||
|
||||
test('binance requires open-banking feature flag', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
|
||||
|
|
@ -141,7 +98,7 @@ test('binance api_key and api_secret are required and must be at least 10 charac
|
|||
->assertJsonValidationErrors(['api_key', 'api_secret']);
|
||||
});
|
||||
|
||||
test('binance creates single crypto portfolio account with user currency', function () {
|
||||
test('binance stores pending accounts with user currency', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create(['currency_code' => 'USD']);
|
||||
|
|
@ -163,13 +120,47 @@ test('binance creates single crypto portfolio account with user currency', funct
|
|||
|
||||
$response->assertOk();
|
||||
|
||||
expect($user->accounts()->count())->toBe(1);
|
||||
$connection = BankingConnection::where('user_id', $user->id)->where('provider', 'binance')->first();
|
||||
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::AwaitingMapping);
|
||||
expect($connection->pending_accounts_data)->toHaveCount(1);
|
||||
expect($connection->pending_accounts_data[0]['currency'])->toBe('USD');
|
||||
});
|
||||
|
||||
test('binance auto-creates accounts during onboarding', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->notOnboarded()->create(['currency_code' => 'EUR']);
|
||||
Feature::for($user)->activate('open-banking');
|
||||
|
||||
Http::fake([
|
||||
'api.binance.com/api/v3/account*' => Http::response([
|
||||
'balances' => [
|
||||
['asset' => 'BTC', 'free' => '0.5', 'locked' => '0.0'],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->postJson('/open-banking/binance/connect', [
|
||||
'api_key' => 'valid-test-api-key-12345',
|
||||
'api_secret' => 'valid-test-api-secret-12345',
|
||||
'country' => 'ES',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('redirect_url', route('onboarding', ['step' => 'create-account']));
|
||||
|
||||
$connection = BankingConnection::where('user_id', $user->id)->where('provider', 'binance')->first();
|
||||
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::Active);
|
||||
expect($connection->pending_accounts_data)->toBeNull();
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'name' => 'Crypto Portfolio',
|
||||
'currency_code' => 'USD',
|
||||
'type' => AccountType::Investment->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => 'binance-portfolio',
|
||||
'type' => 'investment',
|
||||
]);
|
||||
|
||||
Queue::assertPushed(SyncBankingConnectionJob::class);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\AccountType;
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Jobs\SyncBankingConnectionJob;
|
||||
use App\Models\BankingConnection;
|
||||
|
|
@ -42,23 +41,19 @@ test('users can connect a bitpanda account with valid credentials', function ()
|
|||
$response->assertOk();
|
||||
$response->assertJsonStructure(['redirect_url', 'connection_id']);
|
||||
|
||||
$this->assertDatabaseHas('banking_connections', [
|
||||
$connection = BankingConnection::where('user_id', $user->id)->where('provider', 'bitpanda')->first();
|
||||
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::AwaitingMapping);
|
||||
expect($connection->pending_accounts_data)->toHaveCount(1);
|
||||
expect($connection->pending_accounts_data[0]['uid'])->toBe('bitpanda-portfolio');
|
||||
expect($connection->pending_accounts_data[0]['name'])->toBe('Crypto Portfolio');
|
||||
|
||||
$this->assertDatabaseMissing('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'bitpanda',
|
||||
'aspsp_name' => 'Bitpanda',
|
||||
'aspsp_country' => 'ES',
|
||||
'status' => BankingConnectionStatus::Active->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'external_account_id' => 'bitpanda-portfolio',
|
||||
'type' => AccountType::Investment->value,
|
||||
'currency_code' => 'EUR',
|
||||
'name' => 'Crypto Portfolio',
|
||||
]);
|
||||
|
||||
Queue::assertPushed(SyncBankingConnectionJob::class);
|
||||
Queue::assertNothingPushed();
|
||||
});
|
||||
|
||||
test('invalid bitpanda credentials return 422', function () {
|
||||
|
|
@ -83,54 +78,6 @@ test('invalid bitpanda credentials return 422', function () {
|
|||
]);
|
||||
});
|
||||
|
||||
test('bitpanda connection with account-mapping flag returns mapping redirect', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create(['currency_code' => 'EUR']);
|
||||
Feature::for($user)->activate('open-banking');
|
||||
Feature::for($user)->activate('account-mapping');
|
||||
|
||||
Http::fake([
|
||||
'api.bitpanda.com/v1/wallets' => Http::response([
|
||||
'data' => [
|
||||
[
|
||||
'type' => 'wallet',
|
||||
'attributes' => [
|
||||
'cryptocoin_id' => '1',
|
||||
'cryptocoin_symbol' => 'BTC',
|
||||
'balance' => '1.00000000',
|
||||
'is_default' => true,
|
||||
'name' => 'BTC wallet',
|
||||
'deleted' => false,
|
||||
],
|
||||
'id' => 'wallet-uuid-1',
|
||||
],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->postJson('/open-banking/bitpanda/connect', [
|
||||
'api_key' => 'valid-test-api-key-12345',
|
||||
'country' => 'ES',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$connection = BankingConnection::where('user_id', $user->id)->where('provider', 'bitpanda')->first();
|
||||
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::AwaitingMapping);
|
||||
expect($connection->pending_accounts_data)->toHaveCount(1);
|
||||
expect($connection->pending_accounts_data[0]['uid'])->toBe('bitpanda-portfolio');
|
||||
expect($connection->pending_accounts_data[0]['name'])->toBe('Crypto Portfolio');
|
||||
|
||||
$this->assertDatabaseMissing('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'banking_connection_id' => $connection->id,
|
||||
]);
|
||||
|
||||
Queue::assertNothingPushed();
|
||||
});
|
||||
|
||||
test('bitpanda requires open-banking feature flag', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
|
||||
|
|
@ -158,7 +105,7 @@ test('bitpanda api_key is required and must be at least 10 characters', function
|
|||
->assertJsonValidationErrors(['api_key']);
|
||||
});
|
||||
|
||||
test('bitpanda creates single crypto portfolio account with user currency', function () {
|
||||
test('bitpanda stores pending accounts with user currency', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create(['currency_code' => 'USD']);
|
||||
|
|
@ -190,13 +137,57 @@ test('bitpanda creates single crypto portfolio account with user currency', func
|
|||
|
||||
$response->assertOk();
|
||||
|
||||
expect($user->accounts()->count())->toBe(1);
|
||||
$connection = BankingConnection::where('user_id', $user->id)->where('provider', 'bitpanda')->first();
|
||||
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::AwaitingMapping);
|
||||
expect($connection->pending_accounts_data)->toHaveCount(1);
|
||||
expect($connection->pending_accounts_data[0]['currency'])->toBe('USD');
|
||||
});
|
||||
|
||||
test('bitpanda auto-creates accounts during onboarding', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->notOnboarded()->create(['currency_code' => 'EUR']);
|
||||
Feature::for($user)->activate('open-banking');
|
||||
|
||||
Http::fake([
|
||||
'api.bitpanda.com/v1/wallets' => Http::response([
|
||||
'data' => [
|
||||
[
|
||||
'type' => 'wallet',
|
||||
'attributes' => [
|
||||
'cryptocoin_id' => '1',
|
||||
'cryptocoin_symbol' => 'BTC',
|
||||
'balance' => '0.50000000',
|
||||
'is_default' => true,
|
||||
'name' => 'BTC wallet',
|
||||
'deleted' => false,
|
||||
],
|
||||
'id' => 'wallet-uuid-1',
|
||||
],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->postJson('/open-banking/bitpanda/connect', [
|
||||
'api_key' => 'valid-test-api-key-12345',
|
||||
'country' => 'ES',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('redirect_url', route('onboarding', ['step' => 'create-account']));
|
||||
|
||||
$connection = BankingConnection::where('user_id', $user->id)->where('provider', 'bitpanda')->first();
|
||||
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::Active);
|
||||
expect($connection->pending_accounts_data)->toBeNull();
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'name' => 'Crypto Portfolio',
|
||||
'currency_code' => 'USD',
|
||||
'type' => AccountType::Investment->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => 'bitpanda-portfolio',
|
||||
'type' => 'investment',
|
||||
]);
|
||||
|
||||
Queue::assertPushed(SyncBankingConnectionJob::class);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\AccountType;
|
||||
use App\Enums\BankingConnectionStatus;
|
||||
use App\Jobs\SyncBankingConnectionJob;
|
||||
use App\Models\Bank;
|
||||
|
|
@ -39,21 +38,18 @@ test('users can connect an indexa capital account with valid token', function ()
|
|||
$response->assertOk();
|
||||
$response->assertJsonStructure(['redirect_url', 'connection_id']);
|
||||
|
||||
$this->assertDatabaseHas('banking_connections', [
|
||||
$connection = BankingConnection::where('user_id', $user->id)->where('provider', 'indexacapital')->first();
|
||||
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::AwaitingMapping);
|
||||
expect($connection->pending_accounts_data)->toHaveCount(1);
|
||||
expect($connection->pending_accounts_data[0]['uid'])->toBe('IC-001');
|
||||
|
||||
$this->assertDatabaseMissing('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'provider' => 'indexacapital',
|
||||
'aspsp_name' => 'Indexa Capital',
|
||||
'aspsp_country' => 'ES',
|
||||
'status' => BankingConnectionStatus::Active->value,
|
||||
'banking_connection_id' => $connection->id,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'external_account_id' => 'IC-001',
|
||||
'type' => AccountType::Investment->value,
|
||||
]);
|
||||
|
||||
Queue::assertPushed(SyncBankingConnectionJob::class);
|
||||
Queue::assertNothingPushed();
|
||||
});
|
||||
|
||||
test('invalid token returns 422', function () {
|
||||
|
|
@ -77,43 +73,6 @@ test('invalid token returns 422', function () {
|
|||
]);
|
||||
});
|
||||
|
||||
test('connection with account-mapping flag returns mapping redirect', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create();
|
||||
Feature::for($user)->activate('open-banking');
|
||||
Feature::for($user)->activate('account-mapping');
|
||||
|
||||
Http::fake([
|
||||
'api.indexacapital.com/users/me' => Http::response([
|
||||
'accounts' => [
|
||||
['account_number' => 'IC-001', 'status' => 'active', 'type' => 'mutual'],
|
||||
['account_number' => 'IC-002', 'status' => 'active', 'type' => 'pension'],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->postJson('/open-banking/indexa-capital/connect', [
|
||||
'api_token' => 'valid-test-token-12345',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$connection = BankingConnection::where('user_id', $user->id)->where('provider', 'indexacapital')->first();
|
||||
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::AwaitingMapping);
|
||||
expect($connection->pending_accounts_data)->toHaveCount(2);
|
||||
expect($connection->pending_accounts_data[0]['uid'])->toBe('IC-001');
|
||||
expect($connection->pending_accounts_data[1]['uid'])->toBe('IC-002');
|
||||
|
||||
$this->assertDatabaseMissing('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'banking_connection_id' => $connection->id,
|
||||
]);
|
||||
|
||||
Queue::assertNothingPushed();
|
||||
});
|
||||
|
||||
test('requires open-banking feature flag', function () {
|
||||
$user = User::factory()->onboarded()->create();
|
||||
|
||||
|
|
@ -139,7 +98,7 @@ test('api_token is required and must be at least 10 characters', function () {
|
|||
->assertJsonValidationErrors(['api_token']);
|
||||
});
|
||||
|
||||
test('creates multiple accounts for multiple indexa portfolios', function () {
|
||||
test('stores multiple pending accounts for multiple indexa portfolios', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->onboarded()->create();
|
||||
|
|
@ -160,16 +119,56 @@ test('creates multiple accounts for multiple indexa portfolios', function () {
|
|||
|
||||
$response->assertOk();
|
||||
|
||||
expect($user->accounts()->where('type', AccountType::Investment->value)->count())->toBe(2);
|
||||
$connection = BankingConnection::where('user_id', $user->id)->where('provider', 'indexacapital')->first();
|
||||
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::AwaitingMapping);
|
||||
expect($connection->pending_accounts_data)->toHaveCount(2);
|
||||
expect($connection->pending_accounts_data[0]['uid'])->toBe('IC-001');
|
||||
expect($connection->pending_accounts_data[0]['name'])->toBe('Investment Portfolio (IC-001)');
|
||||
expect($connection->pending_accounts_data[1]['uid'])->toBe('IC-002');
|
||||
expect($connection->pending_accounts_data[1]['name'])->toBe('Pension Plan (IC-002)');
|
||||
});
|
||||
|
||||
test('indexa capital auto-creates accounts during onboarding', function () {
|
||||
Queue::fake();
|
||||
|
||||
$user = User::factory()->notOnboarded()->create();
|
||||
Feature::for($user)->activate('open-banking');
|
||||
|
||||
Http::fake([
|
||||
'api.indexacapital.com/users/me' => Http::response([
|
||||
'accounts' => [
|
||||
['account_number' => 'IC-001', 'status' => 'active', 'type' => 'mutual'],
|
||||
['account_number' => 'IC-002', 'status' => 'active', 'type' => 'pension'],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->postJson('/open-banking/indexa-capital/connect', [
|
||||
'api_token' => 'valid-test-token-12345',
|
||||
]);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('redirect_url', route('onboarding', ['step' => 'create-account']));
|
||||
|
||||
$connection = BankingConnection::where('user_id', $user->id)->where('provider', 'indexacapital')->first();
|
||||
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::Active);
|
||||
expect($connection->pending_accounts_data)->toBeNull();
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => 'IC-001',
|
||||
'name' => 'Investment Portfolio (IC-001)',
|
||||
'type' => 'investment',
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('accounts', [
|
||||
'user_id' => $user->id,
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => 'IC-002',
|
||||
'name' => 'Pension Plan (IC-002)',
|
||||
'type' => 'investment',
|
||||
]);
|
||||
|
||||
Queue::assertPushed(SyncBankingConnectionJob::class);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -656,7 +656,11 @@ test('sends auth failed email immediately for indexa capital 401 error', functio
|
|||
$mockQueueJob->shouldReceive('fail')->once();
|
||||
$job->job = $mockQueueJob;
|
||||
|
||||
$job->handle($transactionSync, $balanceSync);
|
||||
try {
|
||||
$job->handle($transactionSync, $balanceSync);
|
||||
} catch (RequestException) {
|
||||
// Expected for auth failures after manually failing the job.
|
||||
}
|
||||
|
||||
$connection->refresh();
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::Error);
|
||||
|
|
@ -696,7 +700,11 @@ test('auth error sends email and fails job on any attempt', function () {
|
|||
$job->job->shouldReceive('hasFailed')->andReturn(false);
|
||||
$job->job->shouldReceive('fail')->once();
|
||||
|
||||
$job->handle($transactionSync, $balanceSync);
|
||||
try {
|
||||
$job->handle($transactionSync, $balanceSync);
|
||||
} catch (RequestException) {
|
||||
// Expected for auth failures after manually failing the job.
|
||||
}
|
||||
|
||||
$connection->refresh();
|
||||
expect($connection->status)->toBe(BankingConnectionStatus::Error);
|
||||
|
|
@ -810,7 +818,11 @@ test('sends auth failed email immediately for binance 403 error', function () {
|
|||
$job->job->shouldReceive('hasFailed')->andReturn(false);
|
||||
$job->job->shouldReceive('fail')->once();
|
||||
|
||||
$job->handle($transactionSync, $balanceSync);
|
||||
try {
|
||||
$job->handle($transactionSync, $balanceSync);
|
||||
} catch (RequestException) {
|
||||
// Expected for auth failures after manually failing the job.
|
||||
}
|
||||
|
||||
Mail::assertQueued(BankingConnectionAuthFailedEmail::class, function ($mail) use ($user, $connection) {
|
||||
return $mail->hasTo($user->email)
|
||||
|
|
|
|||
|
|
@ -226,7 +226,11 @@ test('auth error sets consecutive failures beyond the cap', function () {
|
|||
$job->job->shouldReceive('hasFailed')->andReturn(false);
|
||||
$job->job->shouldReceive('fail')->once();
|
||||
|
||||
$job->handle($transactionSync, $balanceSync);
|
||||
try {
|
||||
$job->handle($transactionSync, $balanceSync);
|
||||
} catch (RequestException) {
|
||||
// Expected for auth failures after manually failing the job.
|
||||
}
|
||||
|
||||
$connection->refresh();
|
||||
expect($connection->consecutive_sync_failures)->toBe(SyncBankingConnectionJob::MAX_SCHEDULED_RETRIES + 1);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ use App\Jobs\SyncAllBankingConnectionsJob;
|
|||
use App\Jobs\SyncBankingConnectionJob;
|
||||
use App\Models\BankingConnection;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
use function Pest\Laravel\artisan;
|
||||
|
|
@ -140,3 +142,31 @@ test('banking:sync does not set fullSync by default', function () {
|
|||
return $job->fullSync === false;
|
||||
});
|
||||
});
|
||||
|
||||
test('banking:sync --sync fails for auth errors instead of reporting success', function () {
|
||||
$user = User::factory()->create(['email' => 'test@example.com']);
|
||||
$connection = BankingConnection::factory()->indexaCapital()->for($user)->create([
|
||||
'last_synced_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
$user->accounts()->create([
|
||||
'name' => 'Indexa Capital Account',
|
||||
'name_iv' => null,
|
||||
'encrypted' => false,
|
||||
'bank_id' => null,
|
||||
'currency_code' => 'EUR',
|
||||
'type' => 'investment',
|
||||
'banking_connection_id' => $connection->id,
|
||||
'external_account_id' => 'IC-001',
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'api.indexacapital.com/*' => Http::response(['error' => 'Unauthorized'], 401),
|
||||
]);
|
||||
|
||||
expect(fn () => artisan('banking:sync', [
|
||||
'--user' => 'test@example.com',
|
||||
'--connection' => $connection->id,
|
||||
'--sync' => true,
|
||||
]))->toThrow(RequestException::class);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue