feat: migrate advisor investment goals
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""Contracts for structured investment-goal collection."""
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
LiquidityRequirement = Literal[
|
||||
"daily",
|
||||
"within_7_days",
|
||||
"within_30_days",
|
||||
"over_30_days",
|
||||
]
|
||||
|
||||
|
||||
class InvestmentGoalCreate(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
|
||||
customer_id: int | None = Field(default=None, gt=0)
|
||||
annualized_return_lower_pct: Decimal = Field(ge=0, le=100, max_digits=7, decimal_places=4)
|
||||
annualized_return_upper_pct: Decimal = Field(ge=0, le=100, max_digits=7, decimal_places=4)
|
||||
max_drawdown_pct: Decimal = Field(ge=0, le=100, max_digits=7, decimal_places=4)
|
||||
liquidity_requirement: LiquidityRequirement
|
||||
investment_horizon_months: int = Field(ge=1, le=600)
|
||||
benchmark_name: str = Field(min_length=1, max_length=128)
|
||||
notes: str | None = Field(default=None, max_length=1000)
|
||||
|
||||
@field_validator("benchmark_name", "notes")
|
||||
@classmethod
|
||||
def normalize_text(cls, value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
normalized = value.strip()
|
||||
if not normalized:
|
||||
raise ValueError("text must not be blank")
|
||||
if any(ord(char) < 32 and char not in "\n\t" for char in normalized):
|
||||
raise ValueError("text must not contain control characters")
|
||||
return normalized
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_return_range(self) -> "InvestmentGoalCreate":
|
||||
if self.annualized_return_lower_pct > self.annualized_return_upper_pct:
|
||||
raise ValueError("annualized return lower bound must not exceed upper bound")
|
||||
return self
|
||||
|
||||
|
||||
class InvestmentGoalConfirmation(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
|
||||
confirmed: Literal[True]
|
||||
|
||||
|
||||
class InvestmentGoalBookReview(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
|
||||
decision: Literal["approved", "rejected"]
|
||||
comment: str | None = Field(default=None, max_length=1000)
|
||||
|
||||
@field_validator("comment")
|
||||
@classmethod
|
||||
def normalize_comment(cls, value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
normalized = value.strip()
|
||||
return normalized or None
|
||||
|
||||
|
||||
class InvestmentGoalBookPublish(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
|
||||
publish: Literal[True]
|
||||
|
||||
|
||||
class InvestmentGoalQuery(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid", frozen=True)
|
||||
Reference in New Issue
Block a user