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

76 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 не затрагиваются.