feat: add portfolio
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# portfolio-view Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Позволяет авторизованному User просматривать нормализованный текущий состав и агрегаты Portfolio одного доступного Brokerage Account без валютной конвертации.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Получение Portfolio выбранного Brokerage Account
|
||||
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts/:accountId/portfolio`. Она MUST возвращать нормализованный полный Portfolio только для открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Ответ MUST содержать `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`.
|
||||
|
||||
#### Scenario: Получение текущего Portfolio
|
||||
- **WHEN** User запрашивает Portfolio доступного Brokerage Account, а обязательные upstream-данные успешно получены
|
||||
- **THEN** система отвечает `200` полным нормализованным Portfolio с `isStale: false` и временем успешного получения финансового снимка
|
||||
|
||||
#### Scenario: Недоступный Brokerage Account
|
||||
- **WHEN** `accountId` не принадлежит User, закрыт, неподдерживаем или больше не доступен через текущий T-Bank connection
|
||||
- **THEN** система отвечает `404` без Portfolio
|
||||
|
||||
### Requirement: Нормализованный состав Portfolio
|
||||
Система MUST возвращать только обычные Portfolio Position с ненулевым количеством и доступные Cash Balance с ненулевой суммой. Cash Balance MUST быть отдельным от Portfolio Position элементом и агрегироваться до одного значения на валюту. Порядок Portfolio Position и Cash Balance MUST соответствовать upstream-порядку; система MUST NOT сортировать или объединять Portfolio Position с одинаковым FIGI. Виртуальные позиции и заблокированные денежные остатки MUST NOT входить в Portfolio.
|
||||
|
||||
Каждая Portfolio Position MUST содержать нормализованный тип `STOCK`, `BOND`, `FUND`, `CURRENCY`, `FUTURE`, `OPTION`, `STRUCTURED_NOTE`, `DIGITAL_FINANCIAL_ASSET` либо `OTHER`, строковое `quantity`, nullable `currentValue`, nullable `averagePurchasePrice` и nullable `absoluteReturn`. Система MUST искать metadata по instrument UID при его наличии, затем по FIGI. Отображаемый идентификатор MUST предпочитать name, затем ticker, а при недоступности metadata использовать FIGI, не скрывая финансовые значения.
|
||||
|
||||
#### Scenario: Позиция с неизвестным upstream-типом
|
||||
- **WHEN** upstream возвращает обычную ненулевую позицию с нераспознанным типом инструмента
|
||||
- **THEN** система возвращает её в исходном порядке с типом `OTHER`
|
||||
|
||||
#### Scenario: Позиция без metadata инструмента
|
||||
- **WHEN** lookup metadata по FIGI не дал ticker или name либо временно недоступен
|
||||
- **THEN** система возвращает позицию с FIGI как fallback-идентификатором и всеми доступными финансовыми значениями
|
||||
|
||||
#### Scenario: Имя инструмента доступно
|
||||
- **WHEN** metadata инструмента по FIGI содержит непустое name
|
||||
- **THEN** система возвращает name как отображаемый идентификатор позиции вместо FIGI или ticker
|
||||
|
||||
#### Scenario: FIGI не разрешается, но доступен instrument UID
|
||||
- **WHEN** FIGI позиции не даёт metadata, а её instrument UID возвращает непустое name
|
||||
- **THEN** система возвращает name, полученное по instrument UID
|
||||
|
||||
#### Scenario: Денежные остатки в одной валюте
|
||||
- **WHEN** upstream возвращает несколько ненулевых доступных денежных остатков одной валюты
|
||||
- **THEN** система возвращает один Cash Balance с их точной суммой
|
||||
|
||||
#### Scenario: Штатный пустой Portfolio
|
||||
- **WHEN** у доступного Brokerage Account нет ненулевых обычных позиций и доступных денежных остатков
|
||||
- **THEN** система отвечает `200` с пустыми `positions`, `cashBalances` и `totalsByCurrency`
|
||||
|
||||
### Requirement: Точные значения и валютные агрегаты
|
||||
Все количества и финансовые значения в Portfolio MUST передаваться десятичными строками без экспоненциальной записи и без незначащих завершающих нулей. Денежное значение MUST содержать ISO-код валюты. Система MUST корректно нормализовать отрицательные целую и дробную части upstream-значений. Отсутствующее значение MUST передаваться как `null`, а не как ноль.
|
||||
|
||||
`currentValue` Portfolio Position MUST быть точным произведением количества на текущую цену при наличии валидных множителей и валюты цены. `absoluteReturn` MUST быть точной величиной `(currentPrice - averagePurchasePrice) * quantity` только когда обе цены имеют одинаковую известную валюту. Позиция без вычислимой текущей стоимости MUST оставаться в ответе, но MUST NOT входить в агрегаты. `totalsByCurrency` MUST содержать точную сумму currentValue позиций и Cash Balance в каждой валюте без валютной конвертации.
|
||||
|
||||
#### Scenario: Мультивалютный Portfolio
|
||||
- **WHEN** Portfolio содержит финансовые значения в RUB и USD
|
||||
- **THEN** система возвращает отдельные агрегаты RUB и USD без общего сконвертированного итога
|
||||
|
||||
#### Scenario: Позиция без вычислимой стоимости
|
||||
- **WHEN** у ненулевой позиции отсутствует либо невалидна цена, количество или валюта цены
|
||||
- **THEN** система возвращает позицию с `currentValue: null` и не включает её в `totalsByCurrency`
|
||||
|
||||
#### Scenario: Отрицательная дробная часть upstream-значения
|
||||
- **WHEN** upstream возвращает сумму с `units: 0` и отрицательной дробной частью `nano: -500000000`
|
||||
- **THEN** система передаёт её как строку `"-0.5"`
|
||||
|
||||
### Requirement: Свежесть и fallback полного Portfolio Snapshot
|
||||
Система MUST кэшировать полный успешный Portfolio Snapshot отдельно для каждой пары User и Brokerage Account в памяти на 30 секунд. После истечения свежего кэша система MUST получить новый полный снимок; при временной upstream-недоступности она SHALL вернуть последний успешный полный снимок, если ему не более пяти минут, с `isStale: true`. Metadata инструмента MUST NOT влиять на успешность или свежесть Portfolio Snapshot, но MUST быть получено до формирования нового Snapshot, чтобы при доступном upstream-ответе вернуть отображаемое имя инструмента.
|
||||
|
||||
Система MUST очищать все Portfolio Snapshot User после успешной замены или удаления его T-Bank connection. Успешные metadata инструмента и подтверждённое отсутствие инструмента MUST кэшироваться глобально по FIGI на 24 часа; ошибка metadata lookup MUST NOT кэшироваться.
|
||||
|
||||
#### Scenario: Свежий Snapshot
|
||||
- **WHEN** User повторно запрашивает тот же Portfolio в течение 30 секунд после успешного получения
|
||||
- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank
|
||||
|
||||
#### Scenario: Медленный lookup инструмента
|
||||
- **WHEN** metadata инструмента ещё отсутствует и его lookup не завершился
|
||||
- **THEN** система ожидает lookup в пределах gRPC deadline и возвращает name при успешном ответе upstream
|
||||
|
||||
#### Scenario: Fallback после временной upstream-ошибки
|
||||
- **WHEN** обновление устаревшего Snapshot неуспешно из-за временной недоступности T-Bank, а последний полный успешный Snapshot получен не более пяти минут назад
|
||||
- **THEN** система отвечает `200` этим Snapshot с `isStale: true` и исходным `lastSuccessfulAt`
|
||||
|
||||
#### Scenario: Недействительный token не маскируется fallback-данными
|
||||
- **WHEN** T-Bank отклоняет token как неаутентифицированный или без прав при обновлении Portfolio
|
||||
- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN`
|
||||
|
||||
### Requirement: Ошибки и состояния Portfolio на клиенте
|
||||
Клиент SHALL загружать Portfolio при открытии защищённого маршрута `/accounts/:accountId`. Он MUST использовать успешный ответ в течение 30 секунд, без automatic retry, polling или refetch при возврате фокуса. После истечения срока клиент MUST скрыть предыдущий ответ и показать loading-состояние до нового результата.
|
||||
|
||||
Клиент MUST показать штатное empty-состояние только при пустых `positions` и `cashBalances`. При `isStale: true` клиент MUST показывать Snapshot, `lastSuccessfulAt` и нейтральное предупреждение о временной недоступности T-Bank. При `404` клиент MUST показать состояние недоступного счёта с действием возврата к списку счетов. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` или иной ошибке клиент MUST показать безопасное error-состояние с доступными для ошибки действиями; действие повтора разрешено только для временной и неизвестной ошибки.
|
||||
|
||||
Клиент MUST отображать `STOCK` и `BOND` отдельными секциями. Внутри каждой секции MUST сохраняться upstream-порядок позиций. Позиции иных типов MUST оставаться доступными в отдельной общей секции. При форматировании денежных значений в русской локали клиент MUST отображать RUB как символ `₽`; другие валюты MUST сохранять понятное обозначение.
|
||||
|
||||
#### Scenario: Показ устаревшего Portfolio
|
||||
- **WHEN** клиент получает успешный ответ с `isStale: true`
|
||||
- **THEN** клиент показывает позиции и остатки, время последнего успешного снимка и предупреждение без действия ручного обновления
|
||||
|
||||
#### Scenario: Недоступный счёт на клиенте
|
||||
- **WHEN** клиент получает `404` для Portfolio
|
||||
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора
|
||||
|
||||
#### Scenario: Портфель отображается на мобильном устройстве
|
||||
- **WHEN** User открывает загруженный Portfolio на узком экране
|
||||
- **THEN** клиент сохраняет читаемое одноколоночное отображение агрегатов, позиций и Cash Balance
|
||||
|
||||
#### Scenario: Отдельные акции и облигации
|
||||
- **WHEN** Portfolio содержит позиции `STOCK` и `BOND`
|
||||
- **THEN** клиент отображает их в отдельных секциях «Акции» и «Облигации» без изменения порядка в каждой секции
|
||||
Reference in New Issue
Block a user