"""Net-worth read endpoints. Reads from `fire_planner.account_snapshot` (populated hourly by the wealthfolio ingest). Two views: - GET /networth → latest snapshot per account, totals - GET /networth/history → daily totals + per-account series, for charts """ from __future__ import annotations from collections import defaultdict from datetime import date from decimal import Decimal from fastapi import APIRouter, Depends, Query from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from fire_planner.api.dependencies import get_session from fire_planner.api.schemas import ( AccountSnapshotOut, NetWorthCurrent, NetWorthHistory, NetWorthHistoryPoint, ) from fire_planner.db import AccountSnapshot router = APIRouter(prefix="/networth", tags=["networth"]) @router.get("", response_model=NetWorthCurrent) async def current_networth(session: AsyncSession = Depends(get_session)) -> NetWorthCurrent: """Latest snapshot per account + GBP total.""" latest_date = (await session.execute( select(AccountSnapshot.snapshot_date).order_by( AccountSnapshot.snapshot_date.desc()).limit(1))).scalar() if latest_date is None: return NetWorthCurrent(snapshot_date=date.today(), total_gbp=Decimal("0"), accounts=[]) rows = (await session.execute( select(AccountSnapshot).where( AccountSnapshot.snapshot_date == latest_date))).scalars().all() accounts = [AccountSnapshotOut.model_validate(r) for r in rows] total = sum((a.market_value_gbp for a in accounts), Decimal("0")) return NetWorthCurrent(snapshot_date=latest_date, total_gbp=total, accounts=accounts) @router.get("/history", response_model=NetWorthHistory) async def networth_history( session: AsyncSession = Depends(get_session), days: int = Query(default=365, ge=1, le=3650, description="Look-back window."), ) -> NetWorthHistory: """Daily NW total + per-account breakdown for a stacked area chart. Picks one row per (account_id, snapshot_date) — wealthfolio ingest upserts daily so this is already de-duped, but we group defensively. """ rows = (await session.execute( select( AccountSnapshot.snapshot_date, AccountSnapshot.account_name, AccountSnapshot.market_value_gbp, ).order_by(AccountSnapshot.snapshot_date))).all() if not rows: return NetWorthHistory(points=[]) by_date: dict[date, dict[str, Decimal]] = defaultdict(lambda: defaultdict(lambda: Decimal("0"))) for snap_date, name, value in rows: by_date[snap_date][name] += Decimal(str(value)) cutoff_idx = max(0, len(by_date) - days) sorted_dates = sorted(by_date.keys())[cutoff_idx:] points = [ NetWorthHistoryPoint( snapshot_date=d, total_gbp=sum(by_date[d].values(), Decimal("0")), by_account=dict(by_date[d]), ) for d in sorted_dates ] return NetWorthHistory(points=points)