Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.

Commit 92500b8

Browse files
committed
Add multi-account overview
1 parent 080c371 commit 92500b8

15 files changed

Lines changed: 1246 additions & 6 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,14 @@ See `backend/app/db/schema.sql`. Key tables:
6262
## API Endpoints
6363
OpenAPI: `backend/app/openapi.yaml`
6464
- Auth: `/auth/register`, `/auth/login`, `/auth/refresh`
65-
- Expenses: CRUD `/expenses`
65+
- Expenses: CRUD `/expenses` with optional `account_id` linking
6666
- Bills: CRUD `/bills`, pay/mark `/bills/{id}/pay`
6767
- Reminders: CRUD `/reminders`, trigger `/reminders/run`
6868
- Insights: `/insights/monthly`, `/insights/budget-suggestion`
69+
- Accounts: CRUD `/accounts`, consolidated `/accounts/overview`
70+
71+
### Multi-Account Overview
72+
FinMind supports user-owned financial accounts for checking, savings, credit cards, investments, loans, cash, and other balances. Create accounts with `POST /accounts`, link transactions by passing `account_id` to `/expenses`, and read `GET /accounts/overview?month=YYYY-MM` for assets, liabilities, net worth, currency groups, account type breakdown, per-account monthly income/expense activity, and recent linked transactions.
6973

7074
## MVP UI/UX Plan
7175
- Auth screens: register/login.

