"""Transport schemas only; business validation lives in the shared services.""" from decimal import Decimal from typing import Annotated, Literal from pydantic import BaseModel, BeforeValidator, ConfigDict, Field, StrictBool, model_validator from models import cents, valid_date AssetType = Literal['stock', 'etf', 'bond', 'crypto', 'interest', 'other'] Category = Literal['dividend', 'interest', 'distribution', 'other'] Identifier = Annotated[int, Field(strict=True, ge=1, le=9223372036854775807)] MoneyString = Annotated[str, Field(pattern=r'^-?\d+\.\d{2}$', examples=['0.04'])] def parse_amount(value): if not isinstance(value, (str, Decimal)): raise ValueError('Betrag als Dezimalstring senden, zum Beispiel "0.04".') return Decimal(cents(value)) / 100 Amount = Annotated[Decimal, BeforeValidator(parse_amount, json_schema_input_type=str)] Day = Annotated[str, BeforeValidator(valid_date)] class RequestModel(BaseModel): model_config = ConfigDict(extra='forbid') class PatchModel(RequestModel): @model_validator(mode='before') @classmethod def reject_required_nulls(cls, data): if isinstance(data, dict): for name, value in data.items(): if value is None and name not in {'note', 'ticker'}: raise ValueError('Nur Notiz und Ticker dürfen null sein.') return data class AssetCreate(RequestModel): name: str = Field(min_length=1, max_length=150) ticker: str | None = Field(default=None, max_length=30) asset_type: AssetType active: StrictBool = True class AssetPatch(PatchModel): name: str | None = Field(default=None, min_length=1, max_length=150) ticker: str | None = Field(default=None, max_length=30) asset_type: AssetType | None = None active: StrictBool | None = None class AssetResponse(BaseModel): id: int name: str ticker: str | None asset_type: AssetType active: bool created_at: str class IncomeCreate(RequestModel): date: Day asset_id: Identifier category: Category amount: Amount note: str | None = Field(default=None, max_length=2000) expected: StrictBool = False received: StrictBool = True class IncomePatch(PatchModel): date: Day | None = None asset_id: Identifier | None = None category: Category | None = None amount: Amount | None = None note: str | None = Field(default=None, max_length=2000) expected: StrictBool | None = None received: StrictBool | None = None class IncomeResponse(BaseModel): id: int date: str asset: str = Field(description='Normalisierter Positionsname') asset_id: int category: Category amount: MoneyString note: str | None expected: bool received: bool created_at: str updated_at: str class SummaryResponse(BaseModel): current_month: MoneyString current_month_previous_year: MoneyString current_month_yoy_percent: MoneyString | None current_year: MoneyString previous_year: MoneyString current_year_yoy_percent: MoneyString | None all_time: MoneyString current_year_payment_count: int class MonthResponse(BaseModel): month: int amount: MoneyString class MonthlyResponse(BaseModel): year: int months: list[MonthResponse] total: MoneyString class AssetShareResponse(BaseModel): asset_id: int asset: str amount: MoneyString percentage: MoneyString | None class CategoryShareResponse(BaseModel): category: Category amount: MoneyString percentage: MoneyString | None class MetaResponse(BaseModel): name: str = 'Finance Dashboard' api_version: str = 'v1' database: Literal['ok'] = 'ok'