Files
2026-08-13 22:48:40 +03:00

11 KiB
Raw Permalink Blame History

Context

См. мотивацию в proposal.md и поведение в specs/portfolio-view/spec.md. Backend уже хранит один зашифрованный T-Bank token на User, централизует TLS gRPC, limiter, deadline и retry, а также публикует authenticated OpenAPI. BrokerageAccount уже является нормализованной границей доступного открытого инвестиционного счёта. Маршрут /accounts/:accountId существует как placeholder; frontend использует FSD, Mantine, React Query и generated OpenAPI types.

T-Bank GetPortfolio предоставляет обычные позиции с количеством и ценами, а GetPositions -- доступные и заблокированные денежные остатки. Обе protobuf-модели используют int64/nano; JavaScript number не обеспечивает безопасной финансовой арифметики.

Goals / Non-Goals

Goals:

  • Расширить существующую server-side T-Bank boundary полным read-path одного Portfolio.
  • Нормализовать upstream-данные в стабильный публичный HTTP contract с точными decimal strings.
  • Кэшировать полный user-scoped Portfolio Snapshot и глобальные metadata инструментов без новой инфраструктуры.
  • Реализовать адаптивный detail UI в визуальном направлении references/fin-tracker, следуя FSD и code conventions references/frontend-ai-template, без переноса кода из референсов.
  • Документировать фактическое поведение в docs/features/portfolio-view.md после реализации.

Non-Goals:

  • Новые зависимости, persistent/distributed cache, ограничение размера in-memory cache или поддержка нескольких backend instances.
  • История Portfolio, операции, виртуальные позиции, заблокированные средства, вывод средств, графики, polling, ручное обновление и валютная конвертация.
  • Изменение Prisma schema, T-Bank connection lifecycle или существующей общей resilience policy.

Decisions

Read-path, авторизация счёта и error mapping

GET /api/v1/brokerage-accounts/:accountId/portfolio читает connection User и использует GetAccounts для проверки, что requested id остаётся открытым поддерживаемым Brokerage Account. Отсутствующий, закрытый, неподдерживаемый или недоступный id получает 404 до финансовых RPC. При валидном счёте GetPortfolio и GetPositions выполняются параллельно; новый Snapshot создаётся только при успехе обоих вызовов. UNAUTHENTICATED и PERMISSION_DENIED остаются 400 INVALID_TBANK_TOKEN, отсутствие connection -- 409 TBANK_NOT_CONNECTED, остальные upstream-сбои -- 503 TBANK_UNAVAILABLE.

Это переиспользует единственную T-Bank policy и не допускает частичного финансового состояния. Прямая доверенность accountId позволила бы запрашивать устаревшие или чужие ids; частичный ответ смешивал бы разные моменты upstream-состояния.

Нормализованная модель и точная арифметика

Backend добавляет публичные Portfolio DTO и один сервис нормализации. Он берёт обычные позиции только из GetPortfolio.positions, доступные деньги только из GetPositions.money, исключает virtual и blocked данные, удаляет нулевые элементы и сохраняет upstream-порядок. Инструментные типы переводятся в фиксированный union с OTHER fallback. Metadata по FIGI обогащает отображаемый идентификатор, но не входит в критерий успешности Snapshot.

Десятичные protobuf-пары преобразуются в signed integer nanos и обратно в canonical decimal strings через Node BigInt. Этим же представлением вычисляются currentValue, absoluteReturn, Cash Balance и агрегаты валют; API никогда не передаёт финансовые number. absoluteReturn публикуется только при одинаковой известной валюте текущей и средней цены.

Альтернативы: number теряет точность int64 и дробных значений; decimal dependency увеличивает surface там, где достаточно BigInt; использование GetPositions для ценных бумаг создало бы дубли и не содержит требуемых оценок.

Snapshot и metadata caches

