- All domain monetary fields now use Decimal: - Account.balance, Asset.value, Asset.growth_rate - Liability.balance, Liability.interest, Liability.payment - Transaction.amount, RecurringCashflow.amount - ExchangeRate.rate - ForecastScenario.income_multiplier, expense_multiplier, growth_multiplier - CurrencyConverter: all arithmetic in Decimal, quantize to 0.01 with ROUND_HALF_UP - ForecastService: Decimal arithmetic throughout (income, expenses, balance, growth, liability cost) - ScenarioService: Decimal multipliers, deepcopy safe with Decimal fields - assistant.py: _DecimalEncoder for json.dumps (Decimal -> str in JSON) Pydantic v2 + Decimal: - model_dump(mode='json') converts Decimal to str (JSON-safe) - model_validate() parses str back to Decimal - Round-trip preserves precision (100.50 stays 100.50) Tests: 63/63 pass.
28 lines
774 B
Python
28 lines
774 B
Python
from decimal import Decimal
|
|
from uuid import UUID, uuid4
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
|
|
class RecurringCashflow(BaseModel):
|
|
id: UUID = Field(default_factory=uuid4)
|
|
start_date: str = ""
|
|
end_date: str = ""
|
|
frequency: str = "monthly"
|
|
amount: Decimal = Field(default=Decimal("0"))
|
|
category: str = ""
|
|
|
|
@field_validator("frequency")
|
|
@classmethod
|
|
def _frequency_known(cls, v: str) -> str:
|
|
if v not in {"daily", "weekly", "monthly", "yearly"}:
|
|
raise ValueError(f"unknown frequency: {v}")
|
|
return v
|
|
|
|
@field_validator("amount")
|
|
@classmethod
|
|
def _amount_nonzero(cls, v: Decimal) -> Decimal:
|
|
if v == 0:
|
|
raise ValueError("amount must be non-zero")
|
|
return v
|