whisper-money/tests/Feature/OpenBanking/AccountMappingTest.php

339 lines
10 KiB
PHP

<?php
use App\Enums\BankingConnectionStatus;
use App\Jobs\SyncBankingConnectionJob;
use App\Models\Account;
use App\Models\Bank;
use App\Models\BankingConnection;
use App\Models\User;
use Illuminate\Support\Facades\Queue;
beforeEach(function () {
config([
'services.enablebanking.app_id' => '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');
});