feat: add operations list

This commit is contained in:
2026-08-14 13:48:26 +03:00
parent cdc5ae8995
commit e5517cceae
6 changed files with 242 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-08-14
@@ -0,0 +1,61 @@
## Контекст
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; постоянные пользовательские данные не требуют очистки.
@@ -0,0 +1,25 @@
## Зачем
Текущий экран счёта показывает только состояние Portfolio на момент запроса. Пользователь не может понять, какими операциями изменялись деньги и инструменты счёта, включая выплаты купонов и дивидендов, налоги и комиссии.
## Что меняется
- Добавить read-only журнал операций одного Brokerage Account на основе исполненных операций T-Bank.
- Добавить краткий блок пяти последних операций на странице счёта и отдельную страницу полного журнала.
- Добавить выбор периода, фильтрацию по категориям и cursor-пагинацию полного журнала.
- Показывать исходные денежные значения и детали одной upstream-операции без синтетического разделения комиссий, конвертации валют или расчёта остатка после операции.
## Возможности
### Новые возможности
- `brokerage-account-ledger`: просмотр исполненной истории операций и последних событий одного брокерского счёта.
### Изменяемые возможности
_Нет._
## Влияние
- Backend: T-Bank OperationsService, новый защищённый endpoint операций и его OpenAPI-контракт.
- Frontend: generated API types, entity операций, маршрут истории и блок последних событий на странице счёта.
- Документация: глоссарий WealthLedger и описание завершенной feature.
@@ -0,0 +1,60 @@
## Purpose
Позволяет пользователю просматривать исполненные операции одного брокерского счёта, чтобы понимать фактические денежные и инструментальные движения.
## ADDED Requirements
### Requirement: User can retrieve an account ledger
Система ДОЛЖНА предоставлять аутентифицированный журнал только для чтения одного Brokerage Account, принадлежащего текущему User. Журнал ДОЛЖЕН содержать только исполненные операции T-Bank и при отсутствии периода по умолчанию охватывать интервал от начала текущего календарного года до момента запроса.
#### Scenario: Получение журнала с периодом по умолчанию
- **WHEN** аутентифицированный User запрашивает журнал принадлежащего ему Brokerage Account без периода
- **THEN** система возвращает его исполненные операции от начала текущего календарного года до момента запроса
#### Scenario: Отклонение недоступного счёта
- **WHEN** аутентифицированный User запрашивает журнал счёта, который не является открытым поддерживаемым счётом, доступным через его T-Bank connection
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает операции
### Requirement: User can browse a filtered ledger
Система ДОЛЖНА позволять User ограничивать журнал включительным периодом дат и любым подмножеством категорий trade, income, tax, fee, transfer, other. Система ДОЛЖНА разбивать упорядоченный результат на страницы с непрозрачным cursor и возвращать признак доступности следующей страницы.
#### Scenario: Фильтрация доходных операций за период
- **WHEN** User запрашивает период дат и категорию income
- **THEN** результат содержит только исполненные доходные операции с датой в пределах этого периода
#### Scenario: Продолжение истории
- **WHEN** User запрашивает cursor следующей страницы, возвращённый предыдущим ответом журнала
- **THEN** система возвращает следующую страницу без повторения записей предыдущей страницы
### Requirement: Ledger entries preserve broker operation meaning
Система ДОЛЖНА представлять одну upstream-операцию одной записью журнала и ДОЛЖНА передавать её timestamp, тип операции, категорию, описание, доступные идентификаторы инструмента, денежную выплату, количество, цену, НКД и комиссию. Денежные поля ДОЛЖНЫ сохранять исходную валюту и точное десятичное значение.
#### Scenario: Отображение купонной выплаты
- **WHEN** T-Bank возвращает исполненную купонную операцию с выплатой и данными инструмента
- **THEN** запись журнала определяет её как income и передаёт исходную сумму выплаты, валюту и данные инструмента
#### Scenario: Отображение комиссии сделки без синтетической записи
- **WHEN** T-Bank возвращает исполненную сделку, содержащую выплату и комиссию
- **THEN** журнал содержит одну запись сделки с комиссией в качестве её детали
#### Scenario: Отображение движения бумаг без денег
- **WHEN** T-Bank возвращает исполненную операцию, изменяющую состав инструментов, но без денежной выплаты
- **THEN** запись журнала передаёт доступное количество и не содержит искусственной денежной выплаты с нулевым значением
### Requirement: Account overview includes recent ledger events
Обзор счёта ДОЛЖЕН показывать пять последних исполненных записей журнала этого счёта и действие перехода к его полному журналу. Неуспешный или пустой запрос последних записей ДОЛЖЕН оставаться локальным состоянием блока последних событий и НЕ ДОЛЖЕН заменять успешно отображённый обзор Portfolio.
#### Scenario: Отображение последних событий
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал
#### Scenario: Последние записи недоступны
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается ошибкой
- **THEN** обзор Portfolio остаётся доступным, а блок последних событий отображает собственное состояние восстановления
### Requirement: Full ledger presents compact rows
Страница полного журнала ДОЛЖНА показывать для каждой записи дату, тип операции, инструмент и денежное изменение в исходной валюте. Она ДОЛЖНА обозначать записи без движения денег и НЕ ДОЛЖНА показывать цену или количество.
#### Scenario: Отображение записи только с инструментом
- **WHEN** полный журнал отображает запись без денежной выплаты
- **THEN** страница определяет её как запись без движения денег
@@ -0,0 +1,32 @@
## 1. Backend-контракт журнала
- [x] 1.1 Добавить структурные типы request/response операций T-Bank и метод клиента `GetOperationsByCursor`, включая тесты клиента на передачу запроса и upstream-ошибки.
- [x] 1.2 Добавить DTO журнала и mapper, сохраняющий одну upstream-операцию одной записью, точные денежные строки, optional-детали и шесть стабильных категорий.
- [x] 1.3 Добавить тесты mapper для купонов, дивидендов, налогов, комиссий, сделок с комиссией, неизвестных типов и движений только инструментов.
## 2. Backend read path журнала
- [x] 2.1 Добавить валидируемые query-параметры журнала для периода, категорий, cursor и ограниченного limit.
- [x] 2.2 Добавить аутентифицированный сервис журнала Brokerage Account с проверкой принадлежности счёта, запросами исполненного состояния, кратким cache точного query и существующей семантикой ошибок T-Bank.
- [x] 2.3 Добавить `GET /brokerage-accounts/:accountId/operations`, Swagger responses и module wiring.
- [x] 2.4 Добавить тесты сервиса и controller для периода по умолчанию, включительных фильтров периода/категорий, продолжения по cursor, недоступного счёта и ошибок T-Bank.
## 3. Frontend API-интеграция
- [x] 3.1 Синхронизировать frontend OpenAPI snapshot и generated types из backend-контракта.
- [x] 3.2 Добавить public entity `broker-operation` с API/query hook на общем типизированном client и стабильным cache key.
- [x] 3.3 Добавить unit-тесты формирования query-параметров и поведения состояний query.
## 4. UI журнала счёта
- [x] 4.1 Добавить защищённый маршрут `/accounts/:accountId/operations` и страницу журнала счёта с изолированными loading, error и empty states.
- [x] 4.2 Добавить controls периода, выбор категорий и cursor-пагинацию; сбрасывать pagination при каждом изменении query.
- [x] 4.3 Отображать компактные строки операций в исходных валютах и обозначать записи без движения денег без цены, количества и раскрытия деталей.
- [x] 4.4 Добавить в обзор счёта блок из пяти последних событий и ссылку полного журнала, не связывая его состояние с загрузкой Portfolio.
- [x] 4.5 Добавить русские i18n-сообщения и UI-тесты фильтрации, страниц, раскрытия деталей, empty/error states и устойчивости последних событий.
## 5. Документация и проверка
- [x] 5.1 Добавить `Account Ledger` и `Ledger Entry` в глоссарий WealthLedger.
- [x] 5.2 Задокументировать завершённое поведение журнала в `docs/features`.
- [x] 5.3 Выполнить backend lint, tests и build; выполнить frontend verification и `openspec validate add-brokerage-account-ledger --type change --store wealth-ledger-specs`.
@@ -0,0 +1,62 @@
# Brokerage Account Ledger Specification
## Purpose
Позволяет пользователю просматривать исполненные операции одного брокерского счёта, чтобы понимать фактические денежные и инструментальные движения.
## Requirements
### Requirement: User can retrieve an account ledger
Система ДОЛЖНА предоставлять аутентифицированный журнал только для чтения одного Brokerage Account, принадлежащего текущему User. Журнал ДОЛЖЕН содержать только исполненные операции T-Bank и при отсутствии периода по умолчанию охватывать интервал от начала текущего календарного года до момента запроса.
#### Scenario: Получение журнала с периодом по умолчанию
- **WHEN** аутентифицированный User запрашивает журнал принадлежащего ему Brokerage Account без периода
- **THEN** система возвращает его исполненные операции от начала текущего календарного года до момента запроса
#### Scenario: Отклонение недоступного счёта
- **WHEN** аутентифицированный User запрашивает журнал счёта, который не является открытым поддерживаемым счётом, доступным через его T-Bank connection
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает операции
### Requirement: User can browse a filtered ledger
Система ДОЛЖНА позволять User ограничивать журнал включительным периодом дат и любым подмножеством категорий trade, income, tax, fee, transfer, other. Система ДОЛЖНА разбивать упорядоченный результат на страницы с непрозрачным cursor и возвращать признак доступности следующей страницы.
#### Scenario: Фильтрация доходных операций за период
- **WHEN** User запрашивает период дат и категорию income
- **THEN** результат содержит только исполненные доходные операции с датой в пределах этого периода
#### Scenario: Продолжение истории
- **WHEN** User запрашивает cursor следующей страницы, возвращённый предыдущим ответом журнала
- **THEN** система возвращает следующую страницу без повторения записей предыдущей страницы
### Requirement: Ledger entries preserve broker operation meaning
Система ДОЛЖНА представлять одну upstream-операцию одной записью журнала и ДОЛЖНА передавать её timestamp, тип операции, категорию, описание, доступные идентификаторы инструмента, денежную выплату, количество, цену, НКД и комиссию. Денежные поля ДОЛЖНЫ сохранять исходную валюту и точное десятичное значение.
#### Scenario: Отображение купонной выплаты
- **WHEN** T-Bank возвращает исполненную купонную операцию с выплатой и данными инструмента
- **THEN** запись журнала определяет её как income и передаёт исходную сумму выплаты, валюту и данные инструмента
#### Scenario: Отображение комиссии сделки без синтетической записи
- **WHEN** T-Bank возвращает исполненную сделку, содержащую выплату и комиссию
- **THEN** журнал содержит одну запись сделки с комиссией в качестве её детали
#### Scenario: Отображение движения бумаг без денег
- **WHEN** T-Bank возвращает исполненную операцию, изменяющую состав инструментов, но без денежной выплаты
- **THEN** запись журнала передаёт доступное количество и не содержит искусственной денежной выплаты с нулевым значением
### Requirement: Account overview includes recent ledger events
Обзор счёта ДОЛЖЕН показывать пять последних исполненных записей журнала этого счёта и действие перехода к его полному журналу. Неуспешный или пустой запрос последних записей ДОЛЖЕН оставаться локальным состоянием блока последних событий и НЕ ДОЛЖЕН заменять успешно отображённый обзор Portfolio.
#### Scenario: Отображение последних событий
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал
#### Scenario: Последние записи недоступны
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается ошибкой
- **THEN** обзор Portfolio остаётся доступным, а блок последних событий отображает собственное состояние восстановления
### Requirement: Full ledger presents compact rows
Страница полного журнала ДОЛЖНА показывать для каждой записи дату, тип операции, инструмент и денежное изменение в исходной валюте. Она ДОЛЖНА обозначать записи без движения денег и НЕ ДОЛЖНА показывать цену или количество.
#### Scenario: Отображение записи только с инструментом
- **WHEN** полный журнал отображает запись без денежной выплаты
- **THEN** страница определяет её как запись без движения денег