app/src/App.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Dashboard } from "./pages/Dashboard";
88
import { Budgets } from "./pages/Budgets";
99
import { Bills } from "./pages/Bills";
1010
import { Analytics } from "./pages/Analytics";
11+
import { Accounts } from "./pages/Accounts";
1112
import Reminders from "./pages/Reminders";
1213
import Expenses from "./pages/Expenses";
1314
import { SignIn } from "./pages/SignIn";
@@ -67,6 +68,14 @@ const App = () => (
6768
</ProtectedRoute>
6869
}
6970
/>
71+
<Route
72+
path="accounts"
73+
element={
74+
<ProtectedRoute>
75+
<Accounts />
76+
</ProtectedRoute>
77+
}
78+
/>
7079
<Route
7180
path="analytics"
7281
element={
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import React from 'react';
2+
import { render, screen, waitFor } from '@testing-library/react';
3+
import userEvent from '@testing-library/user-event';
4+
import { Accounts } from '@/pages/Accounts';
5+
6+
jest.mock('@/components/ui/button', () => ({
7+
Button: ({ children, ...props }: React.PropsWithChildren & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
8+
<button {...props}>{children}</button>
9+
),
10+
}));
11+
jest.mock('@/components/ui/input', () => ({
12+
Input: ({ ...props }: React.InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
13+
}));
14+
jest.mock('@/components/ui/label', () => ({
15+
Label: ({ children, ...props }: React.PropsWithChildren & React.LabelHTMLAttributes<HTMLLabelElement>) => (
16+
<label {...props}>{children}</label>
17+
),
18+
}));
19+
jest.mock('@/components/ui/financial-card', () => ({
20+
FinancialCard: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
21+
FinancialCardHeader: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
22+
FinancialCardContent: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
23+
FinancialCardTitle: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
24+
FinancialCardDescription: ({ children }: React.PropsWithChildren) => <div>{children}</div>,
25+
}));
26+
27+
const toastMock = jest.fn();
28+
jest.mock('@/hooks/use-toast', () => ({
29+
useToast: () => ({ toast: toastMock }),
30+
}));
31+
32+
const getAccountsOverviewMock = jest.fn();
33+
const createAccountMock = jest.fn();
34+
jest.mock('@/api/accounts', () => ({
35+
getAccountsOverview: (...args: unknown[]) => getAccountsOverviewMock(...args),
36+
createAccount: (...args: unknown[]) => createAccountMock(...args),
37+
}));
38+
39+
describe('Accounts integration', () => {
40+
beforeEach(() => {
41+
jest.clearAllMocks();
42+
getAccountsOverviewMock.mockResolvedValue({
43+
period: { month: '2026-05' },
44+
summary: {
45+
account_count: 2,
46+
assets: 7000,
47+
liabilities: 300,
48+
net_worth: 6700,
49+
},
50+
accounts: [
51+
{
52+
id: 1,
53+
name: 'Main Checking',
54+
account_type: 'CHECKING',
55+
balance: 2000,
56+
currency: 'USD',
57+
institution: 'Acme Bank',
58+
active: true,
59+
monthly_income: 3000,
60+
monthly_expenses: 120,
61+
monthly_net_flow: 2880,
62+
transaction_count: 2,
63+
},
64+
],
65+
by_type: [{ account_type: 'CHECKING', count: 1, balance: 2000 }],
66+
by_currency: [{ currency: 'USD', assets: 7000, liabilities: 300, net_worth: 6700 }],
67+
recent_transactions: [
68+
{
69+
id: 10,
70+
account_id: 1,
71+
account_name: 'Main Checking',
72+
description: 'Groceries',
73+
amount: 120,
74+
currency: 'USD',
75+
date: '2026-05-04',
76+
type: 'EXPENSE',
77+
},
78+
],
79+
});
80+
createAccountMock.mockResolvedValue({ id: 3 });
81+
});
82+
83+
it('renders account overview data', async () => {
84+
render(<Accounts />);
85+
await waitFor(() => expect(getAccountsOverviewMock).toHaveBeenCalled());
86+
87+
expect(screen.getByRole('heading', { name: /accounts/i })).toBeInTheDocument();
88+
expect(screen.getAllByText(/main checking/i).length).toBeGreaterThan(0);
89+
expect(screen.getByText(/acme bank/i)).toBeInTheDocument();
90+
expect(screen.getByText(/groceries/i)).toBeInTheDocument();
91+
expect(screen.getByText(/currency groups/i)).toBeInTheDocument();
92+
});
93+
94+
it('creates an account and reloads overview', async () => {
95+
const user = userEvent.setup();
96+
render(<Accounts />);
97+
await waitFor(() => expect(getAccountsOverviewMock).toHaveBeenCalledTimes(1));
98+
99+
await user.type(screen.getByLabelText(/account name/i), 'Savings');
100+
await user.selectOptions(screen.getByLabelText(/account type/i), 'SAVINGS');
101+
await user.clear(screen.getByLabelText(/account balance/i));
102+
await user.type(screen.getByLabelText(/account balance/i), '1500');
103+
await user.clear(screen.getByLabelText(/account currency/i));
104+
await user.type(screen.getByLabelText(/account currency/i), 'usd');
105+
await user.type(screen.getByLabelText(/account institution/i), 'Credit Union');
106+
await user.click(screen.getByRole('button', { name: /create account/i }));
107+
108+
await waitFor(() =>
109+
expect(createAccountMock).toHaveBeenCalledWith(
110+
expect.objectContaining({
111+
name: 'Savings',
112+
account_type: 'SAVINGS',
113+
balance: 1500,
114+
currency: 'USD',
115+
institution: 'Credit Union',
116+
}),
117+
),
118+
);
119+
await waitFor(() => expect(getAccountsOverviewMock).toHaveBeenCalledTimes(2));
120+
});
121+
});

app/src/api/accounts.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { api } from './client';
2+
3+
export type AccountType =
4+
| 'CHECKING'
5+
| 'SAVINGS'
6+
| 'CREDIT_CARD'
7+
| 'INVESTMENT'
8+
| 'LOAN'
9+
| 'CASH'
10+
| 'OTHER';
11+
12+
export type FinancialAccount = {
13+
id: number;
14+
name: string;
15+
account_type: AccountType | string;
16+
balance: number;
17+
currency: string;
18+
institution: string | null;
19+
active: boolean;
20+
monthly_income?: number;
21+
monthly_expenses?: number;
22+
monthly_net_flow?: number;
23+
transaction_count?: number;
24+
};
25+
26+
export type AccountsOverview = {
27+
period: { month: string };
28+
summary: {
29+
account_count: number;
30+
assets: number;
31+
liabilities: number;
32+
net_worth: number;
33+
};
34+
accounts: FinancialAccount[];
35+
by_type: Array<{ account_type: string; count: number; balance: number }>;
36+
by_currency: Array<{
37+
currency: string;
38+
assets: number;
39+
liabilities: number;
40+
net_worth: number;
41+
}>;
42+
recent_transactions: Array<{
43+
id: number;
44+
account_id: number;
45+
account_name: string;
46+
description: string;
47+
amount: number;
48+
currency: string;
49+
date: string;
50+
type: 'INCOME' | 'EXPENSE' | string;
51+
}>;
52+
};
53+
54+
export type NewAccount = {
55+
name: string;
56+
account_type: AccountType;
57+
balance: number;
58+
currency: string;
59+
institution?: string;
60+
};
61+
62+
export async function getAccountsOverview(month?: string): Promise<AccountsOverview> {
63+
const query = month ? `?month=${encodeURIComponent(month)}` : '';
64+
return api<AccountsOverview>(`/accounts/overview${query}`);
65+
}
66+
67+
export async function createAccount(payload: NewAccount): Promise<FinancialAccount> {
68+
return api<FinancialAccount>('/accounts', {
69+
method: 'POST',
70+
body: payload,
71+
});
72+
}

app/src/components/layout/Navbar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const navigation = [
1212
{ name: 'Bills', href: '/bills' },
1313
{ name: 'Reminders', href: '/reminders' },
1414
{ name: 'Expenses', href: '/expenses' },
15+
{ name: 'Accounts', href: '/accounts' },
1516
{ name: 'Analytics', href: '/analytics' },
1617
];
1718

0 commit comments

Comments
 (0)