421 lines
13 KiB
PHP
421 lines
13 KiB
PHP
<?php
|
|
|
|
use App\Enums\AccountType;
|
|
use App\Enums\BankingConnectionStatus;
|
|
use App\Models\Account;
|
|
use App\Models\Bank;
|
|
use App\Models\BankingConnection;
|
|
use App\Models\RealEstateDetail;
|
|
use App\Models\User;
|
|
|
|
use function Pest\Laravel\actingAs;
|
|
|
|
function createManualAccountTypeViaUi($page, string $displayName, string $bankName, string $type, string $currency = 'EUR', ?string $balance = null): void
|
|
{
|
|
$page->assertSee('Bank accounts')
|
|
->click('Create Account')
|
|
->waitForText('Manual', 5)
|
|
->click('Manual')
|
|
->wait(0.5)
|
|
->fill('#display_name', $displayName)
|
|
->click('[data-testid="bank-select"]')
|
|
->wait(0.5)
|
|
->fill('input[placeholder="Search bank..."]', $bankName)
|
|
->wait(0.5)
|
|
->click($bankName)
|
|
->click('button[name="type"]')
|
|
->wait(0.5)
|
|
->click("[role=\"option\"]:has-text(\"{$type}\")")
|
|
->wait(0.3)
|
|
->click('button[name="currency_code"]')
|
|
->wait(0.5)
|
|
->click("[role=\"option\"]:has-text(\"{$currency}\")")
|
|
->wait(0.3);
|
|
|
|
if ($balance !== null) {
|
|
$page->fill('#balance', $balance);
|
|
}
|
|
|
|
$page->click('[data-testid="submit-account"]')
|
|
->wait(2)
|
|
->assertNoJavascriptErrors();
|
|
}
|
|
|
|
it('can view bank accounts page', function () {
|
|
$user = User::factory()->onboarded()->create([
|
|
'email_verified_at' => now(),
|
|
]);
|
|
|
|
actingAs($user);
|
|
|
|
$page = visit('/settings/accounts');
|
|
|
|
$page->assertSee('Bank accounts')
|
|
->assertSee('Manage your bank accounts')
|
|
->assertNoJavascriptErrors();
|
|
});
|
|
|
|
it('shows existing accounts in list', function () {
|
|
$user = User::factory()->onboarded()->create();
|
|
$bank = Bank::factory()->create(['name' => 'Test Bank', 'logo' => null]);
|
|
Account::factory()->create([
|
|
'user_id' => $user->id,
|
|
'bank_id' => $bank->id,
|
|
'name' => 'My Checking',
|
|
'type' => 'checking',
|
|
'currency_code' => 'USD',
|
|
]);
|
|
|
|
actingAs($user);
|
|
|
|
$page = visit('/settings/accounts');
|
|
$page->navigate('/settings/accounts', ['waitUntil' => 'domcontentloaded'])->wait(2);
|
|
|
|
$page->assertSee('Bank accounts')
|
|
->waitForText('Test Bank')
|
|
->assertSee('Checking')
|
|
->assertSee('USD')
|
|
->assertNoJavascriptErrors();
|
|
});
|
|
|
|
it('can open create account dialog', function () {
|
|
$user = User::factory()->onboarded()->create();
|
|
|
|
actingAs($user);
|
|
|
|
$page = visit('/settings/accounts');
|
|
|
|
$page->assertSee('Bank accounts')
|
|
->click('Create Account')
|
|
->waitForText('Manual', 5)
|
|
->assertSee('Add a bank account, loan, or property to your workspace.')
|
|
->assertSee('Manual')
|
|
->assertSee('Connected')
|
|
->assertNoJavascriptErrors();
|
|
});
|
|
|
|
it('redirects free users to subscribe when reconnecting a bank connection', function () {
|
|
config(['subscriptions.enabled' => true]);
|
|
|
|
$user = User::factory()->onboarded()->create();
|
|
BankingConnection::factory()->error()->create([
|
|
'user_id' => $user->id,
|
|
'provider' => 'enablebanking',
|
|
'aspsp_name' => 'CaixaBank',
|
|
'aspsp_country' => 'ES',
|
|
'status' => BankingConnectionStatus::Error,
|
|
'error_message' => 'Authentication failed. Your credentials may have expired or been revoked.',
|
|
]);
|
|
|
|
actingAs($user);
|
|
|
|
$page = visit('/settings/connections');
|
|
|
|
$page->assertSee('Connections')
|
|
->assertSee('Reconnect')
|
|
->click('Reconnect')
|
|
->wait(3)
|
|
->assertPathIs('/subscribe')
|
|
->assertNoJavascriptErrors();
|
|
});
|
|
|
|
it('can create a new bank account', function () {
|
|
$user = User::factory()->onboarded()->create();
|
|
$bank = Bank::factory()->create(['name' => 'My Bank', 'logo' => null]);
|
|
|
|
actingAs($user);
|
|
|
|
$page = visit('/settings/accounts');
|
|
|
|
$page->assertSee('Bank accounts')
|
|
->click('Create Account')
|
|
->waitForText('Manual', 5)
|
|
->click('Manual')
|
|
->wait(0.5)
|
|
->fill('#display_name', 'My Savings Account')
|
|
->click('[data-testid="bank-select"]')
|
|
->wait(0.5)
|
|
->fill('input[placeholder="Search bank..."]', 'My Bank')
|
|
->wait(0.5)
|
|
->click('My Bank')
|
|
->click('button[name="type"]')
|
|
->wait(0.5)
|
|
->click('[role="option"]:has-text("Savings")')
|
|
->wait(0.3)
|
|
->click('button[name="currency_code"]')
|
|
->wait(0.5)
|
|
->click('[role="option"]:has-text("EUR")')
|
|
->wait(0.3)
|
|
->click('[data-testid="submit-account"]')
|
|
->wait(2)
|
|
->assertNoJavascriptErrors();
|
|
|
|
$this->assertDatabaseHas('accounts', [
|
|
'user_id' => $user->id,
|
|
'bank_id' => $bank->id,
|
|
'type' => 'savings',
|
|
'currency_code' => 'EUR',
|
|
]);
|
|
});
|
|
|
|
it('can create a loan account with balance and loan details', function () {
|
|
$user = User::factory()->onboarded()->create();
|
|
$bank = Bank::factory()->create(['name' => 'Mortgage Bank', 'logo' => null]);
|
|
|
|
actingAs($user);
|
|
|
|
$page = visit('/settings/accounts');
|
|
|
|
$page->assertSee('Bank accounts')
|
|
->click('Create Account')
|
|
->waitForText('Manual', 5)
|
|
->click('Manual')
|
|
->wait(0.5)
|
|
->fill('#display_name', 'Home Mortgage')
|
|
->click('[data-testid="bank-select"]')
|
|
->wait(0.5)
|
|
->fill('input[placeholder="Search bank..."]', 'Mortgage Bank')
|
|
->wait(0.5)
|
|
->click('Mortgage Bank')
|
|
->click('button[name="type"]')
|
|
->wait(0.5)
|
|
->click('[role="option"]:has-text("Loan")')
|
|
->wait(0.3)
|
|
->click('button[name="currency_code"]')
|
|
->wait(0.5)
|
|
->click('[role="option"]:has-text("EUR")')
|
|
->wait(0.3)
|
|
->fill('#balance', '180000')
|
|
->fill('#annual_interest_rate', '3.5')
|
|
->fill('#loan_term_months', '360')
|
|
->fill('#loan_start_date', '2024-01-01')
|
|
->fill('#original_amount', '250000')
|
|
->click('[data-testid="submit-account"]')
|
|
->wait(2)
|
|
->assertNoJavascriptErrors();
|
|
|
|
$loan = Account::query()
|
|
->where('user_id', $user->id)
|
|
->where('type', AccountType::Loan)
|
|
->first();
|
|
|
|
expect($loan)->not->toBeNull();
|
|
expect($loan->currency_code)->toBe('EUR');
|
|
$balances = $loan->balances()->orderBy('balance_date')->get();
|
|
expect($balances->count())->toBeGreaterThan(1);
|
|
expect($balances->first()->balance)->toBe(25000000);
|
|
expect($balances->last()->balance)->toBe(18000000);
|
|
expect($loan->loanDetail)->not->toBeNull();
|
|
expect($loan->loanDetail->loan_term_months)->toBe(360);
|
|
expect((string) $loan->loanDetail->annual_interest_rate)->toBe('3.500');
|
|
expect($loan->loanDetail->original_amount)->toBe(25000000);
|
|
});
|
|
|
|
it('can create the remaining manual account types', function (string $typeLabel, AccountType $type, ?string $balance) {
|
|
$user = User::factory()->onboarded()->create();
|
|
$bank = Bank::factory()->create(['name' => 'Coverage Bank', 'logo' => null]);
|
|
|
|
actingAs($user);
|
|
|
|
$page = visit('/settings/accounts');
|
|
|
|
createManualAccountTypeViaUi(
|
|
$page,
|
|
"{$typeLabel} Coverage Account",
|
|
'Coverage Bank',
|
|
$typeLabel,
|
|
'EUR',
|
|
$balance,
|
|
);
|
|
|
|
$account = Account::query()
|
|
->where('user_id', $user->id)
|
|
->where('type', $type)
|
|
->first();
|
|
|
|
expect($account)->not->toBeNull();
|
|
expect($account->currency_code)->toBe('EUR');
|
|
|
|
if ($balance === null) {
|
|
expect($account->balances)->toHaveCount(0);
|
|
} else {
|
|
expect($account->balances)->toHaveCount(1);
|
|
}
|
|
})->with([
|
|
'credit card' => ['Credit Card', AccountType::CreditCard, null],
|
|
'investment' => ['Investment', AccountType::Investment, '125000'],
|
|
'retirement' => ['Retirement / Pension', AccountType::Retirement, '250000'],
|
|
'others' => ['Others', AccountType::Others, null],
|
|
]);
|
|
|
|
it('can create a real estate account linked to an existing loan', function () {
|
|
$user = User::factory()->onboarded()->create();
|
|
|
|
$loanBank = Bank::factory()->create(['name' => 'Linked Mortgage Bank', 'logo' => null]);
|
|
|
|
$loanAccount = Account::factory()->create([
|
|
'user_id' => $user->id,
|
|
'bank_id' => $loanBank->id,
|
|
'name' => 'Primary Mortgage',
|
|
'type' => AccountType::Loan,
|
|
'currency_code' => 'EUR',
|
|
]);
|
|
|
|
actingAs($user);
|
|
|
|
$page = visit('/settings/accounts');
|
|
|
|
$page->assertSee('Bank accounts')
|
|
->click('Create Account')
|
|
->waitForText('Manual', 5)
|
|
->click('Manual')
|
|
->wait(0.5)
|
|
->fill('#display_name', 'City Apartment')
|
|
->click('button[name="type"]')
|
|
->wait(0.5)
|
|
->click('[role="option"]:has-text("Real Estate")')
|
|
->wait(0.3)
|
|
->click('button[name="currency_code"]')
|
|
->wait(0.5)
|
|
->click('[role="option"]:has-text("EUR")')
|
|
->wait(0.3)
|
|
->fill('#balance', '320000')
|
|
->click('button[name="property_type"]')
|
|
->wait(0.5)
|
|
->click('[role="option"]:has-text("Residential")')
|
|
->wait(0.3)
|
|
->fill('#address', '123 Linked Street')
|
|
->fill('#purchase_price', '275000')
|
|
->fill('#purchase_date', '2024-02-01')
|
|
->fill('#area_value', '82')
|
|
->click('button[name="area_unit"]')
|
|
->wait(0.5)
|
|
->click('[role="option"]:has-text("m²")')
|
|
->wait(0.3)
|
|
->click('button[name="linked_loan_account_id"]')
|
|
->wait(0.5)
|
|
->click('[role="option"]:has-text("Primary Mortgage")')
|
|
->wait(0.3)
|
|
->fill('#notes', 'Main residence')
|
|
->fill('#revaluation_percentage', '2.5')
|
|
->click('[data-testid="submit-account"]')
|
|
->wait(2)
|
|
->assertNoJavascriptErrors();
|
|
|
|
$account = Account::query()
|
|
->where('user_id', $user->id)
|
|
->where('type', AccountType::RealEstate)
|
|
->first();
|
|
|
|
expect($account)->not->toBeNull();
|
|
// Historical balances are generated from purchase_date (2024-02-01) to today
|
|
expect($account->balances()->count())->toBeGreaterThan(1);
|
|
expect($account->balances()->whereDate('balance_date', today())->first()->balance)->toBe(32000000);
|
|
|
|
$detail = RealEstateDetail::query()
|
|
->where('account_id', $account->id)
|
|
->first();
|
|
|
|
expect($detail)->not->toBeNull();
|
|
expect($detail->linked_loan_account_id)->toBe($loanAccount->id);
|
|
expect($detail->address)->toBe('123 Linked Street');
|
|
});
|
|
|
|
it('shows empty state when no accounts exist', function () {
|
|
$user = User::factory()->onboarded()->create();
|
|
|
|
actingAs($user);
|
|
|
|
$page = visit('/settings/accounts');
|
|
|
|
$page->assertSee('Bank accounts')
|
|
->waitForText('No accounts found')
|
|
->assertNoJavascriptErrors();
|
|
});
|
|
|
|
it('can filter accounts by name', function () {
|
|
$user = User::factory()->onboarded()->create();
|
|
$bank = Bank::factory()->create(['name' => 'Test Bank', 'logo' => null]);
|
|
Account::factory()->create([
|
|
'user_id' => $user->id,
|
|
'bank_id' => $bank->id,
|
|
'name' => 'Checking Account',
|
|
]);
|
|
Account::factory()->create([
|
|
'user_id' => $user->id,
|
|
'bank_id' => $bank->id,
|
|
'name' => 'Savings Account',
|
|
]);
|
|
|
|
actingAs($user);
|
|
|
|
$page = visit('/settings/accounts');
|
|
$page->navigate('/settings/accounts', ['waitUntil' => 'domcontentloaded'])->wait(2);
|
|
|
|
$page->assertSee('Bank accounts')
|
|
->waitForText('Test Bank')
|
|
->fill('input[placeholder="Filter accounts..."]', 'Checking')
|
|
->wait(0.5)
|
|
->assertNoJavascriptErrors();
|
|
});
|
|
|
|
it('can edit an existing account via dropdown menu', function () {
|
|
$user = User::factory()->onboarded()->create();
|
|
$bank = Bank::factory()->create(['name' => 'Edit Bank', 'logo' => null]);
|
|
|
|
actingAs($user);
|
|
|
|
$page = visit('/settings/accounts');
|
|
|
|
// Create account via UI to ensure it syncs to IndexedDB
|
|
createAccountViaUI($page, 'Old Account Name', 'Edit Bank', 'Checking', 'USD');
|
|
|
|
$page->navigate('/settings/accounts', ['waitUntil' => 'domcontentloaded'])->wait(5);
|
|
|
|
$page->assertSee('Bank accounts')
|
|
->click('button[aria-label="Open menu"]')
|
|
->wait(0.5)
|
|
->click('Edit')
|
|
->wait(1)
|
|
->assertSee('Edit Account')
|
|
->fill('#display_name', 'Updated Account Name')
|
|
->click('button[type="submit"]:has-text("Update")')
|
|
->wait(2);
|
|
|
|
$page->navigate('/settings/accounts', ['waitUntil' => 'domcontentloaded'])->wait(5);
|
|
|
|
$page->assertSee('Updated Account Name')
|
|
->assertNoJavascriptErrors();
|
|
});
|
|
|
|
it('can delete an account via dropdown menu', function () {
|
|
$user = User::factory()->onboarded()->create();
|
|
$bank = Bank::factory()->create(['name' => 'Delete Bank', 'logo' => null]);
|
|
|
|
actingAs($user);
|
|
|
|
$page = visit('/settings/accounts');
|
|
|
|
// Create account via UI to ensure it syncs to IndexedDB
|
|
createAccountViaUI($page, 'Account To Delete', 'Delete Bank', 'Checking', 'USD');
|
|
|
|
$page->navigate('/settings/accounts', ['waitUntil' => 'domcontentloaded'])->wait(5);
|
|
|
|
$page->assertSee('Bank accounts')
|
|
->assertSee('Account To Delete')
|
|
->click('button[aria-label="Open menu"]')
|
|
->wait(0.5)
|
|
->click('Delete')
|
|
->wait(0.5)
|
|
->assertSee('Delete Account')
|
|
->fill('#confirm', 'DELETE')
|
|
->wait(0.3)
|
|
->click('button[type="submit"]:has-text("Delete")')
|
|
->wait(2);
|
|
|
|
$page->navigate('/settings/accounts', ['waitUntil' => 'domcontentloaded'])->wait(5);
|
|
|
|
$page->assertDontSee('Account To Delete')
|
|
->assertNoJavascriptErrors();
|
|
});
|