feat: migrate services to ApiEnvelopePayload, controllers to plain returns

This commit is contained in:
Sergey Krylov 2026-06-24 19:39:30 +03:00
parent 3a89cef76c
commit b092caf8d6
13 changed files with 82 additions and 154 deletions

View File

@ -41,13 +41,7 @@ export class AuthController {
async register(@Body() dto: RegisterDto, @Res({ passthrough: true }) res: Response) { async register(@Body() dto: RegisterDto, @Res({ passthrough: true }) res: Response) {
const result = await this.authService.register(dto); const result = await this.authService.register(dto);
res.cookie(REFRESH_COOKIE, result.refreshToken, COOKIE_OPTIONS); res.cookie(REFRESH_COOKIE, result.refreshToken, COOKIE_OPTIONS);
return { return { user: result.user, accessToken: result.accessToken };
data: {
user: result.user,
accessToken: result.accessToken,
},
meta: { fromCache: false, cachedAt: null },
};
} }
@Public() @Public()
@ -57,13 +51,7 @@ export class AuthController {
async login(@Body() dto: LoginDto, @Res({ passthrough: true }) res: Response) { async login(@Body() dto: LoginDto, @Res({ passthrough: true }) res: Response) {
const result = await this.authService.login(dto); const result = await this.authService.login(dto);
res.cookie(REFRESH_COOKIE, result.refreshToken, COOKIE_OPTIONS); res.cookie(REFRESH_COOKIE, result.refreshToken, COOKIE_OPTIONS);
return { return { user: result.user, accessToken: result.accessToken };
data: {
user: result.user,
accessToken: result.accessToken,
},
meta: { fromCache: false, cachedAt: null },
};
} }
@Public() @Public()
@ -75,13 +63,7 @@ export class AuthController {
const token = req.cookies?.[REFRESH_COOKIE]; const token = req.cookies?.[REFRESH_COOKIE];
const result = await this.authService.refresh(token); const result = await this.authService.refresh(token);
res.cookie(REFRESH_COOKIE, result.refreshToken, COOKIE_OPTIONS); res.cookie(REFRESH_COOKIE, result.refreshToken, COOKIE_OPTIONS);
return { return { user: result.user, accessToken: result.accessToken };
data: {
user: result.user,
accessToken: result.accessToken,
},
meta: { fromCache: false, cachedAt: null },
};
} }
@Post('logout') @Post('logout')
@ -92,10 +74,7 @@ export class AuthController {
async logout(@CurrentUser() user: JwtPayload, @Res({ passthrough: true }) res: Response) { async logout(@CurrentUser() user: JwtPayload, @Res({ passthrough: true }) res: Response) {
await this.authService.logout(user.sub); await this.authService.logout(user.sub);
res.clearCookie(REFRESH_COOKIE, { path: '/api/v1/auth' }); res.clearCookie(REFRESH_COOKIE, { path: '/api/v1/auth' });
return { return { message: 'Logged out successfully' };
data: { message: 'Logged out successfully' },
meta: { fromCache: false, cachedAt: null },
};
} }
@Get('me') @Get('me')
@ -103,11 +82,7 @@ export class AuthController {
@ApiOperation({ summary: 'Get current user profile' }) @ApiOperation({ summary: 'Get current user profile' })
@ApiOkResponse({ type: AuthProfileResponseDto }) @ApiOkResponse({ type: AuthProfileResponseDto })
async getProfile(@CurrentUser() user: JwtPayload) { async getProfile(@CurrentUser() user: JwtPayload) {
const profile = await this.authService.getProfile(user.sub); return this.authService.getProfile(user.sub);
return {
data: profile,
meta: { fromCache: false, cachedAt: null },
};
} }
@Patch('me') @Patch('me')
@ -115,10 +90,6 @@ export class AuthController {
@ApiOperation({ summary: 'Update current user profile' }) @ApiOperation({ summary: 'Update current user profile' })
@ApiOkResponse({ type: AuthProfileResponseDto }) @ApiOkResponse({ type: AuthProfileResponseDto })
async updateProfile(@CurrentUser() user: JwtPayload, @Body() dto: UpdateProfileDto) { async updateProfile(@CurrentUser() user: JwtPayload, @Body() dto: UpdateProfileDto) {
const profile = await this.authService.updateProfile(user.sub, dto); return this.authService.updateProfile(user.sub, dto);
return {
data: profile,
meta: { fromCache: false, cachedAt: null },
};
} }
} }

View File

@ -1,6 +1,7 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { MoexClientService } from '../moex-client/moex-client.service'; import { MoexClientService } from '../moex-client/moex-client.service';
import { CacheService } from '../cache/cache.service'; import { CacheService } from '../cache/cache.service';
import { ApiEnvelopePayload } from '../../common/dto/api-response.dto';
@Injectable() @Injectable()
export class BondsService { export class BondsService {
@ -32,8 +33,8 @@ export class BondsService {
'marketDataTtl', 'marketDataTtl',
); );
return { return new ApiEnvelopePayload(
data: { {
secid: bond.secid, secid: bond.secid,
isin: bond.isin, isin: bond.isin,
name: bond.shortName, name: bond.shortName,
@ -70,8 +71,9 @@ export class BondsService {
: new Date().toISOString(), : new Date().toISOString(),
}, },
}, },
meta: { fromCache, cachedAt }, fromCache,
}; cachedAt,
);
} }
async getMarketData(secid: string) { async getMarketData(secid: string) {
@ -90,8 +92,8 @@ export class BondsService {
throw new NotFoundException(`Market data for bond ${secid} not found`); throw new NotFoundException(`Market data for bond ${secid} not found`);
} }
return { return new ApiEnvelopePayload(
data: { {
price: mkt.last ?? 0, price: mkt.last ?? 0,
yieldToMaturity: mkt.yield ?? null, yieldToMaturity: mkt.yield ?? null,
duration: mkt.duration ?? null, duration: mkt.duration ?? null,
@ -107,8 +109,9 @@ export class BondsService {
? new Date().toISOString().split('T')[0] + 'T' + mkt.updateTime ? new Date().toISOString().split('T')[0] + 'T' + mkt.updateTime
: new Date().toISOString(), : new Date().toISOString(),
}, },
meta: { fromCache, cachedAt }, fromCache,
}; cachedAt,
);
} }
async getHistory(secid: string, from: string, till: string) { async getHistory(secid: string, from: string, till: string) {
@ -119,14 +122,15 @@ export class BondsService {
'historyTtl', 'historyTtl',
); );
return { return new ApiEnvelopePayload(
data: data.map((h) => ({ data.map((h) => ({
date: h.tradeDate, date: h.tradeDate,
closePrice: h.legalClosePrice ?? h.close ?? 0, closePrice: h.legalClosePrice ?? h.close ?? 0,
yieldClose: h.yieldClose ?? null, yieldClose: h.yieldClose ?? null,
duration: h.duration ?? null, duration: h.duration ?? null,
})), })),
meta: { fromCache, cachedAt }, fromCache,
}; cachedAt,
);
} }
} }

