375 lines
43 KiB
Markdown
375 lines
43 KiB
Markdown
# portfolio-view Specification
|
||
|
||
## Purpose
|
||
|
||
Позволяет авторизованному User просматривать нормализованный текущий состав и агрегаты Portfolio одного доступного Brokerage Account без валютной конвертации.
|
||
|
||
## Requirements
|
||
|
||
### Requirement: Получение Portfolio выбранного Brokerage Account
|
||
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts/:accountId/portfolio`. Она MUST возвращать нормализованный полный Portfolio Snapshot только для открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Ответ MUST иметь envelope `{ data, meta }`: `data` MUST содержать сохранённый полный Portfolio Snapshot с `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`; `meta` MUST содержать наблюдаемое состояние resource по `tbank-data-sync`, включая freshness, полноту, время последнего успешного Snapshot, последнюю часовую попытку и доступность. Обычный GET MUST читать сохранённый Snapshot и его наблюдаемое состояние без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio.
|
||
|
||
Portfolio Snapshot является результатом наблюдения Portfolio на конкретный момент. Он MUST NOT означать, что WealthLedger хранит Portfolio как долговременный агрегат или подтверждает доступность Brokerage Account вне текущего T-Bank connection.
|
||
|
||
#### Scenario: Получение текущего Portfolio
|
||
- **WHEN** User запрашивает Portfolio доступного Brokerage Account, а полный Snapshot текущего подключения успешно сохранён
|
||
- **THEN** система отвечает `200` с полным нормализованным Portfolio Snapshot в `data`, его `lastSuccessfulAt` и `isStale`, а также наблюдаемым состоянием в `meta`
|
||
|
||
#### Scenario: Недоступный Brokerage Account
|
||
- **WHEN** `accountId` не принадлежит User, закрыт, неподдерживаем или больше не доступен через текущий T-Bank connection
|
||
- **THEN** система отвечает `404` без Portfolio Snapshot
|
||
|
||
### Requirement: Нормализованный состав Portfolio
|
||
Система MUST возвращать только обычные Portfolio Position с ненулевым количеством и доступные Cash Balance с ненулевой суммой. Cash Balance MUST быть отдельным от Portfolio Position элементом и агрегироваться до одного значения на валюту. Portfolio Position с типом `CURRENCY` MUST означать позицию по торгуемому валютному Instrument и MUST NOT смешиваться с Cash Balance. Порядок 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`, nullable `isin`, nullable `ticker` и nullable `logoName` — имя файла логотипа бренда инструмента. Имя файла логотипа MUST заполняться из metadata инструмента, когда upstream предоставляет бренд с непустым именем файла логотипа; во всех остальных случаях оно MUST быть `null`. Логотип является presentation metadata: он не влияет на количество, стоимость, агрегаты, успешность или свежесть Portfolio Snapshot. Система MUST искать metadata по instrument UID при его наличии, затем по FIGI. Отображаемый идентификатор MUST предпочитать name, затем ticker, а при недоступности metadata использовать FIGI, не скрывая финансовые значения.
|
||
|
||
#### Scenario: Позиция с неизвестным upstream-типом
|
||
- **WHEN** upstream возвращает обычную ненулевую позицию с нераспознанным типом инструмента
|
||
- **THEN** система возвращает её в исходном порядке с типом `OTHER`
|
||
|
||
#### Scenario: Позиция валютного инструмента не является денежным остатком
|
||
- **WHEN** upstream возвращает ненулевую Portfolio Position валютного инструмента и ненулевой Cash Balance той же валюты
|
||
- **THEN** система возвращает Portfolio Position с типом `CURRENCY` отдельно от Cash Balance и не объединяет их между собой
|
||
|
||
#### 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: ISIN инструмента доступен
|
||
- **WHEN** metadata инструмента позиции содержит непустой `isin`
|
||
- **THEN** система возвращает его в поле `isin` позиции
|
||
|
||
#### Scenario: ISIN инструмента отсутствует
|
||
- **WHEN** у инструмента позиции нет ISIN либо metadata недоступна
|
||
- **THEN** система возвращает позицию с `isin: null`, не скрывая финансовые значения
|
||
|
||
#### Scenario: Тикер инструмента доступен
|
||
- **WHEN** metadata инструмента позиции содержит непустой `ticker`
|
||
- **THEN** система возвращает его в поле `ticker` позиции
|
||
|
||
#### Scenario: Тикер инструмента отсутствует
|
||
- **WHEN** у инструмента позиции нет ticker либо metadata недоступна
|
||
- **THEN** система возвращает позицию с `ticker: null`, не скрывая финансовые значения
|
||
|
||
#### Scenario: Имя файла логотипа бренда доступно
|
||
- **WHEN** metadata инструмента позиции содержит бренд с непустым именем файла логотипа
|
||
- **THEN** система возвращает его в поле `logoName` позиции
|
||
|
||
#### Scenario: Имя файла логотипа бренда отсутствует
|
||
- **WHEN** у инструмента позиции нет бренда, имя файла логотипа пустое либо metadata недоступна
|
||
- **THEN** система возвращает позицию с `logoName: null`, не скрывая финансовые значения
|
||
|
||
#### Scenario: Денежные остатки в одной валюте
|
||
- **WHEN** upstream возвращает несколько ненулевых доступных денежных остатков одной валюты
|
||
- **THEN** система возвращает один Cash Balance с их точной суммой
|
||
|
||
#### Scenario: Штатный пустой Portfolio
|
||
- **WHEN** у доступного Brokerage Account нет ненулевых обычных позиций и доступных денежных остатков
|
||
- **THEN** система отвечает `200` с пустыми `positions`, `cashBalances` и `totalsByCurrency`
|
||
|
||
### Requirement: Точные значения и валютные агрегаты
|
||
Все количества и финансовые значения в Portfolio Snapshot MUST передаваться десятичными строками без экспоненциальной записи и без незначащих завершающих нулей. Денежное значение MUST быть единым Money-значением с 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. Обычный GET MUST возвращать `{ data, meta }`, где `data` является сохранённым Snapshot, а `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`, читать его без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio, даже если Snapshot отсутствует или старше 60 минут; обновлением владеет часовой обход. Portfolio source MUST считаться свежим в течение 60 минут после успешного получения. Ошибка обхода MUST сохранять прежний полный Snapshot и его время без лимита fallback. Если состав получен полностью, но отдельная позиция не имеет цены, система MUST сохранить новый состав с неполной оценкой; metadata MUST NOT определять успешность или свежесть финансового Snapshot. Сохранённая доступная identity позиции MAY оставаться доступной при недоступности online-справочника.
|
||
|
||
Система MUST делать прежние личные datasets недоступными после успешной замены или удаления подключения. Поздняя работа прежнего подключения MUST NOT публиковаться в новом. Самостоятельные справочные сведения и результаты lookup MUST запрашиваться онлайн, а не сохраняться как долговременный справочник; ошибка lookup не подтверждает отсутствие инструмента.
|
||
|
||
#### Scenario: Свежий Snapshot
|
||
- **WHEN** User повторно запрашивает тот же Portfolio в течение 60 минут после успешного получения
|
||
- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank
|
||
|
||
#### Scenario: Медленный lookup инструмента
|
||
- **WHEN** сведения об инструменте ещё запрашиваются онлайн
|
||
- **THEN** система не задерживает локальный ответ Portfolio ради lookup и показывает сохранённую доступную identity
|
||
|
||
#### Scenario: Fallback после временной upstream-ошибки
|
||
- **WHEN** часовой обход Portfolio неуспешен из-за временной недоступности 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`
|
||
|
||
#### Scenario: Неполная оценка при новом составе
|
||
- **WHEN** успешно получен полный состав позиций, но у одной позиции нет вычислимой цены
|
||
- **THEN** новый состав становится текущим Portfolio Snapshot с неизвестной стоимостью позиции без подстановки нуля
|
||
|
||
### Requirement: Состояния и ошибки Portfolio на клиенте
|
||
Клиент SHALL загружать Portfolio при открытии защищённого маршрута `/accounts/:accountId`. Он MUST использовать успешный ответ в течение 30 секунд без automatic retry или refetch при возврате фокуса. Открытый экран MUST перечитывать сохранённый результат backend каждые 30 минут без запуска синхронизации T-Bank. Клиент MUST показывать freshness, полноту, время последнего успешного Snapshot, последнюю часовую попытку и доступность из `meta` текстом или доступно именованным элементом, а не только цветом. Клиент MUST NOT предоставлять действие ручной синхронизации Portfolio. При отсутствии ранее успешного ответа он MUST показывать структурный skeleton с доступным обозначением занятости без вымышленных сумм; skeleton MUST NOT быть интерактивным или получать клавиатурный фокус.
|
||
|
||
Истечение срока свежести MUST NOT скрывать Client Cached Snapshot. Во время фонового перечитывания и после его ошибки клиент MUST сохранять ранее полученные данные того же Portfolio, исходное `lastSuccessfulAt` и показывать неблокирующую индикацию либо предупреждение с действием повтора только HTTP-чтения. Сохранённые данные MUST NOT считаться новым успешным снимком или подтверждением текущей доступности Brokerage Account. При `isStale: true` клиент MUST показывать Snapshot, исходное время и нейтральное предупреждение без действия ручного обновления. Server-side stale и ошибка Client Cached Snapshot MUST оставаться раздельными состояниями.
|
||
|
||
При отсутствии Client Cached Snapshot и ответе `404` клиент MUST показать недоступный счёт с возвратом к списку без действия повтора. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` либо иной ошибке клиент MUST показать безопасное error-состояние; повтор HTTP-чтения разрешён только для временной и неизвестной ошибки.
|
||
|
||
#### Scenario: Показ устаревшего Portfolio
|
||
- **WHEN** клиент получает успешный ответ с `isStale: true`
|
||
- **THEN** клиент показывает Portfolio, исходное время и нейтральное предупреждение без ручной синхронизации
|
||
|
||
#### Scenario: Первичная загрузка Portfolio
|
||
- **WHEN** клиент ожидает Portfolio без ранее полученного успешного ответа
|
||
- **THEN** он показывает структурный skeleton без вымышленных или нулевых сумм и без фокусируемых placeholders
|
||
|
||
#### Scenario: Неуспешная последняя попытка сохраняет Portfolio
|
||
- **WHEN** клиент получает полный Portfolio и `meta` с неуспехом последней часовой попытки
|
||
- **THEN** он сохраняет Portfolio и исходное время видимыми и отдельно показывает текстовое либо доступно именованное предупреждение о неуспехе попытки
|
||
|
||
#### Scenario: Недоступный счёт на клиенте
|
||
- **WHEN** для текущего Portfolio отсутствует Client Cached Snapshot и клиент получает `404`
|
||
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора
|
||
|
||
#### Scenario: Обновление после истечения срока свежести
|
||
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое HTTP-перечитывание
|
||
- **THEN** клиент сохраняет прежние валютные итоги и время, показывает неблокирующую индикацию и не заменяет данные skeleton-состоянием
|
||
|
||
#### Scenario: Ошибка обновления после успешного ответа
|
||
- **WHEN** Client Cached Snapshot Portfolio уже видим, а повторное HTTP-чтение завершается ошибкой
|
||
- **THEN** клиент сохраняет прежние данные и время, показывает неблокирующее предупреждение и действие повтора чтения
|
||
|
||
#### Scenario: Ошибка клиентского обновления при server-side stale
|
||
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое перечитывание завершается ошибкой
|
||
- **THEN** оба предупреждения остаются различимыми, а действие повтора относится только к HTTP-чтению
|
||
|
||
#### Scenario: Повторное обновление успешно
|
||
- **WHEN** после ошибки HTTP-чтения User запускает повторное чтение и получает успешный Portfolio Snapshot
|
||
- **THEN** клиент показывает значения и `lastSuccessfulAt` успешного ответа, убирает предупреждение об ошибке чтения и отображает server-side предупреждение только по новому `isStale`
|
||
|
||
#### Scenario: Штатный пустой Portfolio на клиенте
|
||
- **WHEN** успешно полученный Portfolio содержит пустые `positions` и `cashBalances`
|
||
- **THEN** клиент показывает empty-состояние без валютных карточек с вымышленными нулевыми итогами
|
||
|
||
### Requirement: Представление карточек позиций на клиенте
|
||
Клиент MUST отображать `STOCK` и `BOND` отдельными секциями. Внутри каждой секции MUST сохраняться upstream-порядок позиций. Позиции иных типов MUST оставаться доступными в отдельной общей секции.
|
||
|
||
Карточки секций `STOCK` и `BOND` MUST отображаться по одной в ряду при любой ширине экрана. Карточка акции или облигации MUST показывать визуальный логотип бренда инструмента рядом с отображаемым идентификатором позиции; при отсутствии имени файла логотипа либо неудачной загрузке изображения вместо логотипа MUST отображаться первая буква отображаемого идентификатора позиции. Абсолютный доход в карточке акции или облигации MUST отображаться значением с явным знаком (`+` для положительного, `-` для отрицательного, без знака при нуле) с цветовой индикацией знака и без текстовой подписи доходности.
|
||
|
||
Карточка `STOCK` MUST показывать только `ticker` под отображаемым идентификатором позиции, если тикер доступен; при `ticker: null` она MUST сохранять текущее отображение `isin` как fallback. Карточки `BOND` и общей секции иных типов MUST сохранять отображение `isin`. Карточки общей секции иных типов MUST сохранять прежнее поведение: компактная плитка в адаптивном grid (одна колонка на узком экране, несколько колонок на широком), абсолютный доход с текстовой подписью доходности и цветовой индикацией знака, без визуального логотипа. Плитки всех секций MUST показывать отображаемый идентификатор позиции, количество, `currentValue` как главную метрику, `absoluteReturn`, `averagePurchasePrice` и долю позиции среди позиций той же валюты. Отсутствующие метрики MUST быть скрыты, а не отображаться заглушкой. При форматировании денежных значений в русской локали клиент MUST отображать RUB как символ `₽`; другие валюты MUST сохранять понятное обозначение.
|
||
|
||
#### Scenario: Отдельные акции и облигации
|
||
- **WHEN** Portfolio содержит позиции `STOCK` и `BOND`
|
||
- **THEN** клиент отображает их в отдельных секциях «Акции» и «Облигации» без изменения порядка в каждой секции
|
||
|
||
#### Scenario: Портфель отображается на мобильном устройстве
|
||
- **WHEN** User открывает загруженный Portfolio на узком экране
|
||
- **THEN** клиент сохраняет читаемое одноколоночное отображение агрегатов, позиций и Cash Balance
|
||
|
||
#### Scenario: Карточка акции с доступным тикером
|
||
- **WHEN** позиция `STOCK` имеет непустой `ticker` и ISIN
|
||
- **THEN** клиент показывает под отображаемым идентификатором только тикер и не показывает ISIN
|
||
|
||
#### Scenario: Карточка акции без тикера
|
||
- **WHEN** позиция `STOCK` имеет `ticker: null` и непустой ISIN
|
||
- **THEN** клиент показывает ISIN под отображаемым идентификатором
|
||
|
||
#### Scenario: Карточка облигации сохраняет ISIN
|
||
- **WHEN** позиция `BOND` имеет доступные ticker и ISIN
|
||
- **THEN** клиент показывает ISIN и не заменяет его тикером
|
||
|
||
#### Scenario: Карточка позиции отображает доступные метрики
|
||
- **WHEN** позиция, не относящаяся к `STOCK`, имеет значение текущей стоимости, абсолютного дохода, средней цены покупки и ISIN
|
||
- **THEN** клиент показывает идентификатор, количество, текущую стоимость, абсолютный доход, среднюю цену покупки, долю позиции среди позиций той же валюты и ISIN
|
||
|
||
#### Scenario: Отсутствующие метрики скрыты
|
||
- **WHEN** у позиции отсутствует значение текущей стоимости, средней цены покупки или дохода
|
||
- **THEN** клиент скрывает соответствующую метрику вместо отображения заглушки
|
||
|
||
#### Scenario: Доход окрашен по знаку
|
||
- **WHEN** у позиции абсолютный доход положительный или отрицательный
|
||
- **THEN** клиент отображает его зелёным для положительного и красным для отрицательного, без цвета при его отсутствии
|
||
|
||
#### Scenario: Доля позиции в валюте
|
||
- **WHEN** позиция имеет вычислимую текущую стоимость в валюте
|
||
- **THEN** клиент отображает её долю среди всех позиций той же валюты, не включая Cash Balance
|
||
|
||
#### Scenario: Одноколоночные секции акций и облигаций
|
||
- **WHEN** User открывает загруженный Portfolio с позициями `STOCK` или `BOND` на широком экране
|
||
- **THEN** клиент отображает карточки соответствующей секции по одной в ряду
|
||
|
||
#### Scenario: Логотип акции или облигации отображается
|
||
- **WHEN** позиция `STOCK` или `BOND` содержит непустое имя файла логотипа бренда и изображение успешно загружается
|
||
- **THEN** клиент показывает изображение логотипа рядом с отображаемым идентификатором позиции
|
||
|
||
#### Scenario: Фолбэк без логотипа у акции или облигации
|
||
- **WHEN** у позиции `STOCK` или `BOND` имя файла логотипа отсутствует либо изображение не загружается
|
||
- **THEN** вместо логотипа клиент показывает первую букву отображаемого идентификатора позиции
|
||
|
||
#### Scenario: Доходность акции или облигации со знаком без подписи
|
||
- **WHEN** карточка акции или облигации показывает абсолютный доход
|
||
- **THEN** клиент отображает значение с явным знаком `+` или `-` (без знака при нуле) и без текстовой подписи доходности
|
||
|
||
#### Scenario: Секция облигаций в едином стиле с акциями
|
||
- **WHEN** User открывает загруженный Portfolio с позициями `BOND`
|
||
- **THEN** клиент отображает облигации по одной карточке в ряду с визуальным логотипом бренда либо фолбэком первой буквы и абсолютным доходом с явным знаком без текстовой подписи
|
||
|
||
#### Scenario: Секция иных типов сохраняет прежний вид
|
||
- **WHEN** User открывает загруженный Portfolio с позициями иных типов (не `STOCK` и не `BOND`)
|
||
- **THEN** клиент отображает их компактными плитками в адаптивном grid с текстовой подписью доходности и без визуального логотипа
|
||
|
||
### Requirement: Компактная сводка валютных итогов Portfolio
|
||
|
||
На странице Brokerage Account клиент SHALL показывать каждый полученный
|
||
валютный итог Portfolio отдельной компактной карточкой. При достаточной
|
||
ширине карточки валютная метка и сумма MUST располагаться в одной строке;
|
||
сумма MUST оставаться основной читаемой величиной. Обычная сводка с одной
|
||
или тремя валютами MUST занимать меньше высоты, чем прежнее представление
|
||
с меткой над суммой при тех же значениях и ширине экрана.
|
||
|
||
На широком экране несколько карточек SHALL располагаться рядом при наличии
|
||
места; на узком экране MUST сохраняться одна карточка в ряду. Если полное
|
||
значение не помещается рядом с меткой, клиент MUST предоставлять ему полную
|
||
доступную ширину карточки и переносить содержимое с увеличением высоты.
|
||
Сводка MUST NOT обрезать суммы, заменять их многоточием или требовать
|
||
горизонтальной прокрутки для чтения карточек и значений.
|
||
|
||
#### Scenario: Обычная сводка на широком экране
|
||
|
||
- **WHEN** User открывает Portfolio с итогами `125000.50` RUB, `320.00` USD
|
||
и `9876.54321` EUR на широком экране с местом для трёх карточек
|
||
- **THEN** клиент показывает три отдельные карточки рядом, метку и полную
|
||
сумму в одной строке каждой карточки, с меньшей высотой сводки, чем при
|
||
прежнем размещении метки над суммой в тех же условиях
|
||
|
||
#### Scenario: Обычная сводка на узком экране
|
||
|
||
- **WHEN** User открывает тот же Portfolio с тремя валютными итогами
|
||
на узком экране
|
||
- **THEN** клиент показывает три карточки по одной в ряду, сохраняя полные
|
||
читаемые суммы и меньшую общую высоту сводки, чем при прежнем размещении
|
||
метки над суммой в тех же условиях
|
||
|
||
#### Scenario: Portfolio с одной валютой
|
||
|
||
- **WHEN** Portfolio содержит один валютный итог
|
||
- **THEN** клиент показывает одну компактную карточку без пустых карточек
|
||
для других валют; обычная сумма и метка помещаются в одну строку при
|
||
достаточной ширине на широком и узком экране
|
||
|
||
#### Scenario: Длинная сумма на узком экране
|
||
|
||
- **WHEN** User просматривает итог `123456789012.345678901` RUB при ширине
|
||
экрана 320 пикселей и значение не помещается рядом с валютной меткой
|
||
- **THEN** сумма получает полную доступную ширину карточки и при необходимости
|
||
переносится; все цифры и знак валюты читаемы без обрезания, многоточия
|
||
и горизонтальной прокрутки
|
||
|
||
### Requirement: Обозначение и доступная идентичность валюты в сводке
|
||
|
||
В валютных метках и суффиксах сумм сводки клиент MUST обозначать RUB знаком
|
||
`₽`, USD знаком `$`, EUR знаком `€` вместо видимых ISO-кодов этих валют.
|
||
Каждая сумма MUST оставаться однозначно связанной со своей исходной валютой,
|
||
включая чтение вспомогательными технологиями. Для RUB, USD и EUR MUST быть
|
||
доступны названия «Российский рубль», «Доллар США» и «Евро» соответственно.
|
||
Символ или цвет сами по себе MUST NOT быть единственным доступным способом
|
||
определения валюты.
|
||
|
||
Другие полученные валюты MUST оставаться в сводке с понятным обозначением
|
||
и доступной идентичностью. Если знак отсутствует или неоднозначен, клиент
|
||
MUST сохранять возможность различить исходную валюту по её названию или
|
||
ISO-коду, в том числе визуально; неизвестное полное название MUST NOT
|
||
подменяться вымышленным. Совпадающие знаки разных валют MUST NOT приводить
|
||
к объединению карточек или неразличимому представлению их валют.
|
||
|
||
#### Scenario: Рубль, доллар США и евро показаны знаками
|
||
|
||
- **WHEN** сводка содержит RUB, USD и EUR
|
||
- **THEN** метки показывают `₽`, `$` и `€`, а суммы заканчиваются теми же
|
||
знаками; видимые RUB, USD и EUR не подменяют выбранные знаки
|
||
|
||
#### Scenario: Валютная сумма читается вспомогательной технологией
|
||
|
||
- **WHEN** User читает валютный итог сводки вспомогательной технологией
|
||
- **THEN** ему доступны полное значение и однозначная валютная идентичность,
|
||
включая «Российский рубль», «Доллар США» или «Евро» для соответствующих валют
|
||
|
||
#### Scenario: Получена другая валюта
|
||
|
||
- **WHEN** сводка содержит валюту помимо RUB, USD и EUR
|
||
- **THEN** её отдельный итог остаётся доступным с понятным обозначением
|
||
и доступным названием либо исходным ISO-кодом при неизвестном названии
|
||
|
||
#### Scenario: У разных валют совпадает знак
|
||
|
||
- **WHEN** Portfolio содержит итоги двух исходных валют с одинаковым знаком
|
||
- **THEN** сводка показывает два отдельных итога, и User может различить
|
||
их валюты визуально и вспомогательной технологией без догадки по сумме
|
||
|
||
### Requirement: Полные точные значения и смысл валютной сводки
|
||
|
||
Сводка MUST показывать полученные валютные итоги без изменения их знака,
|
||
цифр и дробной точности. Для русской локали клиент SHALL сохранять группировку
|
||
разрядов пробелами и десятичную запятую, не округляя значение, не отбрасывая
|
||
дробные позиции и не сокращая сумму до тысяч, миллионов или экспоненциальной
|
||
записи. Требование MUST сохраняться для длинных, отрицательных значений
|
||
и отрицательных дробей с нулевой целой частью.
|
||
|
||
Каждая карточка MUST представлять только полученный итог позиций и Cash
|
||
Balance своей исходной валюты. Сводка MUST NOT конвертировать или складывать
|
||
разные валюты, показывать рублёвый итог как общую стоимость Portfolio,
|
||
повторно добавлять Cash Balance либо обещать полную оценку при наличии
|
||
позиций без вычислимой стоимости. Для отсутствующего валютного итога клиент
|
||
MUST NOT создавать нулевую карточку. Отсутствие итогов само по себе MUST NOT
|
||
обозначать пустой Portfolio, если позиции или Cash Balance присутствуют.
|
||
|
||
#### Scenario: Обычные суммы сохраняют дробные позиции
|
||
|
||
- **WHEN** в сводку поступают значения `125000.50` RUB, `320.00` USD
|
||
и `9876.54321` EUR
|
||
- **THEN** клиент показывает `125 000,50 ₽`, `320,00 $` и `9 876,54321 €`
|
||
с сохранением каждой исходной цифры и дробной позиции
|
||
|
||
#### Scenario: Длинные и отрицательные значения не теряют точность
|
||
|
||
- **WHEN** в сводку поступают `123456789012.345678901` RUB,
|
||
`-1234567.890123` USD и `987654321.000001` EUR
|
||
- **THEN** пользователь видит полные `123 456 789 012,345678901 ₽`,
|
||
`-1 234 567,890123 $` и `987 654 321,000001 €` без округления,
|
||
сокращений и потери отрицательного знака на широком и узком экране
|
||
|
||
#### Scenario: Отрицательная сумма меньше единицы
|
||
|
||
- **WHEN** полученный валютный итог равен `-0.5` USD
|
||
- **THEN** сводка показывает `-0,5 $`, сохраняя знак и дробную величину
|
||
|
||
#### Scenario: Несколько валют и денежные остатки
|
||
|
||
- **WHEN** полученные итоги RUB и USD уже включают позиции и Cash Balance
|
||
соответствующих валют
|
||
- **THEN** клиент показывает эти два итога без повторного добавления денег,
|
||
межвалютного итога или представления RUB как общей стоимости Portfolio
|
||
|
||
#### Scenario: Стоимость части позиций неизвестна
|
||
|
||
- **WHEN** Portfolio содержит позиции без вычислимой стоимости и доступные
|
||
валютные итоги, сформированные без этих позиций
|
||
- **THEN** сводка показывает только полученные итоги, не добавляет нули
|
||
за отсутствующие стоимости и не называет результат полной оценкой Portfolio
|
||
|
||
#### Scenario: Итоги отсутствуют при непустом Portfolio
|
||
|
||
- **WHEN** `totalsByCurrency` пуст, но `positions` или `cashBalances` непусты
|
||
- **THEN** клиент не создаёт валютные карточки и не заменяет доступный
|
||
состав Portfolio состоянием пустого портфеля
|