feat: add brocker account list

This commit is contained in:
2026-08-13 20:59:38 +03:00
parent 7edfcb9dba
commit e9075425df
6 changed files with 257 additions and 2 deletions
@@ -0,0 +1,65 @@
## Context
См. мотивацию в `proposal.md` и поведение в `specs/brokerage-accounts/spec.md`. Backend уже хранит один зашифрованный T-Bank token на User, проверяет его через `UsersService/GetAccounts` и применяет общую policy TLS, rate limit, deadline и retry. Этот RPC пока отбрасывает response body, а `/accounts` во frontend является placeholder-страницей. OpenAPI backend остаётся источником transport contract для frontend.
## Goals / Non-Goals
**Goals:**
- Использовать существующую T-Bank boundary и resilience policy для user-scoped read-path списка счетов.
- Отделить публичную модель инвестиционного счёта от protobuf enum и банковских типов T-Bank.
- Дать `/accounts` понятные loading, empty, error и loaded состояния в FSD-структуре, сохранив стиль codebase и визуальный язык `references/fin-tracker` без переноса его кода или неподдерживаемых данных.
- Зафиксировать feature-документацию с пользовательским потоком, API и MVP-ограничениями после реализации.
**Non-Goals:**
- Суммы, доходность, агрегация, графики, структура портфеля и любые вычисления по счетам.
- Кэш backend, timestamp последнего обновления, ручная синхронизация или частичные результаты.
- Закрытые, банковские и неизвестные типы счетов, операции и изменение T-Bank connection из списка.
## Decisions
### Read-path списка и public contract
Новый защищённый `GET /api/v1/brokerage-accounts` сначала читает `TbankConnection` User. При её отсутствии он немедленно возвращает `409 TBANK_NOT_CONNECTED`; gRPC не вызывается. При connection сервис расшифровывает token только для вызова T-Bank и получает открытые счета через существующий общий client, поэтому к списку применяются те же TLS, limiter, deadline и retry, что и к проверке token.
Успех возвращает прямой JSON-массив, а не envelope: сейчас ответ не содержит метаданных, и обёртка не несёт семантики. Ошибка не возвращает данные. При gRPC `UNAUTHENTICATED` и `PERMISSION_DENIED` connection не удаляется, а клиент получает `400 INVALID_TBANK_TOKEN`; другие ошибки остаются `503 TBANK_UNAVAILABLE`.
Альтернативы: использовать `/accounts` для API смешало бы инвестиционные и банковские счета; статус `200` без connection сделал бы обязательную интеграцию неявным UI-флагом; сохранение или возврат старого списка потребовало бы cache invalidation и вводило бы пользователя в заблуждение об актуальности.
### Нормализация Brokerage Account
Backend добавляет нормализованный DTO `BrokerageAccount` с `id`, `name`, `type` и `openedOn`. Трансляция включает только upstream-типы `ACCOUNT_TYPE_TINKOFF`, `ACCOUNT_TYPE_TINKOFF_IIS`, `ACCOUNT_TYPE_INVEST_BOX` и `ACCOUNT_TYPE_INVEST_FUND`, преобразуя их соответственно в `BROKERAGE`, `INDIVIDUAL_INVESTMENT`, `INVEST_BOX` и `MONEY_MARKET_FUND`. Остальные и неизвестные типы отфильтровываются, порядок upstream сохраняется.
`name` нормализуется через trim и при пустом результате заменяется fallback названием типа. Timestamp `openedDate` переводится в nullable `YYYY-MM-DD`; время намеренно не публикуется, потому что экран показывает календарную дату открытия, а не момент события.
Альтернативы: передача protobuf enum привязала бы frontend к T-Bank contract; показ неизвестных типов как инвестиционных расширил бы доменную границу без решения пользователя; отображение технического id вместо fallback-названия ухудшило бы интерфейс без пользы для MVP.
### UI и React Query
Новый entity slice `brokerage-account` инкапсулирует typed API query, локальные transport aliases из generated `paths` и display helpers. `AccountsPage` использует entity через public barrel и выбирает одно из состояний: loading, known-error, generic error, empty либо loaded. Query имеет `staleTime: 60_000` и отключённый automatic retry; отключённый focus refetch сохраняется глобальной настройкой. Свежий React Query cache исключает повторный GET в течение минуты; после его устаревания `isFetching` заменяет прежний список loading-state. Retry для временной и неизвестной ошибки вызывает `refetch`; кнопка настроек ведёт на `/settings`.
Успешная замена или удаление connection инвалидирует query списка. Карточки реализуются Mantine-компонентами и локальным CSS module: белая поверхность, тонкая slate-граница, умеренная мягкая тень, синий accent/kicker и заметный hover/focus. Сетка адаптивна: две колонки при доступной ширине и одна на узком экране. Карточка является доступной ссылкой на `/accounts/:accountId`, показывает имя, локализованный тип и при наличии дату открытия. Не добавляются hero, sparkline и финансовые метрики, поскольку текущий контракт их не предоставляет.
Альтернативы: размещение query прямо в page смешало бы transport и presentation; копирование разметки или mock-метрик FinTracker нарушило бы границы references и исказило бы реальные данные; фиксированная двухколоночная mobile-сетка ухудшила бы читаемость.
### Документация и проверка
После реализации `docs/features/brokerage-accounts.md` описывает пользовательский поток, endpoint, коды ошибок, нормализованные типы и отсутствие кэша/времени актуальности; последовательность запроса фиксируется Mermaid-диаграммой. Постоянный ADR не требуется: backend boundary, OpenAPI и React Query policy уже закреплены ADR 003, 005 и 007, а новая фильтрация типов является ограниченной feature-деталью.
## Risks / Trade-offs
- [Токен может быть отозван после подключения] → Список возвращает `INVALID_TBANK_TOKEN` и ведёт в настройки, но не удаляет connection автоматически.
- [Данные меняются у T-Bank между загрузками] → Frontend использует результат до 60 секунд, но backend не хранит кэш и не передаёт метку свежести.
- [T-Bank добавит новый инвестиционный тип] → Неизвестный тип не показывается до явного расширения доменной модели.
- [Визуальный референс содержит финансовые показатели, которых нет в API] → Сохраняются лишь стилистические признаки, не генерируются placeholder-значения.
## Migration Plan
1. Расширить T-Bank client получением response body и добавить user-scoped сервис, controller и OpenAPI DTO списка.
2. Добавить backend unit/e2e проверки нормализации, отсутствующего connection, error mapping и auth.
3. Перегенерировать frontend API client из работающего backend OpenAPI.
4. Добавить entity, query, account cards и все состояния `/accounts`; инвалидировать query из T-Bank connection feature.
5. Проверить backend и frontend quality gates, визуально проверить desktop/mobile и создать `docs/features/brokerage-accounts.md`.
Rollback до release удаляет endpoint и frontend-срез; схема базы данных не изменяется. Удалять `TbankConnection` или шифротекст при rollback не требуется.
@@ -5,8 +5,8 @@
## What Changes
- Добавить защищенный API списка активных T-Bank инвестиционных счетов пользователя через нормализованные backend DTO.
- Добавить страницу `/accounts` со списком счетов всех поддерживаемых T-Bank типов, пустым состоянием подключения и безопасным состоянием upstream-ошибок.
- Показывать фактическое время последнего успешного получения данных T-Bank и не скрывать полученные счета при частичной ошибке.
- Добавить страницу `/accounts` со списком поддерживаемых инвестиционных типов T-Bank, пустым состоянием подключения и безопасными состояниями upstream-ошибок.
- Не показывать частичные либо сохранённые ранее счета: результат одного upstream-запроса отображается целиком или заменяется error-state.
- Получать данные при открытии экрана через React Query без focus refetch и без ручной кнопки обновления.
- Документировать пользовательский поток списка счетов и ограничения актуальности данных.
@@ -0,0 +1,81 @@
## Purpose
Позволяет авторизованному пользователю просматривать поддерживаемые активные инвестиционные счета подключенного T-Bank и переходить к их деталям.
## ADDED Requirements
### Requirement: Список активных брокерских счетов
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий массив его открытых поддерживаемых инвестиционных счетов T-Bank в порядке, полученном от T-Bank. Каждый элемент MUST содержать стабильный `id`, непустое `name`, нормализованный `type` (`BROKERAGE`, `INDIVIDUAL_INVESTMENT`, `INVEST_BOX` или `MONEY_MARKET_FUND`) и nullable `openedOn` в формате `YYYY-MM-DD`. Система MUST включать только счета T-Bank типов «брокерский счёт», «ИИС», «Инвесткопилка» и «фонд денежного рынка»; банковские и неизвестные типы MUST NOT возвращаться. Система MUST удалить внешние пробелы из имени и при пустом результате использовать непустое название соответствующего типа.
#### Scenario: Поддерживаемые счета возвращаются в upstream-порядке
- **WHEN** User с действующим T-Bank connection запрашивает список, а T-Bank возвращает открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт
- **THEN** система отвечает `200` массивом брокерского счёта и ИИС в исходном порядке T-Bank без дебетового счёта
#### Scenario: Пустой список поддерживаемых счетов
- **WHEN** User с действующим T-Bank connection не имеет открытых поддерживаемых инвестиционных счетов
- **THEN** система отвечает `200` пустым массивом
#### Scenario: Пустое название получает fallback
- **WHEN** T-Bank возвращает поддерживаемый открытый счёт с пустым либо состоящим из пробелов именем
- **THEN** система отвечает этим счётом с непустым названием его нормализованного типа
#### Scenario: Дата открытия отсутствует
- **WHEN** T-Bank не возвращает дату открытия поддерживаемого открытого счёта
- **THEN** система отвечает этим счётом с `openedOn: null`
### Requirement: Состояния недоступности списка счетов
Система MUST NOT возвращать частичный или сохранённый ранее список счетов. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank отклоняет сохранённый token как неаутентифицированный или без прав, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. При иной неуспешной попытке получить список из T-Bank система MUST отвечать `503` Problem Details с code `TBANK_UNAVAILABLE`.
#### Scenario: Подключение отсутствует
- **WHEN** User без сохранённого T-Bank connection запрашивает список
- **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED`
#### Scenario: Сохранённый token больше не действителен
- **WHEN** T-Bank отклоняет сохранённый token при запросе списка как неаутентифицированный или без прав
- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не удаляет connection
#### Scenario: T-Bank временно недоступен
- **WHEN** запрос списка не завершается из-за временной либо иной upstream-ошибки
- **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` без счетов
### Requirement: Экран списка брокерских счетов
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручного обновления и без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд; после истечения этого срока при новом запросе клиент MUST скрывать ранее показанный список и показывать loading-состояние. При успешном непустом ответе клиент MUST отобразить светлые интерактивные карточки счетов с названием, локализованным типом и, если доступна, локализованной датой открытия без времени; карточки MUST вести на `/accounts/:accountId`. При успешном пустом ответе клиент MUST показать штатное состояние отсутствия инвестиционных счетов. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов.
#### Scenario: Пользователь открывает счёт из списка
- **WHEN** клиент успешно загрузил непустой список и User выбирает карточку счёта
- **THEN** клиент переходит на `/accounts/:accountId` с `id` выбранного счёта
#### Scenario: Список пуст
- **WHEN** клиент получает успешный пустой массив
- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank
#### Scenario: Список берётся из свежего frontend-кэша
- **WHEN** User повторно открывает список в течение 60 секунд после успешной загрузки
- **THEN** клиент показывает сохранённые счета без нового API-запроса
#### Scenario: Устаревший список загружается повторно
- **WHEN** User открывает список после истечения 60 секунд frontend-кэша
- **THEN** клиент не показывает ранее загруженные счета до получения нового результата
### Requirement: Клиентские состояния ошибки списка счетов
Клиент MUST NOT автоматически повторять неуспешный запрос списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторной загрузки и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторной загрузки.
#### Scenario: Подключение требуется
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Известная ошибка не повторяется автоматически
- **WHEN** запрос списка завершается известной ошибкой
- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторной загрузки
#### Scenario: Token требует внимания
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Временная ошибка T-Bank
- **WHEN** клиент получает `TBANK_UNAVAILABLE`
- **THEN** клиент не показывает список и предлагает повторить загрузку либо открыть настройки T-Bank
#### Scenario: Неизвестная ошибка загрузки
- **WHEN** клиент получает ошибку без известного кода списка счетов
- **THEN** клиент не показывает список и предлагает только повторить загрузку
@@ -0,0 +1,26 @@
## 1. Backend read-path счетов
- [x] 1.1 Расширить T-Bank client для возврата данных открытых счетов, сохранив существующую policy TLS, rate limit, deadline, retry и безопасного mapping ошибок.
- [x] 1.2 Добавить аутентифицированные controller, service и OpenAPI DTO для `GET /api/v1/brokerage-accounts`; при отсутствии connection возвращать `TBANK_NOT_CONNECTED` без upstream-вызова.
- [x] 1.3 Нормализовать поддерживаемые типы счетов T-Bank, trimmed/fallback имена и nullable дату открытия в формате `YYYY-MM-DD` в публичный ответ brokerage account, сохраняя upstream-порядок.
- [x] 1.4 Добавить backend-тесты для аутентификации, отсутствующего connection, нормализованных поддерживаемых счетов, отфильтрованных типов, пустого результата, отсутствующей даты, сохранения невалидного token и upstream-недоступности.
## 2. Сгенерированный контракт и frontend data layer
- [x] 2.1 Запустить backend API и перегенерировать frontend OpenAPI client; проверить наличие `GET /api/v1/brokerage-accounts` в сгенерированном контракте.
- [x] 2.2 Добавить FSD entity slice `brokerage-account` с типизированным React Query запросом списка, локальной обработкой кодов ошибок, display helpers и публичными barrel exports.
- [x] 2.3 Инвалидировать query brokerage accounts после успешной замены либо удаления T-Bank connection.
## 3. Страница счетов
- [x] 3.1 Заменить placeholder `/accounts` на состояния loading, empty, known-error, generic-error и loaded, скрывая устаревшие карточки при каждом запросе.
- [x] 3.2 Добавить локализованные доступные карточки счетов со ссылкой на `/accounts/:accountId`, названием, нормализованным типом и опциональной локализованной датой открытия.
- [x] 3.3 Оформить loaded-список адаптивными карточками в одну или две колонки: светлые поверхности, синие акценты, мягкие границы и тени в стиле `references/fin-tracker`, через локальные CSS modules и без вымышленных финансовых данных.
- [x] 3.4 Добавить frontend-тесты для всех состояний страницы, действий известных ошибок, retry, навигации и invalidation после mutation подключения.
## 4. Документация и проверка
- [x] 4.1 Создать `docs/features/brokerage-accounts.md` с пользовательским потоком, Mermaid sequence diagram, API contract, поведением ошибок, поддерживаемыми типами и ограничением отсутствия кэша/данных об актуальности.
- [x] 4.2 Запустить backend lint, tests и build; запустить frontend lint, typecheck, tests, build и no-TODO check.
- [x] 4.3 Визуально проверить страницу счетов на desktop и mobile ширинах, включая состояния loading, empty, error и loaded.
- [x] 4.4 Перед архивированием запустить `openspec validate add-brokerage-accounts --type change --strict --store wealth-ledger-specs` и `opsx-verify`.
+83
View File
@@ -0,0 +1,83 @@
# brokerage-accounts Specification
## Purpose
Позволяет авторизованному пользователю просматривать поддерживаемые активные инвестиционные счета подключенного T-Bank и переходить к их деталям.
## Requirements
### Requirement: Список активных брокерских счетов
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий массив его открытых поддерживаемых инвестиционных счетов T-Bank в порядке, полученном от T-Bank. Каждый элемент MUST содержать стабильный `id`, непустое `name`, нормализованный `type` (`BROKERAGE`, `INDIVIDUAL_INVESTMENT`, `INVEST_BOX` или `MONEY_MARKET_FUND`) и nullable `openedOn` в формате `YYYY-MM-DD`. Система MUST включать только счета T-Bank типов «брокерский счёт», «ИИС», «Инвесткопилка» и «фонд денежного рынка»; банковские и неизвестные типы MUST NOT возвращаться. Система MUST удалить внешние пробелы из имени и при пустом результате использовать непустое название соответствующего типа.
#### Scenario: Поддерживаемые счета возвращаются в upstream-порядке
- **WHEN** User с действующим T-Bank connection запрашивает список, а T-Bank возвращает открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт
- **THEN** система отвечает `200` массивом брокерского счёта и ИИС в исходном порядке T-Bank без дебетового счёта
#### Scenario: Пустой список поддерживаемых счетов
- **WHEN** User с действующим T-Bank connection не имеет открытых поддерживаемых инвестиционных счетов
- **THEN** система отвечает `200` пустым массивом
#### Scenario: Пустое название получает fallback
- **WHEN** T-Bank возвращает поддерживаемый открытый счёт с пустым либо состоящим из пробелов именем
- **THEN** система отвечает этим счётом с непустым названием его нормализованного типа
#### Scenario: Дата открытия отсутствует
- **WHEN** T-Bank не возвращает дату открытия поддерживаемого открытого счёта
- **THEN** система отвечает этим счётом с `openedOn: null`
### Requirement: Состояния недоступности списка счетов
Система MUST NOT возвращать частичный или сохранённый ранее список счетов. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank отклоняет сохранённый token как неаутентифицированный или без прав, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. При иной неуспешной попытке получить список из T-Bank система MUST отвечать `503` Problem Details с code `TBANK_UNAVAILABLE`.
#### Scenario: Подключение отсутствует
- **WHEN** User без сохранённого T-Bank connection запрашивает список
- **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED`
#### Scenario: Сохранённый token больше не действителен
- **WHEN** T-Bank отклоняет сохранённый token при запросе списка как неаутентифицированный или без прав
- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не удаляет connection
#### Scenario: T-Bank временно недоступен
- **WHEN** запрос списка не завершается из-за временной либо иной upstream-ошибки
- **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` без счетов
### Requirement: Экран списка брокерских счетов
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручного обновления и без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд; после истечения этого срока при новом запросе клиент MUST скрывать ранее показанный список и показывать loading-состояние. При успешном непустом ответе клиент MUST отобразить светлые интерактивные карточки счетов с названием, локализованным типом и, если доступна, локализованной датой открытия без времени; карточки MUST вести на `/accounts/:accountId`. При успешном пустом ответе клиент MUST показать штатное состояние отсутствия инвестиционных счетов. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов.
#### Scenario: Пользователь открывает счёт из списка
- **WHEN** клиент успешно загрузил непустой список и User выбирает карточку счёта
- **THEN** клиент переходит на `/accounts/:accountId` с `id` выбранного счёта
#### Scenario: Список пуст
- **WHEN** клиент получает успешный пустой массив
- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank
#### Scenario: Список берётся из свежего frontend-кэша
- **WHEN** User повторно открывает список в течение 60 секунд после успешной загрузки
- **THEN** клиент показывает сохранённые счета без нового API-запроса
#### Scenario: Устаревший список загружается повторно
- **WHEN** User открывает список после истечения 60 секунд frontend-кэша
- **THEN** клиент не показывает ранее загруженные счета до получения нового результата
### Requirement: Клиентские состояния ошибки списка счетов
Клиент MUST NOT автоматически повторять неуспешный запрос списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторной загрузки и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторной загрузки.
#### Scenario: Подключение требуется
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Известная ошибка не повторяется автоматически
- **WHEN** запрос списка завершается известной ошибкой
- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторной загрузки
#### Scenario: Token требует внимания
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Временная ошибка T-Bank
- **WHEN** клиент получает `TBANK_UNAVAILABLE`
- **THEN** клиент не показывает список и предлагает повторить загрузку либо открыть настройки T-Bank
#### Scenario: Неизвестная ошибка загрузки
- **WHEN** клиент получает ошибку без известного кода списка счетов
- **THEN** клиент не показывает список и предлагает только повторить загрузку