docs: improve openapi schema metadata
This commit is contained in:
parent
143b4758c3
commit
835686d886
@ -1,10 +1,21 @@
|
|||||||
import { Controller, Post, Get, Patch, Body, Req, Res, HttpCode, HttpStatus } from '@nestjs/common';
|
import { Controller, Post, Get, Patch, Body, Req, Res, HttpCode, HttpStatus } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiCreatedResponse,
|
||||||
|
ApiOkResponse,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { RegisterDto } from './dto/register.dto';
|
import { RegisterDto } from './dto/register.dto';
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from './dto/login.dto';
|
||||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||||
|
import {
|
||||||
|
AuthLogoutResponseDto,
|
||||||
|
AuthProfileResponseDto,
|
||||||
|
AuthTokenResponseDto,
|
||||||
|
} from './dto/auth-response.dto';
|
||||||
import { CurrentUser } from './decorators/current-user.decorator';
|
import { CurrentUser } from './decorators/current-user.decorator';
|
||||||
import { Public } from './decorators/public.decorator';
|
import { Public } from './decorators/public.decorator';
|
||||||
import { JwtPayload } from './interfaces/jwt-payload.interface';
|
import { JwtPayload } from './interfaces/jwt-payload.interface';
|
||||||
@ -26,6 +37,7 @@ export class AuthController {
|
|||||||
@Public()
|
@Public()
|
||||||
@Post('register')
|
@Post('register')
|
||||||
@ApiOperation({ summary: 'Register new user' })
|
@ApiOperation({ summary: 'Register new user' })
|
||||||
|
@ApiCreatedResponse({ type: AuthTokenResponseDto })
|
||||||
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);
|
||||||
@ -41,6 +53,7 @@ export class AuthController {
|
|||||||
@Public()
|
@Public()
|
||||||
@Post('login')
|
@Post('login')
|
||||||
@ApiOperation({ summary: 'Login with email and password' })
|
@ApiOperation({ summary: 'Login with email and password' })
|
||||||
|
@ApiCreatedResponse({ type: AuthTokenResponseDto })
|
||||||
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);
|
||||||
@ -57,6 +70,7 @@ export class AuthController {
|
|||||||
@Post('refresh')
|
@Post('refresh')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiOperation({ summary: 'Refresh access token' })
|
@ApiOperation({ summary: 'Refresh access token' })
|
||||||
|
@ApiOkResponse({ type: AuthTokenResponseDto })
|
||||||
async refresh(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
|
async refresh(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
|
||||||
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);
|
||||||
@ -74,6 +88,7 @@ export class AuthController {
|
|||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'Logout user' })
|
@ApiOperation({ summary: 'Logout user' })
|
||||||
|
@ApiOkResponse({ type: AuthLogoutResponseDto })
|
||||||
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' });
|
||||||
@ -86,6 +101,7 @@ export class AuthController {
|
|||||||
@Get('me')
|
@Get('me')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'Get current user profile' })
|
@ApiOperation({ summary: 'Get current user profile' })
|
||||||
|
@ApiOkResponse({ type: AuthProfileResponseDto })
|
||||||
async getProfile(@CurrentUser() user: JwtPayload) {
|
async getProfile(@CurrentUser() user: JwtPayload) {
|
||||||
const profile = await this.authService.getProfile(user.sub);
|
const profile = await this.authService.getProfile(user.sub);
|
||||||
return {
|
return {
|
||||||
@ -97,6 +113,7 @@ export class AuthController {
|
|||||||
@Patch('me')
|
@Patch('me')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ summary: 'Update current user profile' })
|
@ApiOperation({ summary: 'Update current user profile' })
|
||||||
|
@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);
|
const profile = await this.authService.updateProfile(user.sub, dto);
|
||||||
return {
|
return {
|
||||||
|
|||||||
60
apps/backend/src/modules/auth/dto/auth-response.dto.ts
Normal file
60
apps/backend/src/modules/auth/dto/auth-response.dto.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
class AuthResponseMetaDto {
|
||||||
|
@ApiProperty({ type: String, nullable: true })
|
||||||
|
cachedAt!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
fromCache!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthUserDto {
|
||||||
|
@ApiProperty()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
email!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ type: String, nullable: true })
|
||||||
|
name!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
role!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthTokenDataDto {
|
||||||
|
@ApiProperty({ type: AuthUserDto })
|
||||||
|
user!: AuthUserDto;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
accessToken!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogoutDataDto {
|
||||||
|
@ApiProperty()
|
||||||
|
message!: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthTokenResponseDto {
|
||||||
|
@ApiProperty({ type: AuthTokenDataDto })
|
||||||
|
data!: AuthTokenDataDto;
|
||||||
|
|
||||||
|
@ApiProperty({ type: AuthResponseMetaDto })
|
||||||
|
meta!: AuthResponseMetaDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthProfileResponseDto {
|
||||||
|
@ApiProperty({ type: AuthUserDto })
|
||||||
|
data!: AuthUserDto;
|
||||||
|
|
||||||
|
@ApiProperty({ type: AuthResponseMetaDto })
|
||||||
|
meta!: AuthResponseMetaDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthLogoutResponseDto {
|
||||||
|
@ApiProperty({ type: LogoutDataDto })
|
||||||
|
data!: LogoutDataDto;
|
||||||
|
|
||||||
|
@ApiProperty({ type: AuthResponseMetaDto })
|
||||||
|
meta!: AuthResponseMetaDto;
|
||||||
|
}
|
||||||
@ -51,7 +51,7 @@ export class AddPositionDto {
|
|||||||
@MaxLength(500)
|
@MaxLength(500)
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: ['DIVIDEND', 'GROWTH'], enum: TAGS })
|
@ApiPropertyOptional({ example: ['DIVIDEND', 'GROWTH'], enum: TAGS, isArray: true })
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsIn(TAGS, { each: true })
|
@IsIn(TAGS, { each: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { EnrichedPosition } from '../portfolio.service';
|
import { PositionWithPriceDto } from './position-with-price.dto';
|
||||||
|
|
||||||
export class PortfolioSummaryDto {
|
export class PortfolioSummaryDto {
|
||||||
@ApiProperty() totalInvested!: number;
|
@ApiProperty() totalInvested!: number;
|
||||||
@ApiProperty() totalValue!: number;
|
@ApiProperty() totalValue!: number;
|
||||||
@ApiProperty() totalPnl!: number;
|
@ApiProperty() totalPnl!: number;
|
||||||
@ApiProperty() totalPnlPercent!: number | null;
|
@ApiProperty({ type: Number, nullable: true }) totalPnlPercent!: number | null;
|
||||||
@ApiProperty() totalDividends!: number;
|
@ApiProperty() totalDividends!: number;
|
||||||
@ApiProperty() totalReturn!: number;
|
@ApiProperty() totalReturn!: number;
|
||||||
@ApiProperty() totalReturnPercent!: number | null;
|
@ApiProperty({ type: Number, nullable: true }) totalReturnPercent!: number | null;
|
||||||
@ApiProperty() positionCount!: number;
|
@ApiProperty() positionCount!: number;
|
||||||
@ApiProperty() weightedYield!: number | null;
|
@ApiProperty({ type: Number, nullable: true }) weightedYield!: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AnalyticsResponseDto {
|
export class AnalyticsResponseDto {
|
||||||
@ApiProperty({ type: [Object] }) positions!: EnrichedPosition[];
|
@ApiProperty({ type: [PositionWithPriceDto] }) positions!: PositionWithPriceDto[];
|
||||||
@ApiProperty() summary!: PortfolioSummaryDto;
|
@ApiProperty() summary!: PortfolioSummaryDto;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,53 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { AnalyticsResponseDto } from './analytics-response.dto';
|
||||||
|
import { PortfolioListResponseDto } from './portfolio-list-response.dto';
|
||||||
|
import { PortfolioDetailResponseDto, PortfolioResponseDto } from './portfolio-response.dto';
|
||||||
|
import { PositionResponseDto } from './position-response.dto';
|
||||||
|
|
||||||
|
export class PortfolioResponseMetaDto {
|
||||||
|
@ApiProperty({ type: String, nullable: true })
|
||||||
|
cachedAt!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
fromCache!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PortfolioListEnvelopeDto {
|
||||||
|
@ApiProperty({ type: [PortfolioListResponseDto] })
|
||||||
|
data!: PortfolioListResponseDto[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: PortfolioResponseMetaDto })
|
||||||
|
meta!: PortfolioResponseMetaDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PortfolioEnvelopeDto {
|
||||||
|
@ApiProperty({ type: PortfolioResponseDto })
|
||||||
|
data!: PortfolioResponseDto;
|
||||||
|
|
||||||
|
@ApiProperty({ type: PortfolioResponseMetaDto })
|
||||||
|
meta!: PortfolioResponseMetaDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PortfolioDetailEnvelopeDto {
|
||||||
|
@ApiProperty({ type: PortfolioDetailResponseDto })
|
||||||
|
data!: PortfolioDetailResponseDto;
|
||||||
|
|
||||||
|
@ApiProperty({ type: PortfolioResponseMetaDto })
|
||||||
|
meta!: PortfolioResponseMetaDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PositionEnvelopeDto {
|
||||||
|
@ApiProperty({ type: PositionResponseDto })
|
||||||
|
data!: PositionResponseDto;
|
||||||
|
|
||||||
|
@ApiProperty({ type: PortfolioResponseMetaDto })
|
||||||
|
meta!: PortfolioResponseMetaDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AnalyticsEnvelopeDto {
|
||||||
|
@ApiProperty({ type: AnalyticsResponseDto })
|
||||||
|
data!: AnalyticsResponseDto;
|
||||||
|
|
||||||
|
@ApiProperty({ type: PortfolioResponseMetaDto })
|
||||||
|
meta!: PortfolioResponseMetaDto;
|
||||||
|
}
|
||||||
@ -1,45 +1,11 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { PortfolioSummaryDto } from './analytics-response.dto';
|
import { PortfolioSummaryDto } from './analytics-response.dto';
|
||||||
|
import { PositionWithPriceDto } from './position-with-price.dto';
|
||||||
class PositionWithPriceDto {
|
|
||||||
@ApiProperty() id!: number;
|
|
||||||
@ApiProperty({ example: 'SBER' }) secid!: string;
|
|
||||||
@ApiPropertyOptional() shortName!: string | null;
|
|
||||||
@ApiProperty({ example: 'share', enum: ['share', 'bond'] }) type!: string;
|
|
||||||
@ApiProperty({ example: 10 }) quantity!: number;
|
|
||||||
@ApiPropertyOptional() buyPrice!: number | null;
|
|
||||||
@ApiPropertyOptional() buyDate!: string | null;
|
|
||||||
@ApiPropertyOptional() notes!: string | null;
|
|
||||||
@ApiPropertyOptional() tags!: string[] | null;
|
|
||||||
@ApiPropertyOptional() currentPrice!: number | null;
|
|
||||||
@ApiPropertyOptional() totalCost!: number | null;
|
|
||||||
@ApiPropertyOptional() currentValue!: number | null;
|
|
||||||
@ApiProperty() weightPercent!: number;
|
|
||||||
@ApiPropertyOptional() pnl!: number | null;
|
|
||||||
@ApiPropertyOptional() pnlPercent!: number | null;
|
|
||||||
@ApiPropertyOptional() dividendIncome!: number | null;
|
|
||||||
@ApiPropertyOptional() totalReturn!: number | null;
|
|
||||||
@ApiPropertyOptional() totalReturnPercent!: number | null;
|
|
||||||
@ApiPropertyOptional() change!: number | null;
|
|
||||||
@ApiPropertyOptional() changePercent!: number | null;
|
|
||||||
@ApiPropertyOptional() yieldToMaturity!: number | null;
|
|
||||||
@ApiPropertyOptional() duration!: number | null;
|
|
||||||
@ApiPropertyOptional() couponValue!: number | null;
|
|
||||||
@ApiPropertyOptional() couponPercent!: number | null;
|
|
||||||
@ApiPropertyOptional() nextCouponDate!: string | null;
|
|
||||||
@ApiPropertyOptional() matDate!: string | null;
|
|
||||||
@ApiPropertyOptional() accruedInt!: number | null;
|
|
||||||
@ApiPropertyOptional() bid!: number | null;
|
|
||||||
@ApiPropertyOptional() offer!: number | null;
|
|
||||||
@ApiPropertyOptional() couponPeriod!: number | null;
|
|
||||||
@ApiPropertyOptional() bondType!: string | null;
|
|
||||||
@ApiPropertyOptional() offerDate!: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PortfolioResponseDto {
|
export class PortfolioResponseDto {
|
||||||
@ApiProperty() id!: number;
|
@ApiProperty() id!: number;
|
||||||
@ApiProperty() name!: string;
|
@ApiProperty() name!: string;
|
||||||
@ApiPropertyOptional() description!: string | null;
|
@ApiPropertyOptional({ type: String, nullable: true }) description!: string | null;
|
||||||
@ApiProperty({ default: 'RUB' }) currency!: string;
|
@ApiProperty({ default: 'RUB' }) currency!: string;
|
||||||
@ApiProperty() createdAt!: string;
|
@ApiProperty() createdAt!: string;
|
||||||
@ApiProperty() updatedAt!: string;
|
@ApiProperty() updatedAt!: string;
|
||||||
|
|||||||
@ -4,8 +4,8 @@ export class PositionResponseDto {
|
|||||||
@ApiProperty() id!: number;
|
@ApiProperty() id!: number;
|
||||||
@ApiProperty({ example: 'SBER' }) secid!: string;
|
@ApiProperty({ example: 'SBER' }) secid!: string;
|
||||||
@ApiProperty({ example: 10 }) quantity!: number;
|
@ApiProperty({ example: 10 }) quantity!: number;
|
||||||
@ApiPropertyOptional() notes!: string | null;
|
@ApiPropertyOptional({ type: String, nullable: true }) notes!: string | null;
|
||||||
@ApiPropertyOptional() tags!: string[] | null;
|
@ApiPropertyOptional({ type: String, isArray: true, nullable: true }) tags!: string[] | null;
|
||||||
@ApiProperty() portfolioId!: number;
|
@ApiProperty() portfolioId!: number;
|
||||||
@ApiProperty() createdAt!: string;
|
@ApiProperty() createdAt!: string;
|
||||||
@ApiProperty() updatedAt!: string;
|
@ApiProperty() updatedAt!: string;
|
||||||
|
|||||||
@ -0,0 +1,99 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class PositionWithPriceDto {
|
||||||
|
@ApiProperty()
|
||||||
|
id!: number;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'SBER' })
|
||||||
|
secid!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: String, nullable: true })
|
||||||
|
shortName!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 'share', enum: ['share', 'bond'] })
|
||||||
|
type!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ example: 10 })
|
||||||
|
quantity!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
buyPrice!: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: String, nullable: true })
|
||||||
|
buyDate!: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: String, nullable: true })
|
||||||
|
notes!: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: String, isArray: true, nullable: true })
|
||||||
|
tags!: string[] | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
currentPrice!: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
totalCost!: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
currentValue!: number | null;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
weightPercent!: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
pnl!: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
pnlPercent!: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
dividendIncome!: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
totalReturn!: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
totalReturnPercent!: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
change?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
changePercent?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
yieldToMaturity?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
duration?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
couponValue?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
couponPercent?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: String, nullable: true })
|
||||||
|
nextCouponDate?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: String, nullable: true })
|
||||||
|
matDate?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
accruedInt?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
bid?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
offer?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||||
|
couponPeriod?: number | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: String, nullable: true })
|
||||||
|
bondType?: string | null;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ type: String, nullable: true })
|
||||||
|
offerDate?: string | null;
|
||||||
|
}
|
||||||
@ -45,7 +45,7 @@ export class UpdatePositionDto {
|
|||||||
@MaxLength(500)
|
@MaxLength(500)
|
||||||
notes?: string;
|
notes?: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: ['DIVIDEND'], enum: TAGS })
|
@ApiPropertyOptional({ example: ['DIVIDEND'], enum: TAGS, isArray: true })
|
||||||
@IsArray()
|
@IsArray()
|
||||||
@IsIn(TAGS, { each: true })
|
@IsIn(TAGS, { each: true })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@ -1,23 +1,51 @@
|
|||||||
import { Controller, Get, Post, Patch, Delete, Body, Param, ParseIntPipe } from '@nestjs/common';
|
import { Controller, Get, Post, Patch, Delete, Body, Param, ParseIntPipe } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiBearerAuth, ApiOkResponse } from '@nestjs/swagger';
|
import {
|
||||||
import { PortfolioListResponseDto } from './dto/portfolio-list-response.dto';
|
ApiTags,
|
||||||
import { PortfolioDetailResponseDto } from './dto/portfolio-response.dto';
|
ApiOperation,
|
||||||
|
ApiBearerAuth,
|
||||||
|
ApiOkResponse,
|
||||||
|
ApiCreatedResponse,
|
||||||
|
ApiExtraModels,
|
||||||
|
getSchemaPath,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
import { PortfolioService } from './portfolio.service';
|
import { PortfolioService } from './portfolio.service';
|
||||||
import { CreatePortfolioDto } from './dto/create-portfolio.dto';
|
import { CreatePortfolioDto } from './dto/create-portfolio.dto';
|
||||||
import { UpdatePortfolioDto } from './dto/update-portfolio.dto';
|
import { UpdatePortfolioDto } from './dto/update-portfolio.dto';
|
||||||
import { AddPositionDto } from './dto/add-position.dto';
|
import { AddPositionDto } from './dto/add-position.dto';
|
||||||
import { UpdatePositionDto } from './dto/update-position.dto';
|
import { UpdatePositionDto } from './dto/update-position.dto';
|
||||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||||
|
import {
|
||||||
|
AnalyticsEnvelopeDto,
|
||||||
|
PortfolioDetailEnvelopeDto,
|
||||||
|
PortfolioEnvelopeDto,
|
||||||
|
PortfolioListEnvelopeDto,
|
||||||
|
PortfolioResponseMetaDto,
|
||||||
|
PositionEnvelopeDto,
|
||||||
|
} from './dto/portfolio-envelope.dto';
|
||||||
|
|
||||||
|
const nullDataEnvelopeSchema = {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
data: {
|
||||||
|
nullable: true,
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
$ref: getSchemaPath(PortfolioResponseMetaDto),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ['data', 'meta'],
|
||||||
|
};
|
||||||
|
|
||||||
@ApiTags('Portfolios')
|
@ApiTags('Portfolios')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
|
@ApiExtraModels(PortfolioResponseMetaDto)
|
||||||
@Controller('portfolios')
|
@Controller('portfolios')
|
||||||
export class PortfolioController {
|
export class PortfolioController {
|
||||||
constructor(private readonly portfolioService: PortfolioService) {}
|
constructor(private readonly portfolioService: PortfolioService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ summary: 'Get all portfolios for current user' })
|
@ApiOperation({ summary: 'Get all portfolios for current user' })
|
||||||
@ApiOkResponse({ type: PortfolioListResponseDto, isArray: true })
|
@ApiOkResponse({ type: PortfolioListEnvelopeDto })
|
||||||
async findAll(@CurrentUser() user: { sub: number }) {
|
async findAll(@CurrentUser() user: { sub: number }) {
|
||||||
const portfolios = await this.portfolioService.findAll(user.sub);
|
const portfolios = await this.portfolioService.findAll(user.sub);
|
||||||
return { data: portfolios, meta: { cachedAt: null, fromCache: false } };
|
return { data: portfolios, meta: { cachedAt: null, fromCache: false } };
|
||||||
@ -25,6 +53,7 @@ export class PortfolioController {
|
|||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({ summary: 'Create a new portfolio' })
|
@ApiOperation({ summary: 'Create a new portfolio' })
|
||||||
|
@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);
|
const portfolio = await this.portfolioService.create(user.sub, dto);
|
||||||
return { data: portfolio, meta: { cachedAt: null, fromCache: false } };
|
return { data: portfolio, meta: { cachedAt: null, fromCache: false } };
|
||||||
@ -32,7 +61,7 @@ export class PortfolioController {
|
|||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@ApiOperation({ summary: 'Get portfolio details with positions and prices' })
|
@ApiOperation({ summary: 'Get portfolio details with positions and prices' })
|
||||||
@ApiOkResponse({ type: PortfolioDetailResponseDto })
|
@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);
|
const portfolio = await this.portfolioService.findOne(user.sub, id);
|
||||||
return { data: portfolio, meta: { cachedAt: null, fromCache: false } };
|
return { data: portfolio, meta: { cachedAt: null, fromCache: false } };
|
||||||
@ -40,6 +69,7 @@ export class PortfolioController {
|
|||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@ApiOperation({ summary: 'Update portfolio' })
|
@ApiOperation({ summary: 'Update portfolio' })
|
||||||
|
@ApiOkResponse({ type: PortfolioEnvelopeDto })
|
||||||
async update(
|
async update(
|
||||||
@CurrentUser() user: { sub: number },
|
@CurrentUser() user: { sub: number },
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@ -51,6 +81,7 @@ export class PortfolioController {
|
|||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@ApiOperation({ summary: 'Delete portfolio' })
|
@ApiOperation({ summary: 'Delete portfolio' })
|
||||||
|
@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 { data: null, meta: { cachedAt: null, fromCache: false } };
|
||||||
@ -58,6 +89,7 @@ export class PortfolioController {
|
|||||||
|
|
||||||
@Post(':id/positions')
|
@Post(':id/positions')
|
||||||
@ApiOperation({ summary: 'Add position to portfolio' })
|
@ApiOperation({ summary: 'Add position to portfolio' })
|
||||||
|
@ApiCreatedResponse({ type: PositionEnvelopeDto })
|
||||||
async addPosition(
|
async addPosition(
|
||||||
@CurrentUser() user: { sub: number },
|
@CurrentUser() user: { sub: number },
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@ -69,6 +101,7 @@ export class PortfolioController {
|
|||||||
|
|
||||||
@Patch(':id/positions/:positionId')
|
@Patch(':id/positions/:positionId')
|
||||||
@ApiOperation({ summary: 'Update position' })
|
@ApiOperation({ summary: 'Update position' })
|
||||||
|
@ApiOkResponse({ type: PositionEnvelopeDto })
|
||||||
async updatePosition(
|
async updatePosition(
|
||||||
@CurrentUser() user: { sub: number },
|
@CurrentUser() user: { sub: number },
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
@ -81,6 +114,7 @@ export class PortfolioController {
|
|||||||
|
|
||||||
@Get(':id/analytics')
|
@Get(':id/analytics')
|
||||||
@ApiOperation({ summary: 'Get portfolio analytics with PnL' })
|
@ApiOperation({ summary: 'Get portfolio analytics with PnL' })
|
||||||
|
@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);
|
const result = await this.portfolioService.getAnalytics(user.sub, id);
|
||||||
return { data: result, meta: { cachedAt: null, fromCache: false } };
|
return { data: result, meta: { cachedAt: null, fromCache: false } };
|
||||||
@ -88,6 +122,7 @@ export class PortfolioController {
|
|||||||
|
|
||||||
@Delete(':id/positions/:positionId')
|
@Delete(':id/positions/:positionId')
|
||||||
@ApiOperation({ summary: 'Remove position from portfolio' })
|
@ApiOperation({ summary: 'Remove position from portfolio' })
|
||||||
|
@ApiOkResponse({ schema: nullDataEnvelopeSchema })
|
||||||
async removePosition(
|
async removePosition(
|
||||||
@CurrentUser() user: { sub: number },
|
@CurrentUser() user: { sub: number },
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param('id', ParseIntPipe) id: number,
|
||||||
|
|||||||
@ -13,13 +13,13 @@ export class ScreenerItemDto {
|
|||||||
@ApiProperty({ enum: ['share', 'bond'] })
|
@ApiProperty({ enum: ['share', 'bond'] })
|
||||||
type!: 'share' | 'bond';
|
type!: 'share' | 'bond';
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 322.35 })
|
@ApiPropertyOptional({ type: Number, nullable: true, example: 322.35 })
|
||||||
price!: number | null;
|
price!: number | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 1.15 })
|
@ApiPropertyOptional({ type: Number, nullable: true, example: 1.15 })
|
||||||
change!: number | null;
|
change!: number | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 0.36 })
|
@ApiPropertyOptional({ type: Number, nullable: true, example: 0.36 })
|
||||||
changePercent!: number | null;
|
changePercent!: number | null;
|
||||||
|
|
||||||
@ApiProperty({ example: 1925163 })
|
@ApiProperty({ example: 1925163 })
|
||||||
@ -28,28 +28,28 @@ export class ScreenerItemDto {
|
|||||||
@ApiProperty({ example: 1 })
|
@ApiProperty({ example: 1 })
|
||||||
listLevel!: number;
|
listLevel!: number;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 6958336818320 })
|
@ApiPropertyOptional({ type: Number, nullable: true, example: 6958336818320 })
|
||||||
capitalization!: number | null;
|
capitalization!: number | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 12.71 })
|
@ApiPropertyOptional({ type: Number, nullable: true, example: 12.71 })
|
||||||
yieldToMaturity!: number | null;
|
yieldToMaturity!: number | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 4.5 })
|
@ApiPropertyOptional({ type: Number, nullable: true, example: 4.5 })
|
||||||
duration!: number | null;
|
duration!: number | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 40.64 })
|
@ApiPropertyOptional({ type: Number, nullable: true, example: 40.64 })
|
||||||
couponValue!: number | null;
|
couponValue!: number | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 8.15 })
|
@ApiPropertyOptional({ type: Number, nullable: true, example: 8.15 })
|
||||||
couponPercent!: number | null;
|
couponPercent!: number | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 29.48 })
|
@ApiPropertyOptional({ type: Number, nullable: true, example: 29.48 })
|
||||||
accruedInt!: number | null;
|
accruedInt!: number | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: '2027-02-03' })
|
@ApiPropertyOptional({ type: String, nullable: true, example: '2027-02-03' })
|
||||||
matDate!: string | null;
|
matDate!: string | null;
|
||||||
|
|
||||||
@ApiPropertyOptional({ example: 'ОФЗ-ПД' })
|
@ApiPropertyOptional({ type: String, nullable: true, example: 'ОФЗ-ПД' })
|
||||||
bondType!: string | null;
|
bondType!: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,3 +69,19 @@ export class ScreenerResultDto {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
totalPages!: number;
|
totalPages!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ScreenerResponseMetaDto {
|
||||||
|
@ApiProperty({ type: String, nullable: true })
|
||||||
|
cachedAt!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
fromCache!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScreenerResponseDto {
|
||||||
|
@ApiProperty({ type: ScreenerResultDto })
|
||||||
|
data!: ScreenerResultDto;
|
||||||
|
|
||||||
|
@ApiProperty({ type: ScreenerResponseMetaDto })
|
||||||
|
meta!: ScreenerResponseMetaDto;
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { SecuritiesService } from './securities.service';
|
|||||||
import { ScreenerService } from './screener.service';
|
import { ScreenerService } from './screener.service';
|
||||||
import { SearchQueryDto, SecurityType } from './dto/search-query.dto';
|
import { SearchQueryDto, SecurityType } from './dto/search-query.dto';
|
||||||
import { ScreenerQueryDto } from './dto/screener-query.dto';
|
import { ScreenerQueryDto } from './dto/screener-query.dto';
|
||||||
import { ScreenerResultDto } from './dto/screener-response.dto';
|
import { ScreenerResponseDto } from './dto/screener-response.dto';
|
||||||
|
|
||||||
@ApiTags('Securities')
|
@ApiTags('Securities')
|
||||||
@Controller('securities')
|
@Controller('securities')
|
||||||
@ -27,7 +27,7 @@ export class SecuritiesController {
|
|||||||
|
|
||||||
@Get('screener')
|
@Get('screener')
|
||||||
@ApiOperation({ summary: 'Фильтр ценных бумаг по параметрам' })
|
@ApiOperation({ summary: 'Фильтр ценных бумаг по параметрам' })
|
||||||
@ApiOkResponse({ type: ScreenerResultDto })
|
@ApiOkResponse({ type: ScreenerResponseDto })
|
||||||
async screener(@Query(ValidationPipe) query: ScreenerQueryDto) {
|
async screener(@Query(ValidationPipe) query: ScreenerQueryDto) {
|
||||||
const result = await this.screenerService.screen(query);
|
const result = await this.screenerService.screen(query);
|
||||||
return { data: result, meta: { cachedAt: null, fromCache: false } };
|
return { data: result, meta: { cachedAt: null, fromCache: false } };
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { join, resolve } from 'node:path';
|
import { join, resolve } from 'node:path';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { load } = require('js-yaml');
|
||||||
|
|
||||||
describe('checked-in OpenAPI artifacts', () => {
|
describe('checked-in OpenAPI artifacts', () => {
|
||||||
const rootDir = resolve(process.cwd(), '../..');
|
const rootDir = resolve(process.cwd(), '../..');
|
||||||
const frontendTypes = readFileSync(join(rootDir, 'apps/frontend/src/api/types.ts'), 'utf8');
|
const frontendTypes = readFileSync(join(rootDir, 'apps/frontend/src/api/types.ts'), 'utf8');
|
||||||
const openapiYaml = readFileSync(join(rootDir, 'docs/openapi/openapi.yaml'), 'utf8');
|
const openapiYaml = readFileSync(join(rootDir, 'docs/openapi/openapi.yaml'), 'utf8');
|
||||||
|
const openapi = load(openapiYaml) as any;
|
||||||
|
|
||||||
const requiredPaths = [
|
const requiredPaths = [
|
||||||
'/api/v1/auth/register',
|
'/api/v1/auth/register',
|
||||||
@ -31,4 +35,62 @@ describe('checked-in OpenAPI artifacts', () => {
|
|||||||
expect(openapiYaml).toContain(`${path}:`);
|
expect(openapiYaml).toContain(`${path}:`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('checked-in artifacts do not leak the local alternate codegen port', () => {
|
||||||
|
expect(frontendTypes).not.toContain('localhost:3001');
|
||||||
|
expect(frontendTypes).not.toContain('3001');
|
||||||
|
expect(openapiYaml).not.toContain('localhost:3001');
|
||||||
|
expect(openapiYaml).not.toContain('3001');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('nullable primitive schemas are typed explicitly', () => {
|
||||||
|
const screenerItem = openapi.components.schemas.ScreenerItemDto;
|
||||||
|
|
||||||
|
expect(screenerItem.properties.price).toMatchObject({
|
||||||
|
type: 'number',
|
||||||
|
nullable: true,
|
||||||
|
});
|
||||||
|
expect(screenerItem.properties.matDate).toMatchObject({
|
||||||
|
type: 'string',
|
||||||
|
nullable: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('position tags request schemas are arrays of known tags', () => {
|
||||||
|
for (const schemaName of ['AddPositionDto', 'UpdatePositionDto']) {
|
||||||
|
const tags = openapi.components.schemas[schemaName].properties.tags;
|
||||||
|
|
||||||
|
expect(tags).toMatchObject({
|
||||||
|
type: 'array',
|
||||||
|
});
|
||||||
|
expect(tags.items.enum).toContain('DIVIDEND');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('current auth, screener and portfolio operations have typed JSON responses', () => {
|
||||||
|
const requiredJsonResponses = [
|
||||||
|
['post', '/api/v1/auth/register', 201],
|
||||||
|
['post', '/api/v1/auth/login', 201],
|
||||||
|
['post', '/api/v1/auth/refresh', 200],
|
||||||
|
['post', '/api/v1/auth/logout', 200],
|
||||||
|
['get', '/api/v1/auth/me', 200],
|
||||||
|
['patch', '/api/v1/auth/me', 200],
|
||||||
|
['get', '/api/v1/securities/screener', 200],
|
||||||
|
['get', '/api/v1/portfolios', 200],
|
||||||
|
['post', '/api/v1/portfolios', 201],
|
||||||
|
['get', '/api/v1/portfolios/{id}', 200],
|
||||||
|
['patch', '/api/v1/portfolios/{id}', 200],
|
||||||
|
['delete', '/api/v1/portfolios/{id}', 200],
|
||||||
|
['post', '/api/v1/portfolios/{id}/positions', 201],
|
||||||
|
['patch', '/api/v1/portfolios/{id}/positions/{positionId}', 200],
|
||||||
|
['delete', '/api/v1/portfolios/{id}/positions/{positionId}', 200],
|
||||||
|
['get', '/api/v1/portfolios/{id}/analytics', 200],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const [method, path, status] of requiredJsonResponses) {
|
||||||
|
expect(
|
||||||
|
openapi.paths[path][method].responses[status].content?.['application/json'],
|
||||||
|
).toBeDefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -395,12 +395,41 @@ export interface components {
|
|||||||
/** @example John */
|
/** @example John */
|
||||||
name?: string;
|
name?: string;
|
||||||
};
|
};
|
||||||
|
AuthUserDto: {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
name: string | null;
|
||||||
|
role: string;
|
||||||
|
};
|
||||||
|
AuthTokenDataDto: {
|
||||||
|
user: components['schemas']['AuthUserDto'];
|
||||||
|
accessToken: string;
|
||||||
|
};
|
||||||
|
AuthResponseMetaDto: {
|
||||||
|
cachedAt: string | null;
|
||||||
|
fromCache: boolean;
|
||||||
|
};
|
||||||
|
AuthTokenResponseDto: {
|
||||||
|
data: components['schemas']['AuthTokenDataDto'];
|
||||||
|
meta: components['schemas']['AuthResponseMetaDto'];
|
||||||
|
};
|
||||||
LoginDto: {
|
LoginDto: {
|
||||||
/** @example user@example.com */
|
/** @example user@example.com */
|
||||||
email: string;
|
email: string;
|
||||||
/** @example securePass123 */
|
/** @example securePass123 */
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
LogoutDataDto: {
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
AuthLogoutResponseDto: {
|
||||||
|
data: components['schemas']['LogoutDataDto'];
|
||||||
|
meta: components['schemas']['AuthResponseMetaDto'];
|
||||||
|
};
|
||||||
|
AuthProfileResponseDto: {
|
||||||
|
data: components['schemas']['AuthUserDto'];
|
||||||
|
meta: components['schemas']['AuthResponseMetaDto'];
|
||||||
|
};
|
||||||
UpdateProfileDto: {
|
UpdateProfileDto: {
|
||||||
/** @example John Doe */
|
/** @example John Doe */
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -415,31 +444,31 @@ export interface components {
|
|||||||
/** @enum {string} */
|
/** @enum {string} */
|
||||||
type: 'share' | 'bond';
|
type: 'share' | 'bond';
|
||||||
/** @example 322.35 */
|
/** @example 322.35 */
|
||||||
price?: Record<string, never>;
|
price?: number | null;
|
||||||
/** @example 1.15 */
|
/** @example 1.15 */
|
||||||
change?: Record<string, never>;
|
change?: number | null;
|
||||||
/** @example 0.36 */
|
/** @example 0.36 */
|
||||||
changePercent?: Record<string, never>;
|
changePercent?: number | null;
|
||||||
/** @example 1925163 */
|
/** @example 1925163 */
|
||||||
volume: number;
|
volume: number;
|
||||||
/** @example 1 */
|
/** @example 1 */
|
||||||
listLevel: number;
|
listLevel: number;
|
||||||
/** @example 6958336818320 */
|
/** @example 6958336818320 */
|
||||||
capitalization?: Record<string, never>;
|
capitalization?: number | null;
|
||||||
/** @example 12.71 */
|
/** @example 12.71 */
|
||||||
yieldToMaturity?: Record<string, never>;
|
yieldToMaturity?: number | null;
|
||||||
/** @example 4.5 */
|
/** @example 4.5 */
|
||||||
duration?: Record<string, never>;
|
duration?: number | null;
|
||||||
/** @example 40.64 */
|
/** @example 40.64 */
|
||||||
couponValue?: Record<string, never>;
|
couponValue?: number | null;
|
||||||
/** @example 8.15 */
|
/** @example 8.15 */
|
||||||
couponPercent?: Record<string, never>;
|
couponPercent?: number | null;
|
||||||
/** @example 29.48 */
|
/** @example 29.48 */
|
||||||
accruedInt?: Record<string, never>;
|
accruedInt?: number | null;
|
||||||
/** @example 2027-02-03 */
|
/** @example 2027-02-03 */
|
||||||
matDate?: Record<string, never>;
|
matDate?: string | null;
|
||||||
/** @example ОФЗ-ПД */
|
/** @example ОФЗ-ПД */
|
||||||
bondType?: Record<string, never>;
|
bondType?: string | null;
|
||||||
};
|
};
|
||||||
ScreenerResultDto: {
|
ScreenerResultDto: {
|
||||||
items: components['schemas']['ScreenerItemDto'][];
|
items: components['schemas']['ScreenerItemDto'][];
|
||||||
@ -448,10 +477,22 @@ export interface components {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
};
|
};
|
||||||
|
ScreenerResponseMetaDto: {
|
||||||
|
cachedAt: string | null;
|
||||||
|
fromCache: boolean;
|
||||||
|
};
|
||||||
|
ScreenerResponseDto: {
|
||||||
|
data: components['schemas']['ScreenerResultDto'];
|
||||||
|
meta: components['schemas']['ScreenerResponseMetaDto'];
|
||||||
|
};
|
||||||
|
PortfolioResponseMetaDto: {
|
||||||
|
cachedAt: string | null;
|
||||||
|
fromCache: boolean;
|
||||||
|
};
|
||||||
PortfolioListResponseDto: {
|
PortfolioListResponseDto: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description?: Record<string, never>;
|
description?: string | null;
|
||||||
/** @default RUB */
|
/** @default RUB */
|
||||||
currency: string;
|
currency: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@ -465,6 +506,10 @@ export interface components {
|
|||||||
/** @description Number of bond positions */
|
/** @description Number of bond positions */
|
||||||
bondCount: number;
|
bondCount: number;
|
||||||
};
|
};
|
||||||
|
PortfolioListEnvelopeDto: {
|
||||||
|
data: components['schemas']['PortfolioListResponseDto'][];
|
||||||
|
meta: components['schemas']['PortfolioResponseMetaDto'];
|
||||||
|
};
|
||||||
CreatePortfolioDto: {
|
CreatePortfolioDto: {
|
||||||
/** @example Мой портфель */
|
/** @example Мой портфель */
|
||||||
name: string;
|
name: string;
|
||||||
@ -476,11 +521,24 @@ export interface components {
|
|||||||
*/
|
*/
|
||||||
currency: 'RUB' | 'USD' | 'EUR' | 'CNY' | 'KZT' | 'BYN';
|
currency: 'RUB' | 'USD' | 'EUR' | 'CNY' | 'KZT' | 'BYN';
|
||||||
};
|
};
|
||||||
|
PortfolioResponseDto: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
/** @default RUB */
|
||||||
|
currency: string;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
PortfolioEnvelopeDto: {
|
||||||
|
data: components['schemas']['PortfolioResponseDto'];
|
||||||
|
meta: components['schemas']['PortfolioResponseMetaDto'];
|
||||||
|
};
|
||||||
PositionWithPriceDto: {
|
PositionWithPriceDto: {
|
||||||
id: number;
|
id: number;
|
||||||
/** @example SBER */
|
/** @example SBER */
|
||||||
secid: string;
|
secid: string;
|
||||||
shortName?: Record<string, never>;
|
shortName?: string | null;
|
||||||
/**
|
/**
|
||||||
* @example share
|
* @example share
|
||||||
* @enum {string}
|
* @enum {string}
|
||||||
@ -488,49 +546,49 @@ export interface components {
|
|||||||
type: 'share' | 'bond';
|
type: 'share' | 'bond';
|
||||||
/** @example 10 */
|
/** @example 10 */
|
||||||
quantity: number;
|
quantity: number;
|
||||||
buyPrice?: Record<string, never>;
|
buyPrice?: number | null;
|
||||||
buyDate?: Record<string, never>;
|
buyDate?: string | null;
|
||||||
notes?: Record<string, never>;
|
notes?: string | null;
|
||||||
tags?: Record<string, never>;
|
tags?: string[] | null;
|
||||||
currentPrice?: Record<string, never>;
|
currentPrice?: number | null;
|
||||||
totalCost?: Record<string, never>;
|
totalCost?: number | null;
|
||||||
currentValue?: Record<string, never>;
|
currentValue?: number | null;
|
||||||
weightPercent: number;
|
weightPercent: number;
|
||||||
pnl?: Record<string, never>;
|
pnl?: number | null;
|
||||||
pnlPercent?: Record<string, never>;
|
pnlPercent?: number | null;
|
||||||
dividendIncome?: Record<string, never>;
|
dividendIncome?: number | null;
|
||||||
totalReturn?: Record<string, never>;
|
totalReturn?: number | null;
|
||||||
totalReturnPercent?: Record<string, never>;
|
totalReturnPercent?: number | null;
|
||||||
change?: Record<string, never>;
|
change?: number | null;
|
||||||
changePercent?: Record<string, never>;
|
changePercent?: number | null;
|
||||||
yieldToMaturity?: Record<string, never>;
|
yieldToMaturity?: number | null;
|
||||||
duration?: Record<string, never>;
|
duration?: number | null;
|
||||||
couponValue?: Record<string, never>;
|
couponValue?: number | null;
|
||||||
couponPercent?: Record<string, never>;
|
couponPercent?: number | null;
|
||||||
nextCouponDate?: Record<string, never>;
|
nextCouponDate?: string | null;
|
||||||
matDate?: Record<string, never>;
|
matDate?: string | null;
|
||||||
accruedInt?: Record<string, never>;
|
accruedInt?: number | null;
|
||||||
bid?: Record<string, never>;
|
bid?: number | null;
|
||||||
offer?: Record<string, never>;
|
offer?: number | null;
|
||||||
couponPeriod?: Record<string, never>;
|
couponPeriod?: number | null;
|
||||||
bondType?: Record<string, never>;
|
bondType?: string | null;
|
||||||
offerDate?: Record<string, never>;
|
offerDate?: string | null;
|
||||||
};
|
};
|
||||||
PortfolioSummaryDto: {
|
PortfolioSummaryDto: {
|
||||||
totalInvested: number;
|
totalInvested: number;
|
||||||
totalValue: number;
|
totalValue: number;
|
||||||
totalPnl: number;
|
totalPnl: number;
|
||||||
totalPnlPercent: Record<string, never>;
|
totalPnlPercent: number | null;
|
||||||
totalDividends: number;
|
totalDividends: number;
|
||||||
totalReturn: number;
|
totalReturn: number;
|
||||||
totalReturnPercent: Record<string, never>;
|
totalReturnPercent: number | null;
|
||||||
positionCount: number;
|
positionCount: number;
|
||||||
weightedYield: Record<string, never>;
|
weightedYield: number | null;
|
||||||
};
|
};
|
||||||
PortfolioDetailResponseDto: {
|
PortfolioDetailResponseDto: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description?: Record<string, never>;
|
description?: string | null;
|
||||||
/** @default RUB */
|
/** @default RUB */
|
||||||
currency: string;
|
currency: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@ -539,6 +597,10 @@ export interface components {
|
|||||||
totalValue: number;
|
totalValue: number;
|
||||||
analytics: components['schemas']['PortfolioSummaryDto'];
|
analytics: components['schemas']['PortfolioSummaryDto'];
|
||||||
};
|
};
|
||||||
|
PortfolioDetailEnvelopeDto: {
|
||||||
|
data: components['schemas']['PortfolioDetailResponseDto'];
|
||||||
|
meta: components['schemas']['PortfolioResponseMetaDto'];
|
||||||
|
};
|
||||||
UpdatePortfolioDto: {
|
UpdatePortfolioDto: {
|
||||||
/** @example Мой портфель */
|
/** @example Мой портфель */
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -566,9 +628,8 @@ export interface components {
|
|||||||
* "DIVIDEND",
|
* "DIVIDEND",
|
||||||
* "GROWTH"
|
* "GROWTH"
|
||||||
* ]
|
* ]
|
||||||
* @enum {string}
|
|
||||||
*/
|
*/
|
||||||
tags?:
|
tags?: (
|
||||||
| 'DIVIDEND'
|
| 'DIVIDEND'
|
||||||
| 'GROWTH'
|
| 'GROWTH'
|
||||||
| 'DEFENSIVE'
|
| 'DEFENSIVE'
|
||||||
@ -576,7 +637,24 @@ export interface components {
|
|||||||
| 'BOND'
|
| 'BOND'
|
||||||
| 'ETF'
|
| 'ETF'
|
||||||
| 'GOVERNMENT'
|
| 'GOVERNMENT'
|
||||||
| 'CASH';
|
| 'CASH'
|
||||||
|
)[];
|
||||||
|
};
|
||||||
|
PositionResponseDto: {
|
||||||
|
id: number;
|
||||||
|
/** @example SBER */
|
||||||
|
secid: string;
|
||||||
|
/** @example 10 */
|
||||||
|
quantity: number;
|
||||||
|
notes?: string | null;
|
||||||
|
tags?: string[] | null;
|
||||||
|
portfolioId: number;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
PositionEnvelopeDto: {
|
||||||
|
data: components['schemas']['PositionResponseDto'];
|
||||||
|
meta: components['schemas']['PortfolioResponseMetaDto'];
|
||||||
};
|
};
|
||||||
UpdatePositionDto: {
|
UpdatePositionDto: {
|
||||||
/** @example 15 */
|
/** @example 15 */
|
||||||
@ -591,9 +669,8 @@ export interface components {
|
|||||||
* @example [
|
* @example [
|
||||||
* "DIVIDEND"
|
* "DIVIDEND"
|
||||||
* ]
|
* ]
|
||||||
* @enum {string}
|
|
||||||
*/
|
*/
|
||||||
tags?:
|
tags?: (
|
||||||
| 'DIVIDEND'
|
| 'DIVIDEND'
|
||||||
| 'GROWTH'
|
| 'GROWTH'
|
||||||
| 'DEFENSIVE'
|
| 'DEFENSIVE'
|
||||||
@ -601,7 +678,16 @@ export interface components {
|
|||||||
| 'BOND'
|
| 'BOND'
|
||||||
| 'ETF'
|
| 'ETF'
|
||||||
| 'GOVERNMENT'
|
| 'GOVERNMENT'
|
||||||
| 'CASH';
|
| 'CASH'
|
||||||
|
)[];
|
||||||
|
};
|
||||||
|
AnalyticsResponseDto: {
|
||||||
|
positions: components['schemas']['PositionWithPriceDto'][];
|
||||||
|
summary: components['schemas']['PortfolioSummaryDto'];
|
||||||
|
};
|
||||||
|
AnalyticsEnvelopeDto: {
|
||||||
|
data: components['schemas']['AnalyticsResponseDto'];
|
||||||
|
meta: components['schemas']['PortfolioResponseMetaDto'];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: never;
|
responses: never;
|
||||||
@ -646,7 +732,9 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': components['schemas']['AuthTokenResponseDto'];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -667,7 +755,9 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': components['schemas']['AuthTokenResponseDto'];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -684,7 +774,9 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': components['schemas']['AuthTokenResponseDto'];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -701,7 +793,9 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': components['schemas']['AuthLogoutResponseDto'];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -718,7 +812,9 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': components['schemas']['AuthProfileResponseDto'];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -739,7 +835,9 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': components['schemas']['AuthProfileResponseDto'];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -803,7 +901,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
'application/json': components['schemas']['ScreenerResultDto'];
|
'application/json': components['schemas']['ScreenerResponseDto'];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1007,7 +1105,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
'application/json': components['schemas']['PortfolioListResponseDto'][];
|
'application/json': components['schemas']['PortfolioListEnvelopeDto'];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1029,7 +1127,9 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': components['schemas']['PortfolioEnvelopeDto'];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1049,7 +1149,7 @@ export interface operations {
|
|||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
'application/json': components['schemas']['PortfolioDetailResponseDto'];
|
'application/json': components['schemas']['PortfolioDetailEnvelopeDto'];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1069,7 +1169,12 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
data: unknown;
|
||||||
|
meta: components['schemas']['PortfolioResponseMetaDto'];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1092,7 +1197,9 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': components['schemas']['PortfolioEnvelopeDto'];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1115,7 +1222,9 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': components['schemas']['PositionEnvelopeDto'];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1135,7 +1244,12 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': {
|
||||||
|
data: unknown;
|
||||||
|
meta: components['schemas']['PortfolioResponseMetaDto'];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1159,7 +1273,9 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': components['schemas']['PositionEnvelopeDto'];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1178,7 +1294,9 @@ export interface operations {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
'application/json': components['schemas']['AnalyticsEnvelopeDto'];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -24,6 +24,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthTokenResponseDto'
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
/api/v1/auth/login:
|
/api/v1/auth/login:
|
||||||
@ -40,6 +44,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthTokenResponseDto'
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
/api/v1/auth/refresh:
|
/api/v1/auth/refresh:
|
||||||
@ -50,6 +58,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthTokenResponseDto'
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
/api/v1/auth/logout:
|
/api/v1/auth/logout:
|
||||||
@ -60,6 +72,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthLogoutResponseDto'
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
security:
|
security:
|
||||||
@ -72,6 +88,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthProfileResponseDto'
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
security:
|
security:
|
||||||
@ -89,6 +109,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AuthProfileResponseDto'
|
||||||
tags:
|
tags:
|
||||||
- Auth
|
- Auth
|
||||||
security:
|
security:
|
||||||
@ -258,7 +282,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/ScreenerResultDto'
|
$ref: '#/components/schemas/ScreenerResponseDto'
|
||||||
tags:
|
tags:
|
||||||
- Securities
|
- Securities
|
||||||
/api/v1/securities/shares/{secid}:
|
/api/v1/securities/shares/{secid}:
|
||||||
@ -471,9 +495,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: array
|
$ref: '#/components/schemas/PortfolioListEnvelopeDto'
|
||||||
items:
|
|
||||||
$ref: '#/components/schemas/PortfolioListResponseDto'
|
|
||||||
tags:
|
tags:
|
||||||
- Portfolios
|
- Portfolios
|
||||||
security:
|
security:
|
||||||
@ -491,6 +513,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PortfolioEnvelopeDto'
|
||||||
tags:
|
tags:
|
||||||
- Portfolios
|
- Portfolios
|
||||||
security:
|
security:
|
||||||
@ -511,7 +537,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/components/schemas/PortfolioDetailResponseDto'
|
$ref: '#/components/schemas/PortfolioDetailEnvelopeDto'
|
||||||
tags:
|
tags:
|
||||||
- Portfolios
|
- Portfolios
|
||||||
security:
|
security:
|
||||||
@ -534,6 +560,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PortfolioEnvelopeDto'
|
||||||
tags:
|
tags:
|
||||||
- Portfolios
|
- Portfolios
|
||||||
security:
|
security:
|
||||||
@ -550,6 +580,18 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
nullable: true
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/PortfolioResponseMetaDto'
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- meta
|
||||||
tags:
|
tags:
|
||||||
- Portfolios
|
- Portfolios
|
||||||
security:
|
security:
|
||||||
@ -573,6 +615,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'201':
|
'201':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PositionEnvelopeDto'
|
||||||
tags:
|
tags:
|
||||||
- Portfolios
|
- Portfolios
|
||||||
security:
|
security:
|
||||||
@ -601,6 +647,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PositionEnvelopeDto'
|
||||||
tags:
|
tags:
|
||||||
- Portfolios
|
- Portfolios
|
||||||
security:
|
security:
|
||||||
@ -622,6 +672,18 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
nullable: true
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/PortfolioResponseMetaDto'
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- meta
|
||||||
tags:
|
tags:
|
||||||
- Portfolios
|
- Portfolios
|
||||||
security:
|
security:
|
||||||
@ -639,6 +701,10 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: ''
|
description: ''
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/AnalyticsEnvelopeDto'
|
||||||
tags:
|
tags:
|
||||||
- Portfolios
|
- Portfolios
|
||||||
security:
|
security:
|
||||||
@ -672,6 +738,54 @@ components:
|
|||||||
required:
|
required:
|
||||||
- email
|
- email
|
||||||
- password
|
- password
|
||||||
|
AuthUserDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
email:
|
||||||
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
role:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- email
|
||||||
|
- name
|
||||||
|
- role
|
||||||
|
AuthTokenDataDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
user:
|
||||||
|
$ref: '#/components/schemas/AuthUserDto'
|
||||||
|
accessToken:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- user
|
||||||
|
- accessToken
|
||||||
|
AuthResponseMetaDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
cachedAt:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
fromCache:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- cachedAt
|
||||||
|
- fromCache
|
||||||
|
AuthTokenResponseDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/AuthTokenDataDto'
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/AuthResponseMetaDto'
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- meta
|
||||||
LoginDto:
|
LoginDto:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -684,6 +798,33 @@ components:
|
|||||||
required:
|
required:
|
||||||
- email
|
- email
|
||||||
- password
|
- password
|
||||||
|
LogoutDataDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- message
|
||||||
|
AuthLogoutResponseDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/LogoutDataDto'
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/AuthResponseMetaDto'
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- meta
|
||||||
|
AuthProfileResponseDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/AuthUserDto'
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/AuthResponseMetaDto'
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- meta
|
||||||
UpdateProfileDto:
|
UpdateProfileDto:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -708,13 +849,16 @@ components:
|
|||||||
- share
|
- share
|
||||||
- bond
|
- bond
|
||||||
price:
|
price:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
example: 322.35
|
example: 322.35
|
||||||
change:
|
change:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
example: 1.15
|
example: 1.15
|
||||||
changePercent:
|
changePercent:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
example: 0.36
|
example: 0.36
|
||||||
volume:
|
volume:
|
||||||
type: number
|
type: number
|
||||||
@ -723,28 +867,36 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
example: 1
|
example: 1
|
||||||
capitalization:
|
capitalization:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
example: 6958336818320
|
example: 6958336818320
|
||||||
yieldToMaturity:
|
yieldToMaturity:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
example: 12.71
|
example: 12.71
|
||||||
duration:
|
duration:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
example: 4.5
|
example: 4.5
|
||||||
couponValue:
|
couponValue:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
example: 40.64
|
example: 40.64
|
||||||
couponPercent:
|
couponPercent:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
example: 8.15
|
example: 8.15
|
||||||
accruedInt:
|
accruedInt:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
example: 29.48
|
example: 29.48
|
||||||
matDate:
|
matDate:
|
||||||
type: object
|
type: string
|
||||||
|
nullable: true
|
||||||
example: '2027-02-03'
|
example: '2027-02-03'
|
||||||
bondType:
|
bondType:
|
||||||
type: object
|
type: string
|
||||||
|
nullable: true
|
||||||
example: ОФЗ-ПД
|
example: ОФЗ-ПД
|
||||||
required:
|
required:
|
||||||
- secid
|
- secid
|
||||||
@ -774,6 +926,38 @@ components:
|
|||||||
- page
|
- page
|
||||||
- pageSize
|
- pageSize
|
||||||
- totalPages
|
- totalPages
|
||||||
|
ScreenerResponseMetaDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
cachedAt:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
fromCache:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- cachedAt
|
||||||
|
- fromCache
|
||||||
|
ScreenerResponseDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/ScreenerResultDto'
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/ScreenerResponseMetaDto'
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- meta
|
||||||
|
PortfolioResponseMetaDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
cachedAt:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
fromCache:
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- cachedAt
|
||||||
|
- fromCache
|
||||||
PortfolioListResponseDto:
|
PortfolioListResponseDto:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -782,7 +966,8 @@ components:
|
|||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
type: object
|
type: string
|
||||||
|
nullable: true
|
||||||
currency:
|
currency:
|
||||||
type: string
|
type: string
|
||||||
default: RUB
|
default: RUB
|
||||||
@ -812,6 +997,18 @@ components:
|
|||||||
- positionCount
|
- positionCount
|
||||||
- shareCount
|
- shareCount
|
||||||
- bondCount
|
- bondCount
|
||||||
|
PortfolioListEnvelopeDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/PortfolioListResponseDto'
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/PortfolioResponseMetaDto'
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- meta
|
||||||
CreatePortfolioDto:
|
CreatePortfolioDto:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -833,6 +1030,39 @@ components:
|
|||||||
- BYN
|
- BYN
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
|
PortfolioResponseDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
currency:
|
||||||
|
type: string
|
||||||
|
default: RUB
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
updatedAt:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- name
|
||||||
|
- currency
|
||||||
|
- createdAt
|
||||||
|
- updatedAt
|
||||||
|
PortfolioEnvelopeDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/PortfolioResponseDto'
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/PortfolioResponseMetaDto'
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- meta
|
||||||
PositionWithPriceDto:
|
PositionWithPriceDto:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -842,7 +1072,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
example: SBER
|
example: SBER
|
||||||
shortName:
|
shortName:
|
||||||
type: object
|
type: string
|
||||||
|
nullable: true
|
||||||
type:
|
type:
|
||||||
type: string
|
type: string
|
||||||
example: share
|
example: share
|
||||||
@ -853,59 +1084,87 @@ components:
|
|||||||
type: number
|
type: number
|
||||||
example: 10
|
example: 10
|
||||||
buyPrice:
|
buyPrice:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
buyDate:
|
buyDate:
|
||||||
type: object
|
type: string
|
||||||
|
nullable: true
|
||||||
notes:
|
notes:
|
||||||
type: object
|
type: string
|
||||||
|
nullable: true
|
||||||
tags:
|
tags:
|
||||||
type: object
|
nullable: true
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
currentPrice:
|
currentPrice:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
totalCost:
|
totalCost:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
currentValue:
|
currentValue:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
weightPercent:
|
weightPercent:
|
||||||
type: number
|
type: number
|
||||||
pnl:
|
pnl:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
pnlPercent:
|
pnlPercent:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
dividendIncome:
|
dividendIncome:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
totalReturn:
|
totalReturn:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
totalReturnPercent:
|
totalReturnPercent:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
change:
|
change:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
changePercent:
|
changePercent:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
yieldToMaturity:
|
yieldToMaturity:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
duration:
|
duration:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
couponValue:
|
couponValue:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
couponPercent:
|
couponPercent:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
nextCouponDate:
|
nextCouponDate:
|
||||||
type: object
|
type: string
|
||||||
|
nullable: true
|
||||||
matDate:
|
matDate:
|
||||||
type: object
|
type: string
|
||||||
|
nullable: true
|
||||||
accruedInt:
|
accruedInt:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
bid:
|
bid:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
offer:
|
offer:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
couponPeriod:
|
couponPeriod:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
bondType:
|
bondType:
|
||||||
type: object
|
type: string
|
||||||
|
nullable: true
|
||||||
offerDate:
|
offerDate:
|
||||||
type: object
|
type: string
|
||||||
|
nullable: true
|
||||||
required:
|
required:
|
||||||
- id
|
- id
|
||||||
- secid
|
- secid
|
||||||
@ -922,17 +1181,20 @@ components:
|
|||||||
totalPnl:
|
totalPnl:
|
||||||
type: number
|
type: number
|
||||||
totalPnlPercent:
|
totalPnlPercent:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
totalDividends:
|
totalDividends:
|
||||||
type: number
|
type: number
|
||||||
totalReturn:
|
totalReturn:
|
||||||
type: number
|
type: number
|
||||||
totalReturnPercent:
|
totalReturnPercent:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
positionCount:
|
positionCount:
|
||||||
type: number
|
type: number
|
||||||
weightedYield:
|
weightedYield:
|
||||||
type: object
|
type: number
|
||||||
|
nullable: true
|
||||||
required:
|
required:
|
||||||
- totalInvested
|
- totalInvested
|
||||||
- totalValue
|
- totalValue
|
||||||
@ -951,7 +1213,8 @@ components:
|
|||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description:
|
description:
|
||||||
type: object
|
type: string
|
||||||
|
nullable: true
|
||||||
currency:
|
currency:
|
||||||
type: string
|
type: string
|
||||||
default: RUB
|
default: RUB
|
||||||
@ -976,6 +1239,16 @@ components:
|
|||||||
- positions
|
- positions
|
||||||
- totalValue
|
- totalValue
|
||||||
- analytics
|
- analytics
|
||||||
|
PortfolioDetailEnvelopeDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/PortfolioDetailResponseDto'
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/PortfolioResponseMetaDto'
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- meta
|
||||||
UpdatePortfolioDto:
|
UpdatePortfolioDto:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -1014,22 +1287,66 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
example: Покупка на дип
|
example: Покупка на дип
|
||||||
tags:
|
tags:
|
||||||
type: string
|
type: array
|
||||||
example:
|
example:
|
||||||
- DIVIDEND
|
- DIVIDEND
|
||||||
- GROWTH
|
- GROWTH
|
||||||
enum:
|
items:
|
||||||
- DIVIDEND
|
type: string
|
||||||
- GROWTH
|
enum:
|
||||||
- DEFENSIVE
|
- DIVIDEND
|
||||||
- SPECULATIVE
|
- GROWTH
|
||||||
- BOND
|
- DEFENSIVE
|
||||||
- ETF
|
- SPECULATIVE
|
||||||
- GOVERNMENT
|
- BOND
|
||||||
- CASH
|
- ETF
|
||||||
|
- GOVERNMENT
|
||||||
|
- CASH
|
||||||
required:
|
required:
|
||||||
- secid
|
- secid
|
||||||
- quantity
|
- quantity
|
||||||
|
PositionResponseDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: number
|
||||||
|
secid:
|
||||||
|
type: string
|
||||||
|
example: SBER
|
||||||
|
quantity:
|
||||||
|
type: number
|
||||||
|
example: 10
|
||||||
|
notes:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
tags:
|
||||||
|
nullable: true
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
portfolioId:
|
||||||
|
type: number
|
||||||
|
createdAt:
|
||||||
|
type: string
|
||||||
|
updatedAt:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
- secid
|
||||||
|
- quantity
|
||||||
|
- portfolioId
|
||||||
|
- createdAt
|
||||||
|
- updatedAt
|
||||||
|
PositionEnvelopeDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/PositionResponseDto'
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/PortfolioResponseMetaDto'
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- meta
|
||||||
UpdatePositionDto:
|
UpdatePositionDto:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@ -1046,15 +1363,39 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
example: Докупка
|
example: Докупка
|
||||||
tags:
|
tags:
|
||||||
type: string
|
type: array
|
||||||
example:
|
example:
|
||||||
- DIVIDEND
|
- DIVIDEND
|
||||||
enum:
|
items:
|
||||||
- DIVIDEND
|
type: string
|
||||||
- GROWTH
|
enum:
|
||||||
- DEFENSIVE
|
- DIVIDEND
|
||||||
- SPECULATIVE
|
- GROWTH
|
||||||
- BOND
|
- DEFENSIVE
|
||||||
- ETF
|
- SPECULATIVE
|
||||||
- GOVERNMENT
|
- BOND
|
||||||
- CASH
|
- ETF
|
||||||
|
- GOVERNMENT
|
||||||
|
- CASH
|
||||||
|
AnalyticsResponseDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
positions:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/PositionWithPriceDto'
|
||||||
|
summary:
|
||||||
|
$ref: '#/components/schemas/PortfolioSummaryDto'
|
||||||
|
required:
|
||||||
|
- positions
|
||||||
|
- summary
|
||||||
|
AnalyticsEnvelopeDto:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
$ref: '#/components/schemas/AnalyticsResponseDto'
|
||||||
|
meta:
|
||||||
|
$ref: '#/components/schemas/PortfolioResponseMetaDto'
|
||||||
|
required:
|
||||||
|
- data
|
||||||
|
- meta
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user