diff --git a/docs/features/broker-accounts-overview/plan.md b/docs/features/broker-accounts-overview/plan.md new file mode 100644 index 0000000..ce931e9 --- /dev/null +++ b/docs/features/broker-accounts-overview/plan.md @@ -0,0 +1,439 @@ +# Broker Accounts Overview Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Превратить `/broker` в информативный обзор одного–трёх брокерских счетов с общей сводкой, независимой загрузкой карточек и устойчивыми состояниями ошибок. + +**Architecture:** `BrokerAccountsPage` получает список счетов и передаёт его в новый hook на основе TanStack Query `useQueries`; query key совпадает с `useBrokerPortfolio`, поэтому детальная страница переиспользует кеш. Чистый модуль агрегации группирует денежные значения по валютам и рассчитывает единственный новый финансовый показатель по формуле из spec. UI разбит на общую сводку, карточку счёта и компактную полосу распределения; backend и OpenAPI не меняются согласно ADR-012. + +**Tech Stack:** React 18, TypeScript, TanStack Query v5, React Router v6, Vitest, Testing Library, CSS custom properties. + +--- + +## Карта файлов + +- Create `apps/frontend/src/pages/broker/brokerAccountsOverview.ts` — чистые типы, агрегация валютных сумм и форматирование дат/денег/процентов. +- Create `apps/frontend/src/pages/broker/brokerAccountsOverview.test.ts` — unit-тесты финансовой агрегации и edge cases. +- Create `apps/frontend/src/hooks/useBrokerAccountPortfolios.ts` — параллельные portfolio queries с общими query keys. +- Create `apps/frontend/src/hooks/useBrokerAccountPortfolios.test.tsx` — проверка независимых query-состояний и кеша. +- Create `apps/frontend/src/pages/broker/BrokerAccountsSummary.tsx` — общая сводка и частичное состояние. +- Create `apps/frontend/src/pages/broker/BrokerAccountCard.tsx` — успешная карточка, skeleton и локальная ошибка с retry. +- Create `apps/frontend/src/pages/broker/BrokerAllocationBar.tsx` — доступная горизонтальная полоса распределения. +- Create `apps/frontend/src/pages/broker/BrokerAccountsPage.test.tsx` — page/component tests. +- Modify `apps/frontend/src/pages/broker/BrokerAccountsPage.tsx` — orchestration новой страницы. +- Modify `apps/frontend/src/pages/broker/BrokerPages.test.tsx` — удалить старый поверхностный тест списка счетов. +- Modify `apps/frontend/src/styles.css` — визуальная система overview, focus/hover, skeleton и responsive rules. +- Modify `docs/features/broker-accounts-overview/tasks.md` — отмечать выполненные задачи. +- Modify `docs/roadmap.md` — отметить фичу реализованной только после всех проверок. + +### Task 1: Чистая модель агрегации + +**Files:** + +- Create: `apps/frontend/src/pages/broker/brokerAccountsOverview.test.ts` +- Create: `apps/frontend/src/pages/broker/brokerAccountsOverview.ts` + +- [ ] **Step 1: Написать падающие unit-тесты** + +Покрыть одним набором тестов: + +```ts +import { describe, expect, it } from 'vitest'; +import type { BrokerPortfolio } from '../../api/responses'; +import { aggregateBrokerAccounts } from './brokerAccountsOverview'; + +function portfolio( + id: string, + currency: string, + total: number, + daily: number | null, + cash: number, +): BrokerPortfolio { + return { + account: { + id, + type: 'brokerage', + name: id, + status: 'ACCOUNT_STATUS_OPEN', + openedAt: '2022-06-16T00:00:00.000Z', + accessLevel: null, + }, + positionCounts: { shares: 1, bonds: 1, etf: 0, other: 0 }, + totals: { + shares: { currency, units: '0', nano: 0, value: total * 0.5 }, + bonds: { currency, units: '0', nano: 0, value: total * 0.3 }, + etf: null, + currencies: { currency, units: '0', nano: 0, value: total * 0.2 }, + futures: null, + options: null, + structuredProducts: null, + dfa: null, + portfolio: { currency, units: '0', nano: 0, value: total }, + }, + yields: { + expectedPercent: 10, + daily: daily === null ? null : { currency, units: '0', nano: 0, value: daily }, + dailyPercent: null, + }, + cash: [{ currency, units: '0', nano: 0, value: cash }], + blockedCash: [], + asOf: '2026-06-19T10:00:00.000Z', + }; +} + +describe('aggregateBrokerAccounts', () => { + it('sums comparable portfolios and uses the specified daily percent formula', () => { + const result = aggregateBrokerAccounts([ + portfolio('a', 'RUB', 1_100, 100, 200), + portfolio('b', 'RUB', 2_200, 200, 300), + ]); + + expect(result.portfolios).toEqual([ + expect.objectContaining({ + currency: 'RUB', + total: 3_300, + daily: 300, + dailyPercent: 10, + allocation: { shares: 1_650, bonds: 990, etf: 0, cash: 660, other: 0 }, + }), + ]); + expect(result.cash).toEqual([{ currency: 'RUB', value: 500 }]); + }); + + it('keeps different currencies separate', () => { + const result = aggregateBrokerAccounts([ + portfolio('rub', 'RUB', 1_100, 100, 200), + portfolio('usd', 'USD', 550, 50, 25), + ]); + + expect(result.portfolios.map(({ currency, total }) => ({ currency, total }))).toEqual([ + { currency: 'RUB', total: 1_100 }, + { currency: 'USD', total: 550 }, + ]); + }); + + it('does not expose a daily percent when one account lacks daily data', () => { + const result = aggregateBrokerAccounts([ + portfolio('a', 'RUB', 1_100, 100, 200), + portfolio('b', 'RUB', 2_000, null, 300), + ]); + + expect(result.portfolios[0]).toMatchObject({ daily: null, dailyPercent: null }); + }); +}); +``` + +- [ ] **Step 2: Запустить unit-тест и подтвердить RED** + +Run: `npx vitest run src/pages/broker/brokerAccountsOverview.test.ts -w apps/frontend` + +Expected: FAIL с ошибкой импорта `./brokerAccountsOverview`. + +- [ ] **Step 3: Реализовать минимальную чистую модель** + +Создать публичные типы `BrokerAccountsAggregate`, `BrokerCurrencyPortfolioSummary`, +`BrokerCurrencyCashSummary` и функцию: + +```ts +export function aggregateBrokerAccounts(portfolios: BrokerPortfolio[]): BrokerAccountsAggregate; +``` + +Правила реализации: + +- пропускать портфель без `totals.portfolio` или без currency; +- группировать `totals.portfolio`, `yields.daily` и классы активов по currency портфеля; +- считать `other` как `max(0, portfolio - shares - bonds - etf - currencies)`; +- группировать `portfolio.cash` независимо по валюте; +- если хотя бы в одном портфеле валютной группы нет сопоставимого `yields.daily`, возвращать для + группы `daily: null` и `dailyPercent: null`; +- иначе считать `dailyPercent = daily / (total - daily) * 100`, только если знаменатель положителен; +- сортировать валютные группы по первому появлению во входном массиве, не по алфавиту. + +- [ ] **Step 4: Запустить unit-тест и подтвердить GREEN** + +Run: `npx vitest run src/pages/broker/brokerAccountsOverview.test.ts -w apps/frontend` + +Expected: PASS, 3 tests. + +- [ ] **Step 5: Добавить edge cases** + +Добавить тесты для неположительной стоимости начала дня, отрицательного residual `other`, пустого +массива, отсутствующего `totals.portfolio` и cash в нескольких валютах. Реализация должна возвращать +конечные числа и никогда не смешивать валюты. + +- [ ] **Step 6: Запустить unit-тесты и commit** + +Run: `npx vitest run src/pages/broker/brokerAccountsOverview.test.ts -w apps/frontend` + +Expected: PASS. + +```bash +git add apps/frontend/src/pages/broker/brokerAccountsOverview.ts apps/frontend/src/pages/broker/brokerAccountsOverview.test.ts +git commit -m "feat: aggregate broker account summaries" +``` + +### Task 2: Независимые portfolio queries + +**Files:** + +- Create: `apps/frontend/src/hooks/useBrokerAccountPortfolios.ts` +- Create: `apps/frontend/src/hooks/useBrokerAccountPortfolios.test.tsx` + +- [ ] **Step 1: Написать падающий hook-тест** + +Mock `getBrokerPortfolio`, отрендерить hook с двумя счетами и проверить, что вызываются `acc-1` и +`acc-2`, а результат сохраняет соответствие `account → query` независимо от порядка завершения +Promise. Второй тест должен создать `QueryClient`, заранее положить портфель в key +`['broker', 'portfolio', 'acc-1']` и подтвердить, что hook использует то же кешированное значение. + +- [ ] **Step 2: Запустить hook-тест и подтвердить RED** + +Run: `npx vitest run src/hooks/useBrokerAccountPortfolios.test.tsx -w apps/frontend` + +Expected: FAIL с ошибкой импорта `useBrokerAccountPortfolios`. + +- [ ] **Step 3: Реализовать hook через `useQueries`** + +```ts +import { useQueries } from '@tanstack/react-query'; +import { getBrokerPortfolio } from '../api/broker'; +import type { BrokerAccount, BrokerPortfolio } from '../api/responses'; + +export function useBrokerAccountPortfolios(accounts: BrokerAccount[]) { + const queries = useQueries({ + queries: accounts.map((account) => ({ + queryKey: ['broker', 'portfolio', account.id], + queryFn: async (): Promise => (await getBrokerPortfolio(account.id)).data, + staleTime: 60_000, + retry: 2, + refetchOnWindowFocus: false, + })), + }); + + return accounts.map((account, index) => ({ account, query: queries[index] })); +} +``` + +- [ ] **Step 4: Запустить hook-тест и подтвердить GREEN** + +Run: `npx vitest run src/hooks/useBrokerAccountPortfolios.test.tsx -w apps/frontend` + +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add apps/frontend/src/hooks/useBrokerAccountPortfolios.ts apps/frontend/src/hooks/useBrokerAccountPortfolios.test.tsx +git commit -m "feat: load broker account portfolios in parallel" +``` + +### Task 3: Компоненты и состояния страницы + +**Files:** + +- Create: `apps/frontend/src/pages/broker/BrokerAccountsSummary.tsx` +- Create: `apps/frontend/src/pages/broker/BrokerAllocationBar.tsx` +- Create: `apps/frontend/src/pages/broker/BrokerAccountCard.tsx` +- Create: `apps/frontend/src/pages/broker/BrokerAccountsPage.test.tsx` +- Modify: `apps/frontend/src/pages/broker/BrokerAccountsPage.tsx` +- Modify: `apps/frontend/src/pages/broker/BrokerPages.test.tsx` + +- [ ] **Step 1: Написать page/component tests до реализации** + +Mock `useBrokerAccounts` и `useBrokerAccountPortfolios`. Проверить отдельными тестами: + +- heading, агрегированную сумму, дневной результат и две карточки; +- подписи `Брокерский счёт` и `ИИС`, форматированную дату открытия и отсутствие ID/raw enum; +- href всей карточки `/broker/:encodedAccountId`; +- skeleton при загрузке списка; +- пустое состояние при `accounts: []`; +- частичную сводку `Доступно по 1 из 2 счетов`; +- локальный alert и кнопку `Повторить` для ошибочного query; +- вызов `query.refetch()` по кнопке retry; +- раздельное отображение RUB и USD без суммирования. + +Для денежных assertions использовать regexp с обычным и non-breaking space, например: + +```ts +expect(screen.getByText(/3[\s\u00a0]?300[\s\u00a0]?₽/)).toBeInTheDocument(); +``` + +- [ ] **Step 2: Запустить page-тест и подтвердить RED** + +Run: `npx vitest run src/pages/broker/BrokerAccountsPage.test.tsx -w apps/frontend` + +Expected: FAIL, потому что новые компоненты и состояния отсутствуют. + +- [ ] **Step 3: Реализовать `BrokerAllocationBar`** + +Компонент получает `BrokerAllocationItem[]`, строит сегменты с inline `width: percent%`, добавляет +`role="img"`, осмысленный `aria-label` со всеми долями и текстовую легенду. Нулевые сегменты не +рендерятся; цвет не является единственным способом различить классы. + +- [ ] **Step 4: Реализовать `BrokerAccountsSummary`** + +Компонент получает aggregate, `loadedCount` и `totalCount`. Он показывает: + +- статус `Совокупный капитал · N счетов` либо `Доступно по N из M счетов`; +- по одному блоку стоимости/дневного результата на валюту; +- свободные деньги отдельным списком валют; +- allocation bar только когда доступна одна portfolio currency; +- placeholder `—`, если ни один портфель ещё не загружен. + +- [ ] **Step 5: Реализовать `BrokerAccountCard`** + +Компонент получает `account` и query result. Три ветки должны иметь стабильную геометрию: + +```tsx +if (query.isPending) return ; +if (query.error || !query.data) { + return query.refetch()} />; +} +return ; +``` + +Успешная карточка — одна `Link` на `/broker/${encodeURIComponent(account.id)}`. Внутри показать +название, тип, дату открытия, стоимость, `yields.daily`, `dailyPercent`, `expectedPercent` и allocation +bar из существующего `buildBrokerAllocation(portfolio).sectors`. + +- [ ] **Step 6: Переписать orchestration `BrokerAccountsPage`** + +Страница должна: + +1. вызвать `useBrokerAccounts()`; +2. передать `accounts ?? []` в `useBrokerAccountPortfolios` без условного вызова hooks; +3. построить aggregate только из `query.data` успешных записей; +4. показать page error только при ошибке списка; +5. показать отдельное empty state для пустого списка; +6. отрендерить summary и вертикальный список карточек. + +- [ ] **Step 7: Удалить устаревший тест из `BrokerPages.test.tsx`** + +Удалить test case `renders broker and IIS accounts` и неиспользуемые imports `accountHook` и +`BrokerAccountsPage`; покрытие новой страницы живёт в `BrokerAccountsPage.test.tsx`. + +- [ ] **Step 8: Запустить component tests и подтвердить GREEN** + +Run: `npx vitest run src/pages/broker/BrokerAccountsPage.test.tsx src/pages/broker/BrokerPages.test.tsx -w apps/frontend` + +Expected: PASS. + +- [ ] **Step 9: Commit** + +```bash +git add apps/frontend/src/pages/broker/BrokerAccountsPage.tsx apps/frontend/src/pages/broker/BrokerAccountsPage.test.tsx apps/frontend/src/pages/broker/BrokerAccountsSummary.tsx apps/frontend/src/pages/broker/BrokerAccountCard.tsx apps/frontend/src/pages/broker/BrokerAllocationBar.tsx apps/frontend/src/pages/broker/BrokerPages.test.tsx +git commit -m "feat: add informative broker accounts overview" +``` + +### Task 4: Визуальная система и responsive QA + +**Files:** + +- Modify: `apps/frontend/src/styles.css` +- Modify: `apps/frontend/src/pages/broker/BrokerAccountsPage.test.tsx` + +- [ ] **Step 1: Добавить семантические CSS-классы** + +Добавить блоки `.broker-accounts`, `__header`, `__summary`, `__summary-metrics`, `__list`, +`.broker-account-card`, `__topline`, `__value`, `__metrics`, `.broker-allocation-bar`, `__track`, +`__legend`, `__error` и `__empty`. + +Визуальное направление: + +- тёплый нейтральный фон страницы и глубокий зелёный summary без градиента; +- serif-акцент только для крупных денежных значений, основной текст наследует текущую гарнитуру; +- тонкие границы и мягкая тень карточек, без вложенных «карточек в карточке»; +- один заметный hover карточки: небольшой подъём и усиление тени; +- `:focus-visible` с контрастным outline; +- positive/negative цвета всегда сопровождаются знаком и текстом; +- `prefers-reduced-motion: reduce` отключает transform/transition. + +- [ ] **Step 2: Добавить responsive rules** + +При `max-width: 720px` summary metrics и card metrics переходят в одну колонку, легенда allocation +переносится, денежные значения уменьшаются через `clamp()`, а карточка и кнопка retry сохраняют +минимальную интерактивную высоту 44px. Горизонтальный overflow на `.broker-accounts` запрещён. + +- [ ] **Step 3: Запустить frontend проверки** + +Run: `npm run test -w apps/frontend` + +Expected: PASS. + +Run: `npm run lint -w apps/frontend` + +Expected: PASS без warnings. + +Run: `npm run build -w apps/frontend` + +Expected: успешный TypeScript и Vite build. + +- [ ] **Step 4: Проверить страницу в локальном браузере** + +Запустить frontend и backend по README. Проверить `/broker` при ширинах 1280px и 390px: + +- нет горизонтального overflow; +- summary визуально доминирует, но карточки остаются читаемыми; +- карточки и retry доступны с клавиатуры; +- loading не меняет геометрию страницы; +- partial error не скрывает успешные счета; +- названия, крупные суммы и allocation legend не перекрываются. + +- [ ] **Step 5: Commit** + +```bash +git add apps/frontend/src/styles.css apps/frontend/src/pages/broker/BrokerAccountsPage.test.tsx +git commit -m "style: polish broker accounts overview" +``` + +### Task 5: Финальная документация и Definition of Done + +**Files:** + +- Modify: `docs/features/broker-accounts-overview/tasks.md` +- Modify: `docs/roadmap.md` + +- [ ] **Step 1: Отметить выполненные tasks** + +Поставить `[x]` только после соответствующих commit и проверок. Не менять ADR-012: решение уже +Accepted и реализация ему соответствует. + +- [ ] **Step 2: Обновить roadmap** + +Изменить строку фичи на: + +```md +- [x] [Информативный обзор брокерских счетов](features/broker-accounts-overview/spec.md) — реализовано. +``` + +- [ ] **Step 3: Выполнить полный verification gate** + +Run: `npm run test -w apps/frontend && npm run lint -w apps/frontend && npm run build -w apps/frontend` + +Expected: все команды завершаются с exit code 0. + +Run: `npm run build -w apps/docs` + +Expected: Docusaurus build succeeds. + +- [ ] **Step 4: Провести code review** + +Использовать `superpowers:requesting-code-review`. Исправить замечания только после технической +проверки; при изменении поведения сначала синхронизировать spec/plan. + +- [ ] **Step 5: Финальный commit документации** + +```bash +git add docs/features/broker-accounts-overview/tasks.md docs/roadmap.md +git commit -m "docs: complete broker accounts overview" +``` + +## Definition of Done + +- Все Acceptance Criteria из `spec.md` имеют component или unit coverage. +- Frontend tests, lint и build проходят. +- Docusaurus build проходит. +- Desktop и mobile UI проверены в браузере. +- `tasks.md` и roadmap соответствуют реализации. +- Нет изменений backend, OpenAPI или `apps/frontend/src/api/types.ts`. +- Нет незавершённых маркеров или отложенных требований. diff --git a/docs/features/broker-accounts-overview/tasks.md b/docs/features/broker-accounts-overview/tasks.md new file mode 100644 index 0000000..3cbff55 --- /dev/null +++ b/docs/features/broker-accounts-overview/tasks.md @@ -0,0 +1,40 @@ +# Информативный обзор брокерских счетов — задачи + +Статус: готово к реализации + +Подробные шаги, команды и ожидаемые результаты находятся в [plan.md](plan.md). + +## 1. Агрегация + +- [ ] Добавить чистую валютно-безопасную агрегацию портфелей. +- [ ] Покрыть формулу дневного процента и edge cases unit-тестами. +- [ ] Не смешивать валюты и не выполнять неявную конвертацию. + +## 2. Загрузка данных + +- [ ] Добавить параллельные portfolio queries для списка счетов. +- [ ] Переиспользовать query keys детальной страницы. +- [ ] Проверить независимое завершение запросов и кеш. + +## 3. Интерфейс + +- [ ] Добавить общую сводку по успешно загруженным счетам. +- [ ] Добавить информативную карточку счёта и allocation bar. +- [ ] Добавить skeleton, empty state и локальную ошибку с retry. +- [ ] Убрать технический ID и сырые T-Bank enum-значения. +- [ ] Обеспечить keyboard navigation и текстовые признаки доходности. + +## 4. Визуальная проверка + +- [ ] Реализовать согласованное зелёно-нейтральное визуальное направление. +- [ ] Проверить desktop 1280px и mobile 390px без horizontal overflow. +- [ ] Проверить loading и partial-error states в браузере. + +## 5. Definition of Done + +- [ ] Frontend tests проходят. +- [ ] Frontend lint проходит. +- [ ] Frontend build проходит. +- [ ] Docusaurus build проходит. +- [ ] Code review завершён. +- [ ] Roadmap отмечает фичу реализованной.