Merge branch 'master' into test-db-ppopulate
This commit is contained in:
@@ -7,6 +7,7 @@ from polyfactory.fields import Use
|
||||
|
||||
from src.models.b4w import B4wExport
|
||||
from src.models.comdirect import ComdirectCreditCard, ComdirectDepot, ComdirectGiro
|
||||
from src.models.transactions import Transaction
|
||||
|
||||
DEFAULT_CATEGORY = "root_category:parent_category:leaf_category"
|
||||
|
||||
@@ -98,3 +99,13 @@ class ComdirectDepotFactory(ModelFactory[ComdirectDepot]):
|
||||
date_val = Use(_german_date)
|
||||
num = Use(_amount_eur)
|
||||
spot_price = Use(_amount_eur)
|
||||
|
||||
|
||||
class TransactionFactory(ModelFactory[Transaction]):
|
||||
__faker__ = Faker("de_DE")
|
||||
__use_examples__ = True
|
||||
|
||||
account_iban = Use(_iban)
|
||||
text = Use(_sentence)
|
||||
other_name = Use(_names)
|
||||
other_iban = Use(_iban_or_none)
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
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
|
||||
|
||||
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="Lebenshaltung:Lebensmittel",
|
||||
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"}
|
||||
Reference in New Issue
Block a user