Verified Commit eb693b95 authored by Jakob Moser's avatar Jakob Moser
Browse files

Add Transaction type

This mainly keeps and formats a few variables relevant to transactions. Not all transactions have a partner and purpose (in my case, specifically, the "Jahresabschluss" has neither of those, but is at least indicated by a different `posting_text`).

As the transactions from FinTS come without any ID, the only way to deduplicate them is by checking if we already have a transaction with the exact same data, and then call that a duplicate (which is not always true, technically, you could run the same transaction twice in a row; e.g., if you want to make two payments in short sequence, and there would be no difference).

We also add navigatability in both directions using SQL Alchemy
parent 96a6bf6a
Loading
Loading
Loading
Loading
+4 −1
Original line number Diff line number Diff line
@@ -4,10 +4,11 @@ from contextlib import contextmanager
from fints.client import FinTS3PinTanClient
from fints.models import SEPAAccount
from schwifty import IBAN
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import Mapped, mapped_column, relationship

from poolpay.model.Base import Base
from poolpay.model.Iban import Iban
from poolpay.model.Transaction import Transaction


class BankAccount(Base):
@@ -35,6 +36,8 @@ class BankAccount(Base):
    # only), we could have just hardcoded the product ID).
    product_id: Mapped[str]

    transactions: Mapped[list[Transaction]] = relationship(back_populates="account")

    @contextmanager
    def _fints_client(self, password: str) -> Generator[FinTS3PinTanClient]:
        client = FinTS3PinTanClient(
+71 −0
Original line number Diff line number Diff line
from __future__ import annotations

from datetime import date
from typing import TYPE_CHECKING, Self
from uuid import UUID

import mt940
from sqlalchemy import and_, select
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.schema import ForeignKey

from poolpay import db

if TYPE_CHECKING:
    from poolpay.model.BankAccount import BankAccount
from poolpay.model.Base import Base


class Transaction(Base):
    """
    A transaction performed at a bank.
    """

    __tablename__ = "transaction"

    date: Mapped[date]
    partner: Mapped[str | None]
    amount_cents: Mapped[int]
    purpose: Mapped[str | None]
    posting_text: Mapped[str]

    account_uuid: Mapped[UUID] = mapped_column(ForeignKey("bank_account.uuid"))
    account: Mapped[BankAccount] = relationship(back_populates="transactions")

    @classmethod
    def get_corresponding(
        cls, transaction: mt940.models.Transaction, account: BankAccount
    ) -> Self | None:
        """
        Returns the Transaction corresponding to the given mt940.models.Transaction,
        or None, if there is no such transaction.
        """
        poolpay_transaction = cls.from_corresponding(transaction, account)

        return db.session.execute(
            select(cls).where(
                and_(
                    cls.date == poolpay_transaction.date,
                    cls.amount_cents == poolpay_transaction.amount_cents,
                    cls.purpose == poolpay_transaction.purpose,
                    cls.partner == poolpay_transaction.partner,
                    cls.posting_text == poolpay_transaction.posting_text,
                )
            )
        ).scalar_one_or_none()

    @classmethod
    def from_corresponding(
        cls, transaction: mt940.models.Transaction, account: BankAccount
    ) -> Self:
        """
        Create a Transaction corresponding to the given mt940.models.Transaction.
        """
        return cls(
            date=transaction.data["date"],
            partner=transaction.data["applicant_name"],
            amount_cents=int(transaction.data["amount"].amount * 100),
            purpose=transaction.data["purpose"],
            posting_text=transaction.data["posting_text"],
            account=account,
        )