From 70a200e1b41992c8909f6f2daae4264afc442b9d Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Mon, 24 Aug 2026 23:41:43 +0300 Subject: [PATCH] docs(specs): archive add-instrument-logos --- .../.openspec.yaml | 7 ++ .../2026-08-24-add-instrument-logos/design.md | 77 ++++++++++++ .../proposal.md | 37 ++++++ .../specs/portfolio-view/spec.md | 111 ++++++++++++++++++ .../2026-08-24-add-instrument-logos/tasks.md | 25 ++++ openspec/specs/portfolio-view/spec.md | 36 +++++- 6 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 openspec/changes/archive/2026-08-24-add-instrument-logos/.openspec.yaml create mode 100644 openspec/changes/archive/2026-08-24-add-instrument-logos/design.md create mode 100644 openspec/changes/archive/2026-08-24-add-instrument-logos/proposal.md create mode 100644 openspec/changes/archive/2026-08-24-add-instrument-logos/specs/portfolio-view/spec.md create mode 100644 openspec/changes/archive/2026-08-24-add-instrument-logos/tasks.md diff --git a/openspec/changes/archive/2026-08-24-add-instrument-logos/.openspec.yaml b/openspec/changes/archive/2026-08-24-add-instrument-logos/.openspec.yaml new file mode 100644 index 0000000..9a756bd --- /dev/null +++ b/openspec/changes/archive/2026-08-24-add-instrument-logos/.openspec.yaml @@ -0,0 +1,7 @@ +schema: wealth-ledger-sdd +created: 2026-08-24 +affected_areas: + - frontend + - backend + - specs + - docs diff --git a/openspec/changes/archive/2026-08-24-add-instrument-logos/design.md b/openspec/changes/archive/2026-08-24-add-instrument-logos/design.md new file mode 100644 index 0000000..16bbd94 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-add-instrument-logos/design.md @@ -0,0 +1,77 @@ +# Design: add-instrument-logos + +## Context + +Backend получает Portfolio и metadata инструментов on-the-fly через существующую T-Bank gRPC интеграцию: клиент запрашивает инструмент по instrument UID либо FIGI и уже сегодня нормализует из ответа ticker/name/isin в глобальный 24h кэш метаданных. Тот же upstream-ответ содержит блок бренда инструмента с именем файла логотипа — сейчас он отбрасывается на границе клиента. Файлы логотипов публично раздаются CDN T-Bank по имени файла с суффиксом размера (`x160`/`x320`/`x640`); авторизация не требуется, собственный хостинг/прокси не вводятся. Frontend рендерит позиции портфеля карточками в общей для трёх секций сетке; изображений в UI пока нет. Контракт frontend ↔ backend фиксируется generated OpenAPI snapshot; расхождение ловит workspace-гейт `runtime-openapi`. + +Ограничения: Node 22, canonical gates `npm ci && npm run verify` в каждом репозитории; облигации и «прочие» типы позиций не меняются; финансовая логика и кэширование Snapshot не затрагиваются. + +## Goals / Non-Goals + +**Goals:** + +- Прокинуть имя файла логотипа бренда из уже получаемого upstream-ответа до Portfolio Position в публичном контракте (nullable, обратно совместимо). +- Переиспользовать существующие lookup и 24h кэш метаданных без новых запросов и нового кэша. +- Секция «Акции»: одна карточка в ряду, аватар с логотипом и текстовым фолбэком, доходность со знаком без подписи. +- Один источник сборки URL логотипа на frontend. + +**Non-Goals:** + +- Никакого проксирования, кэширования или валидации изображений на backend. +- Изменений upcoming events, Account Ledger, секций BOND/OTHER. +- Фильтрации логотипов по типу инструмента на server-side — контракт отдаёт бренд любого инструмента при наличии, решение о показе принимает клиент. +- Solution Assessment: build-vs-buy триггеры не задеты — новая интеграция, storage, auth или инфраструктура не вводятся. + +## Decisions + +**D1 — Сырое `logoName` в контракте вместо готового URL.** +Backend передаёт имя файла как есть из бренда upstream (nullable string). Сборку URL выполняет frontend-хелпер: `{базовый адрес CDN}/{имя без расширения}x160.png`. +*Rationale*: размер изображения — concern отображения (retina/плотность экрана); готовый URL потребовал бы второго поля при смене размера и связывал бы контракт с хостом вендора. *Alternatives*: `imageUrl` целиком на backend — отклонено (жёстче контракт, нет гибкости размеров). + +**D2 — Прямая загрузка браузером с публичного CDN, фолбэк обязателен.** +Изображение запрашивается напрямую; отсутствие `logoName`, ошибка загрузки (в том числе HTTP 403 для несуществующего файла) заменяются текстовым фолбэком — первой буквой displayName. *Rationale*: пользователь и так аутентифицирован в экосистеме T-Bank своим токеном соединения; утечка минимальна и принята Change Owner. *Alternatives*: backend image-proxy с кэшем — отклонён как лишний трафик и код; возврат к нему возможен локальной заменой хелпера D1. + +**D3 — Обогащение существующего пути metadata, без новых вызовов.** +Тип результата instrument lookup и запись глобального кэша расширяются четвёртым полем `logoName`; TTL и семантика кэша («ошибка не кэшируется», подтверждённое отсутствие кэшируется) сохраняются. Snapshot-логика не меняется: логотип наследует гарантии существующей metadata (не влияет на успешность/свежесть). + +**D4 — Вариативность карточки по вызывающей стороне секции.** +Компонент карточки остаётся общим; секция акций передаёт вариант отображения (одноколоночная сетка, аватар, доходность со знаком без подписи), секции BOND/OTHER продолжают использовать текущую адаптивную сетку и текущий вид доходности. Ключ i18n подписи доходности сохраняется для не-акций. *Alternatives*: отдельный компонент StockCard — отклонено (дублирование метрик). + +**D5 — Знак доходности считается в слое представления.** +Для варианта акции к форматированному значению добавляется префикс `+` при положительном значении; отрицательное уже содержит `-`; ноль — без знака. Цветовая индикация остаётся прежней. + +**D6 — Размер изображения `x160`.** +Аватар карточки ~40px; `x160` покрывает retina с запасом при весе ~6KB против ~44KB у `x320`. + +## Delivery Map + +| Area | Status | Architectural outcome | +| --- | --- | --- | +| frontend | `affected` | Generated types обновлены из snapshot; хелпер сборки URL логотипа рядом с форматтерами портфеля; секция STOCK рендерит одноколоночную сетку карточек с аватаром (фолбэк-буква) и доходностью со знаком; unit-тест хелпера; обновление тестов страницы под новое поведение акций. | +| backend | `affected` | Instrument lookup и кэш метаданных расширяются `logoName` из бренда upstream; Portfolio Position DTO дополняется nullable `logoName`; обновление fixtures/тестов сервиса портфеля; регенерация OpenAPI артефакта. | +| specs | `affected` | Delta `portfolio-view` архивируется в main spec после верификации реализации. | +| docs | `affected` | Глоссарий пополняется термином «Instrument Logo»; as-built описание портфеля отражает поле контракта и новый вид секции акций. | + +### Contract Gate Decision + +| Gate | Status | Reason | +| --- | --- | --- | +| runtime-openapi | `required` | Меняются обе стороны API контракта: backend дополняет схему Portfolio Position, frontend обновляет snapshot и typed client; сверка через workspace-скрипт `verify-runtime-openapi`. | + +## Risks / Trade-offs + +- **Формат URL CDN не покрыт официальным контрактом вендора** → риск изменения схемы имён/размеров. Mitigation: базовый адрес и шаблон собраны в одном хелпере; замена на proxy — локальная правка. +- **CDN недоступен части пользователей (сеть/гео)** → карточки деградируют на фолбэк-букву через обработку ошибки загрузки; функциональность метрик не страдает. +- **Приватность**: сторонний CDN видит IP и список запрошенных логотипов → принято осознанно (Change Owner, раунд 2); пересмотр — добавлением proxy без изменения контракта. +- **Дрейф snapshot** между репозиториями → закрывается обязательным Contract Gate `runtime-openapi` и canonical gates обоих репозиториев. + +## Migration Plan + +1. Backend: аддитивное nullable поле + обогащение metadata → `npm run verify`. +2. Frontend: `openapi:sync`, UI секции акций, тесты → `npm run verify`. +3. Workspace: semantic verification + `bash specs/scripts/verify-workspace-change.sh add-instrument-logos`; затем docs (глоссарий, as-built) и archive. +4. Rollback: изменение аддитивное; откат фронта возвращает прежний вид секции акций, поле контракта обратно совместимо и может временно игнорироваться. + +## Open Questions + + diff --git a/openspec/changes/archive/2026-08-24-add-instrument-logos/proposal.md b/openspec/changes/archive/2026-08-24-add-instrument-logos/proposal.md new file mode 100644 index 0000000..cebd2cd --- /dev/null +++ b/openspec/changes/archive/2026-08-24-add-instrument-logos/proposal.md @@ -0,0 +1,37 @@ +# Proposal: add-instrument-logos + +## Why + +Список акций в портфеле сейчас полностью текстовый: позиции распознаются по имени и ISIN, что замедляет визуальный поиск нужной бумаги, когда позиций много. Брендовые логотипы инструментов уже предоставляются upstream T-Bank в составе metadata инструмента, поэтому узнаваемость позиций можно повысить без новых интеграций. Заодно карточки акций получают больше места: переход на одну карточку в ряд улучшает читаемость ключевых метрик и даёт логотипу естественное место в композиции. + +## What Changes + +- В секции «Акции» портфеля каждая карточка позиции показывает визуальный логотип бренда инструмента; если логотипа нет или он недоступен, вместо него показывается текстовый фолбэк (первая буква названия). +- Карточки позиций в секции «Акции» отображаются по одной в ряду. +- В карточке акции абсолютная доходность показывается как значение с явным знаком (`+`/`-`, ноль — без знака) и существующей цветовой индикацией знака, без текстовой подписи «Доход:». +- Ответ Portfolio дополняется опциональной metadata позиции — именем файла логотипа бренда инструмента, когда оно есть upstream; клиенты решают сами, где её показывать. + +**Non-goals:** + +- Облигации и «прочие» типы позиций не меняются: текущая адаптивная многоколоночная сетка, состав метрик и подпись доходности сохраняются. +- События счёта (предстоящие и недавние) и история операций не затрагиваются. +- Собственный хостинг или проксирование изображений не вводятся; изображения загружаются клиентом напрямую из публичного источника бренда T-Bank. +- Финансовые расчёты, нормализация значений и агрегаты Portfolio не меняются. + +## Capabilities + +### New Capabilities + + + +### Modified Capabilities + +- `portfolio-view`: в требования к нормализованному составу Portfolio добавляется опциональная metadata логотипа бренда Portfolio Position (nullable, не влияет на успешность и свежесть Snapshot); в требованиях к отображению на клиенте секция «Акции» получает одноколоночную раскладку, логотип бренда с текстовым фолбэком и доходность со знаком без подписи. Требования для секций облигаций и прочих типов сохраняются без изменений. + +## Impact + +- **Пользователи**: новый вид списка акций в портфеле (логотипы, одна карточка в ряду, доходность со знаком); облигации и прочие типы выглядят как раньше. +- **Публичный контракт Portfolio**: новое необязательное nullable поле metadata позиции; обратно совместимо. +- **Backend**: обогащение metadata инструмента именем файла бренда из upstream. +- **Frontend**: отображение секции «Акции» портфеля; обновление snapshot контракта. +- **Docs**: новый термин «Instrument Logo» в доменном глоссарии; as-built описание портфеля. diff --git a/openspec/changes/archive/2026-08-24-add-instrument-logos/specs/portfolio-view/spec.md b/openspec/changes/archive/2026-08-24-add-instrument-logos/specs/portfolio-view/spec.md new file mode 100644 index 0000000..93848a8 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-add-instrument-logos/specs/portfolio-view/spec.md @@ -0,0 +1,111 @@ +# Delta Spec: portfolio-view + +## 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` и 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: Позиция без 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 инструмента позиции содержит бренд с непустым именем файла логотипа +- **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 на клиенте +Клиент 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 оставаться доступными в отдельной общей секции. + +Карточки секции `STOCK` MUST отображаться по одной в ряду при любой ширине экрана. Карточка акции MUST показывать визуальный логотип бренда инструмента рядом с отображаемым идентификатором позиции; при отсутствии имени файла логотипа либо неудачной загрузке изображения вместо логотипа MUST отображаться первая буква отображаемого идентификатора позиции. Абсолютный доход в карточке акции MUST отображаться значением с явным знаком (`+` для положительного, `-` для отрицательного, без знака при нуле) с цветовой индикацией знака и без текстовой подписи доходности. + +Карточки секции `BOND` и общей секции иных типов 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 + +#### Scenario: Одноколоночная секция акций +- **WHEN** User открывает загруженный Portfolio с позициями `STOCK` на широком экране +- **THEN** клиент отображает карточки акций по одной в ряду + +#### Scenario: Логотип акции отображается +- **WHEN** позиция `STOCK` содержит непустое имя файла логотипа бренда и изображение успешно загружается +- **THEN** клиент показывает изображение логотипа рядом с отображаемым идентификатором позиции + +#### Scenario: Фолбэк без логотипа +- **WHEN** у позиции `STOCK` имя файла логотипа отсутствует либо изображение не загружается +- **THEN** вместо логотипа клиент показывает первую букву отображаемого идентификатора позиции + +#### Scenario: Доходность акции со знаком без подписи +- **WHEN** карточка акции показывает абсолютный доход +- **THEN** клиент отображает значение с явным знаком `+` или `-` (без знака при нуле) и без текстовой подписи доходности + +#### Scenario: Секция облигаций сохраняет прежний вид +- **WHEN** User открывает загруженный Portfolio с позициями `BOND` +- **THEN** клиент отображает облигации плитками в адаптивном grid с текстовой подписью доходности и без визуального логотипа diff --git a/openspec/changes/archive/2026-08-24-add-instrument-logos/tasks.md b/openspec/changes/archive/2026-08-24-add-instrument-logos/tasks.md new file mode 100644 index 0000000..0638fd8 --- /dev/null +++ b/openspec/changes/archive/2026-08-24-add-instrument-logos/tasks.md @@ -0,0 +1,25 @@ +# Tasks: add-instrument-logos + +## 1. Backend — metadata и контракт + +- [x] 1.1 Расширить результат instrument lookup полем `logoName` из бренда upstream и прокинуть его в существующий глобальный 24h кэш метаданных без изменения TTL и семантики кэширования; verification: обновлённые assertions в тестах клиента/сервиса покрывают проброс поля +- [x] 1.2 Дополнить Portfolio Position nullable `logoName` и обновить fixtures сервиса портфеля сценариями «бренд с именем файла логотипа» и «бренд отсутствует → null»; verification: jest-прогоны затронутых suite проходят +- [x] 1.3 Регенерировать OpenAPI артефакт backend с новым полем контракта; verification: canonical gate `npm ci && npm run verify` в backend проходит + +## 2. Frontend — отображение секции акций + +- [x] 2.1 Обновить snapshot и generated types из нового контракта (`npm run openapi:sync`); verification: typecheck видит `logoName` в типе позиции без ручных правок generated файлов +- [x] 2.2 Добавить хелпер сборки URL логотипа (базовый адрес брендового CDN, имя без расширения + суффикс размера x160) рядом с форматтерами портфеля; verification: unit-тест хелпера покрывает имя файла с расширением и пустое значение +- [x] 2.3 Секция STOCK: одноколоночная сетка при любой ширине, аватар с логотипом и фолбэком на первую букву displayName при отсутствии/ошибке загрузки, доходность со знаком `+`/`-` (ноль без знака) без подписи; вызовы секций BOND и OTHER не изменять; verification: реализация соответствует Delta Specs, ручная проверка страницы `/accounts/:accountId` +- [x] 2.4 Обновить тесты страницы портфеля: акции — фолбэк логотипа и доходность со знаком без подписи, облигации — регрессионное сохранение прежнего вида с текстовой подписью доходности; verification: jest-прогоны страницы проходят +- [x] 2.5 Прогнать canonical gate frontend; verification: `npm ci && npm run verify` проходит + +## 3. Workspace — cross-area verification + +- [x] 3.1 Contract Gate runtime-openapi между репозиториями; verification: `bash specs/scripts/verify-runtime-openapi.sh` из workspace проходит +- [x] 3.2 Semantic verification реализации против Proposal, Delta Specs, Design и Tasks плюс independent read-only review; verification: оформленные выводы проверки без Critical/Important findings + +## 4. Docs и завершение change + +- [x] 4.1 Добавить термин «Instrument Logo» в доменный глоссарий и отразить в as-built документации портфеля новое поле контракта и вид секции акций; verification: canonical gate `npm ci && npm run verify` в docs проходит +- [x] 4.2 Актуализировать чеклист по фактически выполненной работе и запустить canonical pre-archive gate; verification: `bash specs/scripts/verify-workspace-change.sh add-instrument-logos` проходит без unchecked tasks и блокирующих findings diff --git a/openspec/specs/portfolio-view/spec.md b/openspec/specs/portfolio-view/spec.md index 42e7d29..4005c2f 100644 --- a/openspec/specs/portfolio-view/spec.md +++ b/openspec/specs/portfolio-view/spec.md @@ -20,7 +20,7 @@ ### 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, не скрывая финансовые значения. +Каждая 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 `logoName` — имя файла логотипа бренда инструмента. Имя файла логотипа MUST заполняться из metadata инструмента, когда upstream предоставляет бренд с непустым именем файла логотипа; во всех остальных случаях оно MUST быть `null`. Логотип является presentation metadata: он не влияет на количество, стоимость, агрегаты, успешность или свежесть Portfolio Snapshot. Система MUST искать metadata по instrument UID при его наличии, затем по FIGI. Отображаемый идентификатор MUST предпочитать name, затем ticker, а при недоступности metadata использовать FIGI, не скрывая финансовые значения. #### Scenario: Позиция с неизвестным upstream-типом - **WHEN** upstream возвращает обычную ненулевую позицию с нераспознанным типом инструмента @@ -46,6 +46,14 @@ - **WHEN** у инструмента позиции нет ISIN либо metadata недоступна - **THEN** система возвращает позицию с `isin: null`, не скрывая финансовые значения +#### Scenario: Имя файла логотипа бренда доступно +- **WHEN** metadata инструмента позиции содержит бренд с непустым именем файла логотипа +- **THEN** система возвращает его в поле `logoName` позиции + +#### Scenario: Имя файла логотипа бренда отсутствует +- **WHEN** у инструмента позиции нет бренда, имя файла логотипа пустое либо metadata недоступна +- **THEN** система возвращает позицию с `logoName: null`, не скрывая финансовые значения + #### Scenario: Денежные остатки в одной валюте - **WHEN** upstream возвращает несколько ненулевых доступных денежных остатков одной валюты - **THEN** система возвращает один Cash Balance с их точной суммой @@ -97,7 +105,11 @@ Клиент 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 сохранять понятное обозначение. +Клиент MUST отображать `STOCK` и `BOND` отдельными секциями. Внутри каждой секции MUST сохраняться upstream-порядок позиций. Позиции иных типов MUST оставаться доступными в отдельной общей секции. + +Карточки секции `STOCK` MUST отображаться по одной в ряду при любой ширине экрана. Карточка акции MUST показывать визуальный логотип бренда инструмента рядом с отображаемым идентификатором позиции; при отсутствии имени файла логотипа либо неудачной загрузке изображения вместо логотипа MUST отображаться первая буква отображаемого идентификатора позиции. Абсолютный доход в карточке акции MUST отображаться значением с явным знаком (`+` для положительного, `-` для отрицательного, без знака при нуле) с цветовой индикацией знака и без текстовой подписи доходности. + +Карточки секции `BOND` и общей секции иных типов MUST сохранять прежнее поведение: компактная плитка в адаптивном grid (одна колонка на узком экране, несколько колонок на широком), абсолютный доход с текстовой подписью доходности и цветовой индикацией знака, без визуального логотипа. Плитки всех секций MUST показывать отображаемый идентификатор позиции, количество, `currentValue` как главную метрику, `absoluteReturn`, `averagePurchasePrice`, долю позиции среди позиций той же валюты и `isin`. Отсутствующие метрики MUST быть скрыты, а не отображаться заглушкой. При форматировании денежных значений в русской локали клиент MUST отображать RUB как символ `₽`; другие валюты MUST сохранять понятное обозначение. #### Scenario: Показ устаревшего Portfolio - **WHEN** клиент получает успешный ответ с `isStale: true` @@ -130,3 +142,23 @@ #### Scenario: Доля позиции в валюте - **WHEN** позиция имеет вычислимую текущую стоимость в валюте - **THEN** клиент отображает её долю среди всех позиций той же валюты, не включая Cash Balance + +#### Scenario: Одноколоночная секция акций +- **WHEN** User открывает загруженный Portfolio с позициями `STOCK` на широком экране +- **THEN** клиент отображает карточки акций по одной в ряду + +#### Scenario: Логотип акции отображается +- **WHEN** позиция `STOCK` содержит непустое имя файла логотипа бренда и изображение успешно загружается +- **THEN** клиент показывает изображение логотипа рядом с отображаемым идентификатором позиции + +#### Scenario: Фолбэк без логотипа +- **WHEN** у позиции `STOCK` имя файла логотипа отсутствует либо изображение не загружается +- **THEN** вместо логотипа клиент показывает первую букву отображаемого идентификатора позиции + +#### Scenario: Доходность акции со знаком без подписи +- **WHEN** карточка акции показывает абсолютный доход +- **THEN** клиент отображает значение с явным знаком `+` или `-` (без знака при нуле) и без текстовой подписи доходности + +#### Scenario: Секция облигаций сохраняет прежний вид +- **WHEN** User открывает загруженный Portfolio с позициями `BOND` +- **THEN** клиент отображает облигации плитками в адаптивном grid с текстовой подписью доходности и без визуального логотипа