## MODIFIED Requirements ### 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` и nullable `isin`. Система 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: ISIN инструмента доступен - **WHEN** metadata инструмента позиции содержит непустой `isin` - **THEN** система возвращает его в поле `isin` позиции #### Scenario: ISIN инструмента отсутствует - **WHEN** у инструмента позиции нет ISIN либо metadata недоступна - **THEN** система возвращает позицию с `isin: null`, не скрывая финансовые значения #### Scenario: Денежные остатки в одной валюте - **WHEN** upstream возвращает несколько ненулевых доступных денежных остатков одной валюты - **THEN** система возвращает один Cash Balance с их точной суммой #### Scenario: Штатный пустой Portfolio - **WHEN** у доступного Brokerage Account нет ненулевых обычных позиций и доступных денежных остатков - **THEN** система отвечает `200` с пустыми `positions`, `cashBalances` и `totalsByCurrency` ### 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 оставаться доступными в отдельной общей секции. Position Card MUST отображаться как компактная плитка в адаптивном grid: одна колонка на узком экране и несколько колонок на широком. Плитка MUST показывать отображаемый идентификатор позиции, количество, `currentValue` как главную метрику, `absoluteReturn` с цветовой индикацией знака, `averagePurchasePrice`, долю позиции среди позиций той же валюты и `isin`. Отсутствующие метрики 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** клиент отображает их в отдельных секциях «Акции» и «Облигации» без изменения порядка в каждой секции #### Scenario: Карточка позиции отображает доступные метрики - **WHEN** позиция имеет значение текущей стоимости, абсолютного дохода, средней цены покупки и ISIN - **THEN** клиент показывает идентификатор, количество, текущую стоимость, абсолютный доход, среднюю цену покупки, долю среди позиций той же валюты и ISIN #### Scenario: Отсутствующие метрики скрыты - **WHEN** у позиции отсутствует значение текущей стоимости, средней цены покупки или дохода - **THEN** клиент скрывает соответствующую метрику вместо отображения заглушки #### Scenario: Доход окрашен по знаку - **WHEN** у позиции абсолютный доход положительный или отрицательный - **THEN** клиент отображает его зелёным для положительного и красным для отрицательного, без цвета при его отсутствии #### Scenario: Доля позиции в валюте - **WHEN** позиция имеет вычислимую текущую стоимость в валюте - **THEN** клиент отображает её долю среди всех позиций той же валюты, не включая Cash Balance