| import os |
| from datetime import date, timedelta |
| from plaid.api import plaid_api |
| from plaid.configuration import Configuration |
| from plaid.api_client import ApiClient |
| from plaid.model.products import Products |
| from plaid.model.country_code import CountryCode |
| from plaid.exceptions import ApiException |
| from plaid.model.accounts_balance_get_request import AccountsBalanceGetRequest |
| from plaid.model.transactions_get_request import TransactionsGetRequest |
| from plaid.model.transactions_get_request_options import TransactionsGetRequestOptions |
| from plaid.model.link_token_create_request import LinkTokenCreateRequest |
| from plaid.model.link_token_create_request_user import LinkTokenCreateRequestUser |
| from plaid.model.item_public_token_exchange_request import ItemPublicTokenExchangeRequest |
|
|
| |
|
|
| def _make_client(): |
| env = os.environ.get("PLAID_ENV", "sandbox") |
| host = { |
| "sandbox": "https://sandbox.plaid.com", |
| "development": "https://development.plaid.com", |
| "production": "https://production.plaid.com", |
| }[env] |
|
|
| config = Configuration( |
| host=host, |
| api_key={ |
| "clientId": os.environ["PLAID_CLIENT_ID"], |
| "secret": os.environ["PLAID_SECRET"] |
| } |
| ) |
| return plaid_api.PlaidApi(ApiClient(config)) |
|
|
| plaid_client = _make_client() |
|
|
|
|
| |
|
|
| def get_balances(access_token: str) -> str: |
| request = AccountsBalanceGetRequest(access_token=access_token) |
| response = plaid_client.accounts_balance_get(request) |
|
|
| lines = ["USER'S CURRENT ACCOUNT BALANCES:"] |
| for account in response['accounts']: |
| name = account['name'] |
| subtype = str(account['subtype']) |
| current = account['balances']['current'] |
|
|
| if current is None: |
| continue |
|
|
| |
| if subtype in ['mortgage', 'line of credit']: |
| continue |
|
|
| if subtype == 'credit card': |
| limit = account['balances']['limit'] or 0 |
| owing = current |
| available = limit - current if limit > 0 else 0 |
| lines.append( |
| f"- {name} (Credit Card): " |
| f"${owing:.2f} owing, ${available:.2f} available out of ${limit:.2f} limit" |
| ) |
| elif subtype == 'checking': |
| available = account['balances']['available'] or current |
| lines.append(f"- {name} (Chequing): ${current:.2f} balance, ${available:.2f} available") |
| elif subtype == 'savings': |
| lines.append(f"- {name} (Savings): ${current:.2f}") |
| elif subtype == 'rrsp': |
| lines.append(f"- {name} (RRSP): ${current:.2f}") |
| else: |
| lines.append(f"- {name} ({subtype}): ${current:.2f}") |
|
|
| return "\n".join(lines) |
| request = AccountsBalanceGetRequest(access_token=access_token) |
| response = plaid_client.accounts_balance_get(request) |
|
|
| lines = ["USER'S CURRENT ACCOUNT BALANCES:"] |
| for account in response['accounts']: |
| name = account['name'] |
| subtype = account['subtype'] |
| current = account['balances']['current'] |
|
|
| if current is None: |
| continue |
|
|
| if subtype == 'credit card': |
| limit = account['balances']['limit'] or 0 |
| available = account['balances']['available'] or (limit - current) |
| lines.append( |
| f"- {name} (Credit Card): " |
| f"${current:.2f} owing, ${available:.2f} available" |
| ) |
| elif subtype in ['mortgage', 'line of credit']: |
| lines.append(f"- {name} ({subtype}): ${current:.2f} outstanding") |
| else: |
| lines.append(f"- {name} ({subtype}): ${current:.2f}") |
|
|
| return "\n".join(lines) |
|
|
| |
|
|
| def get_transactions(access_token: str, days: int = 30) -> str: |
| end_date = date.today() |
| start_date = end_date - timedelta(days=days) |
| week_start = end_date - timedelta(days=end_date.weekday()) |
|
|
| request = TransactionsGetRequest( |
| access_token=access_token, |
| start_date=start_date, |
| end_date=end_date, |
| options=TransactionsGetRequestOptions( |
| count=200, |
| include_personal_finance_category=True |
| ) |
| ) |
| response = plaid_client.transactions_get(request) |
| transactions = response['transactions'] |
|
|
| if not transactions: |
| return "No transactions found in the last 30 days." |
|
|
| today_total = 0.0 |
| week_total = 0.0 |
| month_total = 0.0 |
| income_total = 0.0 |
| category_totals: dict[str, float] = {} |
|
|
| for txn in transactions: |
| amount = txn['amount'] |
| name = txn['name'] |
| txn_date = txn['date'] |
| category = txn.get('personal_finance_category', {}).get('primary', 'OTHER') |
|
|
| |
| if category in ['TRANSFER_IN', 'TRANSFER_OUT']: |
| continue |
|
|
| |
| name_lower = name.lower() |
| if category == 'LOAN_PAYMENTS' and any(w in name_lower for w in ['payroll', 'salary', 'direct dep', 'employer', 'wages']): |
| income_total += amount |
| continue |
|
|
| |
| if category == 'LOAN_PAYMENTS': |
| continue |
|
|
| |
| if amount < 0: |
| income_total += abs(amount) |
| continue |
|
|
| |
| month_total += amount |
| readable = category.replace("_", " ").title() |
| category_totals[readable] = category_totals.get(readable, 0) + amount |
|
|
| if txn_date >= week_start: |
| week_total += amount |
|
|
| if txn_date == end_date: |
| today_total += amount |
|
|
| lines = ["FINANCIAL SUMMARY (pre-calculated, do NOT recalculate):"] |
| lines.append(f"Today's spending: ${today_total:.2f}") |
| lines.append(f"This week's spending (since {week_start}): ${week_total:.2f}") |
| lines.append(f"This month's spending (last {days} days): ${month_total:.2f}") |
|
|
| if income_total > 0: |
| lines.append(f"Income received (last {days} days): +${income_total:.2f}") |
|
|
| lines.append(f"Average daily spending: ${month_total / max(days, 1):.2f}") |
|
|
| lines.append("\nSPENDING BY CATEGORY:") |
| for category, total in sorted(category_totals.items(), key=lambda x: -x[1]): |
| pct = (total / month_total * 100) if month_total > 0 else 0 |
| lines.append(f"- {category}: ${total:.2f} ({pct:.0f}%)") |
|
|
| lines.append("\nRECENT TRANSACTIONS (last 5):") |
| count = 0 |
| for txn in transactions: |
| if count >= 5: |
| break |
| amount = txn['amount'] |
| category = txn.get('personal_finance_category', {}).get('primary', 'OTHER') |
| if amount <= 0 or category in ['TRANSFER_IN', 'TRANSFER_OUT', 'LOAN_PAYMENTS']: |
| continue |
| lines.append(f" - {txn['date']} {txn['name']}: ${amount:.2f}") |
| count += 1 |
|
|
| return "\n".join(lines) |
| end_date = date.today() |
| start_date = end_date - timedelta(days=days) |
| week_start = end_date - timedelta(days=end_date.weekday()) |
|
|
| request = TransactionsGetRequest( |
| access_token=access_token, |
| start_date=start_date, |
| end_date=end_date, |
| options=TransactionsGetRequestOptions( |
| count=100, |
| include_personal_finance_category=True |
| ) |
| ) |
| response = plaid_client.transactions_get(request) |
| transactions = response['transactions'] |
|
|
| if not transactions: |
| return "No transactions found in the last 30 days." |
|
|
| |
| today_total = 0.0 |
| week_total = 0.0 |
| month_total = 0.0 |
| income_total = 0.0 |
| category_totals: dict[str, float] = {} |
| today_transactions: list[str] = [] |
| week_transactions: list[str] = [] |
|
|
| for txn in transactions: |
| amount = txn['amount'] |
| name = txn['name'] |
| txn_date = txn['date'] |
| category = txn.get('personal_finance_category', {}).get('primary', 'OTHER') |
|
|
| |
| if amount < 0: |
| income_total += abs(amount) |
| continue |
|
|
| |
| if category in ['TRANSFER_IN', 'TRANSFER_OUT']: |
| continue |
|
|
| name_lower = name.lower() |
| if category == 'LOAN_PAYMENTS' and any(w in name_lower for w in ['payroll', 'salary', 'direct dep', 'employer', 'wages']): |
| income_total += amount |
| continue |
|
|
| |
| month_total += amount |
| category_totals[category] = category_totals.get(category, 0) + amount |
|
|
| if txn_date >= week_start: |
| week_total += amount |
| week_transactions.append(f" - {txn_date} {name}: ${amount:.2f}") |
|
|
| if txn_date == end_date: |
| today_total += amount |
| today_transactions.append(f" - {name}: ${amount:.2f}") |
|
|
| |
| lines = ["FINANCIAL SUMMARY (pre-calculated, do NOT recalculate):"] |
| lines.append(f"Today's spending: ${today_total:.2f}") |
| lines.append(f"This week's spending (since {week_start}): ${week_total:.2f}") |
| lines.append(f"This month's spending (last {days} days): ${month_total:.2f}") |
| |
| if income_total > 0: |
| lines.append(f"Income received (last {days} days): +${income_total:.2f}") |
|
|
| lines.append(f"Average daily spending: ${month_total / max(days, 1):.2f}") |
|
|
| lines.append("\nSPENDING BY CATEGORY:") |
| for category, total in sorted(category_totals.items(), key=lambda x: -x[1]): |
| readable = category.replace("_", " ").title() |
| pct = (total / month_total * 100) if month_total > 0 else 0 |
| lines.append(f"- {readable}: ${total:.2f} ({pct:.0f}%)") |
|
|
| |
| lines.append("\nRECENT TRANSACTIONS (last 5):") |
| for txn in transactions[:5]: |
| amount = txn['amount'] |
| if amount <= 0: |
| continue |
| prefix = "-" |
| lines.append(f" {prefix} {txn['date']} {txn['name']}: ${abs(amount):.2f}") |
|
|
| return "\n".join(lines) |
|
|
| |
|
|
| def get_financial_snapshot(access_token: str) -> str: |
| try: |
| balances = get_balances(access_token) |
| transactions = get_transactions(access_token) |
| return f"{balances}\n\n{transactions}" |
| except Exception as e: |
| error_str = str(e) |
| print(f"Plaid API Exception caught: {error_str}", flush=True) |
| if "ITEM_LOGIN_REQUIRED" in error_str: |
| return "BANK_REAUTH_REQUIRED" |
| return "NO_BANK_DATA" |
|
|
| |
|
|
| def create_update_link_token(user_id: str, access_token: str) -> str: |
| """ |
| Creates a Plaid Link token in Update Mode for a broken access token. |
| Launches direct bank sync to clear the ITEM_LOGIN_REQUIRED status. |
| """ |
| request = LinkTokenCreateRequest( |
| user=LinkTokenCreateRequestUser(client_user_id=user_id), |
| client_name="FISCAL", |
| country_codes=[CountryCode("CA")], |
| language="en", |
| access_token=access_token |
| ) |
| response = plaid_client.link_token_create(request) |
| return response["link_token"] |
|
|
|
|
| def create_link_token(user_id: str) -> str: |
| """Creates a Plaid Link token for the given user.""" |
| request = LinkTokenCreateRequest( |
| user=LinkTokenCreateRequestUser(client_user_id=user_id), |
| client_name="FISCAL", |
| products=[Products("transactions")], |
| country_codes=[CountryCode("CA")], |
| language="en", |
| ) |
| response = plaid_client.link_token_create(request) |
| return response["link_token"] |
|
|
| def exchange_public_token(public_token: str) -> str: |
| """Exchanges a public token for a permanent access token.""" |
| request = ItemPublicTokenExchangeRequest(public_token=public_token) |
| response = plaid_client.item_public_token_exchange(request) |
| return response["access_token"] |
|
|
|
|
| def get_chart_data(access_token: str) -> dict: |
| """Returns burn-down velocity chart data based on chequing account.""" |
| end_date = date.today() |
| start_date = end_date.replace(day=1) |
|
|
| |
| accounts = [] |
| chequing_balance = 0.0 |
| chequing_account_id = None |
|
|
| try: |
| balance_req = AccountsBalanceGetRequest(access_token=access_token) |
| balance_resp = plaid_client.accounts_balance_get(balance_req) |
| for account in balance_resp['accounts']: |
| current = account['balances']['current'] |
| if current is None: |
| continue |
| subtype = str(account['subtype']) |
| |
| |
| if subtype == 'checking': |
| chequing_balance = current |
| chequing_account_id = account['account_id'] |
|
|
| |
| if subtype in ['mortgage', 'line of credit']: |
| continue |
|
|
| accounts.append({ |
| "name": account['name'], |
| "type": subtype, |
| "balance": round(current, 2), |
| }) |
| except Exception as e: |
| print(f"Balance error in chart_data: {e}", flush=True) |
|
|
| |
| burndown = [] |
| income_total = 0.0 |
| expense_total = 0.0 |
|
|
| try: |
| request = TransactionsGetRequest( |
| access_token=access_token, |
| start_date=start_date, |
| end_date=end_date, |
| options=TransactionsGetRequestOptions( |
| count=300, |
| include_personal_finance_category=True |
| ) |
| ) |
| response = plaid_client.transactions_get(request) |
| transactions = response['transactions'] |
|
|
| |
| daily_net: dict[str, float] = {} |
|
|
| for txn in transactions: |
| amount = txn['amount'] |
| txn_date = str(txn['date']) |
| category = txn.get('personal_finance_category', {}).get('primary', 'OTHER') |
|
|
| |
| if category in ['TRANSFER_IN', 'TRANSFER_OUT']: |
| continue |
|
|
| |
| name_lower = txn['name'].lower() |
| if category == 'LOAN_PAYMENTS' and any(w in name_lower for w in ['payroll', 'salary', 'direct dep', 'employer', 'wages']): |
| income_total += amount |
| daily_net[txn_date] = daily_net.get(txn_date, 0) - amount |
| continue |
|
|
| if amount < 0: |
| |
| income_total += abs(amount) |
| daily_net[txn_date] = daily_net.get(txn_date, 0) + abs(amount) |
| else: |
| |
| expense_total += amount |
| daily_net[txn_date] = daily_net.get(txn_date, 0) - amount |
|
|
| |
| |
| |
| |
| |
| daily_balances: dict[str, float] = {} |
| running = chequing_balance |
| |
| |
| current = end_date |
| while current >= start_date: |
| day_str = str(current) |
| daily_balances[day_str] = running |
| |
| net_today = daily_net.get(day_str, 0) |
| running = running - net_today |
| current -= timedelta(days=1) |
|
|
| |
| current = start_date |
| while current <= end_date: |
| day_str = str(current) |
| balance = daily_balances.get(day_str, 0) |
| day_spend = 0 |
| |
| |
| for txn in transactions: |
| if str(txn['date']) == day_str and txn['amount'] > 0: |
| cat = txn.get('personal_finance_category', {}).get('primary', 'OTHER') |
| if cat not in ['TRANSFER_IN', 'TRANSFER_OUT']: |
| day_spend += txn['amount'] |
|
|
| burndown.append({ |
| "date": current.strftime("%b %d"), |
| "balance": round(balance, 2), |
| "spent": round(day_spend, 2), |
| }) |
| current += timedelta(days=1) |
|
|
| except Exception as e: |
| print(f"Transaction error in chart_data: {e}", flush=True) |
|
|
| return { |
| "burndown": burndown, |
| "income_total": round(income_total, 2), |
| "expense_total": round(expense_total, 2), |
| "accounts": accounts, |
| } |
| """Returns burn-down velocity chart data.""" |
| end_date = date.today() |
| start_date = end_date.replace(day=1) |
|
|
| |
| accounts = [] |
| try: |
| balance_req = AccountsBalanceGetRequest(access_token=access_token) |
| balance_resp = plaid_client.accounts_balance_get(balance_req) |
| for account in balance_resp['accounts']: |
| current = account['balances']['current'] |
| if current is None: |
| continue |
| accounts.append({ |
| "name": account['name'], |
| "type": str(account['subtype']), |
| "balance": round(current, 2), |
| }) |
| except Exception as e: |
| print(f"Balance error in chart_data: {e}", flush=True) |
|
|
| |
| burndown = [] |
| income_total = 0.0 |
| expense_total = 0.0 |
|
|
| try: |
| request = TransactionsGetRequest( |
| access_token=access_token, |
| start_date=start_date, |
| end_date=end_date, |
| options=TransactionsGetRequestOptions( |
| count=200, |
| include_personal_finance_category=True |
| ) |
| ) |
| response = plaid_client.transactions_get(request) |
| transactions = response['transactions'] |
|
|
| |
| daily_income: dict[str, float] = {} |
| daily_spending: dict[str, float] = {} |
|
|
| for txn in transactions: |
| amount = txn['amount'] |
| txn_date = str(txn['date']) |
| category = txn.get('personal_finance_category', {}).get('primary', 'OTHER') |
|
|
| if amount < 0: |
| income_total += abs(amount) |
| daily_income[txn_date] = daily_income.get(txn_date, 0) + abs(amount) |
| continue |
|
|
| if category in ['TRANSFER_IN', 'TRANSFER_OUT']: |
| continue |
|
|
| name_lower = txn['name'].lower() |
| if category == 'LOAN_PAYMENTS' and any(w in name_lower for w in ['payroll', 'salary', 'direct dep', 'employer', 'wages']): |
| income_total += amount |
| daily_income[txn_date] = daily_income.get(txn_date, 0) + amount |
| continue |
|
|
| expense_total += amount |
| daily_spending[txn_date] = daily_spending.get(txn_date, 0) + amount |
|
|
| |
| running_balance = income_total |
| current = start_date |
| while current <= end_date: |
| day_str = str(current) |
| day_spend = daily_spending.get(day_str, 0) |
| day_income = daily_income.get(day_str, 0) |
| running_balance = running_balance - day_spend + (day_income if current != start_date else 0) |
|
|
| burndown.append({ |
| "date": current.strftime("%b %d"), |
| "balance": round(running_balance, 2), |
| "spent": round(day_spend, 2), |
| }) |
| current += timedelta(days=1) |
|
|
| except Exception as e: |
| print(f"Transaction error in chart_data: {e}", flush=True) |
|
|
| return { |
| "burndown": burndown, |
| "income_total": round(income_total, 2), |
| "expense_total": round(expense_total, 2), |
| "accounts": accounts, |
| } |
| """Returns structured JSON for frontend charts.""" |
| end_date = date.today() |
| start_date = end_date - timedelta(days=30) |
|
|
| |
| try: |
| balance_req = AccountsBalanceGetRequest(access_token=access_token) |
| balance_resp = plaid_client.accounts_balance_get(balance_req) |
| accounts = [] |
| for account in balance_resp['accounts']: |
| current = account['balances']['current'] |
| if current is None: |
| continue |
| accounts.append({ |
| "name": account['name'], |
| "type": str(account['subtype']), |
| "balance": round(current, 2), |
| }) |
| except Exception as e: |
| print(f"Balance error in chart_data: {e}", flush=True) |
| accounts = [] |
|
|
| |
| category_chart = [] |
| weekly_data = [] |
| income_total = 0.0 |
| expense_total = 0.0 |
|
|
| try: |
| request = TransactionsGetRequest( |
| access_token=access_token, |
| start_date=start_date, |
| end_date=end_date, |
| options=TransactionsGetRequestOptions( |
| count=200, |
| include_personal_finance_category=True |
| ) |
| ) |
| response = plaid_client.transactions_get(request) |
| transactions = response['transactions'] |
|
|
| category_totals: dict[str, float] = {} |
| daily_spending: dict[str, float] = {} |
| daily_income: dict[str, float] = {} |
|
|
| for txn in transactions: |
| amount = txn['amount'] |
| txn_date = str(txn['date']) |
| category = txn.get('personal_finance_category', {}).get('primary', 'OTHER') |
|
|
| if amount < 0: |
| income_total += abs(amount) |
| daily_income[txn_date] = daily_income.get(txn_date, 0) + abs(amount) |
| continue |
|
|
| if category in ['TRANSFER_IN', 'TRANSFER_OUT']: |
| continue |
|
|
| name_lower = txn['name'].lower() |
| if category == 'LOAN_PAYMENTS' and any(w in name_lower for w in ['payroll', 'salary', 'direct dep', 'employer', 'wages']): |
| income_total += amount |
| daily_income[txn_date] = daily_income.get(txn_date, 0) + amount |
| continue |
|
|
| expense_total += amount |
| readable = category.replace("_", " ").title() |
| category_totals[readable] = category_totals.get(readable, 0) + amount |
| daily_spending[txn_date] = daily_spending.get(txn_date, 0) + amount |
|
|
| category_chart = [ |
| {"name": cat, "value": round(total, 2)} |
| for cat, total in sorted(category_totals.items(), key=lambda x: -x[1]) |
| ] |
|
|
| for i in range(4): |
| week_end = end_date - timedelta(weeks=i) |
| week_start_d = week_end - timedelta(days=6) |
| week_label = f"{week_start_d.strftime('%b %d')} - {week_end.strftime('%b %d')}" |
| week_spending = sum(v for k, v in daily_spending.items() if str(week_start_d) <= k <= str(week_end)) |
| week_income = sum(v for k, v in daily_income.items() if str(week_start_d) <= k <= str(week_end)) |
| weekly_data.append({"week": week_label, "spending": round(week_spending, 2), "income": round(week_income, 2)}) |
| weekly_data.reverse() |
|
|
| except Exception as e: |
| print(f"Transaction error in chart_data: {e}", flush=True) |
|
|
| return { |
| "category_chart": category_chart, |
| "weekly_chart": weekly_data, |
| "income_vs_expenses": { |
| "income": round(income_total, 2), |
| "expenses": round(expense_total, 2), |
| }, |
| "accounts": accounts, |
| } |
| """Returns structured JSON for frontend charts.""" |
| end_date = date.today() |
| start_date = end_date - timedelta(days=30) |
|
|
| |
| request = TransactionsGetRequest( |
| access_token=access_token, |
| start_date=start_date, |
| end_date=end_date, |
| options=TransactionsGetRequestOptions( |
| count=200, |
| include_personal_finance_category=True |
| ) |
| ) |
| response = plaid_client.transactions_get(request) |
| transactions = response['transactions'] |
|
|
| |
| balance_req = AccountsBalanceGetRequest(access_token=access_token) |
| balance_resp = plaid_client.accounts_balance_get(balance_req) |
|
|
| |
| category_totals: dict[str, float] = {} |
| income_total = 0.0 |
| expense_total = 0.0 |
| |
| |
| daily_spending: dict[str, float] = {} |
| daily_income: dict[str, float] = {} |
|
|
| for txn in transactions: |
| amount = txn['amount'] |
| txn_date = str(txn['date']) |
| category = txn.get('personal_finance_category', {}).get('primary', 'OTHER') |
|
|
| if amount < 0: |
| income_total += abs(amount) |
| daily_income[txn_date] = daily_income.get(txn_date, 0) + abs(amount) |
| continue |
|
|
| if category in ['TRANSFER_IN', 'TRANSFER_OUT']: |
| continue |
|
|
| name_lower = txn['name'].lower() |
| if category == 'LOAN_PAYMENTS' and any(w in name_lower for w in ['payroll', 'salary', 'direct dep', 'employer', 'wages']): |
| income_total += amount |
| daily_income[txn_date] = daily_income.get(txn_date, 0) + amount |
| continue |
|
|
| expense_total += amount |
| readable = category.replace("_", " ").title() |
| category_totals[readable] = category_totals.get(readable, 0) + amount |
| daily_spending[txn_date] = daily_spending.get(txn_date, 0) + amount |
|
|
| |
| category_chart = [ |
| {"name": cat, "value": round(total, 2)} |
| for cat, total in sorted(category_totals.items(), key=lambda x: -x[1]) |
| ] |
|
|
| |
| weekly_data = [] |
| for i in range(4): |
| week_end = end_date - timedelta(weeks=i) |
| week_start = week_end - timedelta(days=6) |
| week_label = f"{week_start.strftime('%b %d')} - {week_end.strftime('%b %d')}" |
|
|
| week_spending = sum( |
| v for k, v in daily_spending.items() |
| if str(week_start) <= k <= str(week_end) |
| ) |
| week_income = sum( |
| v for k, v in daily_income.items() |
| if str(week_start) <= k <= str(week_end) |
| ) |
|
|
| weekly_data.append({ |
| "week": week_label, |
| "spending": round(week_spending, 2), |
| "income": round(week_income, 2), |
| }) |
|
|
| weekly_data.reverse() |
|
|
| |
| accounts = [] |
| for account in balance_resp['accounts']: |
| current = account['balances']['current'] |
| if current is None: |
| continue |
| accounts.append({ |
| "name": account['name'], |
| "type": str(account['subtype']), |
| "balance": round(current, 2), |
| }) |
|
|
| return { |
| "category_chart": category_chart, |
| "weekly_chart": weekly_data, |
| "income_vs_expenses": { |
| "income": round(income_total, 2), |
| "expenses": round(expense_total, 2), |
| }, |
| "accounts": accounts, |
| } |