"""Pydantic-Schemas für Anfragen/Antworten (Phase 0: Auth, Benutzer, Einstellungen).""" from datetime import date as date_t, datetime, time as time_t from pydantic import BaseModel, ConfigDict, Field, field_validator from .models import ( AccountType, BudgetTreatment, CategoryKind, IdentifierKind, Role, TxnType, ) ALLOWED_CURRENCIES = {"EUR", "CHF"} def _check_currency(v: str) -> str: v = v.upper() if v not in ALLOWED_CURRENCIES: raise ValueError("Währung muss EUR oder CHF sein") return v class UserOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int username: str role: Role display_name: str | None = None is_active: bool must_change_password: bool created_at: datetime class UserCreate(BaseModel): username: str = Field(min_length=1, max_length=64) password: str = Field(min_length=1, max_length=1024) role: Role = Role.user display_name: str | None = Field(default=None, max_length=120) class UserUpdate(BaseModel): password: str | None = Field(default=None, min_length=1, max_length=1024) role: Role | None = None display_name: str | None = Field(default=None, max_length=120) is_active: bool | None = None class PasswordChange(BaseModel): current_password: str = Field(min_length=1, max_length=1024) new_password: str = Field(min_length=1, max_length=1024) class SettingOut(BaseModel): key: str value: str | None = None # --- Konten ---------------------------------------------------------------- class AccountIdentifierIn(BaseModel): kind: IdentifierKind = IdentifierKind.iban value: str = Field(min_length=1, max_length=64) class AccountIdentifierOut(BaseModel): id: int kind: IdentifierKind value: str class AccountCreate(BaseModel): name: str = Field(min_length=1, max_length=120) type: AccountType = AccountType.girokonto currency: str = "EUR" icon: str = "wallet" color: str = "#3f51b5" opening_balance_cents: int = 0 opening_date: date_t | None = None identifiers: list[AccountIdentifierIn] = Field(default_factory=list) @field_validator("currency") @classmethod def _cur(cls, v: str) -> str: return _check_currency(v) class AccountUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=120) type: AccountType | None = None currency: str | None = None icon: str | None = None color: str | None = None opening_balance_cents: int | None = None opening_date: date_t | None = None is_archived: bool | None = None position: int | None = None # Wenn gesetzt, ersetzt die komplette Identifikator-Liste. identifiers: list[AccountIdentifierIn] | None = None @field_validator("currency") @classmethod def _cur(cls, v: str | None) -> str | None: return _check_currency(v) if v is not None else None class AccountOut(BaseModel): id: int name: str type: AccountType currency: str icon: str color: str opening_balance_cents: int opening_date: date_t | None is_archived: bool position: int identifiers: list[AccountIdentifierOut] balance_cents: int balance_base_cents: int # --- Kategorien ------------------------------------------------------------ class CategoryOut(BaseModel): model_config = ConfigDict(from_attributes=True) id: int group_id: int name: str type: CategoryKind icon: str color: str position: int is_builtin: bool is_archived: bool class CategoryGroupOut(BaseModel): id: int name: str kind: CategoryKind icon: str color: str position: int is_builtin: bool categories: list[CategoryOut] class CategoryGroupCreate(BaseModel): name: str = Field(min_length=1, max_length=120) kind: CategoryKind = CategoryKind.expense icon: str = "list" color: str = "#6b7480" class CategoryGroupUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=120) icon: str | None = None color: str | None = None position: int | None = None class CategoryCreate(BaseModel): group_id: int name: str = Field(min_length=1, max_length=120) icon: str = "more-horizontal" color: str = "#6b7480" class CategoryUpdate(BaseModel): name: str | None = Field(default=None, min_length=1, max_length=120) group_id: int | None = None icon: str | None = None color: str | None = None position: int | None = None is_archived: bool | None = None # --- Transaktionen --------------------------------------------------------- class TransactionCreate(BaseModel): type: TxnType date: date_t time: time_t | None = None account_id: int to_account_id: int | None = None amount_cents: int = Field(gt=0) to_amount_cents: int | None = Field(default=None, gt=0) fx_rate: float | None = Field(default=None, gt=0) category_id: int | None = None budget_treatment: BudgetTreatment | None = None budget_category_id: int | None = None payee: str | None = Field(default=None, max_length=255) description: str | None = Field(default=None, max_length=255) notes: str | None = None class TransactionUpdate(BaseModel): type: TxnType | None = None date: date_t | None = None time: time_t | None = None account_id: int | None = None to_account_id: int | None = None amount_cents: int | None = Field(default=None, gt=0) to_amount_cents: int | None = Field(default=None, gt=0) fx_rate: float | None = Field(default=None, gt=0) category_id: int | None = None budget_treatment: BudgetTreatment | None = None budget_category_id: int | None = None payee: str | None = Field(default=None, max_length=255) description: str | None = Field(default=None, max_length=255) notes: str | None = None class TransactionOut(BaseModel): id: int type: TxnType date: date_t time: time_t | None account_id: int to_account_id: int | None account_currency: str amount_cents: int to_amount_cents: int | None fx_rate: float | None amount_eur_cents: int amount_base_cents: int category_id: int | None budget_treatment: BudgetTreatment | None budget_category_id: int | None payee: str | None description: str | None notes: str | None class RateStatus(BaseModel): currency: str count: int min_date: date_t | None max_date: date_t | None latest_rate: float | None class ConversionOut(BaseModel): amount_cents: int from_currency: str to_currency: str date: date_t converted_cents: int | None rate: float | None