feat: wire up broker analytics endpoint, cache config, and module registration

- Add analytics cache key to TBANK_CACHE_KEYS
- Add tbankAnalyticsTtl to cache configuration
- Register and export BrokerAnalyticsService in TBankModule
- Add BrokerAnalyticsEnvelopeDto to broker envelope DTOs
- Add GET /accounts/:accountId/analytics endpoint to TBankController
- Fix TBankController spec to pass new constructor dependency
This commit is contained in:
Sergey Krylov 2026-06-24 09:41:57 +03:00
parent 5184d34262
commit 5cd56b239a
6 changed files with 29 additions and 3 deletions

View File

@ -35,6 +35,7 @@ export default registerAs('app', () => ({
tbankOperationsTtl: parseInt(process.env.CACHE_TBANK_OPERATIONS_TTL || '300', 10), tbankOperationsTtl: parseInt(process.env.CACHE_TBANK_OPERATIONS_TTL || '300', 10),
tbankPositionsTtl: parseInt(process.env.CACHE_TBANK_POSITIONS_TTL || '60', 10), tbankPositionsTtl: parseInt(process.env.CACHE_TBANK_POSITIONS_TTL || '60', 10),
tbankInstrumentTtl: parseInt(process.env.CACHE_TBANK_INSTRUMENT_TTL || '86400', 10), tbankInstrumentTtl: parseInt(process.env.CACHE_TBANK_INSTRUMENT_TTL || '86400', 10),
tbankAnalyticsTtl: parseInt(process.env.CACHE_TBANK_ANALYTICS_TTL || '300', 10),
}, },
auth: { auth: {
jwtSecret: process.env.JWT_SECRET || 'dev-jwt-secret-change-in-production', jwtSecret: process.env.JWT_SECRET || 'dev-jwt-secret-change-in-production',

View File

@ -5,6 +5,7 @@ import { BrokerOperationSyncResponseDto } from './broker-operation-sync-query.dt
import { BrokerOperationsPageResponseDto } from './broker-operation-response.dto'; import { BrokerOperationsPageResponseDto } from './broker-operation-response.dto';
import { BrokerPositionsPageResponseDto } from './broker-positions-page-response.dto'; import { BrokerPositionsPageResponseDto } from './broker-positions-page-response.dto';
import { BrokerPortfolioResponseDto } from './broker-portfolio-response.dto'; import { BrokerPortfolioResponseDto } from './broker-portfolio-response.dto';
import { BrokerAnalyticsDto } from './broker-analytics-response.dto';
export class BrokerResponseMetaDto { export class BrokerResponseMetaDto {
@ApiProperty({ nullable: true }) @ApiProperty({ nullable: true })
@ -54,6 +55,14 @@ export class BrokerOperationSyncEnvelopeDto {
meta!: BrokerResponseMetaDto; meta!: BrokerResponseMetaDto;
} }
export class BrokerAnalyticsEnvelopeDto {
@ApiProperty({ type: BrokerAnalyticsDto })
data!: BrokerAnalyticsDto;
@ApiProperty({ type: BrokerResponseMetaDto })
meta!: BrokerResponseMetaDto;
}
export class BrokerEventsEnvelopeDto { export class BrokerEventsEnvelopeDto {
@ApiProperty({ type: BrokerEventsDataDto }) @ApiProperty({ type: BrokerEventsDataDto })
data!: BrokerEventsDataDto; data!: BrokerEventsDataDto;

View File

@ -20,4 +20,5 @@ export const TBANK_CACHE_KEYS = {
operations: 'tbank:operations', operations: 'tbank:operations',
instrument: 'tbank:instrument', instrument: 'tbank:instrument',
events: 'tbank:events', events: 'tbank:events',
analytics: 'tbank:analytics',
} as const; } as const;

View File

@ -2,6 +2,7 @@ import { ROLES_KEY } from '../auth/decorators/roles.decorator';
import { ApiResponse } from '../../common/dto/api-response.dto'; import { ApiResponse } from '../../common/dto/api-response.dto';
import { TBankController } from './tbank.controller'; import { TBankController } from './tbank.controller';
import { BrokerAccountsService } from './services/broker-accounts.service'; import { BrokerAccountsService } from './services/broker-accounts.service';
import { BrokerAnalyticsService } from './services/broker-analytics.service';
import { BrokerEventsService } from './services/broker-events.service'; import { BrokerEventsService } from './services/broker-events.service';
import { BrokerOperationSyncService } from './services/broker-operation-sync.service'; import { BrokerOperationSyncService } from './services/broker-operation-sync.service';
import { BrokerOperationsService } from './services/broker-operations.service'; import { BrokerOperationsService } from './services/broker-operations.service';
@ -9,6 +10,7 @@ import { BrokerPortfolioService } from './services/broker-portfolio.service';
describe('TBankController', () => { describe('TBankController', () => {
const accounts = { findAll: vi.fn() } as unknown as BrokerAccountsService; const accounts = { findAll: vi.fn() } as unknown as BrokerAccountsService;
const analytics = { getAnalytics: vi.fn() } as unknown as BrokerAnalyticsService;
const portfolio = { getPortfolio: vi.fn() } as unknown as BrokerPortfolioService; const portfolio = { getPortfolio: vi.fn() } as unknown as BrokerPortfolioService;
const events = { getEvents: vi.fn() } as unknown as BrokerEventsService; const events = { getEvents: vi.fn() } as unknown as BrokerEventsService;
const operations = { getOperations: vi.fn() } as unknown as BrokerOperationsService; const operations = { getOperations: vi.fn() } as unknown as BrokerOperationsService;
@ -37,7 +39,7 @@ describe('TBankController', () => {
meta: { fromCache: true, cachedAt: '2026-06-17T00:00:00.000Z' }, meta: { fromCache: true, cachedAt: '2026-06-17T00:00:00.000Z' },
}); });
const controller = new TBankController(accounts, portfolio, events, operations, sync); const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics);
const response = await controller.getAccounts(); const response = await controller.getAccounts();
expect(response).toBeInstanceOf(ApiResponse); expect(response).toBeInstanceOf(ApiResponse);
@ -48,7 +50,7 @@ describe('TBankController', () => {
it('exposes a sync trigger for durable operation history', async () => { it('exposes a sync trigger for durable operation history', async () => {
vi.mocked(sync.syncAccount).mockResolvedValueOnce({ upserted: 2 }); vi.mocked(sync.syncAccount).mockResolvedValueOnce({ upserted: 2 });
const controller = new TBankController(accounts, portfolio, events, operations, sync); const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics);
const response = await controller.syncOperations('acc-1', { const response = await controller.syncOperations('acc-1', {
from: '2026-06-01T00:00:00.000Z', from: '2026-06-01T00:00:00.000Z',
to: '2026-06-17T00:00:00.000Z', to: '2026-06-17T00:00:00.000Z',
@ -84,7 +86,7 @@ describe('TBankController', () => {
meta: { fromCache: false, cachedAt: '2026-06-22T00:00:00.000Z' }, meta: { fromCache: false, cachedAt: '2026-06-22T00:00:00.000Z' },
}); });
const controller = new TBankController(accounts, portfolio, events, operations, sync); const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics);
const query = { from: '2026-06-22', to: '2026-07-29', types: 'dividend,coupon' }; const query = { from: '2026-06-22', to: '2026-07-29', types: 'dividend,coupon' };
const response = await controller.getEvents('acc-1', query); const response = await controller.getEvents('acc-1', query);

View File

@ -4,6 +4,7 @@ import { ApiResponse } from '../../common/dto/api-response.dto';
import { Roles } from '../auth/decorators/roles.decorator'; import { Roles } from '../auth/decorators/roles.decorator';
import { import {
BrokerAccountsEnvelopeDto, BrokerAccountsEnvelopeDto,
BrokerAnalyticsEnvelopeDto,
BrokerEventsEnvelopeDto, BrokerEventsEnvelopeDto,
BrokerOperationSyncEnvelopeDto, BrokerOperationSyncEnvelopeDto,
BrokerOperationsEnvelopeDto, BrokerOperationsEnvelopeDto,
@ -18,6 +19,7 @@ import { BrokerAccountsService } from './services/broker-accounts.service';
import { BrokerEventsService } from './services/broker-events.service'; import { BrokerEventsService } from './services/broker-events.service';
import { BrokerOperationSyncService } from './services/broker-operation-sync.service'; import { BrokerOperationSyncService } from './services/broker-operation-sync.service';
import { BrokerOperationsService } from './services/broker-operations.service'; import { BrokerOperationsService } from './services/broker-operations.service';
import { BrokerAnalyticsService } from './services/broker-analytics.service';
import { BrokerPortfolioService } from './services/broker-portfolio.service'; import { BrokerPortfolioService } from './services/broker-portfolio.service';
@ApiTags('Broker') @ApiTags('Broker')
@ -31,6 +33,7 @@ export class TBankController {
private readonly brokerEventsService: BrokerEventsService, private readonly brokerEventsService: BrokerEventsService,
private readonly brokerOperationsService: BrokerOperationsService, private readonly brokerOperationsService: BrokerOperationsService,
private readonly brokerOperationSyncService: BrokerOperationSyncService, private readonly brokerOperationSyncService: BrokerOperationSyncService,
private readonly brokerAnalyticsService: BrokerAnalyticsService,
) {} ) {}
@Get('accounts') @Get('accounts')
@ -84,6 +87,14 @@ export class TBankController {
return new ApiResponse(result.data, result.meta.fromCache, result.meta.cachedAt); return new ApiResponse(result.data, result.meta.fromCache, result.meta.cachedAt);
} }
@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);
}
@Post('accounts/:accountId/operations/sync') @Post('accounts/:accountId/operations/sync')
@ApiOperation({ summary: 'Synchronize T-Bank broker account operations into local history' }) @ApiOperation({ summary: 'Synchronize T-Bank broker account operations into local history' })
@ApiOkResponse({ type: BrokerOperationSyncEnvelopeDto }) @ApiOkResponse({ type: BrokerOperationSyncEnvelopeDto })

View File

@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { MoexClientModule } from '../moex-client/moex-client.module'; import { MoexClientModule } from '../moex-client/moex-client.module';
import { TBankController } from './tbank.controller'; import { TBankController } from './tbank.controller';
import { BrokerAccountsService } from './services/broker-accounts.service'; import { BrokerAccountsService } from './services/broker-accounts.service';
import { BrokerAnalyticsService } from './services/broker-analytics.service';
import { BrokerInstrumentsService } from './services/broker-instruments.service'; import { BrokerInstrumentsService } from './services/broker-instruments.service';
import { BrokerEventsService } from './services/broker-events.service'; import { BrokerEventsService } from './services/broker-events.service';
import { BrokerOperationSyncService } from './services/broker-operation-sync.service'; import { BrokerOperationSyncService } from './services/broker-operation-sync.service';
@ -20,6 +21,7 @@ import { TBankClientService } from './services/tbank-client.service';
BrokerEventsService, BrokerEventsService,
BrokerOperationsService, BrokerOperationsService,
BrokerOperationSyncService, BrokerOperationSyncService,
BrokerAnalyticsService,
], ],
exports: [ exports: [
TBankClientService, TBankClientService,