63 lines
2.4 KiB
JavaScript
63 lines
2.4 KiB
JavaScript
// PersonalFinanceAnalyzer
|
|
//
|
|
// Takes normalised SimpleFIN account data and computes:
|
|
// - Net worth (assets - liabilities)
|
|
// - Cash vs investment allocation
|
|
// - Spending by category (last 30 days)
|
|
// - Top spending categories
|
|
// - Income vs expenses summary
|
|
|
|
export class PersonalFinanceAnalyzer {
|
|
analyse(accounts) {
|
|
const assets = accounts.filter((a) => !['CREDIT', 'LOAN'].includes(a.type));
|
|
const liabilities = accounts.filter((a) => ['CREDIT', 'LOAN'].includes(a.type));
|
|
|
|
const totalAssets = assets.reduce((s, a) => s + Math.max(0, a.balance), 0);
|
|
const totalLiabilities = liabilities.reduce((s, a) => s + Math.abs(Math.min(0, a.balance)), 0);
|
|
const netWorth = totalAssets - totalLiabilities;
|
|
|
|
const cash = accounts.filter((a) => ['CHECKING', 'SAVINGS'].includes(a.type));
|
|
const investments = accounts.filter((a) => a.type === 'INVESTMENT');
|
|
const totalCash = cash.reduce((s, a) => s + Math.max(0, a.balance), 0);
|
|
const totalInvest = investments.reduce((s, a) => s + Math.max(0, a.balance), 0);
|
|
|
|
// Aggregate all transactions across accounts
|
|
const allTx = accounts.flatMap((a) => a.transactions);
|
|
|
|
const spending = allTx.filter((tx) => tx.amount < 0 && tx.category !== 'Transfer');
|
|
const income = allTx.filter((tx) => tx.amount > 0 && tx.category === 'Income');
|
|
|
|
const totalSpend = spending.reduce((s, tx) => s + Math.abs(tx.amount), 0);
|
|
const totalIncome = income.reduce((s, tx) => s + tx.amount, 0);
|
|
|
|
// Spending by category
|
|
const byCategory = {};
|
|
for (const tx of spending) {
|
|
byCategory[tx.category] = (byCategory[tx.category] ?? 0) + Math.abs(tx.amount);
|
|
}
|
|
const categoryBreakdown = Object.entries(byCategory)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.map(([category, amount]) => ({
|
|
category,
|
|
amount,
|
|
pct: totalSpend > 0 ? ((amount / totalSpend) * 100).toFixed(1) : '0',
|
|
}));
|
|
|
|
return {
|
|
netWorth,
|
|
totalAssets,
|
|
totalLiabilities,
|
|
totalCash,
|
|
totalInvestments: totalInvest,
|
|
cashPct: totalAssets > 0 ? ((totalCash / totalAssets) * 100).toFixed(1) : '0',
|
|
investPct: totalAssets > 0 ? ((totalInvest / totalAssets) * 100).toFixed(1) : '0',
|
|
totalIncome,
|
|
totalSpend,
|
|
savingsRate:
|
|
totalIncome > 0 ? (((totalIncome - totalSpend) / totalIncome) * 100).toFixed(1) : null,
|
|
categoryBreakdown,
|
|
accounts,
|
|
};
|
|
}
|
|
}
|