import json from pathlib import Path from pydantic import BaseModel, Field from domain.account import Account from domain.asset import Asset from domain.currency import DEFAULT_RATES, ExchangeRate from domain.liability import Liability from domain.recurring import RecurringCashflow from domain.scenario import ForecastScenario from domain.transaction import Transaction class FinancialModel(BaseModel): """Корневая модель финансового плана.""" SCHEMA_VERSION: int = 1 # NB: не Field — это class-level metadata, не pydantic field base_currency: str = "RUB" accounts: list[Account] = Field(default_factory=list) transactions: list[Transaction] = Field(default_factory=list) recurring: list[RecurringCashflow] = Field(default_factory=list) assets: list[Asset] = Field(default_factory=list) liabilities: list[Liability] = Field(default_factory=list) scenarios: list[ForecastScenario] = Field(default_factory=list) exchange_rates: list[ExchangeRate] = Field( default_factory=lambda: [r.model_copy() for r in DEFAULT_RATES] ) def to_dict(self) -> dict: """Сериализация в dict с version и UUID-as-str (для JSON).""" return { "version": self.SCHEMA_VERSION, "base_currency": self.base_currency, "accounts": [a.model_dump(mode="json") for a in self.accounts], "transactions": [t.model_dump(mode="json") for t in self.transactions], "recurring": [r.model_dump(mode="json") for r in self.recurring], "assets": [a.model_dump(mode="json") for a in self.assets], "liabilities": [li.model_dump(mode="json") for li in self.liabilities], "scenarios": [s.model_dump(mode="json") for s in self.scenarios], "exchange_rates": [e.model_dump(mode="json") for e in self.exchange_rates], } @classmethod def from_dict(cls, data: dict) -> "FinancialModel": version = data.get("version", 0) if version == 0: return cls._from_legacy(data) if version == 1: return cls._from_v1(data) raise ValueError(f"Unsupported FinancialModel version: {version}") @classmethod def _from_v1(cls, data: dict) -> "FinancialModel": return cls( base_currency=data.get("base_currency", "RUB"), accounts=[Account.model_validate(a) for a in data.get("accounts", [])], transactions=[Transaction.model_validate(t) for t in data.get("transactions", [])], recurring=[RecurringCashflow.model_validate(r) for r in data.get("recurring", [])], assets=[Asset.model_validate(a) for a in data.get("assets", [])], liabilities=[Liability.model_validate(li) for li in data.get("liabilities", [])], scenarios=[ForecastScenario.model_validate(s) for s in data.get("scenarios", [])], exchange_rates=[ExchangeRate.model_validate(r) for r in data.get("exchange_rates", [])], ) @classmethod def _from_legacy(cls, data: dict) -> "FinancialModel": # Legacy: файлы без version. Структура совпадает с v1. return cls._from_v1(data) def save(self, path: str | Path) -> None: path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) with open(path, "w", encoding="utf-8") as f: json.dump(self.to_dict(), f, indent=2, ensure_ascii=False) @classmethod def load(cls, path: str | Path) -> "FinancialModel": path = Path(path) with open(path, "r", encoding="utf-8") as f: data = json.load(f) return cls.from_dict(data)