Transactions table (#7)
Tests / test (push) Successful in 10s
Tests / lint (push) Successful in 10s

Reviewed-on: #7
Co-authored-by: Mathias <mathias2784@web.de>
This commit was merged in pull request #7.
This commit is contained in:
2026-10-10 20:47:51 +00:00
committed by mathias2784
parent fdc66037ce
commit 6d98939024
17 changed files with 729 additions and 67 deletions
+78
View File
@@ -0,0 +1,78 @@
from datetime import date
from decimal import Decimal
from pydantic import Field, computed_field
from sqlalchemy import Date, ForeignKey, Index, Integer, String, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src.models._common import ExportBaseModel, IngestdbBase, _hash, date2str
class Transaction(ExportBaseModel):
"""Canonical, source-agnostic representation of a bank transaction.
Both input sources (b4w, comdirect giro) are projected into this model.
The deduplication key is a business-key hash of the transaction
attributes, so re-importing an export is idempotent even if
source-internal ids change between exports.
"""
source: str = Field(description="Source name", examples=["b4w", "comdirect_giro"])
source_row_id: int = Field(description="Primary key of the staging table row")
account_iban: str
account_currency: str
date_valid: date
amount: Decimal = Field(description="Signed amount; negative = money out")
amount_currency: str
date_book: date | None = None
text: str | None = None
other_name: str | None = None
other_iban: str | None = None
category_id: int | None = None
@computed_field # type: ignore[prop-decorator]
@property
def hash_id(self) -> str:
return _hash(
self.source,
self.account_iban,
self.date_valid,
f"{self.amount:.2f}",
self.text,
self.other_name,
)
def parameters_to_dict(self) -> dict[str, str]:
return {
"source": f"{self.source}",
"source_row_id": f"{self.source_row_id}",
"account_iban": f"{self.account_iban}",
"account_currency": f"{self.account_currency}",
"date_valid": date2str(self.date_valid),
"amount": f"{self.amount}",
"amount_currency": f"{self.amount_currency}",
"date_book": date2str(self.date_book),
"text": f"{self.text or ''}",
"other_name": f"{self.other_name or ''}",
"other_iban": f"{self.other_iban or ''}",
"category_id": f"{self.category_id}",
}
class TransactionDb(IngestdbBase):
__tablename__ = "transactions"
__table_args__ = (
UniqueConstraint("hash_id", name="uq_transactions_hash"),
Index("ix_transactions_source_row_id", "source", "source_row_id"),
)
source: Mapped[str] = mapped_column(String, nullable=False)
source_row_id: Mapped[int] = mapped_column(Integer, nullable=False)
date_book: Mapped[date | None] = mapped_column(Date, nullable=True)
other_name: Mapped[str | None] = mapped_column(String, nullable=True)
other_iban: Mapped[str | None] = mapped_column(String, nullable=True)
category_id: Mapped[int | None] = mapped_column(
ForeignKey("category.id"), nullable=True
)
category = relationship("CategoryDb")