# Аналитика прибыльности брокерского счёта — Implementation Plan > **For agentic workers:** Use subagent-driven-development or executing-plans to implement this plan task-by-task. **Goal:** Add an analytics tab to the broker account page showing net invested vs received (dividends + coupons). **Architecture:** New backend service aggregates BrokerOperation records by type via Prisma, returns DTO. New frontend tab page displays invested/received blocks. **Tech Stack:** NestJS + Prisma (SQLite), React + TanStack Query + react-router --- ## File Structure ### Backend (new/modified) - Create: `apps/backend/src/modules/tbank/dto/broker-analytics-response.dto.ts` - Create: `apps/backend/src/modules/tbank/services/broker-analytics.service.ts` - Modify: `apps/backend/src/modules/tbank/tbank.controller.ts` — add `GET /analytics` endpoint - Modify: `apps/backend/src/modules/tbank/tbank.module.ts` — register service ### Frontend (new/modified) - Create: `apps/frontend/src/entities/broker-analytics/api/brokerAnalyticsApi.ts` - Create: `apps/frontend/src/entities/broker-analytics/model/useBrokerAnalytics.ts` - Create: `apps/frontend/src/entities/broker-analytics/index.ts` - Create: `apps/frontend/src/pages/broker-analytics/ui/BrokerAnalyticsPage.tsx` - Create: `apps/frontend/src/pages/broker-analytics/index.ts` - Modify: `apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx` - Modify: `apps/frontend/src/app/routing/routeTree.tsx` - Modify: `apps/frontend/src/shared/api/types.ts` --- ### Task 1: Backend DTO и сервис аналитики **Files:** - Create: `apps/backend/src/modules/tbank/dto/broker-analytics-response.dto.ts` - Create: `apps/backend/src/modules/tbank/services/broker-analytics.service.ts` #### Шаг 1.1: Создать DTO ```typescript import { ApiProperty } from '@nestjs/swagger'; export class BrokerAnalyticsDto { @ApiProperty() totalDeposits!: number; @ApiProperty() totalWithdrawn!: number; @ApiProperty() netInvested!: number; @ApiProperty() totalDividends!: number; @ApiProperty() totalCoupons!: number; @ApiProperty() totalReceived!: number; @ApiProperty({ type: Number, nullable: true }) totalReturnPercent!: number | null; @ApiProperty() currency!: string; } ``` #### Шаг 1.2: Создать сервис `BrokerAnalyticsService` ```typescript import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../../prisma/prisma.service'; import { CacheService } from '../../cache/cache.service'; import { BrokerAnalyticsDto } from '../dto/broker-analytics-response.dto'; import { BrokerAccountsService } from './broker-accounts.service'; import { TBANK_CACHE_KEYS } from '../tbank.config'; const DEPOSIT_TYPES = new Set([ 'OPERATION_TYPE_INPUT', 'OPERATION_TYPE_INPUT_SWIFT', 'OPERATION_TYPE_INPUT_ACQUIRING', 'OPERATION_TYPE_INP_MULTI', 'OPERATION_TYPE_OVER_PLACEMENT', 'OPERATION_TYPE_TRANS_IIS_BS', 'OPERATION_TYPE_TRANS_BS_BS', ]); const WITHDRAWAL_TYPES = new Set([ 'OPERATION_TYPE_OUTPUT', 'OPERATION_TYPE_OUTPUT_SWIFT', 'OPERATION_TYPE_OUTPUT_ACQUIRING', 'OPERATION_TYPE_OUT_MULTI', ]); const DIVIDEND_TYPES = new Set(['OPERATION_TYPE_DIVIDEND', 'OPERATION_TYPE_DIV_EXT']); const COUPON_TYPES = new Set(['OPERATION_TYPE_COUPON']); const ANALYTICS_TYPES = new Set([ ...DEPOSIT_TYPES, ...WITHDRAWAL_TYPES, ...DIVIDEND_TYPES, ...COUPON_TYPES, ]); @Injectable() export class BrokerAnalyticsService { constructor( private readonly prisma: PrismaService, private readonly accountsService: BrokerAccountsService, private readonly cacheService: CacheService, ) {} async getAnalytics(accountId: string): Promise<{ data: BrokerAnalyticsDto; meta: { fromCache: boolean; cachedAt: string | null }; }> { const account = await this.accountsService.findById(accountId); if (!account) throw new NotFoundException('Broker account not found'); return this.cacheService.getOrFetch( TBANK_CACHE_KEYS.analytics, [accountId], () => this.computeAnalytics(accountId), 'tbankAnalyticsTtl', ); } private async computeAnalytics(accountId: string): Promise { const operations = await this.prisma.brokerOperation.findMany({ where: { accountId, type: { in: Array.from(ANALYTICS_TYPES) }, payment: { not: null }, }, select: { type: true, payment: true }, }); let totalDeposits = 0; let totalWithdrawn = 0; let totalDividends = 0; let totalCoupons = 0; for (const op of operations) { const payment = JSON.parse(op.payment!); const value = payment.value ?? 0; if (DEPOSIT_TYPES.has(op.type)) { totalDeposits += value; } else if (WITHDRAWAL_TYPES.has(op.type)) { totalWithdrawn += Math.abs(value); } else if (DIVIDEND_TYPES.has(op.type)) { totalDividends += value; } else if (COUPON_TYPES.has(op.type)) { totalCoupons += value; } } const netInvested = totalDeposits - totalWithdrawn; const totalReceived = totalDividends + totalCoupons; const totalReturnPercent = netInvested > 0 ? Math.round((totalReceived / netInvested) * 10000) / 100 : null; return { totalDeposits: Math.round(totalDeposits * 100) / 100, totalWithdrawn: Math.round(totalWithdrawn * 100) / 100, netInvested: Math.round(netInvested * 100) / 100, totalDividends: Math.round(totalDividends * 100) / 100, totalCoupons: Math.round(totalCoupons * 100) / 100, totalReceived: Math.round(totalReceived * 100) / 100, totalReturnPercent, currency: 'RUB', }; } } ``` #### Шаг 1.3: Проверить сборку ```bash npm run build -w apps/backend ``` #### Шаг 1.4: Закоммитить ```bash git add apps/backend/src/modules/tbank/dto/broker-analytics-response.dto.ts git add apps/backend/src/modules/tbank/services/broker-analytics.service.ts git commit -m "feat(backend): add broker analytics DTO and service" ``` --- ### Task 2: Backend controller, module registration и конфиг кеша **Files:** - Modify: `apps/backend/src/modules/tbank/tbank.controller.ts` - Modify: `apps/backend/src/modules/tbank/tbank.module.ts` - Modify: `apps/backend/src/modules/tbank/tbank.config.ts` - Modify: `apps/backend/src/modules/tbank/dto/broker-envelope.dto.ts` - Modify: `apps/backend/src/config/configuration.ts` #### Шаг 2.1: Добавить cache key В `apps/backend/src/modules/tbank/tbank.config.ts`: ```typescript export const TBANK_CACHE_KEYS = { // ... existing keys analytics: 'tbank:analytics', } as const; ``` #### Шаг 2.2: Добавить TTL config В `apps/backend/src/config/configuration.ts`, в секцию `cache`: ```typescript tbankAnalyticsTtl: parseInt(process.env.CACHE_TBANK_ANALYTICS_TTL || '300', 10), ``` #### Шаг 2.3: Зарегистрировать сервис в `TbankModule` В `apps/backend/src/modules/tbank/tbank.module.ts`: - Добавить `BrokerAnalyticsService` в `providers` ```typescript import { BrokerAnalyticsService } from './services/broker-analytics.service'; @Module({ providers: [ // ... existing services BrokerAnalyticsService, ], }) ``` #### Шаг 2.4: Добавить envelope DTO В `apps/backend/src/modules/tbank/dto/broker-envelope.dto.ts`: ```typescript import { BrokerAnalyticsDto } from './broker-analytics-response.dto'; export class BrokerAnalyticsEnvelopeDto { @ApiProperty({ type: BrokerAnalyticsDto }) data!: BrokerAnalyticsDto; @ApiProperty({ type: BrokerResponseMetaDto }) meta!: BrokerResponseMetaDto; } ``` #### Шаг 2.5: Добавить endpoint в `TbankController` ```typescript import { BrokerAnalyticsService } from './services/broker-analytics.service'; import { BrokerAnalyticsEnvelopeDto } from './dto/broker-envelope.dto'; @Get('accounts/:accountId/analytics') @ApiOperation({ summary: 'Get broker account profitability analytics' }) @ApiOkResponse({ type: BrokerAnalyticsEnvelopeDto }) async getAnalytics(@Param('accountId') accountId: string) { const result = await this.brokerAnalyticsService.getAnalytics(accountId); return new ApiResponse(result.data, result.meta.fromCache, result.meta.cachedAt); } ``` Добавить `private readonly brokerAnalyticsService: BrokerAnalyticsService` в конструктор. #### Шаг 2.6: Проверить сборку ```bash npm run build -w apps/backend ``` #### Шаг 2.7: Закоммитить ```bash git add apps/backend/src/modules/tbank/ git add apps/backend/src/config/configuration.ts git commit -m "feat(backend): add broker analytics endpoint with caching" ``` --- ### Task 3: Frontend — shared types barrel, entity API и хук **Files:** - Modify: `apps/frontend/src/shared/api/index.ts` - Modify: `apps/frontend/src/shared/api/types.ts` (codegen) - Create: `apps/frontend/src/entities/broker-analytics/api/brokerAnalyticsApi.ts` - Create: `apps/frontend/src/entities/broker-analytics/model/useBrokerAnalytics.ts` - Create: `apps/frontend/src/entities/broker-analytics/index.ts` #### Шаг 3.1: Добавить тип `BrokerAnalyticsDto` в `types.ts` Добавить в `components['schemas']` секцию `shared/api/types.ts`: ```typescript BrokerAnalyticsDto: { totalDeposits: number totalWithdrawn: number netInvested: number totalDividends: number totalCoupons: number totalReceived: number totalReturnPercent: number | null currency: string } ``` #### Шаг 3.2: Добавить брокерский тип в barrel export В `apps/frontend/src/shared/api/index.ts`: ```typescript // Broker analytics export type BrokerAnalytics = components['schemas']['BrokerAnalyticsDto'] ``` #### Шаг 3.3: Создать API функцию ```typescript // apps/frontend/src/entities/broker-analytics/api/brokerAnalyticsApi.ts import type { ApiResponseMeta, BrokerAnalytics } from '@/shared/api' import { request } from '@/shared/api/kyClient' export function getBrokerAnalytics( accountId: string, ): Promise<{ data: BrokerAnalytics; meta: ApiResponseMeta }> { return request( `/api/v1/broker/accounts/${encodeURIComponent(accountId)}/analytics`, ) } ``` #### Шаг 3.4: Создать хук ```typescript // apps/frontend/src/entities/broker-analytics/model/useBrokerAnalytics.ts import { useQuery } from '@tanstack/react-query' import type { BrokerAnalytics } from '@/shared/api' import { getBrokerAnalytics } from '../api/brokerAnalyticsApi' export function useBrokerAnalytics(accountId: string | undefined) { return useQuery({ queryKey: ['broker', 'analytics', accountId], enabled: Boolean(accountId), queryFn: async () => (await getBrokerAnalytics(accountId!)).data, staleTime: 300_000, retry: 2, refetchOnWindowFocus: false, }) } ``` #### Шаг 3.5: Создать barrel export ```typescript // apps/frontend/src/entities/broker-analytics/index.ts export { getBrokerAnalytics } from './api/brokerAnalyticsApi' export { useBrokerAnalytics } from './model/useBrokerAnalytics' ``` #### Шаг 3.6: Проверить сборку ```bash npm run build -w apps/frontend ``` #### Шаг 3.7: Закоммитить ```bash git add apps/frontend/src/entities/broker-analytics/ git add apps/frontend/src/shared/api/ git commit -m "feat(frontend): add broker analytics data layer" ``` --- ### Task 4: Frontend — страница аналитики **Files:** - Create: `apps/frontend/src/pages/broker-analytics/ui/BrokerAnalyticsPage.tsx` - Create: `apps/frontend/src/pages/broker-analytics/index.ts` #### Шаг 4.1: Создать страницу ```tsx // apps/frontend/src/pages/broker-analytics/ui/BrokerAnalyticsPage.tsx import { useBrokerAccountContext } from '@/widgets/broker-account-layout'; import { useBrokerAnalytics } from '@/entities/broker-analytics'; function formatAmount(value: number, currency: string): string { return `${value.toLocaleString('ru-RU', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${currency}`; } export function BrokerAnalyticsPage() { const { accountId } = useBrokerAccountContext(); const { data, isLoading, isError } = useBrokerAnalytics(accountId); if (isLoading) { return (
Загрузка...
); } if (isError) { return (
Не удалось загрузить аналитику
); } if (!data || (data.totalDeposits === 0 && data.totalReceived === 0)) { return (
Нет данных для аналитики
); } return (
{/* Вложено */}

Вложено

{/* Получено */}

Получено

{/* Сводка */}

Сводка

{data.totalReturnPercent !== null && ( )}
); } function Row({ label, value, bold, positive, negative }: { label: string; value: string; bold?: boolean; positive?: boolean; negative?: boolean; }) { const color = positive ? 'var(--color-text-positive, #22c55e)' : negative ? 'var(--color-text-negative, #ef4444)' : 'var(--color-text-primary)'; return (
{label} {value}
); } function Divider() { return (
); } function SummaryItem({ label, value, positive }: { label: string; value: string; positive?: boolean; }) { return (
{label}
{value}
); } ``` #### Шаг 4.2: Создать barrel ```typescript // apps/frontend/src/pages/broker-analytics/index.ts export { BrokerAnalyticsPage } from './ui/BrokerAnalyticsPage' ``` #### Шаг 4.3: Проверить сборку ```bash npm run build -w apps/frontend ``` #### Шаг 4.4: Закоммитить ```bash git add apps/frontend/src/pages/broker-analytics/ git commit -m "feat(frontend): add broker analytics page" ``` --- ### Task 5: Frontend — роут и таб навигации **Files:** - Modify: `apps/frontend/src/app/routing/routeTree.tsx` - Modify: `apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx` #### Шаг 5.1: Добавить роут В `apps/frontend/src/app/routing/routeTree.tsx`: ```typescript import { BrokerAnalyticsPage } from '@/pages/broker-analytics' const brokerAnalyticsRoute = createRoute({ getParentRoute: () => brokerAccountRoot, path: '/analytics', component: BrokerAnalyticsPage, }) // Добавить в brokerAccountRoot.addChildren([...]) brokerAccountRoot.addChildren([ brokerAccountIndexRoute, brokerSharesRoute, brokerBondsRoute, brokerOperationsRoute, brokerEventsRoute, brokerAnalyticsRoute, // <-- добавить ]) ``` #### Шаг 5.2: Добавить таб в навигацию В `apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx`: ```typescript const links = [ { to: '', label: 'Обзор' }, { to: '/shares', label: 'Акции' }, { to: '/bonds', label: 'Облигации' }, { to: '/operations', label: 'Операции' }, { to: '/events', label: 'События' }, { to: '/analytics', label: 'Аналитика' }, ] ``` #### Шаг 5.3: Проверить сборку ```bash npm run build -w apps/frontend ``` #### Шаг 5.4: Закоммитить ```bash git add apps/frontend/src/app/routing/routeTree.tsx git add apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx git commit -m "feat(frontend): add analytics route and tab" ``` --- ### Task 6: Проверка линта и тестов #### Шаг 6.1: Запустить линт и сборку ```bash npm run lint -w apps/backend && npm run build -w apps/backend npm run lint -w apps/frontend && npm run build -w apps/frontend ``` #### Шаг 6.2: Запустить тесты ```bash npm test -w apps/backend -- --run npm test -w apps/frontend -- --run ``` #### Шаг 6.3: Если всё ок — закоммитить финальные правки и запушить ```bash git add -A git commit -m "chore: fix lint and tests" ```