194 lines
6.4 KiB
Python
194 lines
6.4 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_id 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"
|
|
|
|
|
|
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"}
|