Один in-memory Portfolio cache ключуется парой User и Brokerage Account. Успешный полный Snapshot считается свежим 30 секунд и пригоден как fallback до пяти минут. Если обновление временно неуспешно, сервис возвращает сохранённый Snapshot с isStale: true; invalid token никогда не маскируется fallback. Successful replace/delete connection очищает snapshots этого User.

Отдельный общий in-memory cache по FIGI хранит successful metadata и подтверждённое отсутствие 24 часа. Для нового Snapshot отсутствующие metadata запрашиваются параллельно и ожидаются в пределах gRPC deadline: при успешном ответе используется name. Ошибки metadata lookup не кэшируются. Cache metadata не содержит user data и не очищается при изменении connection.

Ограничение количества записей не добавляется: MVP рассчитан на единственного User, Snapshot живут не более пяти минут. При multi-user deployment заменить эти caches на shared bounded cache вместе с distributed rate-limit, а не добавлять частичный LRU сейчас.

OpenAPI и frontend detail page

Controller документирует DTO через Nest Swagger; frontend генерирует schema из работающего backend и выводит transport aliases из paths[...]. Новый portfolio entity инкапсулирует query с staleTime: 30_000 и отключённым retry; page импортирует entity только через public barrel. После stale time previous data не показывается до нового результата.

AccountPage выбирает loading, empty, unavailable-account, known-error, generic-error или loaded view. Loaded view использует локальные Mantine/CSS-module компоненты: агрегаты по валютам, отдельные секции акций и облигаций с upstream-порядком внутри каждой, общую секцию остальных позиций, Cash Balance и freshness warning. Название и тип счёта берутся из уже имеющегося account-list cache; при прямом переходе остаётся заголовок «Портфель». Денежный formatter выводит RUB как ₽. Дизайн сохраняет светлые поверхности, тонкие slate-границы, синий accent и читаемый single-column mobile layout референса, без его кода и placeholder-метрик.

Альтернативы: ручные frontend DTO расходятся с OpenAPI; дополнительный account endpoint ради заголовка добавляет вызов без доменной пользы; перестановка строк по сумме сравнивала бы разные валюты.

Documentation and validation

docs/features/portfolio-view.md фиксирует пользовательский поток, endpoint, публичную модель, точность значений, cache/fallback, ошибки и Mermaid sequence diagram. ADR не создаётся: server-side boundary, in-memory resilience и frontend query policy уже определены существующими ADR; конкретные TTL и normalisation относятся к feature. ADR 005 уточняется только если его общая формулировка противоречит фактической cache policy после реализации.

Risks / Trade-offs

  • [Snapshot может быть до 30 секунд старым] → это согласованный TTL; после него счёт заново проверяется через T-Bank.
  • [Fallback может показать данные до пяти минут] → клиент явно показывает isStale и исходный lastSuccessfulAt.
  • [Metadata lookup расходует upstream quota] → результат и отсутствие кэшируются 24 часа; финансовый ответ не блокируется его ошибкой.
  • [In-memory cache теряется при restart и не масштабируется] → MVP использует один instance; перед горизонтальным масштабированием нужны shared cache и limiter.
  • [T-Bank вводит новый тип инструмента] → он отображается как OTHER, не скрывается.

Migration Plan

  1. Расширить T-Bank client RPC для accounts, portfolio, positions и instrument metadata; добавить exact decimal normalizer, cache и user-scoped Portfolio service.
  2. Добавить controller, Swagger DTO и backend unit/e2e проверки нормализации, агрегации, fallback, invalid token, 404 и auth.
  3. Запустить backend, перегенерировать frontend OpenAPI client и добавить portfolio entity/query.
  4. Заменить account placeholder state-вьюхами и loaded UI; проверить desktop/mobile.
  5. Создать feature documentation, прогнать backend/frontend quality gates и OpenSpec validation.

Rollback до release удаляет endpoint и frontend entity/page view. Prisma migration отсутствует; connection и его encrypted token не затрагиваются.