# 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 состоянием пустого портфеля