d582f15681
- 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.
106 lines
3.7 KiB
Python
106 lines
3.7 KiB
Python
import uuid
|
|
from collections import defaultdict
|
|
from datetime import date
|
|
|
|
from sqlalchemy import func
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.budgeting.models import PeriodClosure
|
|
from app.budgeting.service import compute_period_state, get_or_create_period
|
|
from app.categorization.service import resolve_category_name
|
|
from app.ledger.models import Account, Category, Operation
|
|
from app.ledger.service import account_balance
|
|
|
|
|
|
def category_spend(
|
|
db: Session, *, space_id: uuid.UUID, date_from: date, date_to: date
|
|
) -> list[dict]:
|
|
expenses = (
|
|
db.query(Operation)
|
|
.filter(
|
|
Operation.space_id == space_id,
|
|
Operation.status == "confirmed",
|
|
Operation.type == "expense",
|
|
Operation.occurred_on >= date_from,
|
|
Operation.occurred_on <= date_to,
|
|
)
|
|
.all()
|
|
)
|
|
totals: dict[uuid.UUID | None, int] = defaultdict(int)
|
|
for expense in expenses:
|
|
refunded = (
|
|
db.query(func.coalesce(func.sum(Operation.amount_kopecks), 0))
|
|
.filter(
|
|
Operation.space_id == space_id,
|
|
Operation.status == "confirmed",
|
|
Operation.type == "refund",
|
|
Operation.parent_operation_id == expense.id,
|
|
)
|
|
.scalar()
|
|
)
|
|
totals[expense.category_id] += expense.amount_kopecks - int(refunded or 0)
|
|
categories = {
|
|
item.id: resolve_category_name(item)
|
|
for item in db.query(Category).filter_by(space_id=space_id).all()
|
|
}
|
|
return [
|
|
{
|
|
"category_id": category_id,
|
|
"category": (
|
|
categories.get(category_id, "category.unnamed")
|
|
if category_id is not None
|
|
else "category.uncategorized"
|
|
),
|
|
"amount_kopecks": amount,
|
|
}
|
|
for category_id, amount in sorted(totals.items(), key=lambda item: item[1], reverse=True)
|
|
]
|
|
|
|
|
|
def cash_flow(db: Session, *, space_id: uuid.UUID, date_from: date, date_to: date) -> list[dict]:
|
|
operations = (
|
|
db.query(Operation)
|
|
.filter(Operation.space_id == space_id, Operation.status == "confirmed")
|
|
.all()
|
|
)
|
|
totals: dict[tuple[date, str], int] = defaultdict(int)
|
|
for operation in operations:
|
|
effective = operation.posted_on or operation.occurred_on
|
|
if not date_from <= effective <= date_to or operation.type == "transfer":
|
|
continue
|
|
signed = (
|
|
operation.amount_kopecks
|
|
if operation.type in {"income", "refund"}
|
|
else -operation.amount_kopecks
|
|
)
|
|
totals[(effective, operation.type)] += signed
|
|
return [
|
|
{"date": day, "type": kind, "amount_kopecks": amount}
|
|
for (day, kind), amount in sorted(totals.items())
|
|
]
|
|
|
|
|
|
def plan_fact(db: Session, *, space_id: uuid.UUID, year: int, month: int) -> dict:
|
|
period = get_or_create_period(db, space_id=space_id, year=year, month=month)
|
|
if period.status == "closed":
|
|
closure = db.query(PeriodClosure).filter_by(period_id=period.id).one_or_none()
|
|
if closure is not None:
|
|
return closure.snapshot
|
|
return compute_period_state(db, space_id=space_id, period=period)
|
|
|
|
|
|
def balances(db: Session, *, space_id: uuid.UUID) -> list[dict]:
|
|
result = []
|
|
for account in db.query(Account).filter_by(space_id=space_id).order_by(Account.created_at):
|
|
balance = account_balance(db, account)
|
|
result.append(
|
|
{
|
|
"account_id": account.id,
|
|
"account": account.name,
|
|
"kind": account.kind,
|
|
"balance_kopecks": balance,
|
|
"debt_kopecks": max(-balance, 0) if account.kind == "credit" else 0,
|
|
}
|
|
)
|
|
return result
|