440 lines
21 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.

# 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<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**
```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 <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`**
Страница должна:
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`.
- Нет незавершённых маркеров или отложенных требований.