21 KiB
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-тесты
Покрыть одним набором тестов:
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 и функцию:
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.
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
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<BrokerPortfolio> => (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
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, например:
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. Три ветки должны иметь стабильную геометрию:
if (query.isPending) return <BrokerAccountCardSkeleton account={account} />;
if (query.error || !query.data) {
return <BrokerAccountCardError account={account} onRetry={() => query.refetch()} />;
}
return <BrokerAccountCardContent account={account} portfolio={query.data} />;
Успешная карточка — одна Link на /broker/${encodeURIComponent(account.id)}. Внутри показать
название, тип, дату открытия, стоимость, yields.daily, dailyPercent, expectedPercent и allocation
bar из существующего buildBrokerAllocation(portfolio).sectors.
- Step 6: Переписать orchestration
BrokerAccountsPage
Страница должна:
- вызвать
useBrokerAccounts(); - передать
accounts ?? []вuseBrokerAccountPortfoliosбез условного вызова hooks; - построить aggregate только из
query.dataуспешных записей; - показать page error только при ошибке списка;
- показать отдельное empty state для пустого списка;
- отрендерить 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
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
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
Изменить строку фичи на:
- [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 документации
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. - Нет незавершённых маркеров или отложенных требований.