Sergey Krylov f7dc338719
Some checks failed
CI / ci (pull_request) Failing after 3m9s
CI / ci (push) Failing after 3m9s
feat: add actual payouts and filter-as-draft UX to broker events calendar
- Backend: actual events from T-Bank operations, types filter, split forecast/actual summary
- Frontend: draft/applied filters with multi-select types, status column (Факт/Прогноз), green actual amounts
- Docs: update spec, plan, tasks
2026-06-22 21:48:03 +03:00

273 lines
13 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 Events And Payouts 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:** Добавить в брокерский счёт T-Bank раздел предстоящих и прошедших событий с прогнозом
будущих выплат, фактическими прошедшими поступлениями и управляемыми фильтрами.
**Architecture:** Backend предоставляет единый read-only endpoint событий поверх существующих данных
T-Bank и MOEX. Слой агрегации объединяет прогнозные события по текущим позициям и фактические
прошедшие выплаты из операций счёта, затем строит раздельный summary факта и прогноза. Frontend
держит фильтры в черновике, применяет их только по кнопке `Показать` и визуально разделяет
`Факт`/`Прогноз`.
**Tech Stack:** NestJS 10, Prisma, CacheService, MOEX client, T-Bank module, React 18, React Router
6, TanStack Query 5, TypeScript, Vitest, Testing Library.
---
## Связанные SDD-артефакты
- Epic: `docs/epics/BrokerPortfolio.md`
- Spec: `docs/features/broker-events-and-payouts/spec.md`
- Tasks: `docs/features/broker-events-and-payouts/tasks.md`
## Gate перед реализацией
До начала кода пользователь должен отдельно подтвердить, что:
- текущая spec остаётся источником истины;
- `plan.md` и `tasks.md` согласованы;
- первая версия действительно ограничивается T-Bank-сценарием и оценкой по текущим позициям;
- follow-up по entitlement, налогам и полной купонной сетке остаются вне реализации.
## Архитектурные решения
### Backend endpoint
Новый endpoint:
```text
GET /api/v1/broker/accounts/:accountId/events?from=YYYY-MM-DD&to=YYYY-MM-DD&types=dividend,coupon,maturity,offer
```
Endpoint возвращает:
- `items` — плоский список событий по текущим позициям счёта;
- `summary` — агрегаты по фактическим и прогнозным денежным событиям за период;
- `asOf` — момент построения read model.
### Backend read model
Новый сервис `BrokerEventsService` живёт внутри `TBankModule` и использует:
- `BrokerAccountsService` для проверки счёта;
- `BrokerPortfolioService` или внутренний shared-path для получения текущих позиций;
- `BrokerInstrumentsService` для сопоставления T-Bank instrument metadata;
- `MoexClientService` для дивидендов и bond enrichment;
- `BrokerOperationsService` для получения фактических прошедших выплат;
- `CacheService` для кэширования результата по `accountId + from + to + types`.
Сервис не записывает события в Prisma и не вводит отдельные таблицы в первой версии.
### Формирование событий
По акциям:
- для каждой позиции, сопоставимой с MOEX share instrument, читаются dividend events;
- в выборку попадают только события, чья `registryCloseDate` лежит в диапазоне;
- `estimatedAmount` рассчитывается как текущий `quantity * payoutPerUnit`, если payout известен.
По облигациям:
- используется уже доступный enrichment-слой MOEX;
- `nextCouponDate` даёт событие `coupon`;
- `matDate` даёт событие `maturity`;
- `offerDate` даёт событие `offer`;
- `couponValue` и `faceValue` используются для оценки `estimatedAmount`, когда они доступны.
### Семантика summary
`summary` агрегирует денежные события раздельно по источнику:
- дивиденды;
- купоны;
- погашения.
Поля прогноза считаются только по `source: 'forecast'`, поля факта — только по `source: 'actual'`.
Оферты остаются в общем списке событий, но не входят в денежные итоги.
### Фактические прошедшие события
Для прошедшей части диапазона backend читает исполненные операции T-Bank:
- `OPERATION_TYPE_DIVIDEND` и `OPERATION_TYPE_DIV_EXT``dividend`;
- `OPERATION_TYPE_COUPON``coupon`;
- `OPERATION_TYPE_BOND_REPAYMENT` и `OPERATION_TYPE_BOND_REPAYMENT_FULL``maturity`.
Фактическая строка получает `source: 'actual'`, `actualAmount` из `operation.payment`, дату операции
как `eventDate`, `estimateMode: null`. Прогнозная строка получает `source: 'forecast'`,
`estimatedAmount`, `estimateMode: 'current_position'`.
### Частичная деградация
Сервис работает по принципу best-effort:
- ошибка по одной бумаге не роняет весь ответ;
- unsupported или несопоставимые позиции пропускаются;
- если сумма события не может быть рассчитана, событие всё равно возвращается без суммы.
## API и типы
### Query contract
Новый query DTO:
```ts
type BrokerEventsQuery = {
from: string;
to: string;
types?: string;
};
```
Обе даты обязательны. `types` опционален: если параметр отсутствует, backend использует все типы.
### Response contract
```ts
type BrokerPortfolioEvent = {
id: string;
type: 'dividend' | 'coupon' | 'maturity' | 'offer';
source: 'forecast' | 'actual';
category: 'cashflow' | 'corporate';
eventDate: string;
paymentDate: string | null;
ticker: string | null;
name: string | null;
instrumentUid: string | null;
instrumentType: 'share' | 'bond' | 'other';
quantitySnapshot: number | null;
payoutPerUnit: number | null;
estimatedAmount: number | null;
actualAmount: number | null;
currency: string | null;
estimateMode: 'current_position' | null;
};
type BrokerEventsSummary = {
eventCount: number;
nearestEventDate: string | null;
totalEstimatedCashflow: number;
actualCashflow: number;
forecastEstimatedCashflow: number;
dividendsTotal: number;
couponsTotal: number;
principalRepaymentTotal: number;
actualDividendsTotal: number;
actualCouponsTotal: number;
actualPrincipalRepaymentTotal: number;
};
```
## Карта файлов
### Backend
- Create `apps/backend/src/modules/tbank/dto/broker-events-query.dto.ts`
- Create `apps/backend/src/modules/tbank/dto/broker-events-response.dto.ts`
- Modify `apps/backend/src/modules/tbank/dto/broker-envelope.dto.ts`
- Modify `apps/backend/src/modules/tbank/types/broker.types.ts`
- Create `apps/backend/src/modules/tbank/services/broker-events.service.ts`
- Create `apps/backend/src/modules/tbank/services/broker-events.service.spec.ts`
- Modify `apps/backend/src/modules/tbank/tbank.controller.ts`
- Modify `apps/backend/src/modules/tbank/tbank.controller.spec.ts`
- Modify `apps/backend/src/modules/tbank/tbank.module.ts`
### Frontend
- Modify `apps/frontend/src/shared/api/responses.ts`
- Modify `apps/frontend/src/shared/api/index.ts`
- Modify `apps/frontend/src/shared/api/types.ts` through codegen if Swagger changes are published
- Create `apps/frontend/src/entities/broker-event/api/brokerEventApi.ts`
- Create `apps/frontend/src/entities/broker-event/model/useBrokerEvents.ts`
- Create `apps/frontend/src/entities/broker-event/index.ts`
- Create `apps/frontend/src/pages/broker-events/ui/BrokerEventsPage.tsx`
- Create `apps/frontend/src/pages/broker-events/index.ts`
- Create `apps/frontend/src/widgets/broker-events-overview/ui/BrokerEventsOverview.tsx`
- Create `apps/frontend/src/widgets/broker-events-overview/index.ts`
- Modify `apps/frontend/src/widgets/broker-account-layout/ui/BrokerAccountLayout.tsx`
- Modify `apps/frontend/src/pages/broker-account/ui/BrokerAccountOverviewPage.tsx`
- Modify `apps/frontend/src/app/routing/AppRoutes.tsx`
### Documentation
- Modify `apps/docs/docs/backend/tbank-invest.md`
- Modify `apps/docs/docs/backend/modules.md` if the public module description changes materially
## Rollout shape
### Overview
Overview получает компактный блок ближайших событий:
- не более 5 событий;
- summary по ближайшим выплатам не обязателен, если он перегружает карточку;
- ссылка ведёт в полный раздел `События`.
### Dedicated page
Страница `События` включает:
- выбор диапазона дат;
- summary cards;
- список событий;
- loading, empty и error states.
## Verification strategy
- Backend unit tests для event mapping, summary и partial failures.
- Backend controller tests для query validation и response shape.
- Frontend hook tests для query lifecycle.
- Frontend page/component tests для фильтра периода, summary и overview-виджета.
- Final verification:
- `npm test -w apps/backend`
- `npm test -w apps/frontend`
- `npm run build -w apps/backend`
- `npm run build -w apps/frontend`
- `npm run build -w apps/docs` если публичные docs менялись
- `npm run codegen -w apps/frontend` если Swagger-контракт обновлён
### Frontend date range UI
Даты и типы хранятся в URL-параметрах `from` / `to` / `types` для возможности поделиться ссылкой.
На странице — два date input из дизайн-системы и multi-select типов событий. dayjs используется для
форматирования, валидации и дефолтов.
Поток данных:
```
useSearchParams → applied filters → useBrokerEvents(query) → TanStack Query
local draft state → button "Показать" → setSearchParams(applied filters)
```
Дефолт при первом визите без параметров: today-7d today+7d и все типы событий. Валидация: to >=
from, выбран хотя бы один тип. При невалидных фильтрах кнопка `Показать` disabled или показывает
ошибку, запрос не выполняется. Изменение полей не запускает запрос до применения.
## Follow-up implementation tasks
### Backend
1. Расширить `BrokerEventsQueryDto` параметром `types` с валидацией comma-separated значений.
2. Расширить `BrokerPortfolioEvent` и Swagger DTO полями `source`, `actualAmount`, nullable
`estimateMode`.
3. Расширить `BrokerEventsSummary` раздельными полями факта и прогноза.
4. Добавить в `BrokerEventsService` фильтрацию типов и cache key с `types`.
5. Инжектировать `BrokerOperationsService` в `BrokerEventsService` и строить actual events по
исполненным операциям для прошедшей части диапазона.
6. Исключить дубли forecast/actual для прошедших cashflow-событий по ключу `type + ticker + date`.
7. Обновить backend unit/controller tests на query `types`, actual events и summary.
### Frontend
1. Расширить handwritten response types и `BrokerEventsQuery` параметром `types`.
2. Обновить `useBrokerEvents` query key с учётом `types`.
3. Переделать `BrokerEventsPage` на applied filters + draft filters + кнопку `Показать`.
4. Добавить multi-select типов событий по дизайн-системе/текущим UI-паттернам проекта.
5. Отобразить `Факт`/`Прогноз`, `Поступило`, зелёное выделение actual-сумм и раздельный summary.
6. Обновить frontend tests на отсутствие запроса при черновом изменении фильтров, применение по
кнопке, multi-select типов и actual event styling.