from datetime import date from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException, UploadFile from pydantic import BaseModel, ConfigDict from sqlalchemy import select from sqlalchemy.orm import Session from app.auth import require_auth from app.config import get_settings from app.db import get_session from app.models.tables import Statement, Transaction from app.services.importer import process_pdf router = APIRouter(prefix="/api/imports", tags=["imports"], dependencies=[Depends(require_auth)]) class StatementOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int filename: str bank: str account_id: int | None = None period_start: date | None = None period_end: date | None = None opening_balance: Decimal | None = None closing_balance: Decimal | None = None status: str error_message: str | None = None class TransactionOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int account_id: int statement_id: int | None = None booking_date: date value_date: date | None = None amount: Decimal purpose: str counterparty: str category_id: int | None = None status: str dedup_hash: str is_duplicate: bool class PreviewOut(BaseModel): statement: StatementOut transactions: list[TransactionOut] balance_ok: bool duplicates: int @router.post("/upload", response_model=StatementOut, status_code=201) def upload(file: UploadFile, session: Session = Depends(get_session)): settings = get_settings() inbox_dir = settings.inbox_dir inbox_dir.mkdir(parents=True, exist_ok=True) dest = inbox_dir / file.filename with dest.open("wb") as f: f.write(file.file.read()) stmt = process_pdf(session, dest) return StatementOut.model_validate(stmt) @router.post("/scan-inbox", response_model=list[StatementOut]) def scan_inbox(session: Session = Depends(get_session)): settings = get_settings() inbox_dir = settings.inbox_dir inbox_dir.mkdir(parents=True, exist_ok=True) results = [] for path in sorted(inbox_dir.glob("*.pdf")): stmt = process_pdf(session, path) results.append(StatementOut.model_validate(stmt)) return results @router.get("", response_model=list[StatementOut]) def list_imports(session: Session = Depends(get_session)): stmts = session.execute(select(Statement).order_by(Statement.id)).scalars() return [StatementOut.model_validate(s) for s in stmts] @router.get("/{statement_id}/preview", response_model=PreviewOut) def preview(statement_id: int, session: Session = Depends(get_session)): stmt = session.get(Statement, statement_id) if stmt is None: raise HTTPException(404, "Import nicht gefunden") txs = session.execute( select(Transaction).where(Transaction.statement_id == statement_id) .order_by(Transaction.booking_date, Transaction.id) ).scalars().all() balance_ok = ( stmt.opening_balance is not None and stmt.closing_balance is not None and (stmt.opening_balance + sum((t.amount for t in txs), Decimal("0")) - stmt.closing_balance) == 0 ) duplicates = sum(1 for t in txs if t.is_duplicate) return PreviewOut( statement=StatementOut.model_validate(stmt), transactions=[TransactionOut.model_validate(t) for t in txs], balance_ok=balance_ok, duplicates=duplicates, ) @router.post("/{statement_id}/confirm", response_model=StatementOut) def confirm(statement_id: int, session: Session = Depends(get_session)): stmt = session.get(Statement, statement_id) if stmt is None: raise HTTPException(404, "Import nicht gefunden") txs = session.execute( select(Transaction).where( Transaction.statement_id == statement_id, Transaction.status == "draft", Transaction.is_duplicate == False, # noqa: E712 ) ).scalars().all() for tx in txs: tx.status = "confirmed" stmt.status = "confirmed" session.commit() session.refresh(stmt) return StatementOut.model_validate(stmt) @router.delete("/{statement_id}", status_code=204) def delete_import(statement_id: int, session: Session = Depends(get_session)): stmt = session.get(Statement, statement_id) if stmt is None: raise HTTPException(404, "Import nicht gefunden") drafts = session.execute( select(Transaction).where( Transaction.statement_id == statement_id, Transaction.status == "draft", ) ).scalars().all() for tx in drafts: session.delete(tx) session.delete(stmt) session.commit()