Transactions table (#7)
Reviewed-on: #7 Co-authored-by: Mathias <mathias2784@web.de>
This commit was merged in pull request #7.
This commit is contained in:
@@ -5,3 +5,5 @@ from src.models.comdirect import ComdirectDepot as ComdirectDepot
|
||||
from src.models.comdirect import ComdirectGiro as ComdirectGiro
|
||||
from src.models.settings import Account as Account
|
||||
from src.models.settings import Source as Source
|
||||
from src.models.transactions import Transaction as Transaction
|
||||
from src.models.transactions import TransactionDb as TransactionDb
|
||||
|
||||
+10
-10
@@ -4,8 +4,8 @@ from decimal import Decimal
|
||||
from enum import Enum
|
||||
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import Column, Date, Integer, Numeric, String
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy import Date, Numeric, String
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
def _hash(*args):
|
||||
@@ -89,11 +89,11 @@ class BaseModelDb(DeclarativeBase):
|
||||
class IngestdbBase(BaseModelDb):
|
||||
__abstract__ = True
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
hash_id = Column(String, nullable=False)
|
||||
account_iban = Column(String, nullable=False)
|
||||
account_currency = Column(String(3), nullable=False)
|
||||
date_valid = Column(Date, nullable=False)
|
||||
amount = Column(Numeric(12, 2), nullable=False)
|
||||
amount_currency = Column(String(3), nullable=False)
|
||||
text = Column(String, nullable=True)
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
hash_id: Mapped[str] = mapped_column(String, nullable=False)
|
||||
account_iban: Mapped[str] = mapped_column(String, nullable=False)
|
||||
account_currency: Mapped[str] = mapped_column(String(3), nullable=False)
|
||||
date_valid: Mapped[date] = mapped_column(Date, nullable=False)
|
||||
amount: Mapped[Decimal] = mapped_column(Numeric(12, 2), nullable=False)
|
||||
amount_currency: Mapped[str] = mapped_column(String(3), nullable=False)
|
||||
text: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
|
||||
+18
-16
@@ -2,8 +2,8 @@ from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import Field, computed_field, field_validator
|
||||
from sqlalchemy import Column, Date, ForeignKey, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy import Date, ForeignKey, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src.constants import IBAN_PATTERN, Currency
|
||||
from src.models._common import (
|
||||
@@ -56,7 +56,7 @@ class B4wExport(ExportBaseModel):
|
||||
"Notes": f"{self.notes}",
|
||||
}
|
||||
|
||||
@computed_field
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def hash_id(self) -> str:
|
||||
return _hash(self.id)
|
||||
@@ -91,7 +91,7 @@ class Category(ExportBaseModel):
|
||||
name: str = Field(pattern=r"[^:\r\n]+")
|
||||
parent: Category | None = None
|
||||
|
||||
@computed_field
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def hash_id(self) -> str:
|
||||
return _hash(self.name, self.parent)
|
||||
@@ -99,7 +99,7 @@ class Category(ExportBaseModel):
|
||||
@classmethod
|
||||
def from_str(cls, s: str) -> Category | None:
|
||||
if not s:
|
||||
return
|
||||
return None
|
||||
components = s.split(":")
|
||||
name = components[-1]
|
||||
rest = ":".join(components[:-1])
|
||||
@@ -124,9 +124,11 @@ class CategoryDb(BaseModelDb):
|
||||
UniqueConstraint("name", "parent_id", name="uq_category_name_parent"),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, nullable=False)
|
||||
parent_id = Column(Integer, ForeignKey("category.id"), nullable=True)
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
name: Mapped[str] = mapped_column(String, nullable=False)
|
||||
parent_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey("category.id"), nullable=True
|
||||
)
|
||||
|
||||
# Relationships
|
||||
parent = relationship(
|
||||
@@ -134,18 +136,18 @@ class CategoryDb(BaseModelDb):
|
||||
)
|
||||
children = relationship("CategoryDb", back_populates="parent")
|
||||
|
||||
UniqueConstraint("name", "parent_id", name="uq_category_name_parent")
|
||||
|
||||
|
||||
class B4wDb(IngestdbBase):
|
||||
__tablename__ = "import_b4w"
|
||||
|
||||
id_b4w = Column(Integer, nullable=False, unique=True)
|
||||
date_book = Column(Date, nullable=True)
|
||||
other_name = Column(String, nullable=True)
|
||||
other_iban = Column(String, nullable=True)
|
||||
category_id = Column(Integer, ForeignKey("category.id"), nullable=True)
|
||||
notes = Column(String, nullable=True)
|
||||
id_b4w: Mapped[int] = mapped_column(Integer, nullable=False, unique=True)
|
||||
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
|
||||
)
|
||||
notes: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
|
||||
# relationship
|
||||
category = relationship("CategoryDb")
|
||||
|
||||
@@ -2,7 +2,8 @@ from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import ConfigDict, Field, computed_field, field_validator
|
||||
from sqlalchemy import Column, Date, String
|
||||
from sqlalchemy import Date, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from src.models._common import (
|
||||
ExportBaseModel,
|
||||
@@ -26,7 +27,7 @@ class ComdirectBase(ExportBaseModel):
|
||||
@classmethod
|
||||
def parse_date_val(cls, date_str: str | None) -> date | None:
|
||||
if date_str is None:
|
||||
return
|
||||
return None
|
||||
return _parse_date_german(date_str)
|
||||
|
||||
@field_validator("amount_eur", mode="before")
|
||||
@@ -68,7 +69,7 @@ class ComdirectGiro(ComdirectBase):
|
||||
)
|
||||
text: str = Field(alias="Buchungstext")
|
||||
|
||||
@computed_field
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def hash_id(self) -> str:
|
||||
return _hash(self.date_val, self.date_book, self.text)
|
||||
@@ -103,9 +104,6 @@ class ComdirectCreditCard(ComdirectGiro):
|
||||
)
|
||||
reference: str = Field(alias="Referenz", default="")
|
||||
text: str = Field(alias="Buchungstext", default="")
|
||||
amount_eur: Decimal | None = Field(
|
||||
alias="Umsatz in EUR", examples=["32,43"], default=None
|
||||
)
|
||||
|
||||
def parameters_to_dict(self, by_name: bool = True) -> dict[str, str]:
|
||||
parameters = dict(**super().parameters_to_dict(by_name=by_name))
|
||||
@@ -155,5 +153,5 @@ class ComdirectBaseDb(IngestdbBase):
|
||||
class ComdirectGiroDb(ComdirectBaseDb):
|
||||
__tablename__ = "comdirect_giro"
|
||||
|
||||
type = Column(String, nullable=True)
|
||||
date_book = Column(Date, nullable=True)
|
||||
type: Mapped[str | None] = mapped_column(String, nullable=True)
|
||||
date_book: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user