View File

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { MoexClientService } from '../moex-client/moex-client.service'; import { MoexClientService } from '../moex-client/moex-client.service';
import { CacheService } from '../cache/cache.service'; import { CacheService } from '../cache/cache.service';
import { CandleInterval } from './dto/candles-query.dto'; import { CandleInterval } from './dto/candles-query.dto';
import { ApiEnvelopePayload } from '../../common/dto/api-response.dto';
@Injectable() @Injectable()
export class CandlesService { export class CandlesService {
@ -29,8 +30,8 @@ export class CandlesService {
'candlesTtl', 'candlesTtl',
); );
return { return new ApiEnvelopePayload(
data: data.map((c) => ({ data.map((c) => ({
open: c.open, open: c.open,
high: c.high, high: c.high,
low: c.low, low: c.low,
@ -40,7 +41,8 @@ export class CandlesService {
begin: c.begin, begin: c.begin,
end: c.end, end: c.end,
})), })),
meta: { fromCache, cachedAt }, fromCache,
}; cachedAt,
);
} }
} }

View File

@ -26,12 +26,8 @@ import {
const nullDataEnvelopeSchema = { const nullDataEnvelopeSchema = {
type: 'object', type: 'object',
properties: { properties: {
data: { data: { type: 'null' },
type: 'null', meta: { $ref: getSchemaPath(PortfolioResponseMetaDto) },
},
meta: {
$ref: getSchemaPath(PortfolioResponseMetaDto),
},
}, },
required: ['data', 'meta'], required: ['data', 'meta'],
}; };
@ -47,24 +43,21 @@ export class PortfolioController {
@ApiOperation({ summary: 'Get all portfolios for current user' }) @ApiOperation({ summary: 'Get all portfolios for current user' })
@ApiOkResponse({ type: PortfolioListEnvelopeDto }) @ApiOkResponse({ type: PortfolioListEnvelopeDto })
async findAll(@CurrentUser() user: { sub: number }) { async findAll(@CurrentUser() user: { sub: number }) {
const portfolios = await this.portfolioService.findAll(user.sub); return this.portfolioService.findAll(user.sub);
return { data: portfolios, meta: { cachedAt: null, fromCache: false } };
} }
@Post() @Post()
@ApiOperation({ summary: 'Create a new portfolio' }) @ApiOperation({ summary: 'Create a new portfolio' })
@ApiCreatedResponse({ type: PortfolioEnvelopeDto }) @ApiCreatedResponse({ type: PortfolioEnvelopeDto })
async create(@CurrentUser() user: { sub: number }, @Body() dto: CreatePortfolioDto) { async create(@CurrentUser() user: { sub: number }, @Body() dto: CreatePortfolioDto) {
const portfolio = await this.portfolioService.create(user.sub, dto); return this.portfolioService.create(user.sub, dto);
return { data: portfolio, meta: { cachedAt: null, fromCache: false } };
} }
@Get(':id') @Get(':id')
@ApiOperation({ summary: 'Get portfolio details with positions and prices' }) @ApiOperation({ summary: 'Get portfolio details with positions and prices' })
@ApiOkResponse({ type: PortfolioDetailEnvelopeDto }) @ApiOkResponse({ type: PortfolioDetailEnvelopeDto })
async findOne(@CurrentUser() user: { sub: number }, @Param('id', ParseIntPipe) id: number) { async findOne(@CurrentUser() user: { sub: number }, @Param('id', ParseIntPipe) id: number) {
const portfolio = await this.portfolioService.findOne(user.sub, id); return this.portfolioService.findOne(user.sub, id);
return { data: portfolio, meta: { cachedAt: null, fromCache: false } };
} }
@Patch(':id') @Patch(':id')
@ -75,8 +68,7 @@ export class PortfolioController {
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
@Body() dto: UpdatePortfolioDto, @Body() dto: UpdatePortfolioDto,
) { ) {
const portfolio = await this.portfolioService.update(user.sub, id, dto); return this.portfolioService.update(user.sub, id, dto);
return { data: portfolio, meta: { cachedAt: null, fromCache: false } };
} }
@Delete(':id') @Delete(':id')
@ -84,7 +76,7 @@ export class PortfolioController {
@ApiOkResponse({ schema: nullDataEnvelopeSchema }) @ApiOkResponse({ schema: nullDataEnvelopeSchema })
async remove(@CurrentUser() user: { sub: number }, @Param('id', ParseIntPipe) id: number) { async remove(@CurrentUser() user: { sub: number }, @Param('id', ParseIntPipe) id: number) {
await this.portfolioService.remove(user.sub, id); await this.portfolioService.remove(user.sub, id);
return { data: null, meta: { cachedAt: null, fromCache: false } }; return null;
} }
@Post(':id/positions') @Post(':id/positions')
@ -95,8 +87,7 @@ export class PortfolioController {
@Param('id', ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
@Body() dto: AddPositionDto, @Body() dto: AddPositionDto,
) { ) {
const position = await this.portfolioService.addPosition(user.sub, id, dto); return this.portfolioService.addPosition(user.sub, id, dto);
return { data: position, meta: { cachedAt: null, fromCache: false } };
} }
@Patch(':id/positions/:positionId') @Patch(':id/positions/:positionId')
@ -108,16 +99,14 @@ export class PortfolioController {
@Param('positionId', ParseIntPipe) positionId: number, @Param('positionId', ParseIntPipe) positionId: number,
@Body() dto: UpdatePositionDto, @Body() dto: UpdatePositionDto,
) { ) {
const position = await this.portfolioService.updatePosition(user.sub, id, positionId, dto); return this.portfolioService.updatePosition(user.sub, id, positionId, dto);
return { data: position, meta: { cachedAt: null, fromCache: false } };
} }
@Get(':id/analytics') @Get(':id/analytics')
@ApiOperation({ summary: 'Get portfolio analytics with PnL' }) @ApiOperation({ summary: 'Get portfolio analytics with PnL' })
@ApiOkResponse({ type: AnalyticsEnvelopeDto }) @ApiOkResponse({ type: AnalyticsEnvelopeDto })
async getAnalytics(@CurrentUser() user: { sub: number }, @Param('id', ParseIntPipe) id: number) { async getAnalytics(@CurrentUser() user: { sub: number }, @Param('id', ParseIntPipe) id: number) {
const result = await this.portfolioService.getAnalytics(user.sub, id); return this.portfolioService.getAnalytics(user.sub, id);
return { data: result, meta: { cachedAt: null, fromCache: false } };
} }
@Delete(':id/positions/:positionId') @Delete(':id/positions/:positionId')
@ -129,6 +118,6 @@ export class PortfolioController {
@Param('positionId', ParseIntPipe) positionId: number, @Param('positionId', ParseIntPipe) positionId: number,
) { ) {
await this.portfolioService.removePosition(user.sub, id, positionId); await this.portfolioService.removePosition(user.sub, id, positionId);
return { data: null, meta: { cachedAt: null, fromCache: false } }; return null;
} }
} }

