docs(specs): archive add-instrument-logos
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
schema: wealth-ledger-sdd
|
||||||
|
created: 2026-08-24
|
||||||
|
affected_areas:
|
||||||
|
- frontend
|
||||||
|
- backend
|
||||||
|
- specs
|
||||||
|
- docs
|
||||||
@@ -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
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
@@ -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
|
||||||
|
|
||||||
|
<!-- none -->
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `portfolio-view`: в требования к нормализованному составу Portfolio добавляется опциональная metadata логотипа бренда Portfolio Position (nullable, не влияет на успешность и свежесть Snapshot); в требованиях к отображению на клиенте секция «Акции» получает одноколоночную раскладку, логотип бренда с текстовым фолбэком и доходность со знаком без подписи. Требования для секций облигаций и прочих типов сохраняются без изменений.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Пользователи**: новый вид списка акций в портфеле (логотипы, одна карточка в ряду, доходность со знаком); облигации и прочие типы выглядят как раньше.
|
||||||
|
- **Публичный контракт Portfolio**: новое необязательное nullable поле metadata позиции; обратно совместимо.
|
||||||
|
- **Backend**: обогащение metadata инструмента именем файла бренда из upstream.
|
||||||
|
- **Frontend**: отображение секции «Акции» портфеля; обновление snapshot контракта.
|
||||||
|
- **Docs**: новый термин «Instrument Logo» в доменном глоссарии; as-built описание портфеля.
|
||||||
+111
@@ -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 с текстовой подписью доходности и без визуального логотипа
|
||||||
@@ -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
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
### Requirement: Нормализованный состав Portfolio
|
### 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.
|
Система 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-типом
|
#### Scenario: Позиция с неизвестным upstream-типом
|
||||||
- **WHEN** upstream возвращает обычную ненулевую позицию с нераспознанным типом инструмента
|
- **WHEN** upstream возвращает обычную ненулевую позицию с нераспознанным типом инструмента
|
||||||
@@ -46,6 +46,14 @@
|
|||||||
- **WHEN** у инструмента позиции нет ISIN либо metadata недоступна
|
- **WHEN** у инструмента позиции нет ISIN либо metadata недоступна
|
||||||
- **THEN** система возвращает позицию с `isin: null`, не скрывая финансовые значения
|
- **THEN** система возвращает позицию с `isin: null`, не скрывая финансовые значения
|
||||||
|
|
||||||
|
#### Scenario: Имя файла логотипа бренда доступно
|
||||||
|
- **WHEN** metadata инструмента позиции содержит бренд с непустым именем файла логотипа
|
||||||
|
- **THEN** система возвращает его в поле `logoName` позиции
|
||||||
|
|
||||||
|
#### Scenario: Имя файла логотипа бренда отсутствует
|
||||||
|
- **WHEN** у инструмента позиции нет бренда, имя файла логотипа пустое либо metadata недоступна
|
||||||
|
- **THEN** система возвращает позицию с `logoName: null`, не скрывая финансовые значения
|
||||||
|
|
||||||
#### Scenario: Денежные остатки в одной валюте
|
#### Scenario: Денежные остатки в одной валюте
|
||||||
- **WHEN** upstream возвращает несколько ненулевых доступных денежных остатков одной валюты
|
- **WHEN** upstream возвращает несколько ненулевых доступных денежных остатков одной валюты
|
||||||
- **THEN** система возвращает один Cash Balance с их точной суммой
|
- **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 показать штатное 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
|
#### Scenario: Показ устаревшего Portfolio
|
||||||
- **WHEN** клиент получает успешный ответ с `isStale: true`
|
- **WHEN** клиент получает успешный ответ с `isStale: true`
|
||||||
@@ -130,3 +142,23 @@
|
|||||||
#### Scenario: Доля позиции в валюте
|
#### Scenario: Доля позиции в валюте
|
||||||
- **WHEN** позиция имеет вычислимую текущую стоимость в валюте
|
- **WHEN** позиция имеет вычислимую текущую стоимость в валюте
|
||||||
- **THEN** клиент отображает её долю среди всех позиций той же валюты, не включая Cash Balance
|
- **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 с текстовой подписью доходности и без визуального логотипа
|
||||||
|
|||||||
Reference in New Issue
Block a user