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.
 
 
 
 
 

148 lines
6.1 KiB

"""Synthetic direct-ORM checks for the PostgreSQL persistence boundary."""
from datetime import date
from decimal import Decimal
import pytest
from django.db import IntegrityError
from django.db.models.deletion import ProtectedError
from finance.models import (
Account, AuditEvent, CardStatement, Category, CategoryGroup, InstallmentPlan, Payee,
SourceRecord, Transaction, Transfer,
)
pytestmark = pytest.mark.django_db(transaction=True)
@pytest.fixture
def account():
return Account.objects.create(name="Synthetic bank", kind="BANK")
def post(account, **changes):
values = dict(account=account, amount=Decimal("-10.00"),
posting_date=date(2026, 9, 1), kind="EXPENSE")
values.update(changes)
return Transaction.objects.create(**values)
@pytest.mark.parametrize("kind,amount", [
("EXPENSE", "0"), ("EXPENSE", "1"), ("INCOME", "-1"),
("REFUND", "-1"), ("TRANSFER", "0"), ("OPENING", "0"),
("INVALID", "-1"),
])
def test_database_rejects_invalid_transaction_signs(account, kind, amount):
with pytest.raises(IntegrityError):
post(account, kind=kind, amount=Decimal(amount))
@pytest.mark.parametrize("currency", ["brl", "B1L", "BR", "ÁBC"])
def test_database_rejects_invalid_currency(currency):
with pytest.raises(IntegrityError):
Account.objects.create(name="Synthetic account", kind="BANK", currency=currency)
def test_database_rejects_invalid_reference_choices():
with pytest.raises(IntegrityError):
Account.objects.create(name="Synthetic account", kind="INVALID")
with pytest.raises(IntegrityError):
Category.objects.create(name="Synthetic category", kind="INVALID")
def test_opening_is_unique_and_cannot_have_associations(account):
post(account, kind="OPENING")
with pytest.raises(IntegrityError):
post(account, kind="OPENING")
payee = Payee.objects.create(name="Synthetic shop")
other = Account.objects.create(name="Other synthetic bank", kind="BANK")
with pytest.raises(IntegrityError):
post(other, kind="OPENING", payee=payee)
def test_refund_reference_only_allowed_on_refund(account):
expense = post(account)
with pytest.raises(IntegrityError):
post(account, refund_of=expense)
with pytest.raises(IntegrityError):
Transaction.objects.filter(pk=expense.pk).update(
kind="REFUND", amount=Decimal("10"), refund_of=expense)
def test_transfer_cannot_have_category(account):
category = Category.objects.create(name="Synthetic expense", kind="EXPENSE")
with pytest.raises(IntegrityError):
post(account, kind="TRANSFER", category=category)
def test_category_name_is_unique_within_each_group(account):
home = CategoryGroup.objects.create(name="Synthetic Home")
travel = CategoryGroup.objects.create(name="Synthetic Travel")
Category.objects.create(name="Synthetic Fees", kind="EXPENSE", group=home)
Category.objects.create(name="Synthetic Fees", kind="EXPENSE", group=travel)
with pytest.raises(IntegrityError):
Category.objects.create(name="Synthetic Fees", kind="EXPENSE", group=home)
def test_installment_pair_number_kind_and_uniqueness(account):
plan = InstallmentPlan.objects.create(account=account, total_installments=10)
for changes in [dict(installment_plan=plan), dict(installment_number=3),
dict(installment_plan=plan, installment_number=0),
dict(installment_plan=plan, installment_number=3,
kind="REFUND", amount=Decimal("10"))]:
with pytest.raises(IntegrityError):
post(account, **changes)
post(account, installment_plan=plan, installment_number=3)
with pytest.raises(IntegrityError):
post(account, installment_plan=plan, installment_number=3)
with pytest.raises(IntegrityError):
InstallmentPlan.objects.create(account=account, total_installments=0)
def test_statement_date_order_and_identity(account):
values = dict(account=account, period_start=date(2026, 8, 1),
period_end=date(2026, 8, 31), due_date=date(2026, 9, 10))
with pytest.raises(IntegrityError):
CardStatement.objects.create(**{**values, "period_end": date(2026, 7, 31)})
with pytest.raises(IntegrityError):
CardStatement.objects.create(**{**values, "due_date": date(2026, 8, 30)})
CardStatement.objects.create(**values)
with pytest.raises(IntegrityError):
CardStatement.objects.create(**values)
def test_source_identity_format_and_protected_history(account):
activity = post(account)
values = dict(transaction=activity, document_hash="a" * 64,
source_format="BANK_CSV", parser_version="synthetic-v1",
item_locator="row:1")
for changes in [dict(document_hash="bad"), dict(source_format="OFX")]:
with pytest.raises(IntegrityError):
SourceRecord.objects.create(**{**values, **changes})
SourceRecord.objects.create(**values)
with pytest.raises(IntegrityError):
SourceRecord.objects.create(**values)
with pytest.raises(ProtectedError):
activity.delete()
with pytest.raises(ProtectedError):
account.delete()
def test_transfer_distinct_and_unique_roles(account):
outgoing = post(account, kind="TRANSFER")
incoming = post(account, kind="TRANSFER", amount=Decimal("10"))
with pytest.raises(IntegrityError):
Transfer.objects.create(outgoing=outgoing, incoming=outgoing)
Transfer.objects.create(outgoing=outgoing, incoming=incoming)
with pytest.raises(IntegrityError):
Transfer.objects.create(outgoing=outgoing, incoming=post(account, amount=Decimal("10"), kind="TRANSFER"))
with pytest.raises(IntegrityError):
Transfer.objects.create(outgoing=post(account, kind="TRANSFER"), incoming=incoming)
def test_audit_actor_and_outcome_choices():
values = dict(actor_kind="HUMAN", actor_identifier="synthetic-owner",
operation="create_account", outcome="SUCCESS")
for changes in [dict(actor_kind="INVALID"), dict(outcome="INVALID")]:
with pytest.raises(IntegrityError):
AuditEvent.objects.create(**{**values, **changes})