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:
parent
62ab1b38db
commit
80274e03a8
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// =============================================================================
|
||||
|
|
|
|||
Loading…
Reference in New Issue