You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

122 lines
4.5 KiB

from datetime import date
from decimal import Decimal
import pytest
from finance.models import Account, Category, Transaction
from finance.reports import (
ReportInputError,
account_balances,
available_currencies,
monthly_summary,
spending_by_category,
)
pytestmark = pytest.mark.django_db
def make_account(name="Synthetic Bank", *, kind=Account.Kind.BANK, currency="BRL"):
return Account.objects.create(name=name, kind=kind, currency=currency)
def post(account, amount, kind, posting_date, *, category=None):
return Transaction.objects.create(
account=account,
amount=Decimal(amount),
kind=kind,
posting_date=posting_date,
category=category,
)
def test_monthly_summary_counts_card_purchase_once_and_refund_in_posting_month():
bank = make_account()
card = make_account("Synthetic Card", kind=Account.Kind.CREDIT_CARD)
post(bank, "500.00", Transaction.Kind.OPENING, date(2025, 12, 31))
post(bank, "1000.00", Transaction.Kind.INCOME, date(2026, 1, 2))
post(card, "-300.00", Transaction.Kind.EXPENSE, date(2026, 1, 3))
post(bank, "-300.00", Transaction.Kind.TRANSFER, date(2026, 1, 20))
post(card, "300.00", Transaction.Kind.TRANSFER, date(2026, 1, 20))
post(card, "50.00", Transaction.Kind.REFUND, date(2026, 1, 25))
post(card, "20.00", Transaction.Kind.REFUND, date(2026, 2, 1))
result = monthly_summary(date(2026, 1, 1), "BRL")
assert result.inflow == Decimal("1000.00")
assert result.expense == Decimal("300.00")
assert result.refund == Decimal("50.00")
assert result.net_spending == Decimal("250.00")
assert result.net_activity == Decimal("750.00")
def test_spending_has_uncategorized_bucket_and_never_combines_currency():
bank = make_account()
usd = make_account("Synthetic USD", currency="USD")
food = Category.objects.create(name="Synthetic Food", kind=Category.Kind.EXPENSE)
post(bank, "-100.00", Transaction.Kind.EXPENSE, date(2026, 1, 2), category=food)
post(bank, "20.00", Transaction.Kind.REFUND, date(2026, 1, 3), category=food)
post(bank, "-20.00", Transaction.Kind.EXPENSE, date(2026, 1, 4))
post(usd, "-999.00", Transaction.Kind.EXPENSE, date(2026, 1, 5), category=food)
rows = spending_by_category(date(2026, 1, 1), "BRL")
assert [(row.category_name, row.net_spending) for row in rows] == [
("Synthetic Food", Decimal("80.00")),
("Uncategorized", Decimal("20.00")),
]
assert rows[0].expense == Decimal("100.00")
assert rows[0].refund == Decimal("20.00")
def test_empty_month_returns_explicit_zero_totals_and_no_spending_rows():
result = monthly_summary(date(2026, 1, 1), "BRL")
assert result.inflow == Decimal("0.00")
assert result.expense == Decimal("0.00")
assert result.refund == Decimal("0.00")
assert result.net_spending == Decimal("0.00")
assert result.net_activity == Decimal("0.00")
assert spending_by_category(date(2026, 1, 1), "BRL") == ()
def test_account_balances_include_openings_and_transfers_through_month_end():
beta = make_account("Synthetic Beta")
alpha = make_account("Synthetic Alpha")
usd = make_account("Synthetic USD", currency="USD")
post(alpha, "100.00", Transaction.Kind.OPENING, date(2025, 12, 1))
post(alpha, "-20.00", Transaction.Kind.TRANSFER, date(2026, 1, 20))
post(beta, "20.00", Transaction.Kind.TRANSFER, date(2026, 1, 20))
post(beta, "-5.00", Transaction.Kind.EXPENSE, date(2026, 2, 1))
post(usd, "1000.00", Transaction.Kind.OPENING, date(2025, 12, 1))
rows = account_balances(date(2026, 1, 1), "BRL")
assert [(row.account_name, row.balance) for row in rows] == [
("Synthetic Alpha", Decimal("80.00")),
("Synthetic Beta", Decimal("20.00")),
]
def test_available_currencies_are_distinct_and_sorted():
make_account("Synthetic BRL", currency="BRL")
make_account("Synthetic USD", currency="USD")
make_account("Synthetic BRL 2", currency="BRL")
assert available_currencies() == ("BRL", "USD")
@pytest.mark.parametrize(
("month", "currency", "errors"),
[
(date(2026, 1, 2), "BRL", {"month": "must_be_first_day"}),
(date(9999, 12, 1), "BRL", {"month": "invalid_month"}),
(date(2026, 1, 1), "brl", {"currency": "invalid_currency"}),
("2026-01", "BRL", {"month": "invalid_date"}),
],
)
def test_report_inputs_are_validated(month, currency, errors):
with pytest.raises(ReportInputError) as caught:
monthly_summary(month, currency)
assert caught.value.errors == errors