fix: align onboarding account types with current asset support (#273)

## Summary
- add real estate to the onboarding account type explainer and update
investment copy to mention crypto and cold wallets
- align onboarding manual account creation with the real-estate feature
flag and real-estate payload requirements
- cover the onboarding copy and real-estate creation flow with browser
tests

## Testing
- php artisan test --compact tests/Browser/OnboardingFlowTest.php
This commit is contained in:
Víctor Falcón 2026-04-12 16:22:38 +01:00 committed by GitHub
parent 62ab1b38db
commit 80274e03a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 161 additions and 21 deletions

View File

@ -1,7 +1,9 @@
import { StepButton } from '@/components/onboarding/step-button';
import { StepHeader } from '@/components/onboarding/step-header';
import { type SharedData } from '@/types';
import { accountIconByType } from '@/types/account';
import { __ } from '@/utils/i18n';
import { usePage } from '@inertiajs/react';
import { Banknote } from 'lucide-react';
interface StepAccountTypesProps {
@ -30,7 +32,7 @@ const accountTypes = [
{
type: 'investment' as const,
nameKey: 'Investment',
descriptionKey: 'Stocks, ETFs, and portfolios',
descriptionKey: 'Stocks, ETFs, crypto, and cold wallets',
hasTransactions: false,
},
{
@ -45,9 +47,20 @@ const accountTypes = [
descriptionKey: 'Mortgages and loans',
hasTransactions: false,
},
{
type: 'real_estate' as const,
nameKey: 'Real Estate',
descriptionKey: 'Properties and real estate assets',
hasTransactions: false,
},
];
export function StepAccountTypes({ onContinue }: StepAccountTypesProps) {
const { features } = usePage<SharedData>().props;
const visibleAccountTypes = features['real-estate']
? accountTypes
: accountTypes.filter((account) => account.type !== 'real_estate');
return (
<div className="flex animate-in flex-col items-center duration-500 fade-in slide-in-from-bottom-4">
<StepHeader
@ -60,7 +73,7 @@ export function StepAccountTypes({ onContinue }: StepAccountTypesProps) {
/>
<div className="grid w-full max-w-2xl gap-3 sm:grid-cols-2">
{accountTypes.map((account) => {
{visibleAccountTypes.map((account) => {
const Icon = accountIconByType(account.type);
return (

View File

@ -74,6 +74,7 @@ export function StepCreateAccount({
const { pricing, subscriptionsEnabled, features, locale } =
usePage<SharedData>().props;
const openBankingEnabled = features['open-banking'];
const realEstateEnabled = features['real-estate'];
const [mode, setMode] = useState<AccountMode>('select');
const [selectedMode, setSelectedMode] = useState<'manual' | 'connected'>(
'manual',
@ -164,35 +165,87 @@ export function StepCreateAccount({
setIsSubmitting(true);
try {
let finalBankId: string;
const isRealEstate = type === 'real_estate';
let finalBankId: string | null = null;
if (customBank) {
if (!customBank.name.trim()) {
setError(__('Please enter a bank name.'));
setIsSubmitting(false);
return;
if (!isRealEstate) {
if (customBank) {
if (!customBank.name.trim()) {
setError(__('Please enter a bank name.'));
setIsSubmitting(false);
return;
}
const createdBankId = await createBankAndGetId();
if (!createdBankId) {
throw new Error('Failed to create bank');
}
finalBankId = createdBankId;
} else {
if (!bankId) {
setError(__('Please select a bank.'));
setIsSubmitting(false);
return;
}
finalBankId = String(bankId);
}
const createdBankId = await createBankAndGetId();
if (!createdBankId) {
throw new Error('Failed to create bank');
}
finalBankId = createdBankId;
} else {
if (!bankId) {
setError(__('Please select a bank.'));
setIsSubmitting(false);
return;
}
finalBankId = String(bankId);
}
const response = await fetch(store.url(), {
method: 'POST',
body: JSON.stringify({
name: displayName,
bank_id: finalBankId,
...(finalBankId ? { bank_id: finalBankId } : {}),
type: type,
currency_code: currencyCode,
...(formDataRef.current.balance !== null
? { balance: formDataRef.current.balance }
: {}),
...(formDataRef.current.realEstate
? {
property_type:
formDataRef.current.realEstate.propertyType,
address:
formDataRef.current.realEstate.address ||
null,
purchase_price:
formDataRef.current.realEstate
.purchasePrice || null,
purchase_date:
formDataRef.current.realEstate.purchaseDate ||
null,
area_value:
formDataRef.current.realEstate.areaValue ||
null,
area_unit:
formDataRef.current.realEstate.areaUnit,
linked_loan_account_id:
formDataRef.current.realEstate
.linkedLoanAccountId,
notes:
formDataRef.current.realEstate.notes || null,
revaluation_percentage:
formDataRef.current.realEstate
.revaluationPercentage || null,
}
: {}),
...(formDataRef.current.loan
? {
annual_interest_rate:
formDataRef.current.loan.annualInterestRate ||
null,
loan_term_months:
formDataRef.current.loan.loanTermMonths ||
null,
loan_start_date:
formDataRef.current.loan.startDate || null,
original_amount:
formDataRef.current.loan.originalAmount ||
null,
}
: {}),
}),
headers: {
'Content-Type': 'application/json',
@ -488,6 +541,9 @@ export function StepCreateAccount({
>
<AccountForm
onChange={handleFormChange}
hiddenAccountTypes={
realEstateEnabled ? [] : ['real_estate']
}
usePrimaryCurrenciesOnly={
isFirstAccount &&
existingAccounts.length === 0 &&

View File

@ -84,10 +84,30 @@ it('navigates from welcome to account types', function () {
->assertSee('Whisper Money')
->click("Let's Get Started")
->wait(1)
->assertSee('Stocks, ETFs, crypto, and cold wallets')
->assertSee('Account Types')
->assertNoJavascriptErrors();
});
it('shows real estate on onboarding account types when feature is enabled', function () {
$user = User::factory()->create([
'onboarded_at' => null,
]);
Feature::for($user)->activate('real-estate');
$this->actingAs($user);
$page = visit('/onboarding');
$page->click("Let's Get Started")
->wait(1)
->assertSee('Account Types')
->assertSee('Real Estate')
->assertSee('Properties and real estate assets')
->assertNoJavascriptErrors();
});
// =============================================================================
// Existing Account Flow Tests
// =============================================================================
@ -212,6 +232,57 @@ it('shows add another account form without first account restriction', function
->assertNoJavascriptErrors();
});
it('creates a real estate account during onboarding when feature is enabled', function () {
$user = User::factory()->create([
'onboarded_at' => null,
]);
Feature::for($user)->activate('real-estate');
$this->actingAs($user);
$page = visit('/onboarding');
$page->click("Let's Get Started")
->wait(1)
->click('Create Your First Account')
->wait(1)
->assertSee('Create an Account')
->click('Manual')
->wait(1)
->click('Continue')
->wait(1)
->fill('#display_name', 'My Apartment')
->click('Select account type')
->wait(1)
->click('[role="option"]:has-text("Real Estate")')
->wait(1)
->click('Select currency')
->wait(1)
->click('[role="option"]:has-text("EUR")')
->wait(1)
->click('Select property type')
->wait(1)
->click('[role="option"]:has-text("Residential")')
->wait(1)
->click('Create Account')
->wait(5)
->assertSee('Set Account Balance')
->assertNoJavascriptErrors();
$user->refresh();
$account = $user->accounts()->first();
expect($account)->not->toBeNull();
expect($account->type->value)->toBe('real_estate');
expect($account->name)->toBe('My Apartment');
expect($account->currency_code)->toBe('EUR');
expect($account->bank_id)->toBeNull();
expect($account->realEstateDetail)->not->toBeNull();
expect($account->realEstateDetail->property_type->value)->toBe('residential');
});
// =============================================================================
// Full End-to-End Flow Test
// =============================================================================