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.
126 lines
4.6 KiB
126 lines
4.6 KiB
from datetime import date |
|
|
|
import pytest |
|
import finance.queries as queries |
|
|
|
from finance.models import Account, Category, Payee, Transaction |
|
from finance.queries import ( |
|
QueryInputError, |
|
TransactionFilters, |
|
filtered_transactions, |
|
) |
|
|
|
|
|
pytestmark = pytest.mark.django_db |
|
|
|
|
|
def make_account(name="Synthetic Bank", currency="BRL"): |
|
return Account.objects.create(name=name, kind=Account.Kind.BANK, currency=currency) |
|
|
|
|
|
def make_transaction(account, *, posting_date, amount="-10.00", **values): |
|
return Transaction.objects.create( |
|
account=account, |
|
amount=amount, |
|
posting_date=posting_date, |
|
kind=values.pop("kind", Transaction.Kind.EXPENSE), |
|
**values, |
|
) |
|
|
|
|
|
def test_filtered_transactions_combines_filters_and_orders_stably(): |
|
account = make_account() |
|
category = Category.objects.create(name="Synthetic Food", kind=Category.Kind.EXPENSE) |
|
older = make_transaction(account, posting_date=date(2026, 1, 5)) |
|
newer = make_transaction(account, posting_date=date(2026, 1, 20)) |
|
make_transaction(account, posting_date=date(2026, 1, 25), category=category) |
|
make_transaction(account, posting_date=date(2026, 2, 1)) |
|
make_transaction(make_account("Synthetic USD", "USD"), posting_date=date(2026, 1, 22)) |
|
|
|
filters = TransactionFilters.from_mapping({ |
|
"date_from": "2026-01-01", |
|
"date_to": "2026-01-31", |
|
"currency": "BRL", |
|
"uncategorized": "yes", |
|
}) |
|
|
|
rows = list(filtered_transactions(filters)) |
|
|
|
assert [row.pk for row in rows] == [newer.pk, older.pk] |
|
|
|
|
|
def test_filtered_transactions_supports_reference_kind_and_categorized_filters(): |
|
account = make_account() |
|
other_account = make_account("Synthetic Other") |
|
category = Category.objects.create(name="Synthetic Food", kind=Category.Kind.EXPENSE) |
|
payee = Payee.objects.create(name="Synthetic Merchant") |
|
expected = make_transaction( |
|
account, |
|
posting_date=date(2026, 1, 10), |
|
category=category, |
|
payee=payee, |
|
) |
|
make_transaction(account, posting_date=date(2026, 1, 11), category=category) |
|
make_transaction(other_account, posting_date=date(2026, 1, 12), category=category, payee=payee) |
|
|
|
filters = TransactionFilters.from_mapping({ |
|
"account": str(account.pk), |
|
"category": str(category.pk), |
|
"payee": str(payee.pk), |
|
"kind": "EXPENSE", |
|
"uncategorized": "no", |
|
}) |
|
|
|
assert list(filtered_transactions(filters)) == [expected] |
|
|
|
|
|
@pytest.mark.parametrize( |
|
("mapping", "errors"), |
|
[ |
|
({"date_from": "31/01/2026"}, {"date_from": "invalid_date"}), |
|
({"date_to": "not-a-date"}, {"date_to": "invalid_date"}), |
|
({"account": "bad"}, {"account": "invalid_uuid"}), |
|
({"category": "bad"}, {"category": "invalid_uuid"}), |
|
({"payee": "bad"}, {"payee": "invalid_uuid"}), |
|
({"kind": "UNKNOWN"}, {"kind": "invalid_choice"}), |
|
({"currency": "brl"}, {"currency": "invalid_currency"}), |
|
({"uncategorized": "maybe"}, {"uncategorized": "invalid_choice"}), |
|
], |
|
) |
|
def test_invalid_filter_is_explicit(mapping, errors): |
|
with pytest.raises(QueryInputError) as caught: |
|
TransactionFilters.from_mapping(mapping) |
|
|
|
assert caught.value.errors == errors |
|
|
|
|
|
def test_filter_errors_are_reported_together_and_date_order_is_checked(): |
|
with pytest.raises(QueryInputError) as caught: |
|
TransactionFilters.from_mapping({ |
|
"date_from": "2026-02-01", |
|
"date_to": "2026-01-01", |
|
"account": "bad", |
|
}) |
|
|
|
assert caught.value.errors == { |
|
"account": "invalid_uuid", |
|
"date_to": "before_date_from", |
|
} |
|
|
|
|
|
def test_reference_lists_include_history_after_active_rows_with_stable_names(): |
|
active_account = make_account("Synthetic Active") |
|
archived_account = make_account("Synthetic Archived") |
|
archived_account.archived = True |
|
archived_account.save(update_fields=["archived"]) |
|
active_category = Category.objects.create(name="Synthetic Active", kind=Category.Kind.EXPENSE) |
|
archived_category = Category.objects.create(name="Synthetic Archived", kind=Category.Kind.EXPENSE, archived=True) |
|
active_payee = Payee.objects.create(name="Synthetic Active") |
|
archived_payee = Payee.objects.create(name="Synthetic Archived", archived=True) |
|
|
|
assert list(queries.listed_accounts()) == [active_account, archived_account] |
|
assert list(queries.listed_categories()) == [active_category, archived_category] |
|
assert list(queries.listed_payees()) == [active_payee, archived_payee] |
|
assert list(queries.active_accounts()) == [active_account] |
|
assert list(queries.active_categories()) == [active_category] |
|
assert list(queries.active_payees()) == [active_payee]
|
|
|