Files
specs/openspec/changes/archive/2026-08-14-add-brokerage-account-ledger/design.md
T
2026-08-14 13:48:26 +03:00

62 lines
7.9 KiB
Markdown
Raw 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.
## Контекст
Portfolio сейчас получает текущее состояние счёта через T-Bank OperationsService и хранит только краткоживущие in-memory Portfolio Snapshots. В WealthLedger нет endpoint операций, типов операций, маршрута или локальной базы операций. Vendored T-Bank proto уже объявляет `GetOperationsByCursor`. Мотивация приведена в proposal.md, а поведение - в delta-spec `brokerage-account-ledger`.
## Цели и нецели
**Цели:**
- Добавить защищённый read path с cursor-пагинацией для исполненных операций T-Bank.
- Сохранить соответствие «одна upstream-операция - одна запись» и точные денежные значения.
- Использовать существующие паттерны T-Bank auth, проверки счёта, типизированного OpenAPI и локальных UI-состояний.
**Не входит в объём:**
- Постоянное хранение, синхронизация или экспорт операций.
- Расчёт последовательных денежных остатков, итогов, налоговой отчётности или исторической оценки портфеля.
- Конвертация валют, рыночные изменения цены или настраиваемое пользователем количество последних событий.
## Решения
### Прямое чтение из T-Bank с кратким in-memory cache
Сервис журнала будет запрашивать `GetOperationsByCursor` по требованию и кратко кэшировать точный результат запроса с ключом из User, счёта и нормализованного query. Это соответствует времени жизни текущего Portfolio Snapshot и исключает Prisma schema, backfill и жизненный цикл синхронизации.
Рассмотренная альтернатива: сохранять и синхронизировать страницы операций, как в `references/moex-vibe`. Отклонена, поскольку запрошенной возможности нужна текущая история только для чтения, а не автономная локальная история или производный расчёт баланса.
### Нормализованная страница операций на границе Brokerage Account
`GET /api/v1/brokerage-accounts/:accountId/operations` будет принимать необязательные включительные границы периода, подмножество категорий и cursor/limit. Ответ содержит id счёта, упорядоченные записи, `nextCursor`, `hasNext` и timestamp наблюдения. Он будет использовать существующую семантику принадлежности счёта и T-Bank token, чтобы недоступное connection, невалидный token и недоступный upstream оставались согласованными с Portfolio.
Backend сопоставляет T-Bank enum-типы шести стабильным категориям: `trade`, `income`, `tax`, `fee`, `transfer`, `other`. Он сохраняет исходный тип для точного label и будущего уточнения категорий. Запрашивается только `OPERATION_STATE_EXECUTED`.
Рассмотренная альтернатива: передавать raw protobuf-объекты операций. Отклонена, поскольку имена protobuf, кодирование денег и соглашения об optional-полях попадут в публичный API.
### Одна upstream-операция - одна запись журнала
Mapper сохраняет payment, commission, price, accrued interest и количества отдельными optional-полями одной записи. Комиссия сделки не разделяется на вымышленную запись комиссии. Записи без payment не получают искусственное нулевое значение.
Это предотвращает двойной учёт, когда T-Bank передаёт и поле комиссии операции, и отдельные fee-операции. Decimal strings и ISO-коды валют следуют существующему контракту Portfolio; frontend только форматирует их и не выполняет финансовую арифметику.
### Независимая устойчивость обзора и полной истории
Обзор счёта выполнит небольшой запрос журнала с `limit=5` вместе с запросом Portfolio. Его loading, empty и error views остаются внутри блока последних событий. Полная страница по `/accounts/:accountId/operations` владеет состояниями периода, категорий и cursor; изменение фильтров сбрасывает cursor. Она компактно отображает строки без цены, количества и раскрытия деталей.
Рассмотренная альтернатива: tab внутри страницы Portfolio. Отклонена, поскольку controls периода и pagination сделают основной обзор громоздким и свяжут несвязанные error states.
### Generated OpenAPI-типы на границе frontend
Swagger-контракт backend синхронизируется до frontend-работ. Frontend entity выводит типы request/response из generated `paths` и использует общий аутентифицированный API client. UI-labels, category controls и сообщения являются i18n-строками.
## Риски и компромиссы
- [T-Bank не передаёт остаток после операции] -> Показывать только фактические изменения; отложить дизайн последовательного баланса до потребности в persisted history и reconciliation.
- [Расширение upstream enum] -> Сопоставлять неизвестные типы с `other`, сохраняя raw type и upstream description.
- [Повторная навигация создаёт upstream-запросы] -> Краткий cache точного query и frontend query cache сокращают дублирование, не делая данные постоянными.
- [Ошибка последних событий может скрыть Portfolio] -> Оставить состояние локальным для блока.
- [Период дат может содержать много операций] -> Использовать T-Bank cursor-пагинацию и ограниченные размеры страниц.
## План миграции
1. Развернуть backend endpoint и OpenAPI-контракт; миграция данных не требуется.
2. Синхронизировать generated frontend API files и развернуть блок последних событий и маршрут истории.
3. Для отката удалить frontend-навигацию и deployment endpoint; постоянные пользовательские данные не требуют очистки.