From 07d8cd53591fca461e1eca057b839099a0b5a204 Mon Sep 17 00:00:00 2001 From: Isaac Norman Date: Tue, 13 Aug 2024 01:43:23 +0100 Subject: [PATCH 1/2] test skeleton, sample data, readme and a couple of bug fixes --- README.md | 89 ++++- backend/crud.py | 51 +++ backend/main.py | 8 +- backend/requirements.txt | 4 +- backend/rest_api/accounts.py | 17 +- {test => backend/test}/__init__.py | 0 backend/test/conftest.py | 93 +++++ backend/test/sample_data_utils.py | 377 ++++++++++++++++++ backend/test/test_endpoints.py | 74 ++++ .../test}/test_monthly_balances.py | 13 +- frontend/.vite/deps/_metadata.json | 8 + frontend/.vite/deps/package.json | 3 + frontend/src/components/WealthPieChart.vue | 3 +- frontend/src/pages/accounts.vue | 2 +- load_sample_data.py | 22 + test/conftest.py | 2 - test/test_endpoints.py | 95 ----- 17 files changed, 732 insertions(+), 129 deletions(-) rename {test => backend/test}/__init__.py (100%) create mode 100644 backend/test/conftest.py create mode 100644 backend/test/sample_data_utils.py create mode 100644 backend/test/test_endpoints.py rename {test => backend/test}/test_monthly_balances.py (80%) create mode 100644 frontend/.vite/deps/_metadata.json create mode 100644 frontend/.vite/deps/package.json create mode 100644 load_sample_data.py delete mode 100644 test/conftest.py delete mode 100644 test/test_endpoints.py diff --git a/README.md b/README.md index e624630..0dc5555 100644 --- a/README.md +++ b/README.md @@ -1 +1,88 @@ -# finances \ No newline at end of file +# Finances - By Isaac Norman + +This readme is just a placeholder of enough info to know what this repo is and run it with sample data. + +# About + +I had a spreadsheet for a long time that I kept all my personal finances in. It was good, but I always found it annoying to keep up to date, copying and pasting stuff in. Also, with my excel skills at least, it didn't really scale well if I wanted to make a new visualisation or remap data. + +I finally had a bit of time and thought I would kill two birds with one stone: replace the spreadsheet with a nice app and also create a project that demonstrated my skills. + +I have managed to create an app that should replace the spreadsheet. However, it's taken a lot longer than I really had time for and it's the opposite of polished code I'd love to show off. I'm mostly a backend developer and a LOT of this work was front end. One day I'll make the code nice but that's not now, so no judging. + +# What does it do? + +Basically you can ingest different data files - ofx, csv - and build up a database of accounts, transactions and data points (currently about job/salary/tax) and then view it all in a web front end. + +## Features +- Summary Page + - Balance over time + - Wealth pie chart + - Stacked account bar chart (accounts summed over time) +- Accounts Page + - View all accounts and navigate to Account Details +- Account Details Page + - List of monthly transactions (like on your banking app) + - Balance over time + - value vs contributions for account types that grow such as share ISAs, houses etc. + - link to account login (i.e. your banking web page) +- Taxable Income Page + - visualisations on tax, income, salary and career +- General Features + - Interpolation of missing transactional data (i.e. if you only have the odd value of a pension/asset it will work out the missing values) + - All filterable by account type and time range + +# What is incomplete? +- The code is extremely prototype - was rushed to just get functional. +- Most of the ingest currently happens via python, the UI for this needs implementing +- Editing accounts and manually inputing transactions +- A proper packed build of the front and back end +- A proper docker image for the front and back end +- Tests are basically a skeleton. They need writing around the sample data. +- Plus lots more + +# What's the architecture? + +Backend: Python FastAPI, SqlAlchemy, Pydantic, Alembic +Frontend: Vue 3, Vuetify 3, TypeScript +DB: Dockerised Postgres + +# How do I run it? + +I will in future build both the front and backend and include in docker images, but for now: + +- check out the code +- create a python venv and install the requirements.txt +- run the docker compose: +``` +docker compose up -d +``` +- initialise the db with alembic: +``` +alembic upgrade head +``` +- optionally load some sample data (recommend if you want to just give it a quick try) +``` +python load_sample_data.py +``` +- run the backend +``` +fastapi dev backend/main.py +``` +- install deps for the frontend +``` +cd frontend +npm install +``` +- run the frontend +``` +cd frontend +npm run dev +``` +- navigate to the frontend web address: http://localhost:3000/ +- if you want to erase the db and start again +``` +docker compose down db +docker volume rm finances_db +docker compose up db -d +``` \ No newline at end of file diff --git a/backend/crud.py b/backend/crud.py index 09174d7..c57550a 100644 --- a/backend/crud.py +++ b/backend/crud.py @@ -43,6 +43,31 @@ def get_accounts( return [api_models.Account.model_validate(result) for result in results] +def create_accounts( + db_session: Session, + accounts: Union[api_models.Account, List[api_models.Account]], + as_db_model: bool = False, +): + if not isinstance(accounts, list): + accounts = [accounts] + + new_accounts: List[db_models.Account] = [ + db_models.Account(**account.model_dump()) for account in accounts + ] + db_session.add_all(new_accounts) + db_session.commit() + for new_account in new_accounts: + db_session.refresh(new_account) + + if not as_db_model: + return [api_models.Account.model_validate(new_account) for new_account in new_accounts] + + if len(new_accounts) == 1: + new_accounts = new_accounts[0] + + return new_accounts + + def get_transactions( db_session: Session, account_id: int, @@ -80,6 +105,32 @@ def get_transactions( return [api_models.Transaction.model_validate(result) for result in results] +def create_transactions( + db_session: Session, + transactions: Union[api_models.Transaction, List[api_models.Transaction]], + as_db_model: bool = False, +): + if not isinstance(transactions, list): + transactions = [transactions] + + new_transactions: List[db_models.Transaction] = [ + db_models.Transaction(**transaction.model_dump()) for transaction in transactions + ] + db_session.add_all(new_transactions) + db_session.commit() + + if not as_db_model: + return [ + api_models.Transaction.model_validate(new_transaction) + for new_transaction in new_transactions + ] + + if len(new_transactions) == 1: + new_transactions = new_transactions[0] + + return new_transactions + + def get_last_transaction_dates(db_session: Session) -> Dict[int, datetime]: # Query to get the last transaction date for each account_id results = ( diff --git a/backend/main.py b/backend/main.py index 3bfba6d..bbf2699 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,7 +1,5 @@ -# from contextlib import asynccontextmanager import logging import traceback -from typing import Union from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError @@ -11,8 +9,7 @@ from starlette.exceptions import HTTPException as StarletteHTTPException from starlette.middleware.base import BaseHTTPMiddleware -from backend import __version__, db_models -from backend.db import engine +from backend import __version__ from backend.rest_api import get_api_router logging.basicConfig( @@ -21,9 +18,6 @@ logger = logging.getLogger(__name__) logging.getLogger("ofxtools").setLevel(logging.ERROR) -# create everything if not using alembic -# db_models.Base.metadata.create_all(bind=engine) - def _configure_app() -> FastAPI: version = f"v{__version__}" diff --git a/backend/requirements.txt b/backend/requirements.txt index ea0bd5a..7587e1f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,6 +1,7 @@ black==24.4.2 isort==5.13.2 SQLAlchemy==2.0.30 +SQLAlchemy-Utils==0.41.2 fastapi==0.111.0 psycopg2==2.9.9 python-dateutil==2.9.0.post0 @@ -8,4 +9,5 @@ ofxtools==0.9.5 httpx==0.27.0 alembic==1.13.2 pytest==8.3.2 -pytest-asyncio==0.23.8 \ No newline at end of file +pytest-asyncio==0.23.8 +pytest-postgresql==6.0.0 diff --git a/backend/rest_api/accounts.py b/backend/rest_api/accounts.py index 99ad70d..e29f090 100644 --- a/backend/rest_api/accounts.py +++ b/backend/rest_api/accounts.py @@ -4,7 +4,7 @@ from typing import Dict, List, Optional, Union from dateutil import parser -from fastapi import APIRouter, Depends, HTTPException, Path, UploadFile, status +from fastapi import APIRouter, Body, Depends, HTTPException, Path, UploadFile, status from sqlalchemy.orm import Session from backend import api_models, crud, db_models @@ -133,20 +133,7 @@ def api_create_account( accounts: Union[api_models.AccountCreate, List[api_models.AccountCreate]], db_session: Session = Depends(get_db_session), ): - if not isinstance(accounts, list): - accounts = [accounts] - - new_accounts: List[db_models.Account] = [ - db_models.Account(**account.model_dump()) for account in accounts - ] - db_session.add_all(new_accounts) - db_session.commit() - for new_account in new_accounts: - db_session.refresh(new_account) - - if len(new_accounts) == 1: - new_accounts = new_accounts[0] - return new_accounts + return crud.create_accounts(db_session=db_session, accounts=accounts) @router.get( diff --git a/test/__init__.py b/backend/test/__init__.py similarity index 100% rename from test/__init__.py rename to backend/test/__init__.py diff --git a/backend/test/conftest.py b/backend/test/conftest.py new file mode 100644 index 0000000..5459533 --- /dev/null +++ b/backend/test/conftest.py @@ -0,0 +1,93 @@ +import os +import uuid +from datetime import datetime +from decimal import Decimal +from typing import List + +import pytest +from pydantic import BaseModel +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy_utils import create_database, database_exists, drop_database + +from backend import crud +from backend.api_models import AccountCreate, AccountType, IngestType +from backend.db import Base, get_db_session +from backend.main import app +from backend.test.sample_data_utils import ( + create_sample_accounts, + create_sample_transactions, +) + + +@pytest.fixture(scope="session") +def unique_test_db_name(request): + worker_id = getattr(request.config, "workerinput", {}).get("workerid", "master") + return f"test_db_{worker_id}_{uuid.uuid4().hex}" + + +@pytest.fixture(scope="session") +def db_engine(unique_test_db_name): + base_url = "postgresql://postgres:postgres@0.0.0.0/" + test_db_url = f"{base_url}{unique_test_db_name}" + + if not database_exists(test_db_url): + create_database(test_db_url) + + engine = create_engine(test_db_url) + yield engine + + engine.dispose() # Ensure all connections are closed + drop_database(test_db_url) + + +@pytest.fixture(scope="function", autouse=True) +def db_session(db_engine): + """ + Automatically use the db_session fixture in all tests. + """ + Base.metadata.create_all(bind=db_engine) + TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=db_engine) + session = TestingSessionLocal() + + yield session + + session.close() # Ensure session is closed after each test + Base.metadata.drop_all(bind=db_engine) # Cleanup tables after each test + + +@pytest.fixture(scope="function", autouse=True) +def setup_test_environment(db_session): + """ + Override the FastAPI dependencies to use the test database session. + """ + + def get_db_override(): + yield db_session + + app.dependency_overrides[get_db_session] = get_db_override + + yield # This allows the fixture to run both at setup and teardown + + app.dependency_overrides.clear() # Clear overrides after the session + + +@pytest.fixture(scope="session") +def sample_accounts(): + return create_sample_accounts() + + +@pytest.fixture(scope="session") +def sample_transactions(): + return create_sample_transactions() + + +@pytest.fixture(scope="function") +def insert_sample_accounts(db_session, sample_accounts): + crud.create_accounts(db_session=db_session, accounts=sample_accounts) + + +@pytest.fixture(scope="function") +def insert_sample_accounts_and_transactions(db_session, sample_accounts, sample_transactions): + crud.create_accounts(db_session=db_session, accounts=sample_accounts) + crud.create_transactions(db_session=db_session, transactions=sample_transactions) diff --git a/backend/test/sample_data_utils.py b/backend/test/sample_data_utils.py new file mode 100644 index 0000000..1d388cc --- /dev/null +++ b/backend/test/sample_data_utils.py @@ -0,0 +1,377 @@ +import random +from datetime import datetime, timedelta +from decimal import ROUND_HALF_UP, Decimal +from typing import List + +from dateutil.relativedelta import relativedelta +from pydantic import BaseModel + +from backend.api_models import ( + AccountCreate, + AccountType, + DataSeriesCreate, + IngestType, + TransactionCreate, +) + + +def two_dp(value): + return Decimal(value).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) + + +def random_date_month(dt): + first_day = dt.replace(day=1) + next_month = first_day.replace(month=dt.month % 12 + 1, day=1) + last_day = next_month - timedelta(days=1) + random_day = random.randint(1, last_day.day) + return dt.replace(day=random_day, hour=0, minute=0, second=0, microsecond=0) + + +class AccountSpec(BaseModel): + id: int + start_date: datetime + end_date: datetime = datetime.now() + institution: str + name: str + description: str | None = None + account_type: AccountType + default_ingest_type: IngestType + is_active: bool + ac_number: str | None = None + external_link: str | None = None + contributions: Decimal | None = None + monthly_withdrawals_max: Decimal | None = None + monthly_withdrawals_min: Decimal | None = None + growth_factor_max: Decimal | None = None + growth_factor_min: Decimal | None = None + value: Decimal | None = Decimal("0.00") + + +ACCOUNT_SPECS = [ + AccountSpec( + id=1, + institution="Nationwide", + name="Current Account", + account_type=AccountType.current_credit, + default_ingest_type=IngestType.ofx_transactions, + is_active=True, + ac_number="31334345", + external_link="https://onlinebanking.nationwide.co.uk/AccountList", + start_date=datetime(2014, 1, 1), + contributions=Decimal("3540.00"), + monthly_withdrawals_max=Decimal("-3800.00"), + monthly_withdrawals_min=Decimal("-2800.00"), + ), + AccountSpec( + id=2, + institution="Amex", + name="Credit Card", + account_type=AccountType.current_credit, + default_ingest_type=IngestType.csv, + is_active=True, + ac_number="8462 9832 3272 1279", + external_link="https://www.americanexpress.com/en-gb/account/login", + start_date=datetime(2014, 1, 1), + monthly_withdrawals_max=Decimal("-1000.00"), + monthly_withdrawals_min=Decimal("-100.00"), + ), + AccountSpec( + id=3, + institution="Aviva", + name="Acme Retirement Plan", + description="Company pension at Acme", + account_type=AccountType.pensions, + default_ingest_type=IngestType.value_and_contrib_csv, + is_active=True, + start_date=datetime(2015, 1, 1), + contributions=Decimal("658.00"), + growth_factor_max=Decimal("1.006"), + growth_factor_min=Decimal("0.999"), + ), + AccountSpec( + id=4, + institution="Property", + name="221b Baker Street", + account_type=AccountType.asset, + default_ingest_type=IngestType.value_and_contrib_csv, + is_active=True, + start_date=datetime(2016, 1, 1), + contributions=Decimal("271821.00"), + growth_factor_max=Decimal("1.005"), + growth_factor_min=Decimal("0.999"), + value=Decimal("217000.00"), + ), + AccountSpec( + id=5, + institution="HSBC", + name="Mortgage", + description="Mortgage on 221b Baker Street", + account_type=AccountType.loans, + default_ingest_type=IngestType.value_and_contrib_csv, + is_active=True, + start_date=datetime(2016, 1, 1), + contributions=Decimal("800.00"), + growth_factor_max=Decimal("1.001"), + growth_factor_min=Decimal("1.001"), + value=Decimal("-207000.00"), + ), + AccountSpec( + id=6, + institution="MoneyFarm", + name="IF-ISA", + account_type=AccountType.savings, + default_ingest_type=IngestType.value_and_contrib_csv, + is_active=True, + start_date=datetime(2014, 3, 1), + contributions=Decimal("100.00"), + growth_factor_max=Decimal("1.005"), + growth_factor_min=Decimal("0.998"), + ), +] + +SAVING_SPEC = next(spec for spec in ACCOUNT_SPECS if spec.account_type == AccountType.savings) + + +def create_tax_data_series(): + random.seed(2001) + dt = datetime(2015, 1, 1) + tax_paid = Decimal("26789.12") + salary = two_dp(tax_paid * 3) + + results = [] + + while dt < datetime.now(): + results.append(DataSeriesCreate(date_time=dt, key="Tax Paid", value=str(tax_paid))) + net_pay = two_dp(tax_paid * random_between(1.9, 2.2)) + results.append(DataSeriesCreate(date_time=dt, key="Net Pay", value=str(net_pay))) + tax_paid *= random_between(0.95, 1.15) + + results.append(DataSeriesCreate(date_time=dt, key="Salary", value=str(salary))) + bonus_target = two_dp(salary * Decimal(0.15)) + results.append(DataSeriesCreate(date_time=dt, key="Bonus Target", value=str(bonus_target))) + salary *= random_between(1, 1.15) + dt += relativedelta(years=1) + + results.append(DataSeriesCreate(date_time=datetime(2015, 1, 1), key="Company", value="Acme")) + results.append( + DataSeriesCreate(date_time=datetime(2018, 1, 1), key="Company", value="Weyland Yutani") + ) + results.append( + DataSeriesCreate(date_time=datetime(2020, 1, 1), key="Company", value="Tyrell Corporation") + ) + results.append( + DataSeriesCreate(date_time=datetime(2022, 1, 1), key="Company", value="Nuka Cola") + ) + + return results + + +def create_sample_accounts(): + return [ + AccountCreate( + institution=spec.institution, + name=spec.name, + account_type=spec.account_type, + default_ingest_type=spec.default_ingest_type, + is_active=spec.is_active, + ) + for spec in ACCOUNT_SPECS + ] + + +def create_sample_transactions(): + random.seed(42) + transactions: List[TransactionCreate] = [] + for spec in ACCOUNT_SPECS: + create_sample_transactions_for_account(spec, transactions) + return transactions + + +def create_sample_transactions_for_account( + spec: AccountSpec, transactions: List[TransactionCreate] +): + dt = spec.start_date + + while dt < spec.end_date: + + create_contribution_tx(spec, dt, transactions) + + if dt == spec.start_date: + create_initial_value_tx(spec, dt, transactions) + else: + create_growth_tx(spec, dt, transactions) + + create_withdrawal_tx(spec, dt, transactions) + + # increase date by one month + dt = dt + relativedelta(months=1) + + +def create_initial_value_tx(spec: AccountSpec, dt: datetime, transactions: List[TransactionCreate]): + if spec.account_type not in [AccountType.asset, AccountType.loans]: + return + + if not spec.value or not spec.contributions: + raise ValueError(f"Asset {spec.id} is missing value or contributions") + + match spec.account_type: + case AccountType.asset: + transactions.append( + TransactionCreate( + account_id=spec.id, + date_time=dt, + amount=spec.contributions, + transaction_type="Deposit", + description="Total Mortgage Contributions", + ) + ) + transactions.append( + TransactionCreate( + account_id=spec.id, + date_time=dt, + amount=spec.value - spec.contributions, + transaction_type="Value Adjustment", + description="Market value", + ) + ) + + case AccountType.loans: + transactions.append( + TransactionCreate( + account_id=spec.id, + date_time=dt, + amount=spec.value, + transaction_type="Withdrawal", + description="Loan drawdown", + ) + ) + + +def create_contribution_tx(spec: AccountSpec, dt: datetime, transactions: List[TransactionCreate]): + match spec.account_type: + case AccountType.current_credit: + description = "Pay" + tx_dt = dt.replace(day=26) + if spec.contributions is None: + # it's a credit card and we clear the last month each month + amount = abs(spec.value) + description = "Clear balance" + else: + amount = spec.contributions + case AccountType.pensions: + description = "Pension contribution" + tx_dt = dt.replace(day=1) + amount = spec.contributions + case AccountType.loans: + description = "Loan repayment" + tx_dt = dt.replace(day=3) + amount = spec.contributions + case AccountType.savings: + description = "Standing Order" + tx_dt = dt.replace(day=5) + amount = spec.contributions + case _: + return + + spec.value += amount + + transactions.append( + TransactionCreate( + account_id=spec.id, + date_time=tx_dt, + amount=amount, + transaction_type="Deposit", + description=description, + ) + ) + + +def random_between(min_val: Decimal, max_val: Decimal) -> Decimal: + return Decimal(random.uniform(float(min_val), float(max_val))) + + +def create_growth_tx(spec: AccountSpec, dt: datetime, transactions: List[TransactionCreate]): + if spec.account_type not in [ + AccountType.asset, + AccountType.pensions, + AccountType.loans, + AccountType.savings, + ]: + return + + growth_factor = random_between(spec.growth_factor_min, spec.growth_factor_max) + amount = two_dp((spec.value * growth_factor) - spec.value) + spec.value += amount + + if spec.account_type == AccountType.loans: + description = "Loan interest" + else: + description = "Market value change" + + if amount > 100000: + assert "too big" + + transactions.append( + TransactionCreate( + account_id=spec.id, + date_time=dt, + amount=amount, + transaction_type="Value Adjustment", + description=description, + ) + ) + + +def create_withdrawal_tx(spec: AccountSpec, dt: datetime, transactions: List[TransactionCreate]): + if spec.account_type != AccountType.current_credit: + return + + if spec.monthly_withdrawals_max is None or spec.monthly_withdrawals_min is None: + raise ValueError(f"Current account {spec.id} is missing withdrawal limits") + + num_tx = random.randint(5, 10) + + for _ in range(num_tx): + amount = two_dp( + random_between( + spec.monthly_withdrawals_min / num_tx, spec.monthly_withdrawals_max / num_tx + ) + ) + + transactions.append( + TransactionCreate( + account_id=spec.id, + date_time=random_date_month(dt), + amount=amount, + transaction_type="Withdrawal", + description="Withdrawal", + ) + ) + spec.value += amount + + if spec.value > 8000: + amount = Decimal(random.randint(1, 7) * 1000) + + tx_date = random_date_month(dt) + + transactions.append( + TransactionCreate( + account_id=spec.id, + date_time=tx_date, + amount=-amount, + transaction_type="Transfer to ISA", + description="Transfer", + ) + ) + spec.value -= amount + + transactions.append( + TransactionCreate( + account_id=SAVING_SPEC.id, + date_time=tx_date, + amount=amount, + transaction_type="Transfer from current account", + description="Deposit", + ) + ) + SAVING_SPEC.value += amount diff --git a/backend/test/test_endpoints.py b/backend/test/test_endpoints.py new file mode 100644 index 0000000..0ea7d20 --- /dev/null +++ b/backend/test/test_endpoints.py @@ -0,0 +1,74 @@ +import pytest +from fastapi.testclient import TestClient + +from backend.api_models import Account, AccountCreate, IngestType +from backend.main import app + +# todo divide into separate files + +client = TestClient(app) + + +@pytest.mark.usefixtures("insert_sample_accounts") +def test_list_accounts(sample_accounts): + response = client.get(f"/api/accounts/") + accounts = [Account.model_validate(val) for val in response.json()] + assert len(accounts) == len(sample_accounts) + assert response.status_code == 200 + + +def test_create_account(sample_accounts): + response = client.post(f"/api/accounts/", json=sample_accounts[0].model_dump()) + assert response.status_code == 200 + + +def test_get_account_summary(): + response = client.get(f"/api/accounts/summary/") + assert response.status_code == 200 + + +@pytest.mark.usefixtures("insert_sample_accounts") +def test_get_account_by_id(): + account_id = 1 + response = client.get(f"/api/accounts/{account_id}/") + assert response.status_code == 200 + + +def test_list_transactions(): + account_id = 1 + response = client.get(f"/api/accounts/{account_id}/transactions/") + assert response.status_code == 200 + + +@pytest.mark.skip(reason="Need sample files to test this") +def test_ingest_transactions(): + account_id = 1 + files = {"upload_file": ("testfile.csv", open("testfile.csv", "rb"), "text/csv")} + response = client.post(f"/api/accounts/{account_id}/transactions/", files=files) + assert response.status_code == 200 + + +def test_get_account_balance(): + account_id = 1 + response = client.get(f"/api/accounts/{account_id}/balance/") + assert response.status_code == 200 + + +def test_get_monthly_account_balance(): + response = client.get(f"/api/balance/monthly/") + assert response.status_code == 200 + + +def test_get_data_series(): + response = client.get(f"/api/dataseries/") + assert response.status_code == 200 + + +def test_add_data_series_values(): + data_series = { + "date_time": "2023-01-01T00:00:00Z", + "key": "example_key", + "value": "example_value", + } + response = client.post(f"/api/dataseries/", json=data_series) + assert response.status_code == 200 diff --git a/test/test_monthly_balances.py b/backend/test/test_monthly_balances.py similarity index 80% rename from test/test_monthly_balances.py rename to backend/test/test_monthly_balances.py index dc71907..71d724e 100644 --- a/test/test_monthly_balances.py +++ b/backend/test/test_monthly_balances.py @@ -1,16 +1,17 @@ from decimal import Decimal -from test.conftest import BASE_URL -import httpx import pytest +from fastapi.testclient import TestClient from backend.api_models import AccountSummary +from backend.main import app +client = TestClient(app) -@pytest.mark.asyncio -async def test_account_summaries(): - async with httpx.AsyncClient() as client: - response = await client.get(f"{BASE_URL}/api/accounts/summary/") + +@pytest.mark.usefixtures("insert_sample_accounts_and_transactions") +def test_account_summaries(): + response = client.get(f"/api/accounts/summary/") assert response.status_code == 200 # convert summaries json to a list of AccountSummary objects diff --git a/frontend/.vite/deps/_metadata.json b/frontend/.vite/deps/_metadata.json new file mode 100644 index 0000000..2a812fe --- /dev/null +++ b/frontend/.vite/deps/_metadata.json @@ -0,0 +1,8 @@ +{ + "hash": "7e6c4635", + "configHash": "e7ef31d6", + "lockfileHash": "e3b0c442", + "browserHash": "39344aa6", + "optimized": {}, + "chunks": {} +} \ No newline at end of file diff --git a/frontend/.vite/deps/package.json b/frontend/.vite/deps/package.json new file mode 100644 index 0000000..3dbc1ca --- /dev/null +++ b/frontend/.vite/deps/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/frontend/src/components/WealthPieChart.vue b/frontend/src/components/WealthPieChart.vue index 336d3cb..1bbccd5 100644 --- a/frontend/src/components/WealthPieChart.vue +++ b/frontend/src/components/WealthPieChart.vue @@ -54,7 +54,8 @@ balance: parseFloat(summary.balance), }; }) - .filter((summary) => summary.account.isActive == true); + .filter((summary) => summary.account.isActive == true) + .filter((summary) => summary.balance > 0); let labels: string[] = []; let data: number[] = []; diff --git a/frontend/src/pages/accounts.vue b/frontend/src/pages/accounts.vue index e045b7b..ab9357d 100644 --- a/frontend/src/pages/accounts.vue +++ b/frontend/src/pages/accounts.vue @@ -51,7 +51,7 @@