Files

78 lines
9.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 -->