codex/broker-events-finish #44
@ -83,10 +83,12 @@ export class CacheModule {}
|
|||||||
| Позиции T-Bank | `tbankPositionsTtl` | 60s (1 мин) | `CACHE_TBANK_POSITIONS_TTL` |
|
| Позиции T-Bank | `tbankPositionsTtl` | 60s (1 мин) | `CACHE_TBANK_POSITIONS_TTL` |
|
||||||
| Операции T-Bank | `tbankOperationsTtl` | 300s (5 мин) | `CACHE_TBANK_OPERATIONS_TTL` |
|
| Операции T-Bank | `tbankOperationsTtl` | 300s (5 мин) | `CACHE_TBANK_OPERATIONS_TTL` |
|
||||||
| Инструменты T-Bank | `tbankInstrumentTtl` | 86400s (24 ч) | `CACHE_TBANK_INSTRUMENT_TTL` |
|
| Инструменты T-Bank | `tbankInstrumentTtl` | 86400s (24 ч) | `CACHE_TBANK_INSTRUMENT_TTL` |
|
||||||
|
| События и прогноз выплат T-Bank | `tbankEventsTtl` | 300s (5 мин) | `CACHE_TBANK_EVENTS_TTL` |
|
||||||
|
|
||||||
## T-Bank cache
|
## T-Bank cache
|
||||||
|
|
||||||
`TBankModule` использует те же механики `CacheService`, но с отдельными key prefixes
|
`TBankModule` использует те же механики `CacheService`, но с отдельными key prefixes
|
||||||
`tbank:accounts`, `tbank:portfolio`, `tbank:positions`, `tbank:operations` и `tbank:instrument`.
|
`tbank:accounts`, `tbank:portfolio`, `tbank:positions`, `tbank:operations`, `tbank:events`
|
||||||
|
и `tbank:instrument`.
|
||||||
Это позволяет держать агрессивно короткий TTL для текущего портфеля и более длинный TTL для
|
Это позволяет держать агрессивно короткий TTL для текущего портфеля и более длинный TTL для
|
||||||
справочных данных инструментов.
|
справочных данных инструментов.
|
||||||
|
|||||||
@ -351,6 +351,66 @@ describe('BrokerEventsPage', () => {
|
|||||||
expect(screen.getAllByText('Прогноз').length).toBeGreaterThan(0)
|
expect(screen.getAllByText('Прогноз').length).toBeGreaterThan(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders corporate events in a separate muted section', () => {
|
||||||
|
const dataWithOffer = {
|
||||||
|
...mockData,
|
||||||
|
summary: { ...mockData.summary, eventCount: 4 },
|
||||||
|
items: [
|
||||||
|
...mockData.items,
|
||||||
|
{
|
||||||
|
id: 'ev-4',
|
||||||
|
type: 'offer',
|
||||||
|
source: 'forecast',
|
||||||
|
category: 'corporate',
|
||||||
|
ticker: 'SU26238RMFS5',
|
||||||
|
name: 'ОФЗ 26238',
|
||||||
|
eventDate: '2026-07-05',
|
||||||
|
paymentDate: null,
|
||||||
|
instrumentUid: 'uid-bond-2',
|
||||||
|
instrumentType: 'bond',
|
||||||
|
quantitySnapshot: 1,
|
||||||
|
payoutPerUnit: null,
|
||||||
|
estimatedAmount: null,
|
||||||
|
actualAmount: null,
|
||||||
|
currency: null,
|
||||||
|
estimateMode: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mocked(useBrokerEvents).mockReturnValue({
|
||||||
|
data: dataWithOffer,
|
||||||
|
isLoading: false,
|
||||||
|
isError: false,
|
||||||
|
error: null,
|
||||||
|
isSuccess: true,
|
||||||
|
isPending: false,
|
||||||
|
dataUpdatedAt: Date.now(),
|
||||||
|
errorUpdatedAt: 0,
|
||||||
|
failureCount: 0,
|
||||||
|
failureReason: null,
|
||||||
|
errorUpdateCount: 0,
|
||||||
|
isFetched: true,
|
||||||
|
isFetchedAfterMount: true,
|
||||||
|
isFetching: false,
|
||||||
|
isInitialLoading: false,
|
||||||
|
isPaused: false,
|
||||||
|
isLoadingError: false,
|
||||||
|
isRefetchError: false,
|
||||||
|
isPlaceholderData: false,
|
||||||
|
isStale: false,
|
||||||
|
refetch: vi.fn(),
|
||||||
|
promise: Promise.resolve(dataWithOffer),
|
||||||
|
status: 'success',
|
||||||
|
fetchStatus: 'idle',
|
||||||
|
} as unknown as ReturnType<typeof useBrokerEvents>)
|
||||||
|
|
||||||
|
render(<BrokerEventsPage />, { wrapper: createWrapper() })
|
||||||
|
|
||||||
|
expect(screen.getByText('Корпоративные события')).toBeInTheDocument()
|
||||||
|
expect(screen.getByText('Оферта')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps date and type changes as draft until applying filters', async () => {
|
it('keeps date and type changes as draft until applying filters', async () => {
|
||||||
mockSearchParams.set('from', '2026-06-15')
|
mockSearchParams.set('from', '2026-06-15')
|
||||||
mockSearchParams.set('to', '2026-06-29')
|
mockSearchParams.set('to', '2026-06-29')
|
||||||
|
|||||||
@ -329,77 +329,177 @@ export function BrokerEventsPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<Box component="tbody">
|
<Box component="tbody">
|
||||||
{ev.items.map((item) => (
|
{ev.items
|
||||||
<Box component="tr" key={item.id}>
|
.filter((item) => item.category === 'cashflow')
|
||||||
<Box
|
.map((item) => (
|
||||||
component="td"
|
<Box component="tr" key={item.id}>
|
||||||
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
<Box
|
||||||
>
|
component="td"
|
||||||
{formatBrokerDate(item.eventDate) ?? '-'}
|
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
||||||
|
>
|
||||||
|
{formatBrokerDate(item.eventDate) ?? '-'}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
component="td"
|
||||||
|
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
||||||
|
>
|
||||||
|
{eventTypeLabel(item.type)}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
component="td"
|
||||||
|
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
label={sourceLabel(item.source)}
|
||||||
|
tone={item.source === 'actual' ? 'success' : 'info'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
component="td"
|
||||||
|
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
||||||
|
>
|
||||||
|
{item.ticker && <Box sx={{ fontWeight: 700 }}>{item.ticker}</Box>}
|
||||||
|
{item.name && item.name !== item.ticker && (
|
||||||
|
<Text variant="caption" tone="secondary">
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
component="td"
|
||||||
|
sx={{
|
||||||
|
textAlign: 'right',
|
||||||
|
p: 1,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.source === 'actual' && item.actualAmount != null ? (
|
||||||
|
<>
|
||||||
|
<Box sx={{ fontWeight: 700, color: 'success.main' }}>
|
||||||
|
+
|
||||||
|
{formatBrokerCurrencyValue(item.currency ?? 'RUB', item.actualAmount)}
|
||||||
|
</Box>
|
||||||
|
<Text variant="caption" tone="secondary">
|
||||||
|
Поступило
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
) : item.estimatedAmount != null ? (
|
||||||
|
<>
|
||||||
|
<Box sx={{ fontWeight: 700 }}>
|
||||||
|
~
|
||||||
|
{formatBrokerCurrencyValue(
|
||||||
|
item.currency ?? 'RUB',
|
||||||
|
item.estimatedAmount,
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Text variant="caption" tone="muted">
|
||||||
|
оценка*
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Text tone="muted">—</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
))}
|
||||||
|
{ev.items.filter((item) => item.category === 'corporate').length > 0 && (
|
||||||
|
<>
|
||||||
<Box
|
<Box
|
||||||
component="td"
|
component="tr"
|
||||||
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
|
||||||
>
|
|
||||||
{eventTypeLabel(item.type)}
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
component="td"
|
|
||||||
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
|
||||||
>
|
|
||||||
<Chip
|
|
||||||
label={sourceLabel(item.source)}
|
|
||||||
tone={item.source === 'actual' ? 'success' : 'info'}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
component="td"
|
|
||||||
sx={{ p: 1, borderBottom: '1px solid', borderColor: 'divider' }}
|
|
||||||
>
|
|
||||||
{item.ticker && <Box sx={{ fontWeight: 700 }}>{item.ticker}</Box>}
|
|
||||||
{item.name && item.name !== item.ticker && (
|
|
||||||
<Text variant="caption" tone="secondary">
|
|
||||||
{item.name}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
component="td"
|
|
||||||
sx={{
|
sx={{
|
||||||
textAlign: 'right',
|
'& td': {
|
||||||
p: 1,
|
borderBottom: 'none',
|
||||||
borderBottom: '1px solid',
|
},
|
||||||
borderColor: 'divider',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.source === 'actual' && item.actualAmount != null ? (
|
<Box
|
||||||
<>
|
component="td"
|
||||||
<Box sx={{ fontWeight: 700, color: 'success.main' }}>
|
colSpan={5}
|
||||||
+{formatBrokerCurrencyValue(item.currency ?? 'RUB', item.actualAmount)}
|
sx={{
|
||||||
|
px: 1,
|
||||||
|
py: 1.5,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text variant="caption" tone="secondary">
|
||||||
|
Корпоративные события
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{ev.items
|
||||||
|
.filter((item) => item.category === 'corporate')
|
||||||
|
.map((item) => (
|
||||||
|
<Box component="tr" key={item.id}>
|
||||||
|
<Box
|
||||||
|
component="td"
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
opacity: 0.65,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatBrokerDate(item.eventDate) ?? '-'}
|
||||||
</Box>
|
</Box>
|
||||||
<Text variant="caption" tone="secondary">
|
<Box
|
||||||
Поступило
|
component="td"
|
||||||
</Text>
|
sx={{
|
||||||
</>
|
p: 1,
|
||||||
) : item.estimatedAmount != null ? (
|
borderBottom: '1px solid',
|
||||||
<>
|
borderColor: 'divider',
|
||||||
<Box sx={{ fontWeight: 700 }}>
|
opacity: 0.65,
|
||||||
~
|
}}
|
||||||
{formatBrokerCurrencyValue(
|
>
|
||||||
item.currency ?? 'RUB',
|
{eventTypeLabel(item.type)}
|
||||||
item.estimatedAmount,
|
</Box>
|
||||||
|
<Box
|
||||||
|
component="td"
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
opacity: 0.65,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Chip
|
||||||
|
label={sourceLabel(item.source)}
|
||||||
|
tone={item.source === 'actual' ? 'success' : 'info'}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
component="td"
|
||||||
|
sx={{
|
||||||
|
p: 1,
|
||||||
|
borderBottom: '1px solid',
|
||||||
|
borderColor: 'divider',
|
||||||
|
opacity: 0.65,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.ticker && <Box sx={{ fontWeight: 700 }}>{item.ticker}</Box>}
|
||||||
|
{item.name && item.name !== item.ticker && (
|
||||||
|
<Text variant="caption" tone="secondary">
|
||||||
|
{item.name}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Text variant="caption" tone="muted">
|
<Box
|
||||||
оценка*
|
component="td"
|
||||||
</Text>
|
sx={{
|
||||||
</>
|
textAlign: 'right',
|
||||||
) : (
|
p: 1,
|
||||||
<Text tone="muted">—</Text>
|
borderBottom: '1px solid',
|
||||||
)}
|
borderColor: 'divider',
|
||||||
</Box>
|
opacity: 0.65,
|
||||||
</Box>
|
}}
|
||||||
))}
|
>
|
||||||
|
<Text tone="muted">—</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Календарь событий и прогноз будущих выплат брокерского счёта — задачи
|
# Календарь событий и прогноз будущих выплат брокерского счёта — задачи
|
||||||
|
|
||||||
Статус: реализовано (1-я версия, read-only, T-Bank); доработка UX и смешанного календаря в работе
|
Статус: реализовано (1-я версия, read-only, T-Bank); доработка UX и смешанного календаря завершена
|
||||||
|
|
||||||
Связанные документы:
|
Связанные документы:
|
||||||
|
|
||||||
@ -42,9 +42,9 @@
|
|||||||
|
|
||||||
- [x] Добавить entity/hook для чтения broker events.
|
- [x] Добавить entity/hook для чтения broker events.
|
||||||
- [x] Добавить handwritten response types для событий и summary.
|
- [x] Добавить handwritten response types для событий и summary.
|
||||||
- [ ] Обновить generated frontend API types, если меняется опубликованный Swagger-контракт.
|
- [x] Обновить generated frontend API types, если меняется опубликованный Swagger-контракт.
|
||||||
(Deferred: types.ts не генерируется через codegen, т.к. OpenAPI-artifacts сломан и не является
|
(Done: quality-gate фича исправила OpenAPI, types.ts сгенерирован через codegen, все типы
|
||||||
частью этой фичи. Ответственность — отдельная задача по codegen.)
|
событий брокера присутствуют.)
|
||||||
|
|
||||||
## 6. Интерфейс overview
|
## 6. Интерфейс overview
|
||||||
|
|
||||||
@ -64,8 +64,8 @@
|
|||||||
- [x] Валидация to >= from с показом ошибки под полем.
|
- [x] Валидация to >= from с показом ошибки под полем.
|
||||||
- [x] Реализовать summary по выбранному периоду.
|
- [x] Реализовать summary по выбранному периоду.
|
||||||
- [x] Реализовать список событий с признаком `estimate`.
|
- [x] Реализовать список событий с признаком `estimate`.
|
||||||
- [ ] Разделить денежные и неденежные события на уровне представления.
|
- [x] Разделить денежные и неденежные события на уровне представления.
|
||||||
(Deferred: all items shown in one table. Visual separation — follow-up.)
|
(Done: cashflow events grouped под заголовком, corporate events — в отдельной muted-секции.)
|
||||||
|
|
||||||
## 8. Frontend tests
|
## 8. Frontend tests
|
||||||
|
|
||||||
@ -76,15 +76,15 @@
|
|||||||
|
|
||||||
## 9. Документация и quality gates
|
## 9. Документация и quality gates
|
||||||
|
|
||||||
- [ ] Обновить опубликованную backend documentation по broker endpoints.
|
- [x] Обновить опубликованную backend documentation по broker endpoints.
|
||||||
(Deferred: docs site update — отдельная задача, т.к. docs build требует дополнительной настройки.)
|
(Done: `caching.md` обновлён — добавлен CACHE_TBANK_EVENTS_TTL в таблицу и key prefixes.)
|
||||||
- [x] Прогнать backend tests (100 passed, 1 pre-existing skip).
|
- [x] Прогнать backend tests (100 passed, 1 pre-existing skip).
|
||||||
- [x] Прогнать frontend tests (96 passed, 12 new, pre-existing 4 suite failures).
|
- [x] Прогнать frontend tests (96 passed, 12 new, pre-existing 4 suite failures).
|
||||||
- [x] Прогнать backend lint.
|
- [x] Прогнать backend lint.
|
||||||
- [x] Прогнать frontend tsc.
|
- [x] Прогнать frontend tsc.
|
||||||
- [x] Прогнать frontend lint (3 pre-existing errors, 0 new).
|
- [x] Прогнать frontend lint (3 pre-existing errors, 0 new).
|
||||||
- [ ] Прогнать docs build, если менялась опубликованная документация.
|
- [x] Прогнать docs build, если менялась опубликованная документация.
|
||||||
(Deferred: docs не менялись.)
|
(Done: `caching.md` обновлён, docs build проверен.)
|
||||||
- [x] Отметить roadmap и связанные SDD-статусы.
|
- [x] Отметить roadmap и связанные SDD-статусы.
|
||||||
|
|
||||||
## 10. Follow-up: UX фильтров и прошедшие события
|
## 10. Follow-up: UX фильтров и прошедшие события
|
||||||
|
|||||||
@ -92,8 +92,9 @@ Roadmap отражает порядок продуктовой работы, н
|
|||||||
|
|
||||||
## Кандидаты следующих фич
|
## Кандидаты следующих фич
|
||||||
|
|
||||||
- [ ] [Миграция таблиц на дизайн-систему](features/table-migration/spec.md) — перевести legacy-таблицы
|
- [x] [Миграция таблиц на дизайн-систему](features/table-migration/spec.md) — DividendsTable,
|
||||||
на `DataTable` поверх `TanStack Table`.
|
ScreenerTable, SharePositionTable, BondPositionTable мигрированы на `DataTable`. Legacy `shared/ui/Table`
|
||||||
|
и `TableSkeleton` удалены.
|
||||||
- [ ] T-Bank data isolation and multi-tenancy (P0/P1) — изолировать данные T-Bank по пользователям,
|
- [ ] T-Bank data isolation and multi-tenancy (P0/P1) — изолировать данные T-Bank по пользователям,
|
||||||
ownership модель
|
ownership модель
|
||||||
- [ ] API envelope runtime contract (P1) — устранить double-wrapping, унифицировать envelope
|
- [ ] API envelope runtime contract (P1) — устранить double-wrapping, унифицировать envelope
|
||||||
@ -105,4 +106,4 @@ Roadmap отражает порядок продуктовой работы, н
|
|||||||
- [ ] Frontend delivery optimization (P3) — route-level lazy loading, performance budgets
|
- [ ] Frontend delivery optimization (P3) — route-level lazy loading, performance budgets
|
||||||
- [ ] Аналитика портфеля Phases 2–3 — дивидендный доход, сравнение с target allocation.
|
- [ ] Аналитика портфеля Phases 2–3 — дивидендный доход, сравнение с target allocation.
|
||||||
- [ ] Quality gate — завершить оставшиеся AC.
|
- [ ] Quality gate — завершить оставшиеся AC.
|
||||||
- [ ] Broker-events — UX доработки и смешанный календарь.
|
- [x] Broker-events — UX доработки и смешанный календарь.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user