134 lines
3.6 KiB
Python
134 lines
3.6 KiB
Python
"""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'
|