Files
finanz-cockpit/tests/test_transactions.py
T
mathias2784 73298b3e49
Tests / test (push) Successful in 11s
Tests / lint (push) Successful in 10s
finalized helper functions for category trees; started writing tests
2026-10-28 21:17:54 +01:00

197 lines
6.5 KiB
Python

from datetime import date
from decimal import Decimal
from pathlib import Path
from sqlalchemy import func, select
from sqlalchemy.orm import Session
from src.ingest.b4w import ingest_b4w, upsert_b4w
from src.ingest.comdirect import ingest_comdirect
from src.ingest.transactions import (
consolidate_transactions,
map_b4w_to_transaction,
map_comdirect_giro_to_transaction,
)
from src.models.b4w import B4wDb
from src.models.comdirect import ComdirectGiroDb
from src.models.transactions import Transaction, TransactionDb
from tests.factories import B4wExportFactory, CategoryFactory
B4W_PATH = Path("tests/test_data/b4w_export_dummy_01.csv")
COMDIRECT_PATH = Path("tests/test_data/comdirect_export_dummy_01.txt")
def test_transaction_hash_is_business_key():
first = Transaction(
source="b4w",
source_row_id=1,
account_iban="DE89370400440532013000",
account_currency="EUR",
date_valid=date(2026, 9, 7),
amount=Decimal("-50.00"),
amount_currency="EUR",
date_book=date(2026, 9, 8),
text="Supermarket",
other_name="Aldi",
)
second = Transaction(
source="b4w",
source_row_id=999,
account_iban="DE89370400440532013000",
account_currency="EUR",
date_valid=date(2026, 9, 7),
amount=Decimal("-50.00"),
amount_currency="EUR",
date_book=date(2026, 9, 8),
text="Supermarket",
other_name="Aldi",
)
# Stable across source-row ids (re-imports) ...
assert second.hash_id == first.hash_id
# ... but source is part of the key, so no cross-source collision.
third = second.model_copy(update={"source": "comdirect_giro"})
assert third.hash_id != first.hash_id
def test_map_b4w_to_transaction():
row = B4wDb(
id=7,
hash_id="unused",
id_b4w=1,
account_iban="DE89370400440532013000",
account_currency="EUR",
date_valid=date(2026, 9, 7),
date_book=date(2026, 9, 8),
amount=Decimal("-50.00"),
amount_currency="EUR",
text="Supermarket",
other_name="Aldi",
other_iban="DE02120300000000202051",
category_id=None,
notes="",
)
transaction = map_b4w_to_transaction(row)
assert isinstance(transaction, Transaction)
assert transaction.source == "b4w"
assert transaction.source_row_id == 7
assert transaction.amount == Decimal("-50.00")
assert transaction.date_book == date(2026, 9, 8)
assert transaction.text == "Supermarket"
assert transaction.other_name == "Aldi"
assert transaction.other_iban == "DE02120300000000202051"
def test_map_comdirect_giro_to_transaction():
row = ComdirectGiroDb(
id=3,
hash_id="unused",
account_iban="DE89370400440532013000",
account_currency="EUR",
amount_currency="EUR",
date_valid=date(2026, 9, 7),
amount=Decimal("32.43"),
date_book=date(2026, 9, 8),
type="Kartenumsatz",
text="Supermarkt",
)
transaction = map_comdirect_giro_to_transaction(row)
assert isinstance(transaction, Transaction)
assert transaction.source == "comdirect_giro"
assert transaction.source_row_id == 3
assert transaction.amount == Decimal("32.43")
assert transaction.other_name is None
assert transaction.other_iban is None
assert transaction.category is None
def test_consolidate_b4w_signed_amount_and_category(db_session: Session):
export = B4wExportFactory.build(
Amt="50.00",
RmtInf="Supermarket",
Category=CategoryFactory.build().to_str(),
CdtDbtInd="DBIT",
)
staging = upsert_b4w(db_session, export, do_commit=False)
assert staging is not None
consolidate_transactions(
db_session, "b4w", B4wDb, map_b4w_to_transaction, do_commit=False
)
db_session.commit()
transaction = db_session.execute(select(TransactionDb)).scalars().one()
assert transaction.source == "b4w"
assert transaction.source_row_id == staging.id
# b4w amount_signed: DBIT -> money out
assert transaction.amount == Decimal("-50.00")
assert transaction.category_id == staging.category_id
assert transaction.text == "Supermarket"
mapped = map_b4w_to_transaction(staging)
assert mapped.category is not None
assert mapped.category.to_str() == export.category.to_str()
def test_ingest_b4w_e2e_idempotent(db_session: Session):
for _ in range(2):
ingest_b4w(B4W_PATH, db_session)
staging_count = db_session.execute(
select(func.count()).select_from(B4wDb)
).scalar_one()
transaction_count = db_session.execute(
select(func.count()).select_from(TransactionDb)
).scalar_one()
assert staging_count > 0
assert transaction_count == staging_count
transactions = db_session.execute(select(TransactionDb)).scalars().all()
assert all(t.source == "b4w" for t in transactions)
assert all(t.source_row_id is not None for t in transactions)
def test_ingest_comdirect_e2e_idempotent(db_session: Session):
for _ in range(2):
ingest_comdirect(
session=db_session,
path=COMDIRECT_PATH,
account_iban="DE89370400440532013000",
account_currency="EUR",
)
staging_count = db_session.execute(
select(func.count()).select_from(ComdirectGiroDb)
).scalar_one()
transaction_count = db_session.execute(
select(func.count()).select_from(TransactionDb)
).scalar_one()
assert staging_count > 0
assert transaction_count == staging_count
transactions = db_session.execute(select(TransactionDb)).scalars().all()
assert all(t.source == "comdirect_giro" for t in transactions)
assert all(t.account_iban == "DE89370400440532013000" for t in transactions)
def test_ingest_mixed_sources_into_one_table(db_session: Session):
ingest_b4w(B4W_PATH, db_session)
ingest_comdirect(
session=db_session,
path=COMDIRECT_PATH,
account_iban="DE89370400440532013000",
account_currency="EUR",
)
b4w_count = db_session.execute(select(func.count()).select_from(B4wDb)).scalar_one()
giro_count = db_session.execute(
select(func.count()).select_from(ComdirectGiroDb)
).scalar_one()
transaction_count = db_session.execute(
select(func.count()).select_from(TransactionDb)
).scalar_one()
assert b4w_count > 0
assert giro_count > 0
assert transaction_count == b4w_count + giro_count
sources = set(db_session.execute(select(TransactionDb.source).distinct()).scalars())
assert sources == {"b4w", "comdirect_giro"}