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.
139 lines
5.6 KiB
139 lines
5.6 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, 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_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})
|
|
|