Files
specs/openspec/specs/portfolio/portfolio-view/spec.md
T
codex-bot b6a9ff2d10
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
spec: require RUB-only behavior (WealthLedger/main#55) (#86)
2026-11-11 09:20:25 +03:00

53 KiB
Raw Blame History

portfolio-view Specification

Purpose

Позволяет авторизованному User просматривать нормализованный текущий состав и агрегаты Portfolio одного доступного Brokerage Account в рублях (RUB).

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, итоговую сумму Portfolio в RUB, positions и Cash Balance; 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 элементом и агрегироваться в один остаток в RUB. Portfolio Position с типом CURRENCY MUST сохранять исходное количество и идентификаторы для контракта, но отражает тот же кэш, что и Cash Balance, и MUST NOT повторно учитываться в итоге и структуре. Позиции, денежные остатки и данные инструментов в валютах, отличных от RUB, MUST отфильтровываться при получении из T-Bank и MUST NOT входить в Portfolio, его итог, структуру или любые другие агрегаты. Клиент MUST показывать этот кэш один раз как Cash Balance в разделе «Деньги», без отдельной карточки CURRENCY. Порядок 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 — имя файла логотипа бренда инструмента, nullable sector — сектор инструмента и nullable issuer — эмитент позиции. Имя файла логотипа MUST заполняться из metadata инструмента, когда upstream предоставляет бренд с непустым именем файла логотипа; во всех остальных случаях оно MUST быть null. Логотип является presentation metadata: он не влияет на количество, стоимость, агрегаты, успешность или свежесть Portfolio Snapshot. Поле sector позиции типа STOCK, BOND или FUND MUST содержать значение сектора из metadata инструмента в том виде, как его определяет T-Bank, без перекодирования и собственной классификации, с обрезанными пробелами в начале и конце; значение из одних пробелов считается отсутствующим. Во всех остальных случаях, включая остальные типы позиций, оно MUST быть null. Поле issuer позиции типа STOCK, BOND или FUND, включая фонд, MUST быть объектом с непустыми строками id — идентификатор бренда актива инструмента — и name — название этого бренда (не название компании), когда T-Bank предоставляет для инструмента бренд актива с идентификатором и названием, непустыми после обрезки пробелов в начале и конце; значение из одних пробелов считается отсутствующим. Во всех остальных случаях, включая остальные типы позиций, оно MUST быть null. Система MUST NOT определять эмитента по совпадению названия, тикера или ISIN. Сектор и эмитент не влияют на количество, стоимость, агрегаты, успешность или свежесть Portfolio Snapshot: их отсутствие либо недоступность MUST NOT делать Snapshot неуспешным или устаревшим и MUST NOT скрывать позицию или её финансовые значения, кроме отказа token по требованию «Свежесть и fallback полного 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: Несколько денежных остатков в RUB

  • WHEN upstream возвращает несколько ненулевых доступных денежных остатков в RUB
  • THEN система возвращает один Cash Balance с их точной суммой

Scenario: Нерублёвые данные не входят в Portfolio

  • WHEN upstream возвращает позиции, денежные остатки или инструменты в валюте, отличной от RUB
  • THEN система отфильтровывает их при получении и не возвращает их в positions, Cash Balance, итоге, структуре или агрегатах

Scenario: Штатный пустой Portfolio

  • WHEN у доступного Brokerage Account нет ненулевых обычных позиций и доступных денежных остатков
  • THEN система отвечает 200 с пустыми positions и без Cash Balance и итоговой суммы

Scenario: Сектор инструмента доступен

  • WHEN metadata инструмента позиции содержит сектор energy
  • THEN система возвращает в поле sector позиции значение energy без изменений

Scenario: Сектор инструмента отсутствует

  • WHEN у инструмента позиции нет сектора, сектор пустой либо metadata недоступна
  • THEN система возвращает позицию с sector: null, не скрывая финансовые значения

Scenario: Эмитент позиции доступен

  • WHEN T-Bank предоставляет для инструмента позиции бренд актива с идентификатором brand-1 и названием «Сбер»
  • THEN система возвращает в поле issuer позиции идентификатор brand-1 и название «Сбер»

Scenario: Название эмитента — название бренда

  • WHEN T-Bank предоставляет для инструмента бренд актива с названием «Сбер» и компанией «ПАО Сбербанк»
  • THEN система возвращает в issuer.name «Сбер»

Scenario: Эмитент у фонда

  • WHEN T-Bank предоставляет для инструмента позиции FUND бренд актива с идентификатором brand-2 и названием «Фонд»
  • THEN система возвращает в issuer позиции идентификатор brand-2 и название «Фонд»

Scenario: Пробелы по краям обрезаются

  • WHEN T-Bank возвращает сектор " energy " и название бренда " Сбер "
  • THEN система возвращает sector energy и issuer.name «Сбер»

Scenario: Значение из одних пробелов

  • WHEN T-Bank возвращает сектор " " либо название бренда " "
  • THEN система возвращает sector: null либо issuer: null соответственно

Scenario: Остальные типы позиций

  • WHEN T-Bank предоставляет сектор и бренд актива для инструмента позиции типа FUTURE
  • THEN система возвращает у этой позиции sector: null и issuer: null

Scenario: Эмитент позиции отсутствует

  • WHEN у инструмента позиции нет бренда актива, идентификатор либо название бренда пустые или данные бренда недоступны
  • THEN система возвращает позицию с issuer: null, не скрывая финансовые значения

Scenario: Эмитент не определяется по названию

  • WHEN две позиции имеют одинаковое название, но разные идентификаторы бренда актива
  • THEN система возвращает в issuer позиций разные идентификаторы бренда и не объединяет их

Scenario: Недоступность сектора и эмитента не нарушает Snapshot

  • GIVEN сектор и бренд актива позиции ещё ни разу не получены
  • WHEN получение сектора и бренда актива завершается ошибкой, а количества и цены позиции получены
  • THEN система сохраняет Snapshot с sector: null и issuer: null у затронутых позиций; их currentValue, итоговая сумма, lastSuccessfulAt и isStale такие же, как при доступных сектора и бренде

Requirement: Точные значения и итоговая сумма в RUB

Все количества и финансовые значения в Portfolio Snapshot MUST передаваться десятичными строками без экспоненциальной записи и без незначащих завершающих нулей. Денежное значение MUST быть Money-значением с ISO-кодом валюты RUB и десятичной строкой значения. Система MUST корректно нормализовать отрицательные целую и дробную части upstream-значений. Отсутствующее значение MUST передаваться как null, а не как ноль.

currentValue Portfolio Position MUST быть точным произведением количества на текущую цену при наличии валидных множителей. absoluteReturn MUST быть точной величиной (currentPrice - averagePurchasePrice) * quantity только когда обе цены известны. Позиция без вычислимой текущей стоимости MUST оставаться в ответе, но MUST NOT входить в агрегаты. Итоговая сумма Portfolio MUST быть точной суммой currentValue позиций, кроме CURRENCY, и Cash Balance в RUB. Структура Portfolio MUST учитывать CURRENCY только как CASH из Cash Balance, без второго вклада.

Scenario: Валютная позиция отражает денежный остаток

  • WHEN Portfolio содержит CURRENCY position стоимостью 100 RUB и Cash Balance 100 RUB
  • THEN система сохраняет CURRENCY position с количеством в positions, но возвращает итог 100 RUB и один вклад CASH 100 RUB в структуре; клиент показывает 100 RUB в «Деньги» без отдельной валютной карточки

Scenario: Позиция без вычислимой стоимости

  • WHEN у ненулевой позиции отсутствует либо невалидна цена или количество
  • THEN система возвращает позицию с currentValue: null и не включает её в итоговую сумму

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, кроме отказа token при получении сектора или бренда актива позиции: он MUST обрабатываться так же, как отказ token при обходе Portfolio (исключение, источник — #48). Сохранённая доступная identity позиции MAY оставаться доступной при недоступности online-справочника.

Система MUST делать прежние личные datasets недоступными после успешной замены или удаления подключения. Поздняя работа прежнего подключения MUST NOT публиковаться в новом. Самостоятельные справочные сведения и результаты lookup MUST запрашиваться онлайн, а не сохраняться как долговременный справочник; ошибка lookup не подтверждает отсутствие инструмента. Исключение: сектор и бренд актива позиции MUST храниться как последнее известное значение и обновляться по требованию «Обновление сектора и эмитента» (#48).

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 с неизвестной стоимостью позиции без подстановки нуля

Scenario: Сектор и бренд читаются из хранимого значения

  • GIVEN сектор позиции получен 10 часов назад
  • WHEN User запрашивает Portfolio
  • THEN система возвращает хранимый sector без обращения к T-Bank за сектором

Scenario: Отказ token при получении бренда

  • WHEN T-Bank отклоняет token как неаутентифицированный или без прав при получении сектора или бренда актива позиции
  • THEN система не возвращает fallback Snapshot и отвечает 400 Problem Details с code INVALID_TBANK_TOKEN, как при отказе token в обходе Portfolio

Requirement: Состояния и ошибки Portfolio на клиенте

Клиент SHALL загружать Portfolio при открытии защищённого маршрута /accounts/:accountId и использовать успешный ответ в течение 30 секунд. Временные сбои и возвращение к экрану с требующими обновления данными SHALL обрабатываться автоматическим восстановлением по frontend-data-resilience. Клиент MUST NOT предоставлять ручное обновление или повторную загрузку.

При первичной загрузке без успешного результата того же Brokerage Account клиент MUST показывать структурный skeleton ожидаемого содержимого, включая сводку, без вымышленных сумм. Область MUST иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным или получать фокус. Неуспешное или затянувшееся первичное получение SHALL переходить в локальную недоступность по frontend-loading-presentation, а не в бесконечную загрузку.

Истечение срока свежести MUST NOT скрывать доступный Client Cached Snapshot. При обновлении и временной ошибке клиент MUST сохранять данные того же Portfolio, включая итоговую сумму, без индикации фоновой работы собственного чтения, технических предупреждений об этом чтении и уведомлений. Исходное lastSuccessfulAt и isStale MUST сохранять внутренний смысл: их показ или скрытие MUST NOT делать сохранённый снимок новым успешным результатом или подтверждением текущего доступа. Server-side stale и ошибка клиентского чтения MUST NOT подменять друг друга.

Состояние источника из meta — время последнего успешного результата, признак устаревания и исход последней часовой попытки — не относится к собственному чтению клиента: клиент MUST представлять его текстом либо доступно именованным элементом по frontend-data-resilience, не превращая в действие пользователя.

Клиент MUST показывать штатное empty-состояние только при подтверждённом полном результате с пустыми positions и без Cash Balance, без вымышленных нулевых карточек. Неполный итог MUST NOT выдаваться за окончательный; доступные независимые данные MUST сохраняться на экране. При подтверждённом 404 клиент MUST скрыть личный Portfolio и предложить возврат к списку счетов. При TBANK_NOT_CONNECTED или INVALID_TBANK_TOKEN он MUST скрыть личные данные и предложить восстановление подключения. Эти состояния MUST NOT маскироваться сохранённым снимком или вызывать техническую повторную загрузку.

Scenario: Показ устаревшего Portfolio

  • WHEN клиент получает успешный ответ с isStale: true в действующей границе доступа
  • THEN клиент показывает позиции, остаток и доступную итоговую сумму и обозначает устаревание источника текстом либо доступно именованным элементом, без ручного обновления

Scenario: Неуспешная последняя попытка сохраняет Portfolio

  • WHEN клиент получает полный Portfolio и meta с неуспехом последней часовой попытки
  • THEN он сохраняет Portfolio и исходное время видимыми и отдельно показывает текстовое либо доступно именованное предупреждение о неуспехе попытки, не предлагая ручной повтор

Scenario: Недоступный счёт на клиенте

  • WHEN клиент получает подтверждённый 404 текущего Portfolio
  • THEN клиент не показывает личный Portfolio, включая ранее сохранённый, и предлагает вернуться к списку Brokerage Account без действия повтора

Scenario: Первичная загрузка Portfolio

  • WHEN клиент ожидает Portfolio этого Brokerage Account без ранее полученного успешного ответа
  • THEN он показывает структурный skeleton, включая место сводки, с доступным обозначением загрузки, без вымышленных или нулевых сумм и без фокусируемых placeholders

Scenario: Обновление после истечения срока свежести

  • WHEN срок свежести показанного Portfolio истёк и началось фоновое обновление данных того же Brokerage Account
  • THEN клиент сохраняет прежнюю итоговую сумму без индикации обновления и не заменяет доступный состав и сводку skeleton-состоянием

Scenario: Ошибка обновления после успешного ответа

  • WHEN клиент уже показывает Client Cached Snapshot Portfolio, а обновление этих же данных завершается временной ошибкой
  • THEN клиент сохраняет данные и суммы без предупреждения и действия повтора; ошибка не делает снимок свежим и не изменяет isStale

Scenario: Ошибка клиентского обновления при server-side stale

  • WHEN клиент показывает Portfolio с isStale: true, а фоновое обновление этого же Portfolio завершается временной ошибкой
  • THEN прежние суммы остаются видимыми без технических предупреждений и ручных действий, сохраняя различие внутренних состояний

Scenario: Повторное обновление успешно

  • WHEN после временной ошибки автоматическое восстановление получает успешный Portfolio Snapshot
  • THEN до ответа прежние суммы остаются видимыми, затем клиент показывает новый результат без уведомления об обновлении и без переноса пользовательского фокуса

Scenario: Штатный пустой Portfolio на клиенте

  • WHEN успешно полученный полный Portfolio содержит пустые positions и без Cash Balance
  • THEN клиент показывает существующее empty-состояние без вымышленной нулевой итоговой суммы

Scenario: Первичный Portfolio пока недоступен

  • WHEN данных Portfolio нет, а первичное получение не удалось или превысило ограниченное время ожидания
  • THEN клиент показывает нейтральную недоступность без кнопки повтора до исчерпания автоматических повторов и автоматически заменяет её содержимым после успешного восстановления

Scenario: Требуется восстановить подключение

  • WHEN клиент получает TBANK_NOT_CONNECTED или INVALID_TBANK_TOKEN при наличии сохранённого личного Portfolio
  • THEN личные данные скрываются, а клиент предлагает необходимое действие подключения без технической повторной загрузки

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 как символ ₽.

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 или CURRENCY, имеет значение текущей стоимости, абсолютного дохода, средней цены покупки и ISIN
  • THEN клиент показывает идентификатор, количество, текущую стоимость, абсолютный доход, среднюю цену покупки, долю позиции среди всех отображаемых позиций и ISIN

Scenario: Отсутствующие метрики скрыты

  • WHEN у позиции отсутствует значение текущей стоимости, средней цены покупки или дохода
  • THEN клиент скрывает соответствующую метрику вместо отображения заглушки

Scenario: Доход окрашен по знаку

  • WHEN у позиции абсолютный доход положительный или отрицательный
  • THEN клиент отображает его зелёным для положительного и красным для отрицательного, без цвета при его отсутствии

Scenario: Доля позиции среди отображаемых позиций

  • WHEN позиция имеет вычислимую текущую стоимость
  • THEN клиент отображает её долю среди всех отображаемых позиций, не включая Cash Balance и зеркальные CURRENCY positions

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 в RUB на странице счёта

На странице Brokerage Account клиент SHALL показывать итоговую сумму Portfolio в рублях отдельной компактной карточкой. Метка валюты и сумма MUST располагаться в одной строке при достаточной ширине карточки; сумма MUST оставаться основной читаемой величиной. Если полное значение не помещается рядом с меткой, клиент MUST предоставлять ему полную доступную ширину карточки и переносить содержимое с увеличением высоты. Сумма MUST NOT обрезаться, заменяться многоточием или требовать горизонтальной прокрутки.

Клиент MUST обозначать RUB знаком ₽ и MUST сохранять доступную идентичность валюты для вспомогательных технологий («Российский рубль»). Знак сам по себе MUST NOT быть единственным способом определения валюты.

Сумма MUST показываться без изменения знака, цифр и дробной точности. Для русской локали клиент SHALL сохранять группировку разрядов пробелами и десятичную запятую, не округляя значение, не отбрасывая дробные позиции и не сокращая сумму до тысяч, миллионов или экспоненциальной записи. Требование MUST сохраняться для длинных, отрицательных значений и отрицательных дробей с нулевой целой частью.

Итоговая сумма MUST NOT повторно добавлять Cash Balance, MUST NOT обещать полную оценку при наличии позиций без вычислимой стоимости и MUST NOT создавать нулевую карточку при отсутствии итоговой суммы. Отсутствие итоговой суммы само по себе MUST NOT обозначать пустой Portfolio, если позиции или Cash Balance присутствуют.

Scenario: Обычная сумма сохраняет дробные позиции

  • WHEN итоговая сумма Portfolio равна 125000.50 RUB
  • THEN клиент показывает 125 000,50 ₽ с сохранением каждой исходной цифры и дробной позиции

Scenario: Длинные и отрицательные значения не теряют точность

  • WHEN итоговая сумма равна 123456789012.345678901 RUB или -1234567.890123 RUB
  • THEN пользователь видит полные 123 456 789 012,345678901 ₽ или -1 234 567,890123 ₽ без округления, сокращений и потери знака на широком и узком экране

Scenario: Отрицательная сумма меньше единицы

  • WHEN итоговая сумма равна -0.5 RUB
  • THEN сводка показывает -0,5 ₽, сохраняя знак и дробную величину

Scenario: Длинная сумма на узком экране

  • WHEN User просматривает итог 123456789012.345678901 RUB при ширине экрана 320 пикселей и значение не помещается рядом с меткой валюты
  • THEN сумма получает полную доступную ширину карточки и при необходимости переносится; все цифры и знак валюты читаемы без обрезания, многоточия и горизонтальной прокрутки

Scenario: Итог включает позиции и Cash Balance без повторного добавления

  • WHEN итоговая сумма уже включает позиции и Cash Balance в RUB
  • THEN клиент показывает её одну, без повторного добавления денег

Scenario: Стоимость части позиций неизвестна

  • WHEN Portfolio содержит позиции без вычислимой стоимости
  • THEN сводка показывает полученную итоговую сумму, не добавляет нули за отсутствующие стоимости и не называет результат полной оценкой Portfolio

Scenario: Итог отсутствует при непустом Portfolio

  • WHEN итоговая сумма отсутствует, но positions или Cash Balance непусты
  • THEN клиент не создаёт итоговую карточку и не заменяет доступный состав Portfolio состоянием пустого портфеля

Requirement: Обновление сектора и эмитента

Система SHALL обновлять сектор и бренд актива позиции при первом обходе по hourly-investment-sync, начавшемся не ранее чем через 24 часа после последнего успешного получения; в остальных обходах значение не меняется. Пока значение не получено, получение повторяется при каждом обходе.

Источник (ненормативно): #48, #48

Scenario: Обход раньше 24 часов

  • GIVEN бренд позиции получен в понедельник в 09:00, а затем T-Bank сменил его название
  • WHEN обход начинается во вторник в 08:59, то есть через 23 ч 59 мин
  • THEN issuer.name остаётся прежним

Scenario: Обход через 24 часа

  • GIVEN бренд позиции получен в понедельник в 09:00, а затем T-Bank сменил его название
  • WHEN обход начинается во вторник в 09:00
  • THEN issuer.name новое

Requirement: Результат получения сектора и эмитента

Результат получения SHALL определять поле позиции и отсчёт 24 часов по таблице; совпадает ровно одна строка.

Исход получения Поле Отсчёт 24 часов
Получено значение Новое значение Начинается заново
Ответ «не найдено» или значения нет null Начинается заново
Ответ о другом инструменте или активе Прежнее значение или null Не меняется; получение неуспешно
Иная ошибка, кроме отказа token Прежнее значение или null Не меняется; получение неуспешно

Неуспешное получение SHALL NOT мешать обновлению остальных данных Portfolio; отказ token — по «Свежесть и fallback полного Portfolio Snapshot».

Источник (ненормативно): #48

Scenario: Ответ без значения заменяет известное значение

  • GIVEN у позиции известен sector energy, 24 часа прошли
  • WHEN T-Bank отвечает, что сектора у инструмента нет
  • THEN sector: null

Scenario: «Не найдено» перезапускает отсчёт

  • GIVEN у позиции известен sector energy, 24 часа прошли, и обход получает от T-Bank «не найдено»
  • WHEN следующий обход начинается через час, а T-Bank уже возвращает сектор financial
  • THEN sector остаётся null; значение financial появляется при первом обходе, начавшемся не ранее чем через 24 часа после ответа «не найдено»

Scenario: Ответ о другом инструменте

  • GIVEN у позиции известен sector energy, 24 часа прошли
  • WHEN T-Bank возвращает данные другого инструмента
  • THEN sector остаётся energy, а следующий обход повторяет получение

Scenario: Недоступность T-Bank сохраняет известное значение

  • GIVEN у позиции известен sector energy, 24 часа прошли
  • WHEN получение сектора завершается недоступностью T-Bank, а цены и количества получены
  • THEN позиция возвращается с sector energy, остальные данные Portfolio обновлены, lastSuccessfulAt новый

Scenario: Значение ещё не известно

  • GIVEN новая позиция, сектор которой ни разу не получен успешно
  • WHEN получение сектора неуспешно при одном обходе и успешно при следующем
  • THEN после первого обхода sector: null, после следующего значение сектора