Files
finance/backend/tests/test_budget.py
T
k1nq 63d065011b Update manifest.json and infra README
- Updated timestamps and AST hashes in manifest.json for various backend and frontend files to reflect recent changes.
- Added new files related to MCC categories and optional hold reasons in the manifest.
- Modified the README in the infra directory to correct the linting tool from 'mypy' to 'ty' for type checking in the backend project.
2026-09-23 10:57:27 +05:00

156 lines
5.5 KiB
Python

from datetime import date
import pytest
from sqlalchemy.orm import Session
from app.budgeting.models import PeriodClosure
from app.budgeting.service import (
close_period,
compute_period_state,
copy_previous_month,
create_category,
get_or_create_period,
reopen_period,
set_assignments,
)
from app.categorization import service as categorization
from app.errors import AppError
from app.identity.models import User
from app.ledger.models import Category
from app.ledger.service import create_account, create_expense
from app.spaces.service import create_space
def seed(db: Session):
user = User(email="budget@example.test", password_hash="unused-in-domain-tests")
db.add(user)
db.flush()
return user, create_space(db, user=user, name="Budget")
def test_uncategorized_expense_blocks_close(db: Session) -> None:
user, space = seed(db)
account = create_account(
db,
space_id=space.id,
user=user,
name="Cash",
kind="cash",
opened_on=date(2026, 3, 1),
opening_amount_kopecks=10_000,
)
create_expense(
db,
space_id=space.id,
user=user,
account_id=account.id,
amount_kopecks=1_000,
occurred_on=date(2026, 3, 2),
)
period = get_or_create_period(db, space_id=space.id, year=2026, month=3)
with pytest.raises(AppError) as exc:
close_period(db, space_id=space.id, user=user, period=period)
assert exc.value.code == "BUDGET_CLOSE_BLOCKED"
def test_default_mcc_categories_are_budgetable_and_editable_mapping(db: Session) -> None:
user, space = seed(db)
defaults = db.query(Category).filter_by(space_id=space.id).all()
by_key = {category.system_key: category for category in defaults}
assert len([key for key in by_key if key not in {None, "savings"}]) == 21
groceries = by_key["groceries"]
others = by_key["others"]
assert {5411, 5422}.issubset(
categorization.get_category_mcc_codes(db, space_id=space.id, category_id=groceries.id)
)
period = get_or_create_period(db, space_id=space.id, year=2026, month=5)
set_assignments(
db,
space_id=space.id,
user=user,
period=period,
items=[{"category_id": groceries.id, "planned_kopecks": 2_000}],
)
account = create_account(
db,
space_id=space.id,
user=user,
name="Cash",
kind="cash",
opened_on=date(2026, 5, 1),
)
create_expense(
db,
space_id=space.id,
user=user,
account_id=account.id,
amount_kopecks=500,
occurred_on=date(2026, 5, 2),
category_id=others.id,
)
assert categorization.category_for_mcc(db, space_id=space.id, code=5999).id == others.id
custom = create_category(db, space_id=space.id, user=user, kind="monthly", name="My shops")
codes = categorization.set_category_mcc_codes(
db, space_id=space.id, category_id=custom.id, codes=[5999, 5994]
)
assert codes == [5994, 5999]
assert categorization.category_for_mcc(db, space_id=space.id, code=5999).id == custom.id
custom.archived = True
assert categorization.category_for_mcc(db, space_id=space.id, code=5999).id == others.id
custom.archived = False
with pytest.raises(AppError) as exc:
categorization.set_category_mcc_codes(
db, space_id=space.id, category_id=custom.id, codes=[5411]
)
assert exc.value.code == "CATEGORY_MCC_TAKEN"
def test_copy_previous_month_clamps_expected_day(db: Session) -> None:
from app.budgeting.service import add_expected_income
user, space = seed(db)
category = create_category(db, space_id=space.id, user=user, kind="monthly", name="Food")
january = get_or_create_period(db, space_id=space.id, year=2026, month=1)
february = get_or_create_period(db, space_id=space.id, year=2026, month=2)
set_assignments(
db,
space_id=space.id,
user=user,
period=january,
items=[{"category_id": category.id, "planned_kopecks": 30_000}],
)
add_expected_income(
db,
space_id=space.id,
user=user,
period=january,
expected_on=date(2026, 1, 31),
amount_kopecks=100_000,
source_name="Salary",
)
result = copy_previous_month(db, space_id=space.id, user=user, period=february)
assert result == {"assignments_copied": 1, "expected_incomes_copied": 1}
state = compute_period_state(db, space_id=space.id, period=february)
assert state["expected_incomes"][0]["expected_on"] == "2026-02-28"
def test_reopen_preserves_immutable_closure_history(db: Session) -> None:
user, space = seed(db)
period = get_or_create_period(db, space_id=space.id, year=2026, month=4)
close_period(db, space_id=space.id, user=user, period=period)
reopen_period(db, space_id=space.id, user=user, period=period)
close_period(db, space_id=space.id, user=user, period=period)
assert db.query(PeriodClosure).filter_by(period_id=period.id).count() == 2
def test_periods_must_close_in_calendar_order(db: Session) -> None:
user, space = seed(db)
august = get_or_create_period(db, space_id=space.id, year=2026, month=8)
september = get_or_create_period(db, space_id=space.id, year=2026, month=9)
with pytest.raises(AppError) as exc:
close_period(db, space_id=space.id, user=user, period=september)
assert exc.value.code == "BUDGET_CLOSE_OUT_OF_ORDER"
close_period(db, space_id=space.id, user=user, period=august)
close_period(db, space_id=space.id, user=user, period=september)