View File

@ -21,19 +21,17 @@ export class SecuritiesController {
@ApiOperation({ summary: 'Поиск по инструментам' }) @ApiOperation({ summary: 'Поиск по инструментам' })
@ApiOkResponse({ type: SearchEnvelopeDto }) @ApiOkResponse({ type: SearchEnvelopeDto })
async search(@Query(ValidationPipe) query: SearchQueryDto) { async search(@Query(ValidationPipe) query: SearchQueryDto) {
const results = await this.securitiesService.search( return this.securitiesService.search(
query.q, query.q,
query.type || SecurityType.ALL, query.type || SecurityType.ALL,
query.limit || 20, query.limit || 20,
); );
return { data: results, meta: { cachedAt: null, fromCache: false } };
} }
@Get('screener') @Get('screener')
@ApiOperation({ summary: 'Фильтр ценных бумаг по параметрам' }) @ApiOperation({ summary: 'Фильтр ценных бумаг по параметрам' })
@ApiOkResponse({ type: ScreenerResponseDto }) @ApiOkResponse({ type: ScreenerResponseDto })
async screener(@Query(ValidationPipe) query: ScreenerQueryDto) { async screener(@Query(ValidationPipe) query: ScreenerQueryDto) {
const result = await this.screenerService.screen(query); return this.screenerService.screen(query);
return { data: result, meta: { cachedAt: null, fromCache: false } };
} }
} }

View File

@ -19,8 +19,7 @@ export class SharesController {
@ApiOperation({ summary: 'Получить спецификацию акции' }) @ApiOperation({ summary: 'Получить спецификацию акции' })
@ApiOkResponse({ type: ShareEnvelopeDto }) @ApiOkResponse({ type: ShareEnvelopeDto })
async getShare(@Param('secid') secid: string) { async getShare(@Param('secid') secid: string) {
const share = await this.sharesService.getShare(secid); return this.sharesService.getShare(secid);
return { data: share, meta: { cachedAt: null, fromCache: false } };
} }
@Get(':secid/marketdata') @Get(':secid/marketdata')

View File

@ -1,6 +1,7 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { MoexClientService } from '../moex-client/moex-client.service'; import { MoexClientService } from '../moex-client/moex-client.service';
import { CacheService } from '../cache/cache.service'; import { CacheService } from '../cache/cache.service';
import { ApiEnvelopePayload } from '../../common/dto/api-response.dto';
@Injectable() @Injectable()
export class SharesService { export class SharesService {
@ -77,8 +78,8 @@ export class SharesService {
throw new NotFoundException(`Market data for ${secid} not found`); throw new NotFoundException(`Market data for ${secid} not found`);
} }
return { return new ApiEnvelopePayload(
data: { {
price: marketData.last ?? 0, price: marketData.last ?? 0,
change: marketData.lastChange ?? 0, change: marketData.lastChange ?? 0,
changePercent: marketData.lastChangePrcnt ?? 0, changePercent: marketData.lastChangePrcnt ?? 0,
@ -92,8 +93,9 @@ export class SharesService {
? new Date().toISOString().split('T')[0] + 'T' + marketData.updateTime ? new Date().toISOString().split('T')[0] + 'T' + marketData.updateTime
: new Date().toISOString(), : new Date().toISOString(),
}, },
meta: { fromCache, cachedAt }, fromCache,
}; cachedAt,
);
} }
async getDividends(secid: string) { async getDividends(secid: string) {
@ -104,14 +106,15 @@ export class SharesService {
'dividendsTtl', 'dividendsTtl',
); );
return { return new ApiEnvelopePayload(
data: data.map((d) => ({ data.map((d) => ({
registryCloseDate: d.registryCloseDate, registryCloseDate: d.registryCloseDate,
value: d.value, value: d.value,
currency: d.currencyId, currency: d.currencyId,
})), })),
meta: { fromCache, cachedAt }, fromCache,
}; cachedAt,
);
} }
async getHistory(secid: string, from: string, till: string) { async getHistory(secid: string, from: string, till: string) {
@ -122,8 +125,8 @@ export class SharesService {
'historyTtl', 'historyTtl',
); );
return { return new ApiEnvelopePayload(
data: data.map((h) => ({ data.map((h) => ({
date: h.tradeDate, date: h.tradeDate,
open: h.open ?? 0, open: h.open ?? 0,
high: h.high ?? 0, high: h.high ?? 0,
@ -132,7 +135,8 @@ export class SharesService {
volume: h.volume, volume: h.volume,
value: h.value, value: h.value,
})), })),
meta: { fromCache, cachedAt }, fromCache,
}; cachedAt,
);
} }
} }

View File

@ -5,6 +5,7 @@ import { TBANK_CACHE_KEYS } from '../tbank.config';
import type { BrokerAccount } from '../types/broker.types'; import type { BrokerAccount } from '../types/broker.types';
import type { TBankAccountsResponse } from '../types/tbank-proto.types'; import type { TBankAccountsResponse } from '../types/tbank-proto.types';
import { TBankClientService } from './tbank-client.service'; import { TBankClientService } from './tbank-client.service';
import { ApiEnvelopePayload } from '../../../common/dto/api-response.dto';
@Injectable() @Injectable()
export class BrokerAccountsService { export class BrokerAccountsService {
@ -13,10 +14,7 @@ export class BrokerAccountsService {
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
) {} ) {}
async findAll(): Promise<{ async findAll(): Promise<ApiEnvelopePayload<BrokerAccount[]>> {
data: BrokerAccount[];
meta: { fromCache: boolean; cachedAt: string | null };
}> {
const result = await this.cacheService.getOrFetch( const result = await this.cacheService.getOrFetch(
TBANK_CACHE_KEYS.accounts, TBANK_CACHE_KEYS.accounts,
['open-brokerage-iis'], ['open-brokerage-iis'],
@ -24,10 +22,7 @@ export class BrokerAccountsService {
'tbankAccountsTtl', 'tbankAccountsTtl',
); );
return { return new ApiEnvelopePayload(result.data, result.fromCache, result.cachedAt);
data: result.data,
meta: { fromCache: result.fromCache, cachedAt: result.cachedAt },
};
} }
async findById(accountId: string): Promise<BrokerAccount | null> { async findById(accountId: string): Promise<BrokerAccount | null> {

View File

@ -1,6 +1,7 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../../prisma/prisma.service'; import { PrismaService } from '../../prisma/prisma.service';
import { CacheService } from '../../cache/cache.service'; import { CacheService } from '../../cache/cache.service';
import { ApiEnvelopePayload } from '../../../common/dto/api-response.dto';
import { BrokerAnalyticsDto } from '../dto/broker-analytics-response.dto'; import { BrokerAnalyticsDto } from '../dto/broker-analytics-response.dto';
import { BrokerAccountsService } from './broker-accounts.service'; import { BrokerAccountsService } from './broker-accounts.service';
import { TBANK_CACHE_KEYS } from '../tbank.config'; import { TBANK_CACHE_KEYS } from '../tbank.config';
@ -41,10 +42,7 @@ export class BrokerAnalyticsService {
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
) {} ) {}
async getAnalytics(accountId: string): Promise<{ async getAnalytics(accountId: string): Promise<ApiEnvelopePayload<BrokerAnalyticsDto>> {
data: BrokerAnalyticsDto;
meta: { fromCache: boolean; cachedAt: string | null };
}> {
const account = await this.accountsService.findById(accountId); const account = await this.accountsService.findById(accountId);
if (!account) throw new NotFoundException('Broker account not found'); if (!account) throw new NotFoundException('Broker account not found');
@ -55,10 +53,7 @@ export class BrokerAnalyticsService {
'tbankAnalyticsTtl', 'tbankAnalyticsTtl',
); );
return { return new ApiEnvelopePayload(result.data, result.fromCache, result.cachedAt);
data: result.data,
meta: { fromCache: result.fromCache, cachedAt: result.cachedAt },
};
} }
private async computeAnalytics(accountId: string): Promise<BrokerAnalyticsDto> { private async computeAnalytics(accountId: string): Promise<BrokerAnalyticsDto> {

View File

@ -1,6 +1,7 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { CacheService } from '../../cache/cache.service'; import { CacheService } from '../../cache/cache.service';
import { MoexClientService } from '../../moex-client/moex-client.service'; import { MoexClientService } from '../../moex-client/moex-client.service';
import { ApiEnvelopePayload } from '../../../common/dto/api-response.dto';
import { TBANK_CACHE_KEYS } from '../tbank.config'; import { TBANK_CACHE_KEYS } from '../tbank.config';
import { mapQuotationToNumber } from '../mappers/money.mapper'; import { mapQuotationToNumber } from '../mappers/money.mapper';
import type { import type {
@ -52,10 +53,7 @@ export class BrokerEventsService {
async getEvents( async getEvents(
accountId: string, accountId: string,
query: BrokerEventsQuery, query: BrokerEventsQuery,
): Promise<{ ): Promise<ApiEnvelopePayload<BrokerEventsData>> {
data: BrokerEventsData;
meta: { fromCache: boolean; cachedAt: string | null };
}> {
const account = await this.accountsService.findById(accountId); const account = await this.accountsService.findById(accountId);
if (!account) throw new NotFoundException('Broker account not found'); if (!account) throw new NotFoundException('Broker account not found');
@ -68,10 +66,7 @@ export class BrokerEventsService {
'tbankPortfolioTtl', 'tbankPortfolioTtl',
); );
return { return new ApiEnvelopePayload(result.data, result.fromCache, result.cachedAt);
data: result.data,
meta: { fromCache: result.fromCache, cachedAt: result.cachedAt },
};
} }
private async buildEvents( private async buildEvents(

View File

@ -1,5 +1,6 @@
import { Injectable, NotFoundException } from '@nestjs/common'; import { Injectable, NotFoundException } from '@nestjs/common';
import { CacheService } from '../../cache/cache.service'; import { CacheService } from '../../cache/cache.service';
import { ApiEnvelopePayload } from '../../../common/dto/api-response.dto';
import type { BrokerOperationQueryDto } from '../dto/broker-operation-query.dto'; import type { BrokerOperationQueryDto } from '../dto/broker-operation-query.dto';
import { mapOperationsPage } from '../mappers/operation.mapper'; import { mapOperationsPage } from '../mappers/operation.mapper';
import { TBANK_CACHE_KEYS } from '../tbank.config'; import { TBANK_CACHE_KEYS } from '../tbank.config';
@ -19,10 +20,7 @@ export class BrokerOperationsService {
async getOperations( async getOperations(
accountId: string, accountId: string,
query: BrokerOperationQueryDto, query: BrokerOperationQueryDto,
): Promise<{ ): Promise<ApiEnvelopePayload<BrokerOperationsPage>> {
data: BrokerOperationsPage;
meta: { fromCache: boolean; cachedAt: string | null };
}> {
const account = await this.accountsService.findById(accountId); const account = await this.accountsService.findById(accountId);
if (!account) throw new NotFoundException('Broker account not found'); if (!account) throw new NotFoundException('Broker account not found');
@ -34,10 +32,7 @@ export class BrokerOperationsService {
'tbankOperationsTtl', 'tbankOperationsTtl',
); );
return { return new ApiEnvelopePayload(result.data, result.fromCache, result.cachedAt);
data: result.data,
meta: { fromCache: result.fromCache, cachedAt: result.cachedAt },
};
} }
private buildRequest(accountId: string, query: BrokerOperationQueryDto): Record<string, unknown> { private buildRequest(accountId: string, query: BrokerOperationQueryDto): Record<string, unknown> {

View File

@ -12,6 +12,7 @@ import type {
import { BrokerAccountsService } from './broker-accounts.service'; import { BrokerAccountsService } from './broker-accounts.service';
import { BrokerInstrumentsService } from './broker-instruments.service'; import { BrokerInstrumentsService } from './broker-instruments.service';
import { TBankClientService } from './tbank-client.service'; import { TBankClientService } from './tbank-client.service';
import { ApiEnvelopePayload } from '../../../common/dto/api-response.dto';
@Injectable() @Injectable()
export class BrokerPortfolioService { export class BrokerPortfolioService {
@ -22,10 +23,7 @@ export class BrokerPortfolioService {
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
) {} ) {}
async getPortfolio(accountId: string): Promise<{ async getPortfolio(accountId: string): Promise<ApiEnvelopePayload<BrokerPortfolio>> {
data: BrokerPortfolio;
meta: { fromCache: boolean; cachedAt: string | null };
}> {
const account = await this.accountsService.findById(accountId); const account = await this.accountsService.findById(accountId);
if (!account) throw new NotFoundException('Broker account not found'); if (!account) throw new NotFoundException('Broker account not found');
@ -43,10 +41,7 @@ export class BrokerPortfolioService {
'tbankPortfolioTtl', 'tbankPortfolioTtl',
); );
return { return new ApiEnvelopePayload(result.data, result.fromCache, result.cachedAt);
data: result.data,
meta: { fromCache: result.fromCache, cachedAt: result.cachedAt },
};
} }
async getPositions( async getPositions(
@ -54,10 +49,7 @@ export class BrokerPortfolioService {
cursor?: string, cursor?: string,
limit = 10, limit = 10,
type?: string, type?: string,
): Promise<{ ): Promise<ApiEnvelopePayload<BrokerPositionsPage>> {
data: BrokerPositionsPage;
meta: { fromCache: boolean; cachedAt: string | null };
}> {
const account = await this.accountsService.findById(accountId); const account = await this.accountsService.findById(accountId);
if (!account) throw new NotFoundException('Broker account not found'); if (!account) throw new NotFoundException('Broker account not found');
@ -87,10 +79,7 @@ export class BrokerPortfolioService {
'tbankPositionsTtl', 'tbankPositionsTtl',
); );
return { return new ApiEnvelopePayload(result.data, result.fromCache, result.cachedAt);
data: result.data,
meta: { fromCache: result.fromCache, cachedAt: result.cachedAt },
};
} }
private async buildInstrumentMap( private async buildInstrumentMap(

View File

@ -1,6 +1,5 @@
import { Controller, Get, Param, Post, Query } from '@nestjs/common'; import { Controller, Get, Param, Post, Query } from '@nestjs/common';
import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
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,
@ -40,16 +39,14 @@ export class TBankController {
@ApiOperation({ summary: 'Get open T-Bank brokerage and IIS accounts' }) @ApiOperation({ summary: 'Get open T-Bank brokerage and IIS accounts' })
@ApiOkResponse({ type: BrokerAccountsEnvelopeDto }) @ApiOkResponse({ type: BrokerAccountsEnvelopeDto })
async getAccounts() { async getAccounts() {
const result = await this.brokerAccountsService.findAll(); return this.brokerAccountsService.findAll();
return new ApiResponse(result.data, result.meta.fromCache, result.meta.cachedAt);
} }
@Get('accounts/:accountId/portfolio') @Get('accounts/:accountId/portfolio')
@ApiOperation({ summary: 'Get T-Bank broker account portfolio with cash and positions' }) @ApiOperation({ summary: 'Get T-Bank broker account portfolio with cash and positions' })
@ApiOkResponse({ type: BrokerPortfolioEnvelopeDto }) @ApiOkResponse({ type: BrokerPortfolioEnvelopeDto })
async getPortfolio(@Param('accountId') accountId: string) { async getPortfolio(@Param('accountId') accountId: string) {
const result = await this.brokerPortfolioService.getPortfolio(accountId); return this.brokerPortfolioService.getPortfolio(accountId);
return new ApiResponse(result.data, result.meta.fromCache, result.meta.cachedAt);
} }
@Get('accounts/:accountId/positions') @Get('accounts/:accountId/positions')
@ -59,13 +56,12 @@ export class TBankController {
@Param('accountId') accountId: string, @Param('accountId') accountId: string,
@Query() query: BrokerPositionQueryDto, @Query() query: BrokerPositionQueryDto,
) { ) {
const result = await this.brokerPortfolioService.getPositions( return this.brokerPortfolioService.getPositions(
accountId, accountId,
query.cursor, query.cursor,
query.limit, query.limit,
query.type, query.type,
); );
return new ApiResponse(result.data, result.meta.fromCache, result.meta.cachedAt);
} }
@Get('accounts/:accountId/operations') @Get('accounts/:accountId/operations')
@ -75,24 +71,21 @@ export class TBankController {
@Param('accountId') accountId: string, @Param('accountId') accountId: string,
@Query() query: BrokerOperationQueryDto, @Query() query: BrokerOperationQueryDto,
) { ) {
const result = await this.brokerOperationsService.getOperations(accountId, query); return this.brokerOperationsService.getOperations(accountId, query);
return new ApiResponse(result.data, result.meta.fromCache, result.meta.cachedAt);
} }
@Get('accounts/:accountId/events') @Get('accounts/:accountId/events')
@ApiOperation({ summary: 'Get broker account calendar events and cashflow' }) @ApiOperation({ summary: 'Get broker account calendar events and cashflow' })
@ApiOkResponse({ type: BrokerEventsEnvelopeDto }) @ApiOkResponse({ type: BrokerEventsEnvelopeDto })
async getEvents(@Param('accountId') accountId: string, @Query() query: BrokerEventsQueryDto) { async getEvents(@Param('accountId') accountId: string, @Query() query: BrokerEventsQueryDto) {
const result = await this.brokerEventsService.getEvents(accountId, query); return this.brokerEventsService.getEvents(accountId, query);
return new ApiResponse(result.data, result.meta.fromCache, result.meta.cachedAt);
} }
@Get('accounts/:accountId/analytics') @Get('accounts/:accountId/analytics')
@ApiOperation({ summary: 'Get broker account profitability analytics' }) @ApiOperation({ summary: 'Get broker account profitability analytics' })
@ApiOkResponse({ type: BrokerAnalyticsEnvelopeDto }) @ApiOkResponse({ type: BrokerAnalyticsEnvelopeDto })
async getAnalytics(@Param('accountId') accountId: string) { async getAnalytics(@Param('accountId') accountId: string) {
const result = await this.brokerAnalyticsService.getAnalytics(accountId); return 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')
@ -102,7 +95,6 @@ export class TBankController {
@Param('accountId') accountId: string, @Param('accountId') accountId: string,
@Query() query: BrokerOperationSyncQueryDto, @Query() query: BrokerOperationSyncQueryDto,
) { ) {
const result = await this.brokerOperationSyncService.syncAccount(accountId, query); return this.brokerOperationSyncService.syncAccount(accountId, query);
return new ApiResponse(result);
} }
} }