from pathlib import Path from sqlalchemy import select from sqlalchemy.orm import Session from app.config import get_settings from app.models.tables import Account, Statement, Transaction from app.parsers.base import ParserError from app.parsers.registry import parse_pdf from app.parsers.validate import balance_difference, dedup_hash from app.services.categorize import apply_rules def _find_or_create_account(session: Session, bank: str, iban: str | None, filename: str) -> Account: if not iban: iban = f"UNBEKANNT-{filename}" acc = session.execute(select(Account).where(Account.iban == iban)).scalar() if acc is not None: return acc acc = Account(bank=bank, iban=iban, name=iban, type="giro") session.add(acc) session.flush() return acc def process_pdf(session: Session, path: Path) -> Statement: settings = get_settings() filename = path.name try: parsed = parse_pdf(path) except ParserError as exc: stmt = Statement(filename=filename, bank="unbekannt", account_id=None, status="error", error_message=str(exc)) session.add(stmt) session.commit() session.refresh(stmt) return stmt account = _find_or_create_account(session, parsed.bank, parsed.iban, filename) stmt = Statement( filename=filename, bank=parsed.bank, account_id=account.id, period_start=parsed.period_start, period_end=parsed.period_end, opening_balance=parsed.opening_balance, closing_balance=parsed.closing_balance, status="draft", ) session.add(stmt) session.flush() diff = balance_difference(parsed) if diff != 0: stmt.status = "error" stmt.error_message = f"Saldo-Differenz {diff} EUR" session.commit() session.refresh(stmt) return stmt drafts = [] for t in parsed.transactions: h = dedup_hash(account.id, t.booking_date, t.amount, t.purpose) is_duplicate = session.execute( select(Transaction).where( Transaction.dedup_hash == h, Transaction.status == "confirmed", ) ).scalar() is not None tx = Transaction( account_id=account.id, statement_id=stmt.id, booking_date=t.booking_date, value_date=t.value_date, amount=t.amount, purpose=t.purpose, counterparty=t.counterparty, status="draft", dedup_hash=h, is_duplicate=is_duplicate, ) session.add(tx) drafts.append(tx) session.flush() apply_rules(session, drafts) stmt.status = "draft" # Invariant: a committed draft implies the PDF left the inbox. Move the # file before committing so a failed move can never leave a committed # draft with the PDF still sitting in the inbox. uploads_dir = settings.uploads_dir uploads_dir.mkdir(parents=True, exist_ok=True) try: path.replace(uploads_dir / filename) except OSError: session.rollback() raise session.commit() session.refresh(stmt) return stmt