codex/frontend-infrastructure-tooling #39

Merged
ksv741 merged 13 commits from codex/frontend-infrastructure-tooling into main 2026-06-23 21:31:16 +03:00
6 changed files with 431 additions and 400 deletions
Showing only changes of commit 063e80c375 - Show all commits

View File

@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
export class ApiResponseMeta {
@ApiProperty({ nullable: true })
@ApiProperty({ type: String, nullable: true })
cachedAt: string | null;
@ApiProperty()

View File

@ -21,37 +21,37 @@ export class BrokerEventItemDto {
@ApiProperty()
eventDate!: string;
@ApiProperty({ nullable: true })
@ApiProperty({ type: String, nullable: true })
paymentDate!: string | null;
@ApiProperty({ nullable: true })
@ApiProperty({ type: String, nullable: true })
ticker!: string | null;
@ApiProperty({ nullable: true })
@ApiProperty({ type: String, nullable: true })
name!: string | null;
@ApiProperty({ nullable: true })
@ApiProperty({ type: String, nullable: true })
instrumentUid!: string | null;
@ApiProperty({ enum: instrumentTypes })
instrumentType!: string;
@ApiProperty({ nullable: true })
@ApiProperty({ type: Number, nullable: true })
quantitySnapshot!: number | null;
@ApiProperty({ nullable: true })
@ApiProperty({ type: Number, nullable: true })
payoutPerUnit!: number | null;
@ApiProperty({ nullable: true })
@ApiProperty({ type: Number, nullable: true })
estimatedAmount!: number | null;
@ApiProperty({ nullable: true })
@ApiProperty({ type: Number, nullable: true })
actualAmount!: number | null;
@ApiProperty({ nullable: true })
@ApiProperty({ type: String, nullable: true })
currency!: string | null;
@ApiProperty({ nullable: true })
@ApiProperty({ type: String, nullable: true, enum: ['current_position'] })
estimateMode!: 'current_position' | null;
}
@ -59,7 +59,7 @@ export class BrokerEventsSummaryDto {
@ApiProperty({ minimum: 0 })
eventCount!: number;
@ApiProperty({ nullable: true })
@ApiProperty({ type: String, nullable: true })
nearestEventDate!: string | null;
@ApiProperty()

View File

@ -8,7 +8,7 @@
"build": "vite build",
"typecheck": "tsc -b",
"preview": "vite preview",
"codegen": "openapi-typescript http://localhost:3000/api/docs-json -o src/api/types.ts",
"codegen": "openapi-typescript http://localhost:3000/api/docs-json -o src/shared/api/types.ts",
"lint": "biome check src/",
"lint:fix": "biome check --write src/",
"format": "biome format --write src/",

View File

@ -8,6 +8,9 @@ export type {
BondMarketData,
BondResponse,
BrokerAccount,
BrokerEventItem,
BrokerEventsData,
BrokerEventsSummary,
BrokerMoney,
BrokerOperation,
BrokerOperationCategory,

View File

@ -1,391 +1,62 @@
export interface ApiResponseMeta {
cachedAt: string | null
fromCache: boolean
}
import type { components } from './types'
// Re-exported from openapi-typescript codegen with friendly names
export type ApiResponseMeta = components['schemas']['ApiResponseMeta']
export interface ApiEnvelope<T> {
data: T
meta: ApiResponseMeta
}
export interface StockMarketData {
price: number | null
change: number | null
changePercent: number | null
open: number | null
high: number | null
low: number | null
volume: number
value: number
issueCapitalization: number | null
updatedAt: string
}
// Auth
export type AuthResponse = components['schemas']['AuthTokenDataDto']
export type UserResponse = components['schemas']['AuthUserDto']
export interface ShareResponse {
secid: string
isin: string
name: string
shortName: string
latName: string | null
listLevel: number
issueSize: number
faceValue: number
faceUnit: string
type: string
marketData: StockMarketData
}
// Shares
export type ShareResponse = components['schemas']['ShareResponseDto']
export type StockMarketData = components['schemas']['StockMarketDataDto']
export type DividendItem = components['schemas']['DividendItemDto']
export type ShareHistoryItem = components['schemas']['HistoryItemDto']
export interface DividendItem {
registryCloseDate: string
value: number
currency: string
}
// Bonds
export type BondResponse = components['schemas']['BondResponseDto']
export type BondMarketData = components['schemas']['BondMarketDataDto']
export type BondHistoryItem = components['schemas']['BondHistoryItemDto']
export interface ShareHistoryItem {
date: string
open: number
high: number
low: number
close: number
volume: number
value: number
}
// Candles
export type CandleItem = components['schemas']['CandleItemDto']
export interface BondMarketData {
price: number | null
yieldToMaturity: number | null
duration: number | null
accruedInt: number | null
couponValue: number | null
couponPercent: number | null
nextCouponDate: string | null
open: number
high: number | null
low: number | null
volume: number
updatedAt: string
}
// Search
export type SearchResultItem = components['schemas']['SearchResultItemDto']
export interface BondResponse {
secid: string
isin: string
name: string
shortName: string
latName: string | null
listLevel: number
issueSize: number
faceValue: number
faceUnit: string
matDate: string
couponValue: number
couponPercent: number | null
couponPeriod: number
nextCoupon: string | null
accruedInt: number
bondType: string
bondSubType: string
offerDate: string | null
buybackDate: string | null
marketData: BondMarketData
}
// Screener
export type ScreenerResult = components['schemas']['ScreenerResultDto']
export type ScreenerItem = components['schemas']['ScreenerItemDto']
export interface BondHistoryItem {
date: string
closePrice: number
yieldClose: number | null
duration: number | null
}
// Portfolio
export type Portfolio = components['schemas']['PortfolioListResponseDto']
export type PortfolioDetail = components['schemas']['PortfolioDetailResponseDto']
export type Position = components['schemas']['PositionResponseDto']
export type PositionWithPrice = components['schemas']['PositionWithPriceDto']
export type PortfolioSummary = components['schemas']['PortfolioSummaryDto']
export type AnalyticsResponse = components['schemas']['AnalyticsResponseDto']
export interface CandleItem {
open: number
high: number
low: number
close: number
volume: number
value: number
begin: string
end: string
}
// Health
export type HealthResponse = components['schemas']['HealthResponseDto']
export interface SearchResultItem {
secid: string
isin: string
shortName: string
type: 'share' | 'bond'
listLevel: number
currency: string | null
price: number | null
}
// Broker — some nullable fields still have Record<string, never> from backend
export type BrokerAccount = components['schemas']['BrokerAccountResponseDto']
export type BrokerPortfolio = components['schemas']['BrokerPortfolioResponseDto']
export type BrokerMoney = components['schemas']['BrokerMoneyDto']
export type BrokerPosition = components['schemas']['BrokerPositionResponseDto']
export type BrokerOperation = components['schemas']['BrokerOperationResponseDto']
export type BrokerOperationsPage = components['schemas']['BrokerOperationsPageResponseDto']
export type BrokerPositionsPage = components['schemas']['BrokerPositionsPageResponseDto']
export type BrokerOperationCategory =
components['schemas']['BrokerOperationResponseDto']['category']
export interface HealthResponse {
status: string
timestamp: string
uptime: number
}
export interface UserResponse {
id: number
email: string
name: string | null
role: string
}
export interface AuthResponse {
user: UserResponse
accessToken: string
}
export interface Portfolio {
id: number
name: string
description: string | null
currency: string
createdAt: string
updatedAt: string
totalValue: number
positionCount: number
shareCount: number
bondCount: number
}
export interface PositionWithPrice {
id: number
portfolioId: number
secid: string
shortName: string | null
type: 'share' | 'bond'
quantity: number
notes: string | null
tags: string[] | null
currentPrice: number | null
buyPrice: number | null
buyDate: string | null
totalCost: number | null
currentValue: number | null
pnl: number | null
pnlPercent: number | null
dividendIncome: number | null
totalReturn: number | null
totalReturnPercent: number | null
weightPercent: number
change?: number | null
changePercent?: number | null
yieldToMaturity?: number | null
duration?: number | null
couponValue?: number | null
couponPercent?: number | null
nextCouponDate?: string | null
matDate?: string | null
accruedInt?: number | null
bid?: number | null
offer?: number | null
couponPeriod?: number | null
bondType?: string | null
offerDate?: string | null
}
export interface PortfolioDetail extends Portfolio {
positions: PositionWithPrice[]
totalValue: number
analytics: PortfolioSummary
}
export interface Position {
id: number
secid: string
quantity: number
notes: string | null
tags: string[] | null
portfolioId: number
createdAt: string
updatedAt: string
}
export interface PortfolioSummary {
totalInvested: number
totalValue: number
totalPnl: number
totalPnlPercent: number | null
totalDividends: number
totalReturn: number
totalReturnPercent: number | null
positionCount: number
weightedYield: number | null
}
export interface AnalyticsResponse {
positions: PositionWithPrice[]
summary: PortfolioSummary
}
export interface ScreenerItem {
secid: string
shortName: string
isin: string
type: 'share' | 'bond'
price: number | null
change: number | null
changePercent: number | null
volume: number
listLevel: number
capitalization: number | null
yieldToMaturity: number | null
duration: number | null
couponValue: number | null
couponPercent: number | null
accruedInt: number | null
matDate: string | null
bondType: string | null
}
export interface ScreenerResult {
items: ScreenerItem[]
total: number
page: number
pageSize: number
totalPages: number
}
export interface BrokerMoney {
currency: string
units: string
nano: number
value: number
}
export interface BrokerAccount {
id: string
type: 'brokerage' | 'iis'
name: string
status: string
openedAt: string | null
accessLevel: string | null
}
export interface BrokerPosition {
figi: string | null
instrumentUid: string | null
positionUid: string | null
ticker: string | null
classCode: string | null
instrumentType: string | null
name: string | null
quantity: number | null
blockedLots: number | null
currentPrice: BrokerMoney | null
currentValue: BrokerMoney | null
averagePositionPrice: BrokerMoney | null
expectedYieldPercent: number | null
dailyYield: BrokerMoney | null
}
export interface BrokerPortfolio {
account: BrokerAccount
positionCounts: {
shares: number
bonds: number
etf: number
other: number
}
totals: {
shares: BrokerMoney | null
bonds: BrokerMoney | null
etf: BrokerMoney | null
currencies: BrokerMoney | null
futures: BrokerMoney | null
options: BrokerMoney | null
structuredProducts: BrokerMoney | null
dfa: BrokerMoney | null
portfolio: BrokerMoney | null
}
yields: {
expectedPercent: number | null
daily: BrokerMoney | null
dailyPercent: number | null
}
cash: BrokerMoney[]
blockedCash: BrokerMoney[]
asOf: string
}
export type BrokerOperationCategory = 'trade' | 'income' | 'tax' | 'fee' | 'transfer' | 'other'
export interface BrokerOperation {
cursor: string | null
accountId: string
id: string | null
parentOperationId: string | null
date: string | null
type: string
category: BrokerOperationCategory
description: string | null
name: string | null
state: string | null
instrumentUid: string | null
figi: string | null
ticker: string | null
classCode: string | null
instrumentType: string | null
payment: BrokerMoney | null
price: BrokerMoney | null
commission: BrokerMoney | null
yield: BrokerMoney | null
accruedInt: BrokerMoney | null
quantity: number | null
quantityDone: number | null
}
export interface BrokerOperationsPage {
accountId: string
items: BrokerOperation[]
nextCursor: string | null
hasNext: boolean
asOf: string
}
export interface BrokerPositionsPage {
accountId: string
items: BrokerPosition[]
nextCursor: string | null
hasNext: boolean
asOf: string
}
export interface BrokerPortfolioEvent {
id: string
type: 'dividend' | 'coupon' | 'maturity' | 'offer'
source: 'forecast' | 'actual'
category: 'cashflow' | 'corporate'
eventDate: string
paymentDate: string | null
ticker: string | null
name: string | null
instrumentUid: string | null
instrumentType: 'share' | 'bond' | 'other'
quantitySnapshot: number | null
payoutPerUnit: number | null
estimatedAmount: number | null
actualAmount: number | null
currency: string | null
estimateMode: 'current_position' | null
}
export interface BrokerEventsSummary {
eventCount: number
nearestEventDate: string | null
totalEstimatedCashflow: number
actualCashflow: number
forecastEstimatedCashflow: number
dividendsTotal: number
couponsTotal: number
principalRepaymentTotal: number
actualDividendsTotal: number
actualCouponsTotal: number
actualPrincipalRepaymentTotal: number
}
export interface BrokerEventsData {
items: BrokerPortfolioEvent[]
summary: BrokerEventsSummary
asOf: string
}
// Broker events
export type BrokerEventItem = components['schemas']['BrokerEventItemDto']
export type BrokerEventsData = components['schemas']['BrokerEventsDataDto']
export type BrokerEventsSummary = components['schemas']['BrokerEventsSummaryDto']

View File

@ -451,6 +451,23 @@ export interface paths {
patch?: never
trace?: never
}
'/api/v1/broker/accounts/{accountId}/events': {
parameters: {
query?: never
header?: never
path?: never
cookie?: never
}
/** Get broker account calendar events and cashflow */
get: operations['TBankController_getEvents']
put?: never
post?: never
delete?: never
options?: never
head?: never
patch?: never
trace?: never
}
'/api/v1/broker/accounts/{accountId}/operations/sync': {
parameters: {
query?: never
@ -472,6 +489,22 @@ export interface paths {
export type webhooks = Record<string, never>
export interface components {
schemas: {
ApiResponseMeta: {
cachedAt: string | null
fromCache: boolean
}
HealthResponseDto: {
/** @example ok */
status: string
/** @example 2026-06-23T06:00:00.000Z */
timestamp: string
/** @example 12345 */
uptime: number
}
HealthEnvelopeDto: {
data: components['schemas']['HealthResponseDto']
meta: components['schemas']['ApiResponseMeta']
}
RegisterDto: {
/** @example user@example.com */
email: string
@ -519,6 +552,26 @@ export interface components {
/** @example John Doe */
name?: string
}
SearchResultItemDto: {
/** @example SBER */
secid: string
/** @example RU0009029540 */
isin: string
/** @example Сбербанк */
shortName: string
/** @enum {string} */
type: 'share' | 'bond'
/** @example 1 */
listLevel: number
/** @example RUB */
currency?: string | null
/** @example 322.35 */
price?: number | null
}
SearchEnvelopeDto: {
data: components['schemas']['SearchResultItemDto'][]
meta: components['schemas']['ApiResponseMeta']
}
ScreenerItemDto: {
/** @example SBER */
secid: string
@ -570,6 +623,215 @@ export interface components {
data: components['schemas']['ScreenerResultDto']
meta: components['schemas']['ScreenerResponseMetaDto']
}
StockMarketDataDto: {
/** @example 322.35 */
price: number
/** @example 1.15 */
change: number
/** @example 0.36 */
changePercent: number
/** @example 321.3 */
open: number
/** @example 322.66 */
high?: Record<string, never>
/** @example 321.2 */
low?: Record<string, never>
/** @example 1925163 */
volume: number
/** @example 620184479 */
value: number
/** @example 6958336818320 */
issueCapitalization?: Record<string, never>
updatedAt: string
}
ShareResponseDto: {
/** @example SBER */
secid: string
/** @example RU0009029540 */
isin: string
/** @example Сбербанк России ПАО ао */
name: string
/** @example Сбербанк */
shortName: string
latName?: Record<string, never>
/** @example 1 */
listLevel: number
/** @example 21586948000 */
issueSize: number
/** @example 3 */
faceValue: number
/** @example RUB */
faceUnit: string
/** @example common_share */
type: string
marketData: components['schemas']['StockMarketDataDto']
}
ShareEnvelopeDto: {
data: components['schemas']['ShareResponseDto']
meta: components['schemas']['ApiResponseMeta']
}
ShareMarketDataResponseDto: {
/** @example 322.35 */
price: number
/** @example 1.15 */
change: number
/** @example 0.36 */
changePercent: number
/** @example 321.3 */
open: number
/** @example 322.66 */
high?: Record<string, never>
/** @example 321.2 */
low?: Record<string, never>
/** @example 1925163 */
volume: number
/** @example 620184479 */
value: number
/** @example 6958336818320 */
issueCapitalization?: Record<string, never>
updatedAt: string
}
ShareMarketDataEnvelopeDto: {
data: components['schemas']['ShareMarketDataResponseDto']
meta: components['schemas']['ApiResponseMeta']
}
DividendItemDto: {
/** @example 2026-05-15 */
registryCloseDate: string
/** @example 33.47 */
value: number
/** @example RUB */
currency: string
}
DividendsEnvelopeDto: {
data: components['schemas']['DividendItemDto'][]
meta: components['schemas']['ApiResponseMeta']
}
HistoryItemDto: {
/** @example 2026-06-01 */
date: string
/** @example 321.3 */
open: number
/** @example 322.66 */
high: number
/** @example 321.2 */
low: number
/** @example 322.35 */
close: number
/** @example 1925163 */
volume: number
/** @example 620184479 */
value: number
}
ShareHistoryEnvelopeDto: {
data: components['schemas']['HistoryItemDto'][]
meta: components['schemas']['ApiResponseMeta']
}
BondMarketDataDto: {
/**
* @description Цена в % от номинала
* @example 100.45
*/
price: number
/** @example 12.71 */
yieldToMaturity?: Record<string, never>
duration?: Record<string, never>
/** @example 29.48 */
accruedInt: number
/** @example 40.64 */
couponValue: number
/** @example 8.15 */
couponPercent?: Record<string, never>
/** @example 2026-08-05 */
nextCouponDate?: Record<string, never>
open: number
high?: Record<string, never>
low?: Record<string, never>
volume: number
updatedAt: string
}
BondResponseDto: {
/** @example SU26207RMFS9 */
secid: string
/** @example RU000A0JS3W6 */
isin: string
/** @example ОФЗ-ПД 26207 03/02/27 */
name: string
/** @example ОФЗ 26207 */
shortName: string
latName?: Record<string, never>
/** @example 1 */
listLevel: number
/** @example 370200604 */
issueSize: number
/** @example 1000 */
faceValue: number
/** @example RUB */
faceUnit: string
/** @example 2027-02-03 */
matDate: string
/** @example 40.64 */
couponValue: number
/** @example 8.15 */
couponPercent?: Record<string, never>
/** @example 182 */
couponPeriod: number
/** @example 2026-08-05 */
nextCoupon: Record<string, never>
/** @example 29.48 */
accruedInt: number
/** @example Фикс с известным купоном */
bondType: string
/** @example До погашения */
bondSubType: string
offerDate?: Record<string, never>
buybackDate?: Record<string, never>
marketData: components['schemas']['BondMarketDataDto']
}
BondEnvelopeDto: {
data: components['schemas']['BondResponseDto']
meta: components['schemas']['ApiResponseMeta']
}
BondMarketDataEnvelopeDto: {
data: components['schemas']['BondMarketDataDto']
meta: components['schemas']['ApiResponseMeta']
}
BondHistoryItemDto: {
/** @example 2026-06-01 */
date: string
/** @example 100.45 */
closePrice: number
/** @example 12.71 */
yieldClose?: number | null
/** @example 4.5 */
duration?: number | null
}
BondHistoryEnvelopeDto: {
data: components['schemas']['BondHistoryItemDto'][]
meta: components['schemas']['ApiResponseMeta']
}
CandleItemDto: {
/** @example 321.3 */
open: number
/** @example 322.66 */
high: number
/** @example 321.2 */
low: number
/** @example 322.35 */
close: number
/** @example 1925163 */
volume: number
/** @example 620184479 */
value: number
/** @example 2026-06-01T10:00:00 */
begin: string
/** @example 2026-06-01T10:59:00 */
end: string
}
CandleEnvelopeDto: {
data: components['schemas']['CandleItemDto'][]
meta: components['schemas']['ApiResponseMeta']
}
PortfolioResponseMetaDto: {
cachedAt: string | null
fromCache: boolean
@ -895,6 +1157,51 @@ export interface components {
data: components['schemas']['BrokerOperationsPageResponseDto']
meta: components['schemas']['BrokerResponseMetaDto']
}
BrokerEventItemDto: {
id: string
/** @enum {string} */
type: 'dividend' | 'coupon' | 'maturity' | 'offer'
/** @enum {string} */
source: 'forecast' | 'actual'
/** @enum {string} */
category: 'cashflow' | 'corporate'
eventDate: string
paymentDate: string | null
ticker: string | null
name: string | null
instrumentUid: string | null
/** @enum {string} */
instrumentType: 'share' | 'bond' | 'other'
quantitySnapshot: number | null
payoutPerUnit: number | null
estimatedAmount: number | null
actualAmount: number | null
currency: string | null
/** @enum {string|null} */
estimateMode: 'current_position' | null
}
BrokerEventsSummaryDto: {
eventCount: number
nearestEventDate: string | null
totalEstimatedCashflow: number
actualCashflow: number
forecastEstimatedCashflow: number
dividendsTotal: number
couponsTotal: number
principalRepaymentTotal: number
actualDividendsTotal: number
actualCouponsTotal: number
actualPrincipalRepaymentTotal: number
}
BrokerEventsDataDto: {
items: components['schemas']['BrokerEventItemDto'][]
summary: components['schemas']['BrokerEventsSummaryDto']
asOf: string
}
BrokerEventsEnvelopeDto: {
data: components['schemas']['BrokerEventsDataDto']
meta: components['schemas']['BrokerResponseMetaDto']
}
BrokerOperationSyncResponseDto: {
/** @example 42 */
upserted: number
@ -925,7 +1232,9 @@ export interface operations {
headers: {
[name: string]: unknown
}
content?: never
content: {
'application/json': components['schemas']['HealthEnvelopeDto']
}
}
}
}
@ -1073,7 +1382,9 @@ export interface operations {
headers: {
[name: string]: unknown
}
content?: never
content: {
'application/json': components['schemas']['SearchEnvelopeDto']
}
}
}
}
@ -1135,7 +1446,9 @@ export interface operations {
headers: {
[name: string]: unknown
}
content?: never
content: {
'application/json': components['schemas']['ShareEnvelopeDto']
}
}
}
}
@ -1154,7 +1467,9 @@ export interface operations {
headers: {
[name: string]: unknown
}
content?: never
content: {
'application/json': components['schemas']['ShareMarketDataEnvelopeDto']
}
}
}
}
@ -1173,7 +1488,9 @@ export interface operations {
headers: {
[name: string]: unknown
}
content?: never
content: {
'application/json': components['schemas']['DividendsEnvelopeDto']
}
}
}
}
@ -1195,7 +1512,9 @@ export interface operations {
headers: {
[name: string]: unknown
}
content?: never
content: {
'application/json': components['schemas']['ShareHistoryEnvelopeDto']
}
}
}
}
@ -1214,7 +1533,9 @@ export interface operations {
headers: {
[name: string]: unknown
}
content?: never
content: {
'application/json': components['schemas']['BondEnvelopeDto']
}
}
}
}
@ -1233,7 +1554,9 @@ export interface operations {
headers: {
[name: string]: unknown
}
content?: never
content: {
'application/json': components['schemas']['BondMarketDataEnvelopeDto']
}
}
}
}
@ -1255,7 +1578,9 @@ export interface operations {
headers: {
[name: string]: unknown
}
content?: never
content: {
'application/json': components['schemas']['BondHistoryEnvelopeDto']
}
}
}
}
@ -1278,7 +1603,9 @@ export interface operations {
headers: {
[name: string]: unknown
}
content?: never
content: {
'application/json': components['schemas']['CandleEnvelopeDto']
}
}
}
}
@ -1301,7 +1628,9 @@ export interface operations {
headers: {
[name: string]: unknown
}
content?: never
content: {
'application/json': components['schemas']['CandleEnvelopeDto']
}
}
}
}
@ -1610,6 +1939,34 @@ export interface operations {
}
}
}
TBankController_getEvents: {
parameters: {
query: {
/** @description Start date inclusive (YYYY-MM-DD) */
from: string
/** @description End date inclusive (YYYY-MM-DD) */
to: string
/** @description Comma-separated event types to include */
types?: string
}
header?: never
path: {
accountId: string
}
cookie?: never
}
requestBody?: never
responses: {
200: {
headers: {
[name: string]: unknown
}
content: {
'application/json': components['schemas']['BrokerEventsEnvelopeDto']
}
}
}
}
TBankController_syncOperations: {
parameters: {
query: {