refactor: group canonical specs by domain and archive account-top-positions (#74)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 14s

This commit was merged in pull request #75.
This commit is contained in:
2026-11-02 21:50:49 +03:00
parent 1fd7e6ad97
commit 3821d9ed58
61 changed files with 707 additions and 1448 deletions
@@ -0,0 +1,180 @@
# brokerage-accounts Specification
## Purpose
Позволяет авторизованному пользователю просматривать поддерживаемые активные инвестиционные счета подключенного T-Bank и переходить к их деталям.
## Requirements
### Requirement: Список активных брокерских счетов
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий `{ data, meta }`, где `data` является сохранённым подтверждённым массивом открытых поддерживаемых инвестиционных счетов T-Bank текущего подключения User в ранее подтверждённом upstream-порядке, а `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`. Обычный GET MUST NOT обращаться к 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 удалить внешние пробелы из имени и при пустом результате использовать непустое название соответствующего типа.
Каждый элемент MUST также содержать nullable `portfolioSummary`. Доступная сводка MUST содержать `totalsByCurrency`: точные Money-итоги последнего Portfolio по исходным ISO-валютам, включающие стоимость позиций и Cash Balance в той же валюте. Она MUST NOT содержать FX-конвертированный или межвалютный итог и MUST NOT выдавать Cash Balance за самостоятельный второй итог. Если последний Portfolio конкретного доступного счёта отсутствует либо временно недоступен, `portfolioSummary` MUST быть `null`, не исключая этот счёт из списка и не делая список неуспешным. Пустой доступный Portfolio MUST передаваться доступной сводкой с пустым `totalsByCurrency`, а не `null`.
Список Brokerage Account и его сводки являются результатом наблюдения текущего брокерского подключения и MUST NOT создавать ожидание, что WealthLedger хранит долговременное право владения счётом или Portfolio независимо от T-Bank connection.
#### Scenario: Поддерживаемые счета возвращаются в upstream-порядке
- **WHEN** часовой обход подтвердил открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт
- **THEN** обычный GET отвечает `200` этими счетами в `data` в исходном порядке и наблюдаемым состоянием в `meta` без обращения к T-Bank
#### Scenario: Пустой список поддерживаемых счетов
- **WHEN** последний успешный обход подтвердил отсутствие открытых поддерживаемых инвестиционных счетов
- **THEN** система отвечает `200` с `data: []` и `meta`, обозначающим доступный полный результат
#### Scenario: Пустое название получает fallback
- **WHEN** последний подтверждённый счёт имеет пустое либо состоящее из пробелов имя
- **THEN** система отвечает этим счётом с непустым названием его нормализованного типа
#### Scenario: Дата открытия отсутствует
- **WHEN** последний подтверждённый счёт не имеет даты открытия
- **THEN** система отвечает этим счётом с `openedOn: null`
#### Scenario: Мультивалютная сводка счёта
- **WHEN** последний Portfolio счёта содержит итоги в RUB и USD, включая Cash Balance в USD
- **THEN** ответ содержит одну доступную `portfolioSummary` с отдельными Money-итогами RUB и USD без FX-конвертации, общего итога или отдельного итога Cash Balance
#### Scenario: Сводка одного счёта временно недоступна
- **WHEN** подтверждённый список содержит счёт без последнего доступного Portfolio
- **THEN** система отвечает `200` всеми счетами в исходном порядке и передаёт для этого счёта `portfolioSummary: null`
#### Scenario: Доступный Portfolio пуст
- **WHEN** последний доступный Portfolio счёта не имеет ненулевых позиций и доступных денежных остатков
- **THEN** система передаёт доступную `portfolioSummary` с пустым `totalsByCurrency`
### Requirement: Состояния недоступности списка счетов
Система MUST читать подтверждённый список текущего подключения локально. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank отклонил сохранённый token как неаутентифицированный или без прав во время часового обхода, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. Временная ошибка обхода MUST сохранять последний подтверждённый список доступным; до первого подтверждённого списка система MUST обозначать продолжающуюся первую загрузку и MUST NOT возвращать частичный список как полный.
#### 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` и не раскрывает личные данные
#### Scenario: Ошибка обхода после успешного списка
- **WHEN** часовой обход списка завершается временной ошибкой после ранее успешной проверки
- **THEN** обычный GET возвращает последний подтверждённый список без ожидания T-Bank
#### Scenario: T-Bank временно недоступен
- **WHEN** User запрашивает список до первого подтверждённого списка, а часовой обход завершился временной либо иной upstream-ошибкой
- **THEN** система сообщает о неуспешной первой загрузке без счетов
#### Scenario: Первый обход ещё не завершён
- **WHEN** User открывает список после подключения до получения первого подтверждённого списка
- **THEN** система сообщает о продолжающейся загрузке без частичного либо неподтверждённого списка
### Requirement: Экран списка брокерских счетов
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts`
без ручного обновления. Клиент MUST использовать успешный frontend-кэш списка
в течение 60 секунд. После истечения этого срока открытие или возвращение
на экран SHALL инициировать получение актуального списка; имеющийся Client
Cached Snapshot MUST оставаться видимым во время обновления и после временной
ошибки без индикации собственного чтения и действий повтора.
Автоматическое восстановление SHALL соответствовать `frontend-data-resilience`.
Открытый экран MAY периодически перечитывать backend-результат, сохраняя
видимый snapshot, и MUST NOT создавать импорт T-Bank. Состояние источника из
`meta` MUST представляться по `frontend-data-resilience`.
При успешном непустом ответе клиент MUST отобразить адаптивный одноколоночный
список широких интерактивных карточек счетов с локализованными типом и, если
доступна, датой открытия без времени; каждая карточка MUST ясно показывать тип,
имя, финансовую сводку, вторичные метаданные и заметное действие открытия счёта,
вести на `/accounts/:accountId` по всей области и сохранять доступное
клавиатурное состояние. После успешной замены или удаления T-Bank connection
клиент MUST инвалидировать список счетов.
Для доступной `portfolioSummary` карточка MUST показывать не более двух
Money-итогов в исходных валютах и обозначать наличие остальных валют, если
они есть; она MUST NOT показывать или подразумевать FX-конвертацию,
межвалютный агрегированный итог либо Cash Balance как отдельный второй итог.
При `portfolioSummary: null` карточка MUST показывать локальное нейтральное
состояние недоступности сводки, сохраняя метаданные счёта и возможность открыть
его. При доступной пустой сводке карточка 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** клиент оставляет этот список видимым во время нового запроса без индикации фонового обновления
#### Scenario: Открытый экран перечитывает список
- **WHEN** экран Accounts остаётся открытым 30 минут после последнего успешного HTTP-чтения
- **THEN** клиент выполняет фоновое HTTP-перечитывание backend-результата, сохраняет видимый snapshot и не запускает импорт T-Bank
#### Scenario: Последняя попытка неуспешна после полного списка
- **WHEN** клиент получает полный `data` списка и `meta` с неуспехом последней часовой попытки
- **THEN** клиент сохраняет список видимым, показывает его исходное время и текстовое либо доступно именованное предупреждение о неуспешной попытке, не предлагая ручной повтор
#### Scenario: Первая загрузка списка выполняется или неуспешна
- **WHEN** клиент получает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** клиент показывает соответственно доступное состояние загрузки либо локальной недоступности без карточек счетов и без представления частичного списка как полного
#### Scenario: Ошибка фонового обновления не скрывает snapshot
- **WHEN** обновление устаревшего списка завершается временной ошибкой при наличии последнего успешного Client Cached Snapshot
- **THEN** клиент сохраняет snapshot видимым без предупреждения и ручного повтора и восстанавливает получение данных автоматически
#### Scenario: Карточка ограничивает валютные итоги
- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах
- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения
#### Scenario: Недоступность сводки локальна карточке
- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null`
- **THEN** он показывает нейтральную недоступность только этой сводки, не скрывает остальные карточки и сохраняет переход на детали счёта
#### Scenario: Пустая сводка не является ошибкой
- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency`
- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной ошибки
### Requirement: Клиентские состояния ошибки списка счетов
Клиент SHALL автоматически восстанавливать временно неуспешное чтение списка
по правилам `frontend-data-resilience` без кнопок обновления и повтора. При
`TBANK_NOT_CONNECTED` клиент MUST показать необходимость подключения и действие
открытия настроек T-Bank. При `INVALID_TBANK_TOKEN` клиент MUST показать
необходимость восстановить подключение и то же действие. Эти подтверждённые
состояния MUST скрывать личный список и MUST NOT запускать автоматические повторы.
При временной недоступности, включая `TBANK_UNAVAILABLE`, клиент MUST сохранять
успешный результат текущей границы доступа без предупреждений. Если результата
нет, после первичного ожидания клиент SHALL показать спокойную локальную
недоступность без технических подробностей. Неизвестная ошибка MUST получать
безопасное локальное представление; отсутствие известного кода MUST NOT
разрешать бесконечные повторы или обход доступа. Временный сбой MUST NOT
предлагать перенастройку действующего подключения как способ повторить загрузку.
#### Scenario: Подключение требуется
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Известная ошибка не повторяется автоматически
- **WHEN** запрос списка завершается подтверждённой ошибкой подключения или доступа, требующей действия пользователя
- **THEN** клиент не выполняет автоматических повторов и предлагает соответствующее действие вместо технической повторной загрузки
#### Scenario: Token требует внимания
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
- **THEN** клиент скрывает личный список и предлагает восстановить подключение в настройках T-Bank
#### Scenario: Временная ошибка T-Bank
- **WHEN** клиент получает `TBANK_UNAVAILABLE` без доступного результата списка
- **THEN** клиент автоматически восстанавливает получение и после первичного ожидания показывает нейтральную недоступность без повторной загрузки и предложения перенастроить подключение
#### Scenario: Неизвестная ошибка загрузки
- **WHEN** клиент получает ошибку без известного кода списка счетов и без доступного результата
- **THEN** клиент показывает безопасное нейтральное состояние без технического сообщения или кнопки повтора, применяя только допустимое ограниченное восстановление
#### Scenario: Известная ошибка при сохранённом snapshot
- **WHEN** фоновое обновление списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE`
- **THEN** клиент сохраняет snapshot видимым без предупреждения, времени получения и ручных действий восстановления