"""Read-only reporting over transactions and budgets (R11). Queries only: nothing in this module mutates state. """ from __future__ import annotations from ..money import Money from ..models import EXPENSE, BudgetLine, CategoryTotal, Month, MonthSummary from ..repository import Repository __all__ = ["ReportingService"] class ReportingService: """Monthly summaries, category breakdowns, and budget status.""" def __init__(self, repo: Repository) -> None: self._repo = repo def monthly_summary(self, month: Month) -> MonthSummary: start, end = month.range income_cents, expense_cents = self._repo.income_expense_totals(start, end) return MonthSummary( month=month, income=Money(cents=income_cents), expenses=Money(cents=expense_cents), ) def category_totals(self, month: Month) -> list[CategoryTotal]: start, end = month.range return [ CategoryTotal( category=row["category"], kind=row["kind"], total=Money(cents=row["total_cents"]), ) for row in self._repo.category_totals(start, end) ] def budget_status(self, month: Month) -> list[BudgetLine]: """Budgeted lines first, then unbudgeted spending (R11).""" start, end = month.range spent = { row["category"]: Money(cents=row["total_cents"]) for row in self._repo.category_totals(start, end) if row["kind"] == EXPENSE } lines: list[BudgetLine] = [] for row in self._repo.list_budgets(): name = row["category"] lines.append( BudgetLine( category=name, budget=Money(cents=row["amount_cents"]), spent=spent.get(name, Money.zero()), ) ) budgeted = {line.category for line in lines} for name, cost in sorted(spent.items()): if name not in budgeted and cost.is_positive: lines.append(BudgetLine(category=name, budget=Money.zero(), spent=cost)) return lines