From 973243277a512b40f46d48cae557d240924fe2cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Falc=C3=B3n?= Date: Tue, 24 Mar 2026 15:56:23 +0000 Subject: [PATCH] feat(accounts): show mortgage data and equity on real estate account page (#243) ## Summary - Add mortgage balance as a dashed line overlay on the property value chart when a real estate account has a linked loan - Display equity (market value - mortgage owed) in the chart header and as a 3-column summary strip (Market Value | Mortgage Owed | Equity) in the property details card - Extend balance evolution API endpoints to include `mortgage_balance` data points for linked loan accounts - Add 6 new tests covering mortgage/equity display in account show page and balance evolution endpoints --- app/Http/Controllers/AccountController.php | 22 +- .../Api/DashboardAnalyticsController.php | 30 +- lang/es.json | 7 + .../accounts/account-balance-chart.tsx | 288 ++++++++++++++++-- resources/js/pages/Accounts/Show.tsx | 244 ++++++++++++++- resources/js/types/account.ts | 2 + tests/Feature/AccountControllerTest.php | 194 ++++++++++++ 7 files changed, 760 insertions(+), 27 deletions(-) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index bfa6a745..cc347959 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers; use App\Models\Account; +use App\Models\AccountBalance; use App\Services\AccountMetricsService; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Http\Request; @@ -45,16 +46,33 @@ class AccountController extends Controller $realEstateDetail = $account->realEstateDetail; if ($realEstateDetail) { + $linkedLoan = $realEstateDetail->linkedLoanAccount; + $data['real_estate_detail'] = [ ...$realEstateDetail->only([ 'id', 'property_type', 'address', 'purchase_price', 'purchase_date', 'area_value', 'area_unit', 'notes', 'linked_loan_account_id', ]), - 'linked_loan_account' => $realEstateDetail->linkedLoanAccount - ? $realEstateDetail->linkedLoanAccount->only(['id', 'name', 'name_iv', 'encrypted', 'type', 'currency_code', 'bank']) + 'linked_loan_account' => $linkedLoan + ? $linkedLoan->only(['id', 'name', 'name_iv', 'encrypted', 'type', 'currency_code', 'bank']) : null, ]; + + // Include current balances for equity calculation + if ($linkedLoan) { + $data['real_estate_detail']['current_loan_balance'] = AccountBalance::query() + ->where('account_id', $linkedLoan->id) + ->where('balance_date', '<=', now()->toDateString()) + ->orderByDesc('balance_date') + ->value('balance') ?? 0; + } + + $data['real_estate_detail']['current_market_value'] = AccountBalance::query() + ->where('account_id', $account->id) + ->where('balance_date', '<=', now()->toDateString()) + ->orderByDesc('balance_date') + ->value('balance') ?? 0; } // Provide available loan accounts for linking diff --git a/app/Http/Controllers/Api/DashboardAnalyticsController.php b/app/Http/Controllers/Api/DashboardAnalyticsController.php index b9beeca3..3930de3e 100644 --- a/app/Http/Controllers/Api/DashboardAnalyticsController.php +++ b/app/Http/Controllers/Api/DashboardAnalyticsController.php @@ -109,7 +109,10 @@ class DashboardAnalyticsController extends Controller $start = Carbon::parse($validated['from']); $end = Carbon::parse($validated['to']); - $lookup = BalanceLookup::forAccounts([$account->id], $start->copy()->startOfMonth(), $end); + $linkedLoanId = $this->getLinkedLoanAccountId($account); + $accountIds = $linkedLoanId ? [$account->id, $linkedLoanId] : [$account->id]; + + $lookup = BalanceLookup::forAccounts($accountIds, $start->copy()->startOfMonth(), $end); $points = []; $current = $start->copy()->startOfMonth(); @@ -127,6 +130,10 @@ class DashboardAnalyticsController extends Controller $point['invested_amount'] = $lookup->getInvestedAmountAt($account->id, $date); } + if ($linkedLoanId) { + $point['mortgage_balance'] = $lookup->getBalanceAt($linkedLoanId, $date); + } + $points[] = $point; $current->addMonth(); } @@ -158,7 +165,10 @@ class DashboardAnalyticsController extends Controller $start = Carbon::parse($validated['from']); $end = Carbon::parse($validated['to']); - $lookup = BalanceLookup::forAccounts([$account->id], $start, $end); + $linkedLoanId = $this->getLinkedLoanAccountId($account); + $accountIds = $linkedLoanId ? [$account->id, $linkedLoanId] : [$account->id]; + + $lookup = BalanceLookup::forAccounts($accountIds, $start, $end); $points = []; $current = $start->copy(); @@ -175,6 +185,10 @@ class DashboardAnalyticsController extends Controller $point['invested_amount'] = $lookup->getInvestedAmountAt($account->id, $date); } + if ($linkedLoanId) { + $point['mortgage_balance'] = $lookup->getBalanceAt($linkedLoanId, $date); + } + $points[] = $point; $current->addDay(); } @@ -336,4 +350,16 @@ class DashboardAnalyticsController extends Controller 'expense' => abs($expense), ]; } + + /** + * Get the linked loan account ID for a real estate account, if any. + */ + private function getLinkedLoanAccountId(Account $account): ?string + { + if ($account->type !== \App\Enums\AccountType::RealEstate) { + return null; + } + + return $account->realEstateDetail?->linked_loan_account_id; + } } diff --git a/lang/es.json b/lang/es.json index 238b1fb5..086bd2d4 100644 --- a/lang/es.json +++ b/lang/es.json @@ -485,6 +485,7 @@ "Entertainment (Movies, Sports, Hobbies)": "Entretenimiento (Pel\u00edculas, Deportes, Hobbies)", "Entertainment \u00b7 Only $8 remaining this month": "Entretenimiento \u00b7 Solo $8 restante este mes", "Entertainment \u2192 $100": "Entretenimiento \u2192 $100", + "Equity": "Patrimonio neto", "Error": "Error", "Errors (": "Errores (", "Espa\u00f1ol": "Espa\u00f1ol", @@ -763,6 +764,8 @@ "Map Columns": "Mapear Columnas", "Market Value": "Valor de Mercado", "Market Values": "Valores de Mercado", + "Market value": "Valor de mercado", + "Market value evolution": "Evolución del valor de mercado", "Match your file columns to transaction fields": "Asocia las columnas de tu archivo con los campos de transacci\u00f3n", "Max": "M\u00e1x", "Maybe later": "Quiz\u00e1s m\u00e1s tarde", @@ -780,6 +783,8 @@ "More": "M\u00e1s", "More actions": "M\u00e1s acciones", "More options": "M\u00e1s opciones", + "Mortgage Owed": "Hipoteca pendiente", + "Mortgage owed": "Hipoteca pendiente", "Mortgages and loans": "Hipotecas y pr\u00e9stamos", "Most Popular": "M\u00e1s Popular", "Move Up \u2014 Share Your Personal Link": "Sube puestos \u2014 Comparte Tu Enlace Personal", @@ -830,6 +835,7 @@ "No labels found.": "No se encontraron etiquetas.", "No limits on bank accounts, transactions, or categories.": "Sin l\u00edmites en cuentas bancarias, transacciones o categor\u00edas.", "No linked loan": "Sin pr\u00e9stamo vinculado", + "No market value data available": "No hay datos de valor de mercado disponibles", "No owed amount data available": "No hay datos de monto adeudado disponibles", "No owed amount records found.": "No se encontraron registros de monto adeudado.", "No results found": "No se encontraron resultados", @@ -1653,6 +1659,7 @@ "violet": "violeta", "vs last month": "vs mes anterior", "vs last period": "vs per\u00edodo anterior", + "vs start": "vs inicio", "will be updated or created.": "ser\u00e1 actualizado o creado.", "yellow": "amarillo", "\u2014 $9/month": "\u2014 $9/mes", diff --git a/resources/js/components/accounts/account-balance-chart.tsx b/resources/js/components/accounts/account-balance-chart.tsx index fd1adbf7..43a1cd06 100644 --- a/resources/js/components/accounts/account-balance-chart.tsx +++ b/resources/js/components/accounts/account-balance-chart.tsx @@ -28,7 +28,11 @@ import { } from '@/hooks/use-chart-views'; import { useLocale } from '@/hooks/use-locale'; import { useIsMobile } from '@/hooks/use-mobile'; -import { Account, supportsInvestedAmount } from '@/types/account'; +import { + Account, + isRealEstateAccount, + supportsInvestedAmount, +} from '@/types/account'; import { formatCurrency } from '@/utils/currency'; import { formatDayFromDate, formatMonthFromYearMonth } from '@/utils/date'; import { __ } from '@/utils/i18n'; @@ -120,11 +124,106 @@ function InvestmentTooltipContent({ ); } -interface BalanceDataPoint { +function RealEstateTooltipContent({ + active, + payload, + valueFormatter, +}: { + active?: boolean; + payload?: Array<{ + dataKey?: string | number; + name?: string; + value?: number | string; + color?: string; + payload?: Record; + }>; + valueFormatter: (value: number) => string; +}) { + if (!active || !payload?.length) return null; + + const marketValueItem = payload.find((p) => p.dataKey === 'value'); + const mortgageItem = payload.find((p) => p.dataKey === 'mortgage_balance'); + + const marketValue = + typeof marketValueItem?.value === 'number' + ? marketValueItem.value + : null; + const mortgageOwed = + typeof mortgageItem?.value === 'number' ? mortgageItem.value : null; + const equity = + marketValue !== null && mortgageOwed !== null + ? marketValue - mortgageOwed + : null; + + return ( +
+
+ {marketValue !== null && ( +
+
+
+ + {__('Market value')} + + + {valueFormatter(marketValue)} + +
+
+ )} + {mortgageOwed !== null && ( +
+
+
+ + {__('Mortgage owed')} + + + {valueFormatter(mortgageOwed)} + +
+
+ )} + {equity !== null && ( +
+
+
+ + {__('Equity')} + + = 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`} + > + {valueFormatter(equity)} + +
+
+ )} +
+
+ ); +} + +export interface BalanceDataPoint { month: string; timestamp: number; value: number; invested_amount?: number | null; + mortgage_balance?: number | null; } interface AccountBalanceData { @@ -143,6 +242,7 @@ interface DailyBalanceDataPoint { timestamp: number; value: number; invested_amount?: number | null; + mortgage_balance?: number | null; } interface AccountDailyBalanceData { @@ -156,11 +256,21 @@ interface AccountDailyBalanceData { }; } +export interface ChartComputedData { + chartData: BalanceDataPoint[]; + currentBalance: number; + currentMortgageBalance: number | null; + hasMortgageData: boolean; + shortTrend: ReturnType; + longTrend: ReturnType; +} + interface AccountBalanceChartProps { account: Account; loading?: boolean; refreshKey?: number; onBalanceClick?: () => void; + onDataLoaded?: (data: ChartComputedData) => void; } function createXAxisFormatter(locale: string, granularity: ChartGranularity) { @@ -216,6 +326,7 @@ function normalizeDailyData(data: DailyBalanceDataPoint[]): BalanceDataPoint[] { timestamp: point.timestamp, value: point.value, invested_amount: point.invested_amount, + mortgage_balance: point.mortgage_balance, })); } @@ -224,10 +335,12 @@ export function AccountBalanceChart({ loading: initialLoading, refreshKey, onBalanceClick, + onDataLoaded, }: AccountBalanceChartProps) { const locale = useLocale(); const isMobile = useIsMobile(); const isLoan = account.type === 'loan'; + const isRealEstate = isRealEstateAccount(account); const [granularity, setGranularity] = useState('monthly'); const [balanceData, setBalanceData] = useState( null, @@ -282,6 +395,8 @@ export function AccountBalanceChart({ chartData, currentBalance, currentInvestedAmount, + currentMortgageBalance, + hasMortgageData, shortTrend, longTrend, } = useMemo(() => { @@ -290,6 +405,8 @@ export function AccountBalanceChart({ chartData: [], currentBalance: 0, currentInvestedAmount: null as number | null, + currentMortgageBalance: null as number | null, + hasMortgageData: false, shortTrend: null, longTrend: null, }; @@ -310,15 +427,58 @@ export function AccountBalanceChart({ } } + // Check if any data point has mortgage data + const hasMortgage = data.some( + (d) => + d.mortgage_balance !== null && d.mortgage_balance !== undefined, + ); + + // Find the most recent mortgage balance + let mortgage: number | null = null; + if (hasMortgage) { + for (let i = data.length - 1; i >= 0; i--) { + if ( + data[i].mortgage_balance !== null && + data[i].mortgage_balance !== undefined + ) { + mortgage = data[i].mortgage_balance!; + break; + } + } + } + return { chartData: data, currentBalance: current, currentInvestedAmount: invested, + currentMortgageBalance: mortgage, + hasMortgageData: hasMortgage, shortTrend: calculateTrend(data, 1), longTrend: calculateTrend(data, data.length - 1), }; }, [balanceData]); + useEffect(() => { + if (onDataLoaded && chartData.length > 0) { + onDataLoaded({ + chartData, + currentBalance, + currentMortgageBalance, + hasMortgageData, + shortTrend, + longTrend, + }); + } + }, [ + chartData, + currentBalance, + currentMortgageBalance, + hasMortgageData, + shortTrend, + longTrend, + onDataLoaded, + ]); + // Convert data for useChartViews hook const { data: hookData, accounts: hookAccounts } = useMemo(() => { return convertSingleAccountData( @@ -352,6 +512,14 @@ export function AccountBalanceChart({ }, } : {}), + ...(hasMortgageData + ? { + mortgage_balance: { + label: __('Mortgage owed'), + color: 'var(--color-chart-5)', + }, + } + : {}), }; const formatXAxisLabel = useMemo( @@ -381,15 +549,26 @@ export function AccountBalanceChart({ ? __('for the last 30 days') : __('for the last 12 months'); + const chartTitle = isRealEstate + ? __('Market value evolution') + : isLoan + ? __('Owed amount evolution') + : __('Balance evolution'); + const emptyMessage = isRealEstate + ? __('No market value data available') + : isLoan + ? __('No owed amount data available') + : __('No balance data available'); + const currentEquity = + hasMortgageData && currentMortgageBalance !== null + ? currentBalance - currentMortgageBalance + : null; + if (initialLoading || isLoading) { return ( - - {isLoan - ? __('Owed amount evolution') - : __('Balance evolution')} - + {chartTitle}
@@ -405,17 +584,11 @@ export function AccountBalanceChart({ return ( - - {isLoan - ? __('Owed amount evolution') - : __('Balance evolution')} - + {chartTitle}
- {isLoan - ? __('No owed amount data available') - : __('No balance data available')} + {emptyMessage}
@@ -427,11 +600,7 @@ export function AccountBalanceChart({
- - {isLoan - ? __('Owed amount evolution') - : __('Balance evolution')} - + {chartTitle} + {currentEquity !== null && ( +
+ + {__('Equity')} + + = 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'}`} + > + + +
+ )} - {granularity === 'daily' ? ( + {hasMortgageData ? ( + + + + + + + + + + } + /> + + + + ) : granularity === 'daily' ? ( (null); + + const handleChartDataLoaded = useCallback((data: ChartComputedData) => { + setChartComputedData(data); + }, []); function handleBalanceUpdated() { setChartRefreshKey((prev) => prev + 1); @@ -218,8 +231,22 @@ export default function AccountShow({ ? undefined : () => setUpdateBalanceOpen(true) } + onDataLoaded={handleChartDataLoaded} /> + {isRealEstate && + realEstateDetail && + chartComputedData?.hasMortgageData && ( + + )} + {isRealEstate && realEstateDetail && ( { + const market = chartData.map((d) => ({ + date: d.month, + value: d.value, + })); + const mortgage = chartData + .filter( + (d) => + d.mortgage_balance !== null && + d.mortgage_balance !== undefined, + ) + .map((d) => ({ date: d.month, value: d.mortgage_balance! })); + const equityArr = chartData + .filter( + (d) => + d.mortgage_balance !== null && + d.mortgage_balance !== undefined, + ) + .map((d) => ({ + date: d.month, + value: d.value - d.mortgage_balance!, + })); + + const currentEquity = + currentMortgageBalance !== null + ? currentBalance - currentMortgageBalance + : null; + + return { + marketHistory: market, + mortgageHistory: mortgage, + equityHistory: equityArr, + equity: currentEquity, + }; + }, [chartData, currentBalance, currentMortgageBalance]); + + const marketTrend = useMemo(() => { + if (marketHistory.length < 2) return null; + const prev = marketHistory[0].value; + const curr = marketHistory[marketHistory.length - 1].value; + if (prev === 0) return null; + return { diff: curr - prev, previous: prev, current: curr }; + }, [marketHistory]); + + const mortgageTrend = useMemo(() => { + if (mortgageHistory.length < 2) return null; + const prev = mortgageHistory[0].value; + const curr = mortgageHistory[mortgageHistory.length - 1].value; + if (prev === 0) return null; + return { diff: curr - prev, previous: prev, current: curr }; + }, [mortgageHistory]); + + const equityTrend = useMemo(() => { + if (equityHistory.length < 2) return null; + const prev = equityHistory[0].value; + const curr = equityHistory[equityHistory.length - 1].value; + if (prev === 0) return null; + return { diff: curr - prev, previous: prev, current: curr }; + }, [equityHistory]); + + const equityLineColor = 'var(--color-emerald-500)'; + const mortgageLineColor = 'var(--color-amber-500)'; + + return ( +
+ + + +
+ ); +} + +function SparklineCard({ + title, + amountInCents, + currencyCode, + history, + lineColor, + trend, +}: { + title: string; + amountInCents: number; + currencyCode: string; + history: Array<{ date: string; value: number }>; + lineColor: string; + trend: { diff: number; previous: number; current: number } | null; +}) { + return ( + + + {title} + + +
+
+
+ +
+ {trend && ( + = 0} + trend={Math.abs(trend.diff)} + label={__('vs start')} + className="text-sm" + previousAmount={trend.previous} + currentAmount={trend.current} + tooltipSide="bottom" + currencyCode={currencyCode} + /> + )} +
+ {history.length > 1 && ( +
+ + + { + if (!active || !payload?.length) + return null; + const data = payload[0].payload as { + date: string; + value: number; + }; + return ( +
+

+ {data.date} +

+

+ +

+
+ ); + }} + /> + +
+
+
+ )} +
+
+
+ ); +} + function PropertyDetailsCard({ detail, account, diff --git a/resources/js/types/account.ts b/resources/js/types/account.ts index c3550e5f..21a1d687 100644 --- a/resources/js/types/account.ts +++ b/resources/js/types/account.ts @@ -96,6 +96,8 @@ export interface RealEstateDetail { notes: string | null; linked_loan_account_id: UUID | null; linked_loan_account: Account | null; + current_market_value: number | null; + current_loan_balance: number | null; } export function formatPropertyType(type: PropertyType): string { diff --git a/tests/Feature/AccountControllerTest.php b/tests/Feature/AccountControllerTest.php index 32f01fd0..63ab9227 100644 --- a/tests/Feature/AccountControllerTest.php +++ b/tests/Feature/AccountControllerTest.php @@ -7,6 +7,7 @@ use App\Models\AutomationRule; use App\Models\Bank; use App\Models\Category; use App\Models\Label; +use App\Models\RealEstateDetail; use App\Models\User; beforeEach(function () { @@ -334,3 +335,196 @@ test('account show includes bank information', function () { ->where('account.bank.name', 'Test Bank') ); }); + +test('real estate account show includes real estate detail with linked loan', function () { + $loanAccount = Account::factory()->create([ + 'user_id' => $this->user->id, + 'type' => AccountType::Loan, + ]); + + $realEstateAccount = Account::factory()->realEstate()->create([ + 'user_id' => $this->user->id, + ]); + + RealEstateDetail::factory()->create([ + 'account_id' => $realEstateAccount->id, + 'linked_loan_account_id' => $loanAccount->id, + ]); + + $response = $this->withoutVite()->get(route('accounts.show', $realEstateAccount)); + + $response->assertOk() + ->assertInertia(fn ($page) => $page + ->component('Accounts/Show') + ->has('account.real_estate_detail') + ->where('account.real_estate_detail.linked_loan_account_id', $loanAccount->id) + ->has('account.real_estate_detail.linked_loan_account') + ->where('account.real_estate_detail.linked_loan_account.id', $loanAccount->id) + ->has('account.available_loan_accounts') + ); +}); + +test('real estate account show includes current market value and loan balance for equity', function () { + $loanAccount = Account::factory()->create([ + 'user_id' => $this->user->id, + 'type' => AccountType::Loan, + ]); + + AccountBalance::factory()->create([ + 'account_id' => $loanAccount->id, + 'balance_date' => now(), + 'balance' => 20000000, // $200,000 + ]); + + $realEstateAccount = Account::factory()->realEstate()->create([ + 'user_id' => $this->user->id, + ]); + + AccountBalance::factory()->create([ + 'account_id' => $realEstateAccount->id, + 'balance_date' => now(), + 'balance' => 35000000, // $350,000 + ]); + + RealEstateDetail::factory()->create([ + 'account_id' => $realEstateAccount->id, + 'linked_loan_account_id' => $loanAccount->id, + ]); + + $response = $this->withoutVite()->get(route('accounts.show', $realEstateAccount)); + + $response->assertOk() + ->assertInertia(fn ($page) => $page + ->component('Accounts/Show') + ->where('account.real_estate_detail.current_market_value', 35000000) + ->where('account.real_estate_detail.current_loan_balance', 20000000) + ); +}); + +test('real estate account show includes market value without linked loan', function () { + $realEstateAccount = Account::factory()->realEstate()->create([ + 'user_id' => $this->user->id, + ]); + + AccountBalance::factory()->create([ + 'account_id' => $realEstateAccount->id, + 'balance_date' => now(), + 'balance' => 35000000, + ]); + + RealEstateDetail::factory()->create([ + 'account_id' => $realEstateAccount->id, + ]); + + $response = $this->withoutVite()->get(route('accounts.show', $realEstateAccount)); + + $response->assertOk() + ->assertInertia(fn ($page) => $page + ->component('Accounts/Show') + ->where('account.real_estate_detail.current_market_value', 35000000) + ->missing('account.real_estate_detail.current_loan_balance') + ); +}); + +test('real estate balance evolution includes mortgage balance data', function () { + $loanAccount = Account::factory()->create([ + 'user_id' => $this->user->id, + 'type' => AccountType::Loan, + ]); + + AccountBalance::factory()->create([ + 'account_id' => $loanAccount->id, + 'balance_date' => now()->subMonthNoOverflow()->endOfMonth(), + 'balance' => 22000000, + ]); + AccountBalance::factory()->create([ + 'account_id' => $loanAccount->id, + 'balance_date' => now()->endOfMonth(), + 'balance' => 21500000, + ]); + + $realEstateAccount = Account::factory()->realEstate()->create([ + 'user_id' => $this->user->id, + ]); + + AccountBalance::factory()->create([ + 'account_id' => $realEstateAccount->id, + 'balance_date' => now()->subMonthNoOverflow()->endOfMonth(), + 'balance' => 35000000, + ]); + AccountBalance::factory()->create([ + 'account_id' => $realEstateAccount->id, + 'balance_date' => now()->endOfMonth(), + 'balance' => 36000000, + ]); + + RealEstateDetail::factory()->create([ + 'account_id' => $realEstateAccount->id, + 'linked_loan_account_id' => $loanAccount->id, + ]); + + $response = $this->getJson('/api/dashboard/account/'.$realEstateAccount->id.'/balance-evolution?'.http_build_query([ + 'from' => now()->subMonthsNoOverflow(2)->startOfMonth()->toDateString(), + 'to' => now()->endOfMonth()->toDateString(), + ])); + + $response->assertOk(); + $data = $response->json(); + + expect($data['data'])->toHaveCount(3); + expect($data['data'][0])->toHaveKey('mortgage_balance'); + + // The last point should have the most recent mortgage balance + $lastPoint = end($data['data']); + expect($lastPoint['mortgage_balance'])->toBe(21500000); + expect($lastPoint['value'])->toBe(36000000); +}); + +test('real estate balance evolution without linked loan has no mortgage data', function () { + $realEstateAccount = Account::factory()->realEstate()->create([ + 'user_id' => $this->user->id, + ]); + + AccountBalance::factory()->create([ + 'account_id' => $realEstateAccount->id, + 'balance_date' => now()->endOfMonth(), + 'balance' => 35000000, + ]); + + RealEstateDetail::factory()->create([ + 'account_id' => $realEstateAccount->id, + ]); + + $response = $this->getJson('/api/dashboard/account/'.$realEstateAccount->id.'/balance-evolution?'.http_build_query([ + 'from' => now()->subMonthsNoOverflow(2)->startOfMonth()->toDateString(), + 'to' => now()->endOfMonth()->toDateString(), + ])); + + $response->assertOk(); + $data = $response->json(); + + expect($data['data'][0])->not->toHaveKey('mortgage_balance'); +}); + +test('non-real-estate account balance evolution has no mortgage data', function () { + $account = Account::factory()->create([ + 'user_id' => $this->user->id, + 'type' => AccountType::Checking, + ]); + + AccountBalance::factory()->create([ + 'account_id' => $account->id, + 'balance_date' => now()->endOfMonth(), + 'balance' => 100000, + ]); + + $response = $this->getJson('/api/dashboard/account/'.$account->id.'/balance-evolution?'.http_build_query([ + 'from' => now()->subMonthsNoOverflow(2)->startOfMonth()->toDateString(), + 'to' => now()->endOfMonth()->toDateString(), + ])); + + $response->assertOk(); + $data = $response->json(); + + expect($data['data'][0])->not->toHaveKey('mortgage_balance'); +});