from datetime import date from decimal import Decimal from fastapi import APIRouter, Depends, HTTPException from pydantic import BaseModel, ConfigDict, field_validator from sqlalchemy import select from sqlalchemy.orm import Session from app.auth import require_auth from app.db import get_session from app.models.tables import Account from app.services.balances import account_balance router = APIRouter(prefix="/api/accounts", tags=["accounts"], dependencies=[Depends(require_auth)]) class AccountIn(BaseModel): bank: str iban: str name: str type: str = "giro" class AccountOut(AccountIn): model_config = ConfigDict(from_attributes=True) id: int balance: Decimal = Decimal("0") anchor_date: date | None = None anchor_balance: Decimal | None = None class AccountPatch(BaseModel): # Alle Felder optional; model_fields_set entscheidet, was tatsaechlich # geaendert wird (Muster wie TransactionPatch.category_id). anchor_date # und anchor_balance muessen gemeinsam gesetzt oder gemeinsam null sein. name: str | None = None anchor_date: date | None = None anchor_balance: Decimal | None = None @field_validator("name") @classmethod def name_not_blank(cls, v: str | None) -> str | None: if v is None: return v v = v.strip() if not v or len(v) > 100: raise ValueError("Name muss 1–100 Zeichen lang sein") return v @router.get("", response_model=list[AccountOut]) def list_accounts(session: Session = Depends(get_session)): out = [] for acc in session.execute(select(Account)).scalars(): item = AccountOut.model_validate(acc) item.balance = account_balance(session, acc) out.append(item) return out @router.get("/{account_id}", response_model=AccountOut) def get_account(account_id: int, session: Session = Depends(get_session)): acc = session.get(Account, account_id) if acc is None: raise HTTPException(404, "Konto nicht gefunden") item = AccountOut.model_validate(acc) item.balance = account_balance(session, acc) return item @router.post("", response_model=AccountOut, status_code=201) def create_account(data: AccountIn, session: Session = Depends(get_session)): if session.execute(select(Account).where(Account.iban == data.iban)).scalar(): raise HTTPException(409, "IBAN existiert bereits") acc = Account(**data.model_dump()) session.add(acc) session.commit() session.refresh(acc) return AccountOut.model_validate(acc) @router.patch("/{account_id}", response_model=AccountOut) def patch_account(account_id: int, data: AccountPatch, session: Session = Depends(get_session)): acc = session.get(Account, account_id) if acc is None: raise HTTPException(404, "Konto nicht gefunden") # Validierung zuerst, VOR jeder Mutation - sonst haengt bei einem 422 auf # dem Anker eine bereits geschriebene name-Aenderung im Session-State. fields = data.model_fields_set if "name" in fields and data.name is None: raise HTTPException(422, "Name darf nicht leer sein") anchor_fields = {"anchor_date", "anchor_balance"} & fields if anchor_fields and ( anchor_fields != {"anchor_date", "anchor_balance"} or (data.anchor_date is None) != (data.anchor_balance is None)): raise HTTPException(422, "Anker braucht Datum und Betrag (oder beide leeren)") if "name" in fields: acc.name = data.name if anchor_fields: acc.anchor_date = data.anchor_date acc.anchor_balance = data.anchor_balance session.commit() session.refresh(acc) item = AccountOut.model_validate(acc) item.balance = account_balance(session, acc) return item