'test-app-id', 'services.enablebanking.private_key_path' => '/tmp/fake-key.pem', 'services.enablebanking.redirect_url' => 'https://example.com/callback', ]); }); test('show returns mapping page with correct props', function () { $user = User::factory()->onboarded()->create(); $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $user->id, ]); $response = $this->actingAs($user) ->get(route('open-banking.map-accounts', $connection)); $response->assertOk(); $response->assertInertia(fn ($page) => $page ->component('open-banking/map-accounts') ->has('connection') ->has('bankAccounts') ->has('existingAccounts') ); }); test('show redirects if no pending accounts', function () { $user = User::factory()->onboarded()->create(); $connection = BankingConnection::factory()->create([ 'user_id' => $user->id, 'pending_accounts_data' => null, ]); $response = $this->actingAs($user) ->get(route('open-banking.map-accounts', $connection)); $response->assertRedirect(route('settings.connections.index')); }); test('show returns 403 for other user\'s connection', function () { $user = User::factory()->onboarded()->create(); $otherUser = User::factory()->onboarded()->create(); $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $otherUser->id, ]); $response = $this->actingAs($user) ->get(route('open-banking.map-accounts', $connection)); $response->assertForbidden(); }); test('store with action create creates new accounts', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $user->id, 'aspsp_name' => 'Test Bank', 'pending_accounts_data' => [ [ 'uid' => 'ext-1', 'currency' => 'EUR', 'name' => 'Test Checking', 'account_id' => ['iban' => 'ES1234567890'], ], ], ]); $response = $this->actingAs($user) ->post(route('open-banking.map-accounts.store', $connection), [ 'mappings' => [ [ 'bank_account_uid' => 'ext-1', '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' => 'ext-1', 'name' => 'Test Checking', 'currency_code' => 'EUR', 'iban' => 'ES1234567890', ]); $connection->refresh(); expect($connection->status)->toBe(BankingConnectionStatus::Active); expect($connection->pending_accounts_data)->toBeNull(); Queue::assertPushed(SyncBankingConnectionJob::class); }); test('store creates investment accounts for bitpanda connections', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); $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(); $user = User::factory()->onboarded()->create(); $bank = Bank::factory()->create(); $existingAccount = Account::factory()->create([ 'user_id' => $user->id, 'bank_id' => $bank->id, 'currency_code' => 'EUR', 'banking_connection_id' => null, ]); $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $user->id, 'aspsp_name' => 'Test Bank', 'pending_accounts_data' => [ [ 'uid' => 'ext-1', 'currency' => 'EUR', 'name' => 'Bank Account', 'account_id' => ['iban' => 'ES1234567890'], ], ], ]); $response = $this->actingAs($user) ->post(route('open-banking.map-accounts.store', $connection), [ 'mappings' => [ [ 'bank_account_uid' => 'ext-1', 'action' => 'link', 'existing_account_id' => $existingAccount->id, ], ], ]); $response->assertRedirect(route('settings.connections.index')); $existingAccount->refresh(); 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); }); test('store with action skip does nothing', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $user->id, 'pending_accounts_data' => [ [ 'uid' => 'ext-1', 'currency' => 'EUR', 'name' => 'Skipped Account', 'account_id' => [], ], ], ]); $response = $this->actingAs($user) ->post(route('open-banking.map-accounts.store', $connection), [ 'mappings' => [ [ 'bank_account_uid' => 'ext-1', 'action' => 'skip', 'existing_account_id' => null, ], ], ]); $response->assertRedirect(route('settings.connections.index')); $this->assertDatabaseMissing('accounts', [ 'banking_connection_id' => $connection->id, ]); $connection->refresh(); expect($connection->status)->toBe(BankingConnectionStatus::Active); }); test('store with mixed actions works correctly', function () { Queue::fake(); $user = User::factory()->onboarded()->create(); $bank = Bank::factory()->create(); $existingAccount = Account::factory()->create([ 'user_id' => $user->id, 'bank_id' => $bank->id, 'currency_code' => 'EUR', 'banking_connection_id' => null, ]); $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $user->id, 'aspsp_name' => 'Test Bank', 'pending_accounts_data' => [ [ 'uid' => 'ext-1', 'currency' => 'EUR', 'name' => 'Account to Create', 'account_id' => [], ], [ 'uid' => 'ext-2', 'currency' => 'EUR', 'name' => 'Account to Link', 'account_id' => [], ], [ 'uid' => 'ext-3', 'currency' => 'EUR', 'name' => 'Account to Skip', 'account_id' => [], ], ], ]); $response = $this->actingAs($user) ->post(route('open-banking.map-accounts.store', $connection), [ 'mappings' => [ [ 'bank_account_uid' => 'ext-1', 'action' => 'create', 'existing_account_id' => null, ], [ 'bank_account_uid' => 'ext-2', 'action' => 'link', 'existing_account_id' => $existingAccount->id, ], [ 'bank_account_uid' => 'ext-3', 'action' => 'skip', 'existing_account_id' => null, ], ], ]); $response->assertRedirect(route('settings.connections.index')); // Created account exists $this->assertDatabaseHas('accounts', [ 'banking_connection_id' => $connection->id, 'external_account_id' => 'ext-1', ]); // Linked account is updated $existingAccount->refresh(); expect($existingAccount->external_account_id)->toBe('ext-2'); expect($existingAccount->linked_at)->not->toBeNull(); // Skipped account was not created $this->assertDatabaseMissing('accounts', [ 'external_account_id' => 'ext-3', 'banking_connection_id' => $connection->id, ]); }); test('validation fails when linking without existing_account_id', function () { $user = User::factory()->onboarded()->create(); $connection = BankingConnection::factory()->awaitingMapping()->create([ 'user_id' => $user->id, 'pending_accounts_data' => [ [ 'uid' => 'ext-1', 'currency' => 'EUR', 'name' => 'Test', 'account_id' => [], ], ], ]); $response = $this->actingAs($user) ->post(route('open-banking.map-accounts.store', $connection), [ 'mappings' => [ [ 'bank_account_uid' => 'ext-1', 'action' => 'link', 'existing_account_id' => null, ], ], ]); $response->assertSessionHasErrors('mappings.0.existing_account_id'); });