339 lines
10 KiB
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');
|
|
});
|