21 KiB
Raw Permalink Blame History

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

Страница должна:

  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
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.
  • Нет незавершённых маркеров или отложенных требований.