Files

9.9 KiB
Raw Permalink Blame History

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