feat: migrate services to ApiEnvelopePayload, controllers to plain returns
This commit is contained in:
parent
3a89cef76c
commit
b092caf8d6
@ -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 },
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 } };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user