spec: deliver compact portfolio totals package
@@ -0,0 +1,2 @@
|
|||||||
|
schema: wealth-ledger-sdd
|
||||||
|
created: 2026-09-09
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
Страница Brokerage Account уже разделяет получение данных в
|
||||||
|
`AccountPage.vm.ts` и отображение в `AccountPageLoaded.view.tsx`.
|
||||||
|
`PortfolioTotals` получает готовые `totalsByCurrency`; текущая сводка использует
|
||||||
|
Mantine `SimpleGrid` и `Paper`, вертикальную метку ISO над суммой, отступ 16 px
|
||||||
|
и верхнюю синюю границу 3 px. Максимальная ширина контента страницы — 960 px.
|
||||||
|
|
||||||
|
`shared/lib/display.ts` уже форматирует decimal string без числового
|
||||||
|
преобразования: сохраняет знак и дробные позиции, группирует целую часть.
|
||||||
|
`formatMoney` меняет только RUB на ₽. Query и VM уже сохраняют успешные данные
|
||||||
|
при refetch/error; `PortfolioRefreshState` отделён от server-side `isStale`.
|
||||||
|
Действующий initial skeleton содержит общий блок высотой 120 px вместо
|
||||||
|
геометрии валютных карточек.
|
||||||
|
|
||||||
|
Основание подхода — утверждённые [Proposal](proposal.md) и
|
||||||
|
[Delta Specs](specs/portfolio-view/spec.md). A1 из Discovery выбран владельцем;
|
||||||
|
ни выбор, ни прежние PNG сами по себе не были approval Design.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:** изменить представление сводки и её loading placeholder в существующем
|
||||||
|
page slice; сохранить строковую точность и валютную идентичность; использовать
|
||||||
|
нынешние primitives, theme tokens и границы VM/view.
|
||||||
|
|
||||||
|
**Non-Goals:** менять финансовые вычисления, транспортные типы, query/cache
|
||||||
|
policy, state machine, общие правила денежных суффиксов приложения или соседние
|
||||||
|
разделы страницы. Новые зависимости, инструментальные витрины и сервисы
|
||||||
|
форматирования не нужны.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Ownership и поток данных
|
||||||
|
|
||||||
|
`PortfolioTotals` остаётся владельцем компактного представления каждого
|
||||||
|
переданного итога. Порядок, membership и ключи берутся из исходных валют;
|
||||||
|
никакой повторной агрегации, сортировки по знакам или дополнения валют нет.
|
||||||
|
VM продолжает выбирать loaded/empty по составу Portfolio, а не по итогам.
|
||||||
|
Пустой список итогов не создаёт карточек или отдельного empty-состояния.
|
||||||
|
|
||||||
|
Loaded view сохраняет своё место сводки после времени снимка и перед составом.
|
||||||
|
Его timestamp, refresh/error и stale sections продолжают получать прежние
|
||||||
|
props. Меняется только сводка и соответствующая область page-owned skeleton.
|
||||||
|
Это удерживает ответственность за устойчивость данных в query/VM, а за
|
||||||
|
геометрию — в presentation.
|
||||||
|
|
||||||
|
### Точное форматирование и валютная идентичность
|
||||||
|
|
||||||
|
Переиспользовать существующий `formatDecimal`, открыв его через публичный
|
||||||
|
`shared/lib` export без изменения алгоритма и поведения `formatMoney`.
|
||||||
|
Выбор метки и суффикса принадлежит сводке: RUB → ₽, USD → $, EUR → €;
|
||||||
|
для остальных валют метка и суффикс — исходный ISO-код. Например, USD/CAD
|
||||||
|
дают `$`/`CAD`, поэтому совпадающие мировые знаки не требуют каталога валют
|
||||||
|
или анализа всей коллекции. Неизвестные валюты не отбрасываются.
|
||||||
|
|
||||||
|
Полные названия трёх валют принадлежат существующему русскому i18n-каталогу.
|
||||||
|
Семантика карточки — пара название/значение (`dl`, `dt`, `dd` либо эквивалентная
|
||||||
|
связь): доступное название содержит «Российский рубль», «Доллар США» или «Евро»;
|
||||||
|
для прочих валют доступен ISO-код. Декоративный badge скрыт от accessibility
|
||||||
|
tree, полное текстовое значение остаётся доступным. Карточка не кнопка,
|
||||||
|
не получает tab stop и не требует tooltip/hover для определения валюты.
|
||||||
|
|
||||||
|
Числа не проходят через `Number`, `parseFloat`, округление или currency-mode
|
||||||
|
`Intl.NumberFormat`. Пробелы группировки и десятичная запятая сохраняются;
|
||||||
|
перенос — CSS layout, а не вставка символов в числовые данные. `-0.5` остаётся
|
||||||
|
`-0,5`, исходные дробные нули сохраняются. Замена ISO-кода в уже собранной строке
|
||||||
|
`formatMoney` отвергнута: она связывала бы сводку с деталями суффикса общего
|
||||||
|
formatter. Новый дубликат decimal formatter также не нужен.
|
||||||
|
|
||||||
|
### Компоновка средствами платформы
|
||||||
|
|
||||||
|
Mantine `Paper` и page-local CSS остаются основой карточек. Сетка сохраняет
|
||||||
|
колонки `base: 1`, `sm: 2`, `md: 3` (48 em / 62 em), с gap 10 px.
|
||||||
|
Одна валюта занимает одну ячейку сетки, как в A1; дополнительные валюты
|
||||||
|
заполняют следующие строки, без фиктивных пустых карточек.
|
||||||
|
|
||||||
|
Внутри карточки метка и сумма образуют переносимую строку. Естественная ширина
|
||||||
|
полного текста определяет вместимость, без JS-измерений и порогов по количеству
|
||||||
|
цифр. При недостатке места сумма переходит ниже метки и получает всю доступную
|
||||||
|
ширину. Ограничения `min-width: 0`, `max-width: 100%`, обычные переносы на
|
||||||
|
пробелах и `overflow-wrap: anywhere` защищают также длинную дробную часть.
|
||||||
|
Не задаются фиксированная высота, `nowrap`, ellipsis, clipping или уменьшение
|
||||||
|
шрифта ради вместимости. На переносимой строке знак минуса не отделяется
|
||||||
|
от первой цифры; остальные цифры и суффикс остаются текстом в исходном порядке.
|
||||||
|
|
||||||
|
Альтернативы: единая общая полоса не соответствует выбранным отдельным карточкам;
|
||||||
|
фиксированная двухстрочная раскладка теряет компактность обычных значений;
|
||||||
|
обрезание/скролл/округление нарушают Delta Specs. Нативный reflow сохраняет
|
||||||
|
простую архитектуру и работает при увеличении текста.
|
||||||
|
|
||||||
|
## UI/UX Applicability and Package
|
||||||
|
|
||||||
|
**Applicability and scope:** `new/changed` — сводка и её initial skeleton на
|
||||||
|
`/accounts/:accountId`. Навигация, heading, время, позиции, Cash Balance и
|
||||||
|
остальные секции на PNG служат контекстом; их внешний вид не является заданием
|
||||||
|
на редизайн. Наличие в runtime дополнительных секций не является расхождением.
|
||||||
|
|
||||||
|
**Package revision:** `a1-symbols-r1`. Один approval относится ко всему этому
|
||||||
|
Markdown и восьми локальным PNG ниже. Они доступны без Pencil, Storybook,
|
||||||
|
исходного чата или внешнего аккаунта.
|
||||||
|
|
||||||
|
**Reused source and differences:** это новый canonical пакет, а не ссылка на
|
||||||
|
approved archived reuse. PNG перенесены byte-for-byte из выбранных Discovery r3
|
||||||
|
exports; их первоначальные имена перечислены ниже. Исходный `.pen` не является
|
||||||
|
зависимостью пакета. Markdown дополняет изображения правилами для других валют,
|
||||||
|
accessibility, промежуточных ширин и произвольно длинных значений.
|
||||||
|
|
||||||
|
### Визуальная геометрия
|
||||||
|
|
||||||
|
Карточка белая, граница gray-3 1 px со всех сторон, radius 8 px, без отдельной
|
||||||
|
синей верхней полосы и тени. Padding 12 px; промежуток метка/сумма 10 px.
|
||||||
|
Слева badge: фон blue-0, текст blue-6, radius 4 px, минимум 40 × 24 px;
|
||||||
|
для ISO-кодов ширина растёт по тексту. Знак 18 px / 600. Сумма — 20 px / 600,
|
||||||
|
line-height 1.55, основной цвет dark-7. Используется системный font stack
|
||||||
|
приложения, без загрузки Roboto только ради совпадения canvas.
|
||||||
|
|
||||||
|
Обычная строка выравнивает сумму к правому краю. После перехода под метку
|
||||||
|
сумма использует всю ширину с чтением слева направо. Предпочтительны переносы
|
||||||
|
на пробелах группировки; длинная непрерывная дробь может переноситься между
|
||||||
|
цифрами. Точная точка переноса зависит от системного шрифта: PNG не задаёт
|
||||||
|
обязательный разрыв после конкретной цифры. Все цифры, минус и суффикс читаемы.
|
||||||
|
|
||||||
|
При базовом масштабе обычная карточка ориентировочно 57 px, три карточки
|
||||||
|
на mobile — 191 px вместе с двумя gap. Это design geometry, а не runtime
|
||||||
|
замер: критерием служат полные значения и уменьшение высоты относительно
|
||||||
|
прежнего UI при одинаковых ширине, шрифте, масштабе и данных. Длинные значения
|
||||||
|
и увеличение текста закономерно увеличивают высоту.
|
||||||
|
|
||||||
|
### Screens, states and interactions
|
||||||
|
|
||||||
|
| Состояние | Решение |
|
||||||
|
| --- | --- |
|
||||||
|
| Loaded, 1/3 валюты | Отдельные A1 карточки; символ слева, полная сумма справа при вместимости. |
|
||||||
|
| Loaded, больше валют / неизвестная валюта | Та же сетка без лимита количества; прочие валюты используют ISO-код в badge и суффиксе. |
|
||||||
|
| Длинные / отрицательные значения | Сумма уходит под badge и переносится; `-0,5 $` сохраняет минус; зелёный/красный тон по знаку не добавляется. |
|
||||||
|
| Initial loading | Три декоративных card placeholders с badge/amount shapes, в той же responsive сетке; число shapes не обещает три валюты. Существующий `LoadingSkeleton` владеет busy label, shapes скрыты от чтения и не фокусируются. Остальные semantic regions loading view сохраняются. |
|
||||||
|
| Refetch | Прежние карточки и `lastSuccessfulAt` плюс существующий compact progress; карточки не заменяются skeleton. |
|
||||||
|
| Client refresh error | Прежние значения и время плюс существующий orange warning и «Повторить». |
|
||||||
|
| Server stale | Прежний blue warning и время; само `isStale` не создаёт retry. |
|
||||||
|
| Error + stale | Оба самостоятельных предупреждения, карточки и время; retry принадлежит клиентской ошибке. |
|
||||||
|
| Retry → success | До ответа сохраняются значения; успешный ответ обновляет значения/время, снимает client error; stale зависит только от нового ответа. |
|
||||||
|
| Нет итогов при непустом составе | Нет карточек; состав и время остаются. |
|
||||||
|
| Пустые позиции и Cash Balance | Существующее empty-представление, без нулевых карточек. |
|
||||||
|
| Ошибка без успешных данных | Существующая error view; `404` ведёт к списку счетов без retry; ошибки подключения/токена сохраняют свои действия, временная/неизвестная ошибка допускает retry. |
|
||||||
|
|
||||||
|
Новых взаимодействий нет. Кнопка повтора сохраняет клавиатурную доступность
|
||||||
|
и существующую callback-связь с VM. Статические суммы не превращаются в live
|
||||||
|
region; обновление сообщает существующий status region.
|
||||||
|
|
||||||
|
**Responsive behavior:** на 320/390 px одна карточка в ряду; при 1440 px и
|
||||||
|
контенте 960 px — три. Между ними действуют указанные breakpoints. При любом
|
||||||
|
числе колонок переполнение значения переводит его ниже метки, а не расширяет
|
||||||
|
grid track. Одна валюта на узком экране занимает всю ширину. Loading использует
|
||||||
|
те же правила; refetch/error/stale не меняют геометрию самих итогов.
|
||||||
|
|
||||||
|
### Materials
|
||||||
|
|
||||||
|
| Material ID (Discovery source) | Relative path | Screen/state and purpose | SHA-256 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| wide (ACV5F) | [visuals/a1-wide.png](visuals/a1-wide.png) | 1440, три обычных итога | `b406552e01098a5b95183d6a54bd59b3bf827387d33d73a6a453d83140b8da10` |
|
||||||
|
| narrow (j6ks4) | [visuals/a1-narrow.png](visuals/a1-narrow.png) | 390, три обычных итога | `8ee71bb406abaa980e08f57c45e007fb66183786dfd251c9280a6ff62205d922` |
|
||||||
|
| single (iwyJA) | [visuals/a1-single-wide.png](visuals/a1-single-wide.png) | 1440, одна валюта | `6f163dd4a4ba461df5e715bab5757f4698986aade56ae38f02585d9c8d366b28` |
|
||||||
|
| long-wide (Hbu1s) | [visuals/a1-long-wide.png](visuals/a1-long-wide.png) | 1440, длинные значения | `ecccaee124c72b0f4f22bd92b31ef97e260c830782761ed9e7aab4c33a7e0af0` |
|
||||||
|
| long-320 (vjlkn) | [visuals/a1-long-320.png](visuals/a1-long-320.png) | 320, перенос и сохранение всех цифр | `48d62eaa497819e6312f5ae56afa393f92c7bb0970543cd9b3abdd5960480534` |
|
||||||
|
| loading (DuAlU) | [visuals/a1-loading-narrow.png](visuals/a1-loading-narrow.png) | 390, placeholders сводки | `138b000b334f54294f6cd8e88d607619608b05620231267fc1dc0477e1f87f46` |
|
||||||
|
| refresh (aP2cU) | [visuals/a1-refresh-narrow.png](visuals/a1-refresh-narrow.png) | 390, сохранение данных при обновлении | `e453606105a96290a2d2358e039935c89187a90127ffe4228374ae6e16c9e252` |
|
||||||
|
| error-stale (z5EeB) | [visuals/a1-error-stale-narrow.png](visuals/a1-error-stale-narrow.png) | 390, отдельные предупреждения и retry | `c63c8a30cc932582f591422dd4871c6663cb55567218cee988d7119d77d4a4e3` |
|
||||||
|
|
||||||
|
## Delivery Map
|
||||||
|
|
||||||
|
| Area | Status | Architectural outcome or N/A reason |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| frontend | `affected` | A1 totals и соответствующий skeleton; точное форматирование с локальными знаками и доступной идентичностью; сохранённые query/VM состояния. |
|
||||||
|
| backend | `N/A` | Готовые итоги, расчёты, Snapshot и HTTP/OpenAPI contract не меняются; серверная реализация не требуется. |
|
||||||
|
| specs | `affected` | Canonical planning, переносимый Design package и нормативное описание `portfolio-view` в рамках lifecycle. |
|
||||||
|
| docs | `affected` | После реализации `features/portfolio-view.md` описывает фактическую компактную сводку, валютные знаки и ограничения оценки. |
|
||||||
|
|
||||||
|
### Contract Gate Decision
|
||||||
|
|
||||||
|
| Gate | Status | Reason |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| runtime-openapi | `N/A` | Ни одна сторона API contract не меняется; существующие decimal strings и исходные currency остаются входом представления. |
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- Разные системные шрифты и zoom меняют вместимость. Нужен browser reflow review
|
||||||
|
обычных и длинных fixtures на узких и широких экранах; PNG не доказывает
|
||||||
|
отсутствие runtime overflow или доступность. Точность и полнота важнее высоты.
|
||||||
|
- Открытие существующего decimal formatter немного расширяет публичный
|
||||||
|
`shared/lib`, зато исключает дублирование денежных преобразований. Риск
|
||||||
|
защищается сохранением текущего `formatMoney` и regression cases для точных
|
||||||
|
цифр, дробных нулей, `-0.5` и валютной идентичности на presentation boundary.
|
||||||
|
- Значение `$` без доступного полного названия неоднозначно; связь пары
|
||||||
|
название/значение проверяется в accessibility tree. Цвет не несёт идентичность.
|
||||||
|
- Placeholder не знает количества валют заранее. Три формы повторяют ожидаемую
|
||||||
|
структуру, а успешный ответ определяет реальное число карточек.
|
||||||
|
- Геометрия и денежное представление требуют scoped acceptance проверки;
|
||||||
|
состояние refetch/error проверяется через существующие account-page tests,
|
||||||
|
без новой state machine или отдельной инфраструктуры тестирования.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
После approval Design и затем Tasks представление и loading geometry
|
||||||
|
поставляются совместно с проверкой точности и сохранённых состояний. После
|
||||||
|
проверенной реализации docs фиксирует as-built поведение; canonical спецификация
|
||||||
|
синхронизируется в рамках штатного lifecycle. Миграции данных, переключатель
|
||||||
|
функции и координированный релиз backend не нужны.
|
||||||
|
|
||||||
|
### Rollback Contract
|
||||||
|
|
||||||
|
| Field | Decision |
|
||||||
|
| --- | --- |
|
||||||
|
| Status | `N/A` |
|
||||||
|
| Reason | Изменение обратимого frontend presentation без изменения auth, данных, API или другого труднообратимого решения. Обычный откат frontend delivery возвращает прежний вид без преобразования данных. |
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
Сводка итогов Portfolio на странице Brokerage Account занимает избыточную
|
||||||
|
высоту, особенно на узком экране, и отдаляет пользователя от состава портфеля.
|
||||||
|
Нужно сделать её компактнее, сохранив читаемость каждой суммы и понимание того,
|
||||||
|
к какой валюте она относится. В Discovery владелец выбрал направление A1:
|
||||||
|
отдельные компактные валютные карточки со знаками ₽, $ и €.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Представить итоги Portfolio компактными отдельными карточками по валютам,
|
||||||
|
следуя выбранному направлению A1. На широком и узком экране обычная сводка
|
||||||
|
занимает меньше высоты, сохраняя читаемость и доступность значений.
|
||||||
|
- Для российского рубля, доллара США и евро показывать ₽, $ и € соответственно
|
||||||
|
в валютных метках и рядом с суммами вместо RUB, USD и EUR. Сохранить
|
||||||
|
однозначность валюты и её полное доступное название; другие валюты остаются
|
||||||
|
различимыми и не исключаются из сводки.
|
||||||
|
- Показывать длинные, дробные и отрицательные суммы полностью: без потери
|
||||||
|
цифр или знака, округления, сокращений до тысяч или миллионов, обрезания
|
||||||
|
и необходимости горизонтальной прокрутки. На узком экране допускать перенос
|
||||||
|
и увеличение высоты ради полного значения.
|
||||||
|
- Сохранить смысл каждой суммы: итог позиций и доступных денег в одной валюте,
|
||||||
|
без сложения разных валют и конвертации. Отсутствующие суммы не заменяются
|
||||||
|
нулями, рублёвый итог не становится общей стоимостью Portfolio, а сводка
|
||||||
|
не обещает полноту оценки при отсутствии стоимости отдельных позиций.
|
||||||
|
- Сохранить существующие состояния: первичное ожидание без вымышленных сумм;
|
||||||
|
прежние суммы и время успешного получения при фоновом обновлении и его
|
||||||
|
ошибке; раздельные сообщения об устаревшем снимке и ошибке обновления,
|
||||||
|
с применимым действием повтора. Существующее пустое состояние сохраняется.
|
||||||
|
|
||||||
|
Вне scope: изменение денежных расчётов, состава или источников данных;
|
||||||
|
редизайн навигации, заголовка, позиций и соседних разделов; изменение
|
||||||
|
форматирования денег во всём приложении. Выбор A1 задаёт направление этой
|
||||||
|
сводки; детальное визуальное решение подлежит отдельному согласованию Design.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
Нет.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `specs/portfolio-view/spec.md`: добавить требования к компактному
|
||||||
|
представлению валютных итогов на странице Brokerage Account, символам
|
||||||
|
валют, доступной идентичности и полному отображению сумм на широком
|
||||||
|
и узком экране при сохранении существующего смысла итогов и состояний.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
Изменение затрагивает пользователей, просматривающих Portfolio одного
|
||||||
|
Brokerage Account на широких и узких экранах, и ограничено сводкой его
|
||||||
|
валютных итогов. При поставке обновляются пользовательское представление,
|
||||||
|
его нормативное описание и документация фактически доступного поведения.
|
||||||
|
Способ получения данных и финансовый смысл итогов сохраняются.
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Компактная сводка валютных итогов Portfolio
|
||||||
|
|
||||||
|
На странице Brokerage Account клиент SHALL показывать каждый полученный
|
||||||
|
валютный итог Portfolio отдельной компактной карточкой. При достаточной
|
||||||
|
ширине карточки валютная метка и сумма MUST располагаться в одной строке;
|
||||||
|
сумма MUST оставаться основной читаемой величиной. Обычная сводка с одной
|
||||||
|
или тремя валютами MUST занимать меньше высоты, чем прежнее представление
|
||||||
|
с меткой над суммой при тех же значениях и ширине экрана.
|
||||||
|
|
||||||
|
На широком экране несколько карточек SHALL располагаться рядом при наличии
|
||||||
|
места; на узком экране MUST сохраняться одна карточка в ряду. Если полное
|
||||||
|
значение не помещается рядом с меткой, клиент MUST предоставлять ему полную
|
||||||
|
доступную ширину карточки и переносить содержимое с увеличением высоты.
|
||||||
|
Сводка MUST NOT обрезать суммы, заменять их многоточием или требовать
|
||||||
|
горизонтальной прокрутки для чтения карточек и значений.
|
||||||
|
|
||||||
|
#### Scenario: Обычная сводка на широком экране
|
||||||
|
|
||||||
|
- **WHEN** User открывает Portfolio с итогами `125000.50` RUB, `320.00` USD
|
||||||
|
и `9876.54321` EUR на широком экране с местом для трёх карточек
|
||||||
|
- **THEN** клиент показывает три отдельные карточки рядом, метку и полную
|
||||||
|
сумму в одной строке каждой карточки, с меньшей высотой сводки, чем при
|
||||||
|
прежнем размещении метки над суммой в тех же условиях
|
||||||
|
|
||||||
|
#### Scenario: Обычная сводка на узком экране
|
||||||
|
|
||||||
|
- **WHEN** User открывает тот же Portfolio с тремя валютными итогами
|
||||||
|
на узком экране
|
||||||
|
- **THEN** клиент показывает три карточки по одной в ряду, сохраняя полные
|
||||||
|
читаемые суммы и меньшую общую высоту сводки, чем при прежнем размещении
|
||||||
|
метки над суммой в тех же условиях
|
||||||
|
|
||||||
|
#### Scenario: Portfolio с одной валютой
|
||||||
|
|
||||||
|
- **WHEN** Portfolio содержит один валютный итог
|
||||||
|
- **THEN** клиент показывает одну компактную карточку без пустых карточек
|
||||||
|
для других валют; обычная сумма и метка помещаются в одну строку при
|
||||||
|
достаточной ширине на широком и узком экране
|
||||||
|
|
||||||
|
#### Scenario: Длинная сумма на узком экране
|
||||||
|
|
||||||
|
- **WHEN** User просматривает итог `123456789012.345678901` RUB при ширине
|
||||||
|
экрана 320 пикселей и значение не помещается рядом с валютной меткой
|
||||||
|
- **THEN** сумма получает полную доступную ширину карточки и при необходимости
|
||||||
|
переносится; все цифры и знак валюты читаемы без обрезания, многоточия
|
||||||
|
и горизонтальной прокрутки
|
||||||
|
|
||||||
|
### Requirement: Обозначение и доступная идентичность валюты в сводке
|
||||||
|
|
||||||
|
В валютных метках и суффиксах сумм сводки клиент MUST обозначать RUB знаком
|
||||||
|
`₽`, USD знаком `$`, EUR знаком `€` вместо видимых ISO-кодов этих валют.
|
||||||
|
Каждая сумма MUST оставаться однозначно связанной со своей исходной валютой,
|
||||||
|
включая чтение вспомогательными технологиями. Для RUB, USD и EUR MUST быть
|
||||||
|
доступны названия «Российский рубль», «Доллар США» и «Евро» соответственно.
|
||||||
|
Символ или цвет сами по себе MUST NOT быть единственным доступным способом
|
||||||
|
определения валюты.
|
||||||
|
|
||||||
|
Другие полученные валюты MUST оставаться в сводке с понятным обозначением
|
||||||
|
и доступной идентичностью. Если знак отсутствует или неоднозначен, клиент
|
||||||
|
MUST сохранять возможность различить исходную валюту по её названию или
|
||||||
|
ISO-коду, в том числе визуально; неизвестное полное название MUST NOT
|
||||||
|
подменяться вымышленным. Совпадающие знаки разных валют MUST NOT приводить
|
||||||
|
к объединению карточек или неразличимому представлению их валют.
|
||||||
|
|
||||||
|
#### Scenario: Рубль, доллар США и евро показаны знаками
|
||||||
|
|
||||||
|
- **WHEN** сводка содержит RUB, USD и EUR
|
||||||
|
- **THEN** метки показывают `₽`, `$` и `€`, а суммы заканчиваются теми же
|
||||||
|
знаками; видимые RUB, USD и EUR не подменяют выбранные знаки
|
||||||
|
|
||||||
|
#### Scenario: Валютная сумма читается вспомогательной технологией
|
||||||
|
|
||||||
|
- **WHEN** User читает валютный итог сводки вспомогательной технологией
|
||||||
|
- **THEN** ему доступны полное значение и однозначная валютная идентичность,
|
||||||
|
включая «Российский рубль», «Доллар США» или «Евро» для соответствующих валют
|
||||||
|
|
||||||
|
#### Scenario: Получена другая валюта
|
||||||
|
|
||||||
|
- **WHEN** сводка содержит валюту помимо RUB, USD и EUR
|
||||||
|
- **THEN** её отдельный итог остаётся доступным с понятным обозначением
|
||||||
|
и доступным названием либо исходным ISO-кодом при неизвестном названии
|
||||||
|
|
||||||
|
#### Scenario: У разных валют совпадает знак
|
||||||
|
|
||||||
|
- **WHEN** Portfolio содержит итоги двух исходных валют с одинаковым знаком
|
||||||
|
- **THEN** сводка показывает два отдельных итога, и User может различить
|
||||||
|
их валюты визуально и вспомогательной технологией без догадки по сумме
|
||||||
|
|
||||||
|
### Requirement: Полные точные значения и смысл валютной сводки
|
||||||
|
|
||||||
|
Сводка MUST показывать полученные валютные итоги без изменения их знака,
|
||||||
|
цифр и дробной точности. Для русской локали клиент SHALL сохранять группировку
|
||||||
|
разрядов пробелами и десятичную запятую, не округляя значение, не отбрасывая
|
||||||
|
дробные позиции и не сокращая сумму до тысяч, миллионов или экспоненциальной
|
||||||
|
записи. Требование MUST сохраняться для длинных, отрицательных значений
|
||||||
|
и отрицательных дробей с нулевой целой частью.
|
||||||
|
|
||||||
|
Каждая карточка MUST представлять только полученный итог позиций и Cash
|
||||||
|
Balance своей исходной валюты. Сводка MUST NOT конвертировать или складывать
|
||||||
|
разные валюты, показывать рублёвый итог как общую стоимость Portfolio,
|
||||||
|
повторно добавлять Cash Balance либо обещать полную оценку при наличии
|
||||||
|
позиций без вычислимой стоимости. Для отсутствующего валютного итога клиент
|
||||||
|
MUST NOT создавать нулевую карточку. Отсутствие итогов само по себе MUST NOT
|
||||||
|
обозначать пустой Portfolio, если позиции или Cash Balance присутствуют.
|
||||||
|
|
||||||
|
#### Scenario: Обычные суммы сохраняют дробные позиции
|
||||||
|
|
||||||
|
- **WHEN** в сводку поступают значения `125000.50` RUB, `320.00` USD
|
||||||
|
и `9876.54321` EUR
|
||||||
|
- **THEN** клиент показывает `125 000,50 ₽`, `320,00 $` и `9 876,54321 €`
|
||||||
|
с сохранением каждой исходной цифры и дробной позиции
|
||||||
|
|
||||||
|
#### Scenario: Длинные и отрицательные значения не теряют точность
|
||||||
|
|
||||||
|
- **WHEN** в сводку поступают `123456789012.345678901` RUB,
|
||||||
|
`-1234567.890123` USD и `987654321.000001` EUR
|
||||||
|
- **THEN** пользователь видит полные `123 456 789 012,345678901 ₽`,
|
||||||
|
`-1 234 567,890123 $` и `987 654 321,000001 €` без округления,
|
||||||
|
сокращений и потери отрицательного знака на широком и узком экране
|
||||||
|
|
||||||
|
#### Scenario: Отрицательная сумма меньше единицы
|
||||||
|
|
||||||
|
- **WHEN** полученный валютный итог равен `-0.5` USD
|
||||||
|
- **THEN** сводка показывает `-0,5 $`, сохраняя знак и дробную величину
|
||||||
|
|
||||||
|
#### Scenario: Несколько валют и денежные остатки
|
||||||
|
|
||||||
|
- **WHEN** полученные итоги RUB и USD уже включают позиции и Cash Balance
|
||||||
|
соответствующих валют
|
||||||
|
- **THEN** клиент показывает эти два итога без повторного добавления денег,
|
||||||
|
межвалютного итога или представления RUB как общей стоимости Portfolio
|
||||||
|
|
||||||
|
#### Scenario: Стоимость части позиций неизвестна
|
||||||
|
|
||||||
|
- **WHEN** Portfolio содержит позиции без вычислимой стоимости и доступные
|
||||||
|
валютные итоги, сформированные без этих позиций
|
||||||
|
- **THEN** сводка показывает только полученные итоги, не добавляет нули
|
||||||
|
за отсутствующие стоимости и не называет результат полной оценкой Portfolio
|
||||||
|
|
||||||
|
#### Scenario: Итоги отсутствуют при непустом Portfolio
|
||||||
|
|
||||||
|
- **WHEN** `totalsByCurrency` пуст, но `positions` или `cashBalances` непусты
|
||||||
|
- **THEN** клиент не создаёт валютные карточки и не заменяет доступный
|
||||||
|
состав Portfolio состоянием пустого портфеля
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Состояния и ошибки Portfolio на клиенте
|
||||||
|
|
||||||
|
Клиент SHALL загружать Portfolio при открытии защищённого маршрута
|
||||||
|
`/accounts/:accountId`. Он MUST использовать успешный ответ в течение
|
||||||
|
30 секунд, без automatic retry, polling или refetch при возврате фокуса.
|
||||||
|
При загрузке без ранее полученного успешного ответа для этого же Brokerage
|
||||||
|
Account клиент MUST показывать структурное skeleton-состояние ожидаемого
|
||||||
|
содержимого, включая сводку, без вымышленных сумм. Область загрузки MUST
|
||||||
|
иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным
|
||||||
|
или получать клавиатурный фокус.
|
||||||
|
|
||||||
|
Истечение срока свежести MUST NOT само по себе скрывать уже доступный
|
||||||
|
Client Cached Snapshot. Во время фонового обновления и после его ошибки
|
||||||
|
клиент MUST сохранять ранее полученные данные того же Portfolio, включая
|
||||||
|
валютные итоги и исходное `lastSuccessfulAt`. При обновлении клиент MUST
|
||||||
|
показывать компактную неблокирующую индикацию без замены данных skeleton.
|
||||||
|
При ошибке обновления он MUST показывать локализованное неблокирующее
|
||||||
|
предупреждение о последних полученных данных и предоставлять действие
|
||||||
|
повторной попытки. Сохранённые данные MUST NOT считаться новым успешным
|
||||||
|
снимком или подтверждением текущей доступности Brokerage Account.
|
||||||
|
|
||||||
|
Клиент MUST показать штатное empty-состояние только при пустых `positions`
|
||||||
|
и `cashBalances`, без нулевых валютных карточек. Для показываемого состава
|
||||||
|
Portfolio клиент MUST сохранять время успешного получения снимка
|
||||||
|
`lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST
|
||||||
|
показывать Snapshot, его исходное время и нейтральное предупреждение
|
||||||
|
о временной недоступности T-Bank. Это предупреждение само по себе MUST NOT
|
||||||
|
добавлять действие ручного обновления. Server-side stale и ошибка фонового
|
||||||
|
обновления Client Cached Snapshot MUST оставаться раздельными состояниями:
|
||||||
|
ошибка клиента MUST NOT менять или подменять `isStale`, а оба предупреждения
|
||||||
|
и действие повтора клиентского обновления MUST оставаться доступными при
|
||||||
|
одновременном наличии этих состояний.
|
||||||
|
|
||||||
|
При отсутствии ранее успешно полученного Client Cached Snapshot этого же
|
||||||
|
Portfolio и ответе `404` клиент MUST показать состояние недоступного счёта
|
||||||
|
с действием возврата к списку счетов без действия повтора. При отсутствии
|
||||||
|
Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE`
|
||||||
|
или иной ошибке клиент MUST показать безопасное error-состояние с доступными
|
||||||
|
для ошибки действиями; действие повтора разрешено только для временной
|
||||||
|
и неизвестной ошибки.
|
||||||
|
|
||||||
|
#### Scenario: Показ устаревшего Portfolio
|
||||||
|
|
||||||
|
- **WHEN** клиент получает успешный ответ с `isStale: true`
|
||||||
|
- **THEN** клиент показывает позиции, остатки, валютные итоги, время
|
||||||
|
последнего успешного снимка и нейтральное предупреждение; само это
|
||||||
|
предупреждение не добавляет действие ручного обновления
|
||||||
|
|
||||||
|
#### Scenario: Недоступный счёт на клиенте
|
||||||
|
|
||||||
|
- **WHEN** для текущего Portfolio отсутствует ранее успешно полученный
|
||||||
|
Client Cached Snapshot и клиент получает `404`
|
||||||
|
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку
|
||||||
|
Brokerage Account без действия повтора
|
||||||
|
|
||||||
|
#### Scenario: Первичная загрузка Portfolio
|
||||||
|
|
||||||
|
- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее
|
||||||
|
полученного успешного ответа
|
||||||
|
- **THEN** он показывает структурный skeleton, включая место сводки,
|
||||||
|
с доступным обозначением загрузки, без вымышленных или нулевых сумм
|
||||||
|
и без фокусируемых placeholders
|
||||||
|
|
||||||
|
#### Scenario: Обновление после истечения срока свежести
|
||||||
|
|
||||||
|
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое
|
||||||
|
обновление данных того же Brokerage Account
|
||||||
|
- **THEN** клиент сохраняет прежние валютные итоги и время их успешного
|
||||||
|
получения, показывает неблокирующую индикацию обновления и не заменяет
|
||||||
|
доступный состав и сводку skeleton-состоянием
|
||||||
|
|
||||||
|
#### Scenario: Ошибка обновления после успешного ответа
|
||||||
|
|
||||||
|
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio,
|
||||||
|
а обновление этих же данных завершается ошибкой
|
||||||
|
- **THEN** клиент сохраняет данные, суммы и время прежнего снимка,
|
||||||
|
показывает неблокирующее предупреждение и действие повтора;
|
||||||
|
ошибка не делает сохранённый снимок свежим и не изменяет `isStale`
|
||||||
|
|
||||||
|
#### Scenario: Ошибка клиентского обновления при server-side stale
|
||||||
|
|
||||||
|
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое
|
||||||
|
обновление этого же Portfolio завершается ошибкой
|
||||||
|
- **THEN** прежние суммы и время остаются видимыми одновременно
|
||||||
|
с нейтральным server-side предупреждением, отдельным предупреждением
|
||||||
|
об ошибке клиентского обновления и его действием повтора
|
||||||
|
|
||||||
|
#### Scenario: Повторное обновление успешно
|
||||||
|
|
||||||
|
- **WHEN** после ошибки фонового обновления User запускает повторную
|
||||||
|
попытку и получает успешный Portfolio Snapshot
|
||||||
|
- **THEN** до ответа прежние суммы и время остаются видимыми, затем
|
||||||
|
клиент показывает значения и `lastSuccessfulAt` успешного ответа,
|
||||||
|
убирает предупреждение об ошибке клиентского обновления и показывает
|
||||||
|
server-side предупреждение только в соответствии с новым `isStale`
|
||||||
|
|
||||||
|
#### Scenario: Штатный пустой Portfolio на клиенте
|
||||||
|
|
||||||
|
- **WHEN** успешно полученный Portfolio содержит пустые `positions`
|
||||||
|
и `cashBalances`
|
||||||
|
- **THEN** клиент показывает существующее empty-состояние без валютных
|
||||||
|
карточек с вымышленными нулевыми итогами
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
## 1. Frontend: компактная сводка и сохранение состояний
|
||||||
|
|
||||||
|
Основа — утверждённый [Design](design.md), включая пакет `a1-symbols-r1`
|
||||||
|
и все восемь PNG из его Materials. Перед первой Node-командой активировать
|
||||||
|
frontend `.nvmrc` через `nvm use --silent`. Зависимости устанавливать из lockfile
|
||||||
|
через `npm ci`; новую инфраструктуру и зависимости не добавлять.
|
||||||
|
|
||||||
|
- [x] 1.1 [frontend] Открыть существующий `formatDecimal` через публичный `shared/lib` и использовать его в сводке с локальными знаками ₽/$/€, ISO-fallback и доступными названиями валют из i18n, сохранив поведение общего `formatMoney`.
|
||||||
|
- **Covers:** `portfolio-view` — `Обозначение и доступная идентичность валюты в сводке`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`
|
||||||
|
- **Verification:** Scoped Vitest через `npm run test:run -- src/shared/lib/display.test.ts src/pages/account`: полные обычные и длинные decimal strings из Delta Specs, дробные нули, отрицательные значения и `-0.5`; видимые знаки и доступные полные названия RUB/USD/EUR; раздельные USD/CAD и неизвестный ISO; прежние результаты `formatMoney` вне сводки. Дополнить поведенческие тесты на существующей presentation boundary без копии алгоритма formatter в assertions.
|
||||||
|
|
||||||
|
- [x] 1.2 [frontend] Реализовать A1 в `PortfolioTotals` и page-local CSS: отдельные карточки, badge и сумма в строке при вместимости, responsive сетка и полный перенос длинной суммы под метку; сохранить исходные membership, порядок и смысл итогов.
|
||||||
|
- **Covers:** `portfolio-view` — `Компактная сводка валютных итогов Portfolio`; `portfolio-view` — `Обозначение и доступная идентичность валюты в сводке`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`
|
||||||
|
- **Verification:** `npm run test:run -- src/pages/account`: одна/три/больше валют, отсутствие фиктивных карточек, отсутствие повторного добавления Cash Balance или общего межвалютного итога; при неизвестной стоимости позиции показываются только переданные суммы, без обещания полной оценки. Проверить семантическую связь названия с полным значением и отсутствие tab stop у карточки. Реальный reflow проверяется в 1.4, а не выводится из jsdom.
|
||||||
|
|
||||||
|
- [x] 1.3 [frontend] Заменить область сводки в page-owned initial skeleton тремя декоративными A1 placeholders и дополнить существующие account-page regression checks для сохранённых данных и состояний, не меняя query/VM policy.
|
||||||
|
- **Covers:** `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`
|
||||||
|
- **Verification:** `npm run test:run -- src/pages/account src/entities/portfolio`: initial busy label без сумм и фокусируемых shapes; refetch → error → retry → success сохраняет прежние суммы и UTC `lastSuccessfulAt` до успешного ответа; server stale отдельно и вместе с client error; `404` с/без Client Cached Snapshot; штатные действия ошибок подключения/токена/временной и неизвестной ошибки; empty по positions/cashBalances и непустой состав без totals. Переиспользовать имеющиеся assertions cache policy: 30 секунд, без automatic retry/polling/focus refetch; добавлять только недостающее покрытие затронутых инвариантов.
|
||||||
|
|
||||||
|
- [x] 1.4 [frontend] Проверить реализованный пакет в браузере, исправить расхождения в пределах Design и выполнить canonical frontend gate.
|
||||||
|
- **Covers:** `portfolio-view` — `Компактная сводка валютных итогов Portfolio`; `portfolio-view` — `Обозначение и доступная идентичность валюты в сводке`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`
|
||||||
|
- **Verification:** В существующей локальной витрине/приложении сравнить сводку и loading с `a1-symbols-r1` на 320, 390 и 1440 px и по обе стороны 48/62 em; использовать синтетические обычные, длинные, отрицательные значения, одну/три/дополнительные валюты и zoom 200%. Проверить все цифры и знаки, получение полной ширины под меткой, отсутствие обрезания и горизонтального overflow сводки (`scrollWidth <= clientWidth` для сводки/карточек), меньшую высоту обычных одной/трёх валют относительно прежнего UI при одинаковых данных, ширине, шрифте и масштабе. Проверить accessibility tree, клавиатурный retry, initial/refetch/error/stale и неизменность соседних sections. Сохранить observed measurements/screenshots как implementation evidence, не как новую revision Design. Затем после lockfile install выполнить `npm run verify` из frontend; runtime-openapi не требуется по Design.
|
||||||
|
|
||||||
|
## 2. Документация и canonical пакет
|
||||||
|
|
||||||
|
- [x] 2.1 [docs] После проверенной frontend реализации обновить `features/portfolio-view.md` описанием фактической компактной сводки, валютных знаков, точных переносимых сумм и ограничений оценки; сохранить согласованное описание состояний.
|
||||||
|
- **Covers:** `portfolio-view` — `Компактная сводка валютных итогов Portfolio`; `portfolio-view` — `Обозначение и доступная идентичность валюты в сводке`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`
|
||||||
|
- **Verification:** Scoped review соответствия документа реализованному UI и применимых локальных ссылок; из docs после `nvm use --silent` и `npm ci` выполнить `npm run verify`. Не переносить planning или будущие verification results в as-built документацию.
|
||||||
|
|
||||||
|
- [x] 2.2 [specs] Подготовить к поставке полный canonical change package: утверждённые planning artifacts, восемь Materials `a1-symbols-r1` и фактические отметки завершения implementation Tasks; сохранить неизменными approval-bound содержимое и относительную структуру пакета.
|
||||||
|
- **Covers:** `N/A` — задача не вводит отдельного пользовательского поведения; specs владеет поставкой уже подготовленного normative/UI пакета и checklist progress согласно Delivery Map. Main-spec sync и archive остаются последующей Finalization.
|
||||||
|
- **Verification:** Из specs под активированным frontend runtime выполнить `openspec schema validate wealth-ledger-sdd --verbose`, `openspec validate compact-portfolio-totals --type change --strict --store wealth-ledger-specs` и `openspec validate --specs --strict --store wealth-ledger-specs`. Сверить полный native Delta membership, prerequisite approval fingerprints и Tasks fingerprint с нормализацией только checklist progress; проверить все пути/SHA-256 материалов из Design. Не создавать независимый PASS, Completion Evidence или archive в этой task.
|
||||||
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 47 KiB |