- Add CURRENCY_DECIMALS dict (JPY=0, BHD/KWD/OMR=3, rest=2) and DEFAULT_CURRENCY_DECIMALS=2
- Add CurrencyConverter.format_money() staticmethod with ROUND_HALF_UP; symbol-prefix for known, code-prefix for unknown
- Replace 14 :,.2f literals across services.py / info.py / config_cmd.py with format_money() calls
- Refactor 2 i18n templates (transaction_add.ok, recurring_add.ok): {amount:.2f} -> {amount_str}; callers pass pre-formatted string
- New _account_currency() helper for transaction currency lookup
- Extend test_currency.py: 8 new tests (JPY/BHD/USD/CHF/negative/zero/float-int/lowercase)
- 75/75 tests pass; 0 :,.2f literals remain in infrastructure/cli/
65 lines
2.2 KiB
Python
65 lines
2.2 KiB
Python
"""Composition root + общие хелперы для CLI-команд."""
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
|
|
from rich.console import Console
|
|
|
|
from application.forecast import ForecastService
|
|
from application.scenarios import ScenarioService
|
|
from domain import CurrencyConverter, FinancialModel
|
|
from infrastructure.ai.assistant import AssistantService
|
|
from infrastructure.cli.paths import MODEL_PATH
|
|
from infrastructure.repositories import JsonFileRepository
|
|
|
|
|
|
console = Console()
|
|
|
|
_json_repo = JsonFileRepository()
|
|
|
|
|
|
def load_model() -> FinancialModel:
|
|
if MODEL_PATH.exists():
|
|
return _json_repo.load(MODEL_PATH)
|
|
return FinancialModel()
|
|
|
|
|
|
def save_model(model: FinancialModel) -> None:
|
|
_json_repo.save(model, MODEL_PATH)
|
|
|
|
|
|
def build_services(model: FinancialModel) -> tuple[ForecastService, ScenarioService, AssistantService, CurrencyConverter]:
|
|
"""Composition root: собрать граф сервисов с зависимостями."""
|
|
converter = CurrencyConverter(model.exchange_rates)
|
|
forecast_service = ForecastService(model)
|
|
scenario_service = ScenarioService(model)
|
|
assistant_service = AssistantService(
|
|
model, forecast_service=forecast_service, converter=converter
|
|
)
|
|
return forecast_service, scenario_service, assistant_service, converter
|
|
|
|
|
|
def resolve_currency(model: FinancialModel, currency: str | None) -> tuple[str, str]:
|
|
target = currency or model.base_currency
|
|
converter = CurrencyConverter(model.exchange_rates)
|
|
symbol = converter.get_symbol(target)
|
|
return target, symbol
|
|
|
|
|
|
def convert_value(
|
|
converter: CurrencyConverter, amount: Decimal, from_curr: str, to_curr: str
|
|
) -> Decimal:
|
|
if to_curr == from_curr:
|
|
return amount
|
|
return converter.convert(amount, from_curr, to_curr)
|
|
|
|
|
|
def fmt_amount(amount: Decimal | float, currency: str, symbol: str = "") -> str:
|
|
"""Format money via the canonical display rule.
|
|
|
|
`symbol` is retained as a positional parameter for backward compatibility with
|
|
existing callers (compare.py / forecast.py / scenario.py / whatif.py); the
|
|
symbol/layout decision is centralised in `CurrencyConverter.format_money`.
|
|
"""
|
|
return CurrencyConverter.format_money(amount, currency)
|