Files
finance/backend/app/analytics/router.py
T
k1nq d582f15681 feat: add tests for import polling, login page, money utilities, and query keys
- Implemented tests for import polling functionality, ensuring correct behavior for active and terminal import statuses.
- Created tests for the login page, validating user interactions and error handling.
- Added unit tests for money utility functions, covering parsing, formatting, and summation of monetary values.
- Developed tests for query keys, ensuring proper scoping and invalidation roots.

chore: setup testing environment and configuration

- Added setup file for testing with Vitest and Testing Library.
- Configured TypeScript and Vite for the frontend project.
- Created Docker and Nginx configurations for local development and production environments.
- Established a PostgreSQL initialization script for setting up application roles.

docs: add README and environment configuration for infra

- Included README for the infra directory, detailing local development setup and service descriptions.
- Created an example environment configuration file for local development with placeholders for sensitive information.
2026-09-14 09:26:33 +05:00

110 lines
3.4 KiB
Python

from datetime import date
from fastapi import APIRouter, Query
from app.analytics import service
from app.dependencies import Context, Db
router = APIRouter(prefix="/analytics", tags=["analytics"])
@router.get("/category-spend")
def category_spend(
date_from: date = Query(alias="from"),
date_to: date = Query(alias="to"),
*,
db: Db,
context: Context,
) -> list[dict]:
rows = service.category_spend(
db, space_id=context.space.id, date_from=date_from, date_to=date_to
)
return [
{
"categoryId": row["category_id"],
"name": row["category"] if not row["category"].startswith("category.") else None,
"nameKey": row["category"] if row["category"].startswith("category.") else None,
"amountKopecks": row["amount_kopecks"],
}
for row in rows
]
@router.get("/cash-flow")
def cash_flow(
date_from: date = Query(alias="from"),
date_to: date = Query(alias="to"),
*,
db: Db,
context: Context,
) -> list[dict]:
rows = service.cash_flow(db, space_id=context.space.id, date_from=date_from, date_to=date_to)
months: dict[str, dict[str, int | str]] = {}
for row in rows:
month = row["date"].strftime("%Y-%m")
bucket = months.setdefault(
month,
{
"month": month,
"incomeKopecks": 0,
"expenseKopecks": 0,
"refundsKopecks": 0,
"netKopecks": 0,
},
)
amount = int(row["amount_kopecks"])
key = {
"income": "incomeKopecks",
"expense": "expenseKopecks",
"refund": "refundsKopecks",
}[row["type"]]
bucket[key] = int(bucket[key]) + abs(amount)
bucket["netKopecks"] = int(bucket["netKopecks"]) + amount
return list(months.values())
@router.get("/plan-fact")
def plan_fact(month: str, db: Db, context: Context) -> dict:
from app.budgeting.router import parse_month
year, month_number = parse_month(month)
state = service.plan_fact(db, space_id=context.space.id, year=year, month=month_number)
categories = state.get("monthly_categories", [])
return {
"month": month,
"items": [
{
"categoryId": row["category_id"],
"name": row["name"] if not str(row["name"]).startswith("category.") else None,
"nameKey": row["name"] if str(row["name"]).startswith("category.") else None,
"plannedKopecks": row["planned"],
"actualKopecks": row["spent"],
}
for row in categories
],
}
@router.get("/balances")
def balances(db: Db, context: Context) -> dict:
from datetime import UTC, datetime
rows = service.balances(db, space_id=context.space.id)
assets = sum(max(row["balance_kopecks"], 0) for row in rows if row["kind"] != "credit")
liabilities = sum(row["debt_kopecks"] for row in rows)
return {
"asOf": datetime.now(UTC).date(),
"assetsKopecks": assets,
"liabilitiesKopecks": liabilities,
"netKopecks": assets - liabilities,
"items": [
{
"accountId": row["account_id"],
"name": row["account"],
"type": row["kind"],
"balanceKopecks": row["balance_kopecks"],
}
for row in rows
],
}