112 lines
3.7 KiB
Python
112 lines
3.7 KiB
Python
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
|