T3: Decimal for money — all monetary fields migrated from float

- 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.
This commit is contained in:
2026-10-08 16:44:59 +03:00
parent c765f3672a
commit 363d440d53
10 changed files with 118 additions and 73 deletions
+14 -4
View File
@@ -1,4 +1,5 @@
import json
from decimal import Decimal
from infrastructure.ai import prompts
from domain import CurrencyConverter, FinancialModel
@@ -9,6 +10,15 @@ class AssistantError(Exception):
pass
class _DecimalEncoder(json.JSONEncoder):
"""JSON-сериализатор: Decimal → str (для AI-промптов)."""
def default(self, o):
if isinstance(o, Decimal):
return str(o)
return super().default(o)
class AssistantService:
def __init__(
self,
@@ -26,8 +36,8 @@ class AssistantService:
summary = forecast_service.summary(months)
prompt = prompts.format_context(
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False),
forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False),
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False, cls=_DecimalEncoder),
forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False, cls=_DecimalEncoder),
months=months,
base_currency=self.model.base_currency,
display_currency=self.display_currency,
@@ -45,8 +55,8 @@ class AssistantService:
forecast_result = forecast_service.forecast_cashflow(months)
prompt = prompts.ADVICE_PROMPT.format(
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False),
forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False),
model_json=json.dumps(self.model.to_dict(), indent=2, ensure_ascii=False, cls=_DecimalEncoder),
forecast_json=json.dumps(forecast_result, indent=2, ensure_ascii=False, cls=_DecimalEncoder),
question=question,
base_currency=self.model.base_currency,
display_currency=self.display_currency,