mirror of
https://github.com/Zproger/Aurum.git
synced 2026-10-13 02:56:39 +07:00
Initial public release
Aurum — a self-hosted personal finance operating system: cash flow, net worth tracking, budgets, goals, recurring payments, reports, proactive alerts, and a compound-interest returns calculator, all bilingual (RU/EN) and mobile-first. Ships as a three-container Docker Compose stack (Postgres + FastAPI + nginx-served React frontend). See README.md for setup and CONTRIBUTING.md for development.
This commit is contained in:
89
backend/app/services/dashboard_service.py
Normal file
89
backend/app/services/dashboard_service.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""Aggregation logic behind the Overview dashboard."""
|
||||
import calendar
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.category import Category
|
||||
from app.models.enums import TransactionType
|
||||
from app.models.transaction import Transaction
|
||||
from app.schemas.dashboard import CategoryBreakdownItem, DashboardSummary
|
||||
|
||||
# Categorical slots are capped at 8 (dataviz skill: a 9th series folds into "Other",
|
||||
# never a generated hue) — this is also the exact size of the default category set.
|
||||
MAX_CHART_SLICES = 8
|
||||
OTHER_SLICE_COLOR = "#898781" # muted ink, reserved for the non-categorical rollup
|
||||
|
||||
|
||||
def _month_bounds(year: int, month: int) -> tuple[date, date]:
|
||||
last_day = calendar.monthrange(year, month)[1]
|
||||
return date(year, month, 1), date(year, month, last_day)
|
||||
|
||||
|
||||
async def get_dashboard_summary(session: AsyncSession, year: int, month: int) -> DashboardSummary:
|
||||
start, end = _month_bounds(year, month)
|
||||
|
||||
totals_stmt = (
|
||||
select(Transaction.type, func.coalesce(func.sum(Transaction.amount), 0))
|
||||
.where(Transaction.date >= start, Transaction.date <= end)
|
||||
.group_by(Transaction.type)
|
||||
)
|
||||
totals_result = await session.execute(totals_stmt)
|
||||
totals: dict[TransactionType, Decimal] = {row[0]: row[1] for row in totals_result.all()}
|
||||
|
||||
real_income = totals.get(TransactionType.INCOME, Decimal("0"))
|
||||
spent = totals.get(TransactionType.EXPENSE, Decimal("0"))
|
||||
transferred_out = totals.get(TransactionType.TRANSFER, Decimal("0"))
|
||||
|
||||
breakdown_stmt = (
|
||||
select(
|
||||
Category.id,
|
||||
Category.name,
|
||||
Category.color,
|
||||
Category.icon,
|
||||
func.coalesce(func.sum(Transaction.amount), 0).label("amount"),
|
||||
)
|
||||
.join(Category, Category.id == Transaction.category_id)
|
||||
.where(
|
||||
Transaction.date >= start,
|
||||
Transaction.date <= end,
|
||||
Transaction.type == TransactionType.EXPENSE,
|
||||
)
|
||||
.group_by(Category.id, Category.name, Category.color, Category.icon, Category.sort_order)
|
||||
.order_by(func.sum(Transaction.amount).desc(), Category.sort_order)
|
||||
)
|
||||
breakdown_result = await session.execute(breakdown_stmt)
|
||||
rows = breakdown_result.all()
|
||||
|
||||
top_rows, rest_rows = rows[:MAX_CHART_SLICES], rows[MAX_CHART_SLICES:]
|
||||
|
||||
def _percent(amount: Decimal) -> float:
|
||||
return float(amount / spent * 100) if spent else 0.0
|
||||
|
||||
spending_by_category = [
|
||||
CategoryBreakdownItem(
|
||||
category_id=cat_id, name=name, color=color, icon=icon, amount=amount, percent=_percent(amount)
|
||||
)
|
||||
for cat_id, name, color, icon, amount in top_rows
|
||||
]
|
||||
|
||||
if rest_rows:
|
||||
other_amount = sum((row.amount for row in rest_rows), Decimal("0"))
|
||||
spending_by_category.append(
|
||||
CategoryBreakdownItem(
|
||||
category_id=None, name="Other", color=OTHER_SLICE_COLOR, icon="more-horizontal",
|
||||
amount=other_amount, percent=_percent(other_amount),
|
||||
)
|
||||
)
|
||||
|
||||
return DashboardSummary(
|
||||
year=year,
|
||||
month=month,
|
||||
real_income=real_income,
|
||||
spent=spent,
|
||||
net=real_income - spent,
|
||||
transferred_out=transferred_out,
|
||||
spending_by_category=spending_by_category,
|
||||
)
|
||||
Reference in New Issue
Block a user