From a9132109dab92fbeb1047832f0e4a56809a0ce56 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sat, 20 Jun 2026 13:28:56 +0300 Subject: [PATCH] refactor(frontend): create shared/api layer with re-export shims --- apps/frontend/src/api/client.ts | 249 +-- apps/frontend/src/api/responses.ts | 384 +---- apps/frontend/src/api/types.ts | 1638 +------------------ apps/frontend/src/shared/api/client.test.ts | 153 ++ apps/frontend/src/shared/api/client.ts | 232 +++ apps/frontend/src/shared/api/index.ts | 49 + apps/frontend/src/shared/api/responses.ts | 352 ++++ apps/frontend/src/shared/api/types.ts | 1637 ++++++++++++++++++ 8 files changed, 2473 insertions(+), 2221 deletions(-) create mode 100644 apps/frontend/src/shared/api/client.test.ts create mode 100644 apps/frontend/src/shared/api/client.ts create mode 100644 apps/frontend/src/shared/api/index.ts create mode 100644 apps/frontend/src/shared/api/responses.ts create mode 100644 apps/frontend/src/shared/api/types.ts diff --git a/apps/frontend/src/api/client.ts b/apps/frontend/src/api/client.ts index c6f0873..952ffbe 100644 --- a/apps/frontend/src/api/client.ts +++ b/apps/frontend/src/api/client.ts @@ -1,232 +1,17 @@ -import type { - ApiEnvelope, - ApiResponseMeta, - AuthResponse, - ShareResponse, - StockMarketData, - DividendItem, - ShareHistoryItem, - BondResponse, - BondMarketData, - BondHistoryItem, - CandleItem, - SearchResultItem, - HealthResponse, -} from './responses'; - -const BASE = ''; - -let accessToken: string | null = null; -let onUnauthorized: (() => void) | null = null; -let isRefreshing = false; -let refreshPromise: Promise | null = null; - -export function setAccessToken(token: string | null) { - accessToken = token; -} - -export function getAccessToken(): string | null { - return accessToken; -} - -export function setOnUnauthorized(cb: () => void) { - onUnauthorized = cb; -} - -async function refreshTokens(): Promise { - try { - const res = await fetch(`${BASE}/api/v1/auth/refresh`, { - method: 'POST', - credentials: 'include', - }); - if (!res.ok) return false; - const json = await res.json(); - accessToken = normalizeEnvelope(json).data.accessToken; - return true; - } catch { - return false; - } -} - -function normalizeEnvelope(json: unknown): { data: T; meta: ApiResponseMeta } { - const envelope = json as ApiEnvelope; - if ( - envelope.data && - typeof envelope.data === 'object' && - 'data' in envelope.data && - 'meta' in envelope.data - ) { - return envelope.data as { data: T; meta: ApiResponseMeta }; - } - - return { - data: envelope.data as T, - meta: envelope.meta, - }; -} - -async function handleUnauthorized(): Promise { - if (isRefreshing && refreshPromise) { - return refreshPromise; - } - - isRefreshing = true; - refreshPromise = refreshTokens().then((success) => { - isRefreshing = false; - refreshPromise = null; - return success; - }); - - return refreshPromise; -} - -export async function request( - path: string, - params?: Record, - options?: { method?: string; body?: unknown; skipAuth?: boolean }, -): Promise<{ data: T; meta: ApiResponseMeta }> { - const url = new URL(`${BASE}${path}`, window.location.origin); - if (params) { - for (const [k, v] of Object.entries(params)) { - if (v !== undefined) url.searchParams.set(k, v); - } - } - - const headers: Record = {}; - if (!options?.skipAuth && accessToken) { - headers['Authorization'] = `Bearer ${accessToken}`; - } - if (options?.body && !(options.body instanceof FormData)) { - headers['Content-Type'] = 'application/json'; - } - - const fetchOptions: RequestInit = { - headers, - credentials: 'include' as RequestCredentials, - }; - if (options?.method) { - fetchOptions.method = options.method; - } - if (options?.body !== undefined) { - fetchOptions.body = - options.body instanceof FormData ? options.body : JSON.stringify(options.body); - } - - let res = await fetch(url.toString(), fetchOptions); - - if (res.status === 401 && !options?.skipAuth) { - const refreshed = await handleUnauthorized(); - if (refreshed) { - headers['Authorization'] = `Bearer ${accessToken}`; - res = await fetch(url.toString(), { ...fetchOptions, headers }); - } else { - accessToken = null; - onUnauthorized?.(); - throw new Error('Сессия истекла'); - } - } - - if (!res.ok) { - const text = await res.text().catch(() => ''); - throw new Error(`Ошибка API: ${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`); - } - - const json = await res.json(); - return normalizeEnvelope(json); -} - -export function getHealth(): Promise<{ data: HealthResponse; meta: ApiResponseMeta }> { - return request('/api/v1/health'); -} - -export function searchSecurities( - q: string, - type: 'all' | 'share' | 'bond' = 'all', - limit = 20, -): Promise<{ data: SearchResultItem[]; meta: ApiResponseMeta }> { - return request('/api/v1/securities/search', { - q, - type, - limit: String(limit), - }); -} - -export function getShare(secid: string): Promise<{ data: ShareResponse; meta: ApiResponseMeta }> { - return request(`/api/v1/securities/shares/${encodeURIComponent(secid)}`); -} - -export function getShareMarketData( - secid: string, -): Promise<{ data: StockMarketData; meta: ApiResponseMeta }> { - return request( - `/api/v1/securities/shares/${encodeURIComponent(secid)}/marketdata`, - ); -} - -export function getShareDividends( - secid: string, -): Promise<{ data: DividendItem[]; meta: ApiResponseMeta }> { - return request( - `/api/v1/securities/shares/${encodeURIComponent(secid)}/dividends`, - ); -} - -export function getShareHistory( - secid: string, - from: string, - till: string, -): Promise<{ data: ShareHistoryItem[]; meta: ApiResponseMeta }> { - return request( - `/api/v1/securities/shares/${encodeURIComponent(secid)}/history`, - { from, till }, - ); -} - -export function getBond(secid: string): Promise<{ data: BondResponse; meta: ApiResponseMeta }> { - return request(`/api/v1/securities/bonds/${encodeURIComponent(secid)}`); -} - -export function getBondMarketData( - secid: string, -): Promise<{ data: BondMarketData; meta: ApiResponseMeta }> { - return request( - `/api/v1/securities/bonds/${encodeURIComponent(secid)}/marketdata`, - ); -} - -export function getBondHistory( - secid: string, - from: string, - till: string, -): Promise<{ data: BondHistoryItem[]; meta: ApiResponseMeta }> { - return request( - `/api/v1/securities/bonds/${encodeURIComponent(secid)}/history`, - { from, till }, - ); -} - -export function getShareCandles( - secid: string, - interval: '1h' | '24h', - from: string, - till: string, -): Promise<{ data: CandleItem[]; meta: ApiResponseMeta }> { - return request(`/api/v1/securities/shares/${encodeURIComponent(secid)}/candles`, { - interval, - from, - till, - }); -} - -export function getBondCandles( - secid: string, - interval: '1h' | '24h', - from: string, - till: string, -): Promise<{ data: CandleItem[]; meta: ApiResponseMeta }> { - return request(`/api/v1/securities/bonds/${encodeURIComponent(secid)}/candles`, { - interval, - from, - till, - }); -} +export { + request, + setAccessToken, + getAccessToken, + setOnUnauthorized, + getHealth, + searchSecurities, + getShare, + getShareMarketData, + getShareDividends, + getShareHistory, + getBond, + getBondMarketData, + getBondHistory, + getShareCandles, + getBondCandles, +} from '../shared/api/client'; diff --git a/apps/frontend/src/api/responses.ts b/apps/frontend/src/api/responses.ts index 0dd5305..0ac068e 100644 --- a/apps/frontend/src/api/responses.ts +++ b/apps/frontend/src/api/responses.ts @@ -1,352 +1,32 @@ -export interface ApiResponseMeta { - cachedAt: string | null; - fromCache: boolean; -} - -export interface ApiEnvelope { - 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; -} - -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; -} - -export interface DividendItem { - registryCloseDate: string; - value: number; - currency: string; -} - -export interface ShareHistoryItem { - date: string; - open: number; - high: number; - low: number; - close: number; - volume: number; - value: number; -} - -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; -} - -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; -} - -export interface BondHistoryItem { - date: string; - closePrice: number; - yieldClose: number | null; - duration: number | null; -} - -export interface CandleItem { - open: number; - high: number; - low: number; - close: number; - volume: number; - value: number; - begin: string; - end: string; -} - -export interface SearchResultItem { - secid: string; - isin: string; - shortName: string; - type: 'share' | 'bond'; - listLevel: number; - currency: string | null; - price: number | null; -} - -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 type { + ApiResponseMeta, + ApiEnvelope, + StockMarketData, + ShareResponse, + DividendItem, + ShareHistoryItem, + BondMarketData, + BondResponse, + BondHistoryItem, + CandleItem, + SearchResultItem, + HealthResponse, + UserResponse, + AuthResponse, + Portfolio, + PositionWithPrice, + PortfolioDetail, + Position, + PortfolioSummary, + AnalyticsResponse, + ScreenerItem, + ScreenerResult, + BrokerMoney, + BrokerAccount, + BrokerPosition, + BrokerPortfolio, + BrokerOperationCategory, + BrokerOperation, + BrokerOperationsPage, + BrokerPositionsPage, +} from '../shared/api/responses'; diff --git a/apps/frontend/src/api/types.ts b/apps/frontend/src/api/types.ts index c551319..d729c9f 100644 --- a/apps/frontend/src/api/types.ts +++ b/apps/frontend/src/api/types.ts @@ -1,1637 +1 @@ -/** - * This file was auto-generated by openapi-typescript. - * Do not make direct changes to the file. - */ - -export interface paths { - '/api/v1/health': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Проверка состояния сервиса */ - get: operations['HealthController_check']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/auth/register': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Register new user */ - post: operations['AuthController_register']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/auth/login': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Login with email and password */ - post: operations['AuthController_login']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/auth/refresh': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Refresh access token */ - post: operations['AuthController_refresh']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/auth/logout': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Logout user */ - post: operations['AuthController_logout']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/auth/me': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get current user profile */ - get: operations['AuthController_getProfile']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - /** Update current user profile */ - patch: operations['AuthController_updateProfile']; - trace?: never; - }; - '/api/v1/securities/search': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Поиск по инструментам */ - get: operations['SecuritiesController_search']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/securities/screener': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Фильтр ценных бумаг по параметрам */ - get: operations['SecuritiesController_screener']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/securities/shares/{secid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Получить спецификацию акции */ - get: operations['SharesController_getShare']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/securities/shares/{secid}/marketdata': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Получить рыночные данные акции */ - get: operations['SharesController_getMarketData']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/securities/shares/{secid}/dividends': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Получить дивиденды */ - get: operations['SharesController_getDividends']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/securities/shares/{secid}/history': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Получить дневную историю торгов акции */ - get: operations['SharesController_getHistory']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/securities/bonds/{secid}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Получить спецификацию облигации */ - get: operations['BondsController_getBond']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/securities/bonds/{secid}/marketdata': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Получить рыночные данные облигации */ - get: operations['BondsController_getMarketData']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/securities/bonds/{secid}/history': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Получить дневную историю торгов облигации */ - get: operations['BondsController_getHistory']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/securities/shares/{secid}/candles': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Получить свечи акции */ - get: operations['CandlesController_getShareCandles']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/securities/bonds/{secid}/candles': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Получить свечи облигации */ - get: operations['CandlesController_getBondCandles']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/portfolios': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get all portfolios for current user */ - get: operations['PortfolioController_findAll']; - put?: never; - /** Create a new portfolio */ - post: operations['PortfolioController_create']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/portfolios/{id}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get portfolio details with positions and prices */ - get: operations['PortfolioController_findOne']; - put?: never; - post?: never; - /** Delete portfolio */ - delete: operations['PortfolioController_remove']; - options?: never; - head?: never; - /** Update portfolio */ - patch: operations['PortfolioController_update']; - trace?: never; - }; - '/api/v1/portfolios/{id}/positions': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Add position to portfolio */ - post: operations['PortfolioController_addPosition']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/portfolios/{id}/positions/{positionId}': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - post?: never; - /** Remove position from portfolio */ - delete: operations['PortfolioController_removePosition']; - options?: never; - head?: never; - /** Update position */ - patch: operations['PortfolioController_updatePosition']; - trace?: never; - }; - '/api/v1/portfolios/{id}/analytics': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get portfolio analytics with PnL */ - get: operations['PortfolioController_getAnalytics']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/broker/accounts': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get open T-Bank brokerage and IIS accounts */ - get: operations['TBankController_getAccounts']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/broker/accounts/{accountId}/portfolio': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get T-Bank broker account portfolio with cash and positions */ - get: operations['TBankController_getPortfolio']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/broker/accounts/{accountId}/positions': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get paginated T-Bank broker account positions */ - get: operations['TBankController_getPositions']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/broker/accounts/{accountId}/operations': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get paginated T-Bank broker account operations */ - get: operations['TBankController_getOperations']; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; - '/api/v1/broker/accounts/{accountId}/operations/sync': { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - get?: never; - put?: never; - /** Synchronize T-Bank broker account operations into local history */ - post: operations['TBankController_syncOperations']; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; -} -export type webhooks = Record; -export interface components { - schemas: { - RegisterDto: { - /** @example user@example.com */ - email: string; - /** @example securePass123 */ - password: string; - /** @example John */ - 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: { - /** @example user@example.com */ - email: string; - /** @example securePass123 */ - password: string; - }; - LogoutDataDto: { - message: string; - }; - AuthLogoutResponseDto: { - data: components['schemas']['LogoutDataDto']; - meta: components['schemas']['AuthResponseMetaDto']; - }; - AuthProfileResponseDto: { - data: components['schemas']['AuthUserDto']; - meta: components['schemas']['AuthResponseMetaDto']; - }; - UpdateProfileDto: { - /** @example John Doe */ - name?: string; - }; - ScreenerItemDto: { - /** @example SBER */ - secid: string; - /** @example Сбербанк */ - shortName: string; - /** @example RU0009029540 */ - isin: string; - /** @enum {string} */ - type: 'share' | 'bond'; - /** @example 322.35 */ - price?: number | null; - /** @example 1.15 */ - change?: number | null; - /** @example 0.36 */ - changePercent?: number | null; - /** @example 1925163 */ - volume: number; - /** @example 1 */ - listLevel: number; - /** @example 6958336818320 */ - capitalization?: number | null; - /** @example 12.71 */ - yieldToMaturity?: number | null; - /** @example 4.5 */ - duration?: number | null; - /** @example 40.64 */ - couponValue?: number | null; - /** @example 8.15 */ - couponPercent?: number | null; - /** @example 29.48 */ - accruedInt?: number | null; - /** @example 2027-02-03 */ - matDate?: string | null; - /** @example ОФЗ-ПД */ - bondType?: string | null; - }; - ScreenerResultDto: { - items: components['schemas']['ScreenerItemDto'][]; - total: number; - page: number; - pageSize: 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: { - id: number; - name: string; - description?: string | null; - /** @default RUB */ - currency: string; - createdAt: string; - updatedAt: string; - /** @description Total market value of all positions */ - totalValue: number; - /** @description Total number of positions */ - positionCount: number; - /** @description Number of share positions */ - shareCount: number; - /** @description Number of bond positions */ - bondCount: number; - }; - PortfolioListEnvelopeDto: { - data: components['schemas']['PortfolioListResponseDto'][]; - meta: components['schemas']['PortfolioResponseMetaDto']; - }; - CreatePortfolioDto: { - /** @example Мой портфель */ - name: string; - /** @example Описание портфеля */ - description?: string; - /** - * @default RUB - * @enum {string} - */ - 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: { - id: number; - /** @example SBER */ - secid: string; - shortName?: string | null; - /** - * @example share - * @enum {string} - */ - type: 'share' | 'bond'; - /** @example 10 */ - quantity: number; - buyPrice?: number | null; - buyDate?: string | null; - notes?: string | null; - tags?: string[] | null; - currentPrice?: number | null; - totalCost?: number | null; - currentValue?: number | null; - weightPercent: number; - pnl?: number | null; - pnlPercent?: number | null; - dividendIncome?: number | null; - totalReturn?: number | null; - totalReturnPercent?: number | null; - 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; - }; - PortfolioSummaryDto: { - totalInvested: number; - totalValue: number; - totalPnl: number; - totalPnlPercent: number | null; - totalDividends: number; - totalReturn: number; - totalReturnPercent: number | null; - positionCount: number; - weightedYield: number | null; - }; - PortfolioDetailResponseDto: { - id: number; - name: string; - description?: string | null; - /** @default RUB */ - currency: string; - createdAt: string; - updatedAt: string; - positions: components['schemas']['PositionWithPriceDto'][]; - totalValue: number; - analytics: components['schemas']['PortfolioSummaryDto']; - }; - PortfolioDetailEnvelopeDto: { - data: components['schemas']['PortfolioDetailResponseDto']; - meta: components['schemas']['PortfolioResponseMetaDto']; - }; - UpdatePortfolioDto: { - /** @example Мой портфель */ - name?: string; - /** @example Обновлённое описание */ - description?: string; - /** - * @default RUB - * @enum {string} - */ - currency: 'RUB' | 'USD' | 'EUR' | 'CNY' | 'KZT' | 'BYN'; - }; - AddPositionDto: { - /** @example SBER */ - secid: string; - /** @example 10 */ - quantity: number; - /** @example 250.5 */ - buyPrice?: number; - /** @example 2026-06-01 */ - buyDate?: string; - /** @example Покупка на дип */ - notes?: string; - /** - * @example [ - * "DIVIDEND", - * "GROWTH" - * ] - */ - tags?: ( - | 'DIVIDEND' - | 'GROWTH' - | 'DEFENSIVE' - | 'SPECULATIVE' - | 'BOND' - | 'ETF' - | 'GOVERNMENT' - | '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: { - /** @example 15 */ - quantity?: number; - /** @example 260 */ - buyPrice?: number; - /** @example 2026-06-15 */ - buyDate?: string; - /** @example Докупка */ - notes?: string; - /** - * @example [ - * "DIVIDEND" - * ] - */ - tags?: ( - | 'DIVIDEND' - | 'GROWTH' - | 'DEFENSIVE' - | 'SPECULATIVE' - | 'BOND' - | 'ETF' - | 'GOVERNMENT' - | 'CASH' - )[]; - }; - AnalyticsResponseDto: { - positions: components['schemas']['PositionWithPriceDto'][]; - summary: components['schemas']['PortfolioSummaryDto']; - }; - AnalyticsEnvelopeDto: { - data: components['schemas']['AnalyticsResponseDto']; - meta: components['schemas']['PortfolioResponseMetaDto']; - }; - BrokerAccountResponseDto: { - id: string; - /** @enum {string} */ - type: 'brokerage' | 'iis'; - name: string; - status: string; - openedAt: Record | null; - accessLevel: Record | null; - }; - BrokerResponseMetaDto: { - cachedAt: Record | null; - fromCache: boolean; - }; - BrokerAccountsEnvelopeDto: { - data: components['schemas']['BrokerAccountResponseDto'][]; - meta: components['schemas']['BrokerResponseMetaDto']; - }; - BrokerPortfolioPositionCountsDto: { - shares: number; - bonds: number; - etf: number; - other: number; - }; - BrokerMoneyDto: { - currency: string; - units: string; - nano: number; - value: number; - }; - BrokerPortfolioTotalsDto: { - shares: components['schemas']['BrokerMoneyDto'] | null; - bonds: components['schemas']['BrokerMoneyDto'] | null; - etf: components['schemas']['BrokerMoneyDto'] | null; - currencies: components['schemas']['BrokerMoneyDto'] | null; - futures: components['schemas']['BrokerMoneyDto'] | null; - options: components['schemas']['BrokerMoneyDto'] | null; - structuredProducts: components['schemas']['BrokerMoneyDto'] | null; - dfa: components['schemas']['BrokerMoneyDto'] | null; - portfolio: components['schemas']['BrokerMoneyDto'] | null; - }; - BrokerPortfolioYieldsDto: { - expectedPercent: Record | null; - daily: components['schemas']['BrokerMoneyDto'] | null; - dailyPercent: Record | null; - }; - BrokerPortfolioResponseDto: { - account: components['schemas']['BrokerAccountResponseDto']; - positionCounts: components['schemas']['BrokerPortfolioPositionCountsDto']; - totals: components['schemas']['BrokerPortfolioTotalsDto']; - yields: components['schemas']['BrokerPortfolioYieldsDto']; - cash: components['schemas']['BrokerMoneyDto'][]; - blockedCash: components['schemas']['BrokerMoneyDto'][]; - asOf: string; - }; - BrokerPortfolioEnvelopeDto: { - data: components['schemas']['BrokerPortfolioResponseDto']; - meta: components['schemas']['BrokerResponseMetaDto']; - }; - BrokerPositionResponseDto: { - figi: Record | null; - instrumentUid: Record | null; - positionUid: Record | null; - ticker: Record | null; - classCode: Record | null; - instrumentType: Record | null; - name: Record | null; - quantity: Record | null; - blockedLots: Record | null; - currentPrice: components['schemas']['BrokerMoneyDto'] | null; - currentValue: components['schemas']['BrokerMoneyDto'] | null; - averagePositionPrice: components['schemas']['BrokerMoneyDto'] | null; - expectedYieldPercent: Record | null; - dailyYield: components['schemas']['BrokerMoneyDto'] | null; - }; - BrokerPositionsPageResponseDto: { - accountId: string; - items: components['schemas']['BrokerPositionResponseDto'][]; - nextCursor: Record | null; - hasNext: boolean; - asOf: string; - }; - BrokerPositionsEnvelopeDto: { - data: components['schemas']['BrokerPositionsPageResponseDto']; - meta: components['schemas']['BrokerResponseMetaDto']; - }; - BrokerOperationResponseDto: { - cursor: Record | null; - accountId: string; - id: Record | null; - parentOperationId: Record | null; - date: Record | null; - type: string; - /** @enum {string} */ - category: 'trade' | 'income' | 'tax' | 'fee' | 'transfer' | 'other'; - description: Record | null; - name: Record | null; - state: Record | null; - instrumentUid: Record | null; - figi: Record | null; - ticker: Record | null; - classCode: Record | null; - instrumentType: Record | null; - payment: components['schemas']['BrokerMoneyDto'] | null; - price: components['schemas']['BrokerMoneyDto'] | null; - commission: components['schemas']['BrokerMoneyDto'] | null; - yield: components['schemas']['BrokerMoneyDto'] | null; - accruedInt: components['schemas']['BrokerMoneyDto'] | null; - quantity: Record | null; - quantityDone: Record | null; - }; - BrokerOperationsPageResponseDto: { - accountId: string; - items: components['schemas']['BrokerOperationResponseDto'][]; - nextCursor: Record | null; - hasNext: boolean; - asOf: string; - }; - BrokerOperationsEnvelopeDto: { - data: components['schemas']['BrokerOperationsPageResponseDto']; - meta: components['schemas']['BrokerResponseMetaDto']; - }; - BrokerOperationSyncResponseDto: { - /** @example 42 */ - upserted: number; - }; - BrokerOperationSyncEnvelopeDto: { - data: components['schemas']['BrokerOperationSyncResponseDto']; - meta: components['schemas']['BrokerResponseMetaDto']; - }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; -} -export type $defs = Record; -export interface operations { - HealthController_check: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - AuthController_register: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['RegisterDto']; - }; - }; - responses: { - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AuthTokenResponseDto']; - }; - }; - }; - }; - AuthController_login: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['LoginDto']; - }; - }; - responses: { - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AuthTokenResponseDto']; - }; - }; - }; - }; - AuthController_refresh: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AuthTokenResponseDto']; - }; - }; - }; - }; - AuthController_logout: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AuthLogoutResponseDto']; - }; - }; - }; - }; - AuthController_getProfile: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AuthProfileResponseDto']; - }; - }; - }; - }; - AuthController_updateProfile: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['UpdateProfileDto']; - }; - }; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AuthProfileResponseDto']; - }; - }; - }; - }; - SecuritiesController_search: { - parameters: { - query: { - /** @description Поисковый запрос (тикер, название, ISIN) */ - q: string; - type?: 'all' | 'share' | 'bond'; - limit?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SecuritiesController_screener: { - parameters: { - query: { - type: 'share' | 'bond'; - priceMin?: number; - priceMax?: number; - volumeMin?: number; - listLevel?: number; - changePercentMin?: number; - changePercentMax?: number; - capitalizationMin?: number; - yieldMin?: number; - yieldMax?: number; - durationMin?: number; - durationMax?: number; - couponMin?: number; - couponMax?: number; - couponPercentMin?: number; - couponPercentMax?: number; - maturityBefore?: string; - maturityAfter?: string; - bondType?: string; - sortBy?: string; - sortOrder?: string; - page?: number; - pageSize?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ScreenerResponseDto']; - }; - }; - }; - }; - SharesController_getShare: { - parameters: { - query?: never; - header?: never; - path: { - secid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SharesController_getMarketData: { - parameters: { - query?: never; - header?: never; - path: { - secid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SharesController_getDividends: { - parameters: { - query?: never; - header?: never; - path: { - secid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - SharesController_getHistory: { - parameters: { - query: { - from: string; - till: string; - }; - header?: never; - path: { - secid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - BondsController_getBond: { - parameters: { - query?: never; - header?: never; - path: { - secid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - BondsController_getMarketData: { - parameters: { - query?: never; - header?: never; - path: { - secid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - BondsController_getHistory: { - parameters: { - query: { - from: string; - till: string; - }; - header?: never; - path: { - secid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - CandlesController_getShareCandles: { - parameters: { - query: { - interval: '1h' | '24h'; - from: string; - till: string; - }; - header?: never; - path: { - secid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - CandlesController_getBondCandles: { - parameters: { - query: { - interval: '1h' | '24h'; - from: string; - till: string; - }; - header?: never; - path: { - secid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; - PortfolioController_findAll: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['PortfolioListEnvelopeDto']; - }; - }; - }; - }; - PortfolioController_create: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['CreatePortfolioDto']; - }; - }; - responses: { - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['PortfolioEnvelopeDto']; - }; - }; - }; - }; - PortfolioController_findOne: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['PortfolioDetailEnvelopeDto']; - }; - }; - }; - }; - PortfolioController_remove: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data: null; - meta: components['schemas']['PortfolioResponseMetaDto']; - }; - }; - }; - }; - }; - PortfolioController_update: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['UpdatePortfolioDto']; - }; - }; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['PortfolioEnvelopeDto']; - }; - }; - }; - }; - PortfolioController_addPosition: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['AddPositionDto']; - }; - }; - responses: { - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['PositionEnvelopeDto']; - }; - }; - }; - }; - PortfolioController_removePosition: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - positionId: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - data: null; - meta: components['schemas']['PortfolioResponseMetaDto']; - }; - }; - }; - }; - }; - PortfolioController_updatePosition: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - positionId: number; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['UpdatePositionDto']; - }; - }; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['PositionEnvelopeDto']; - }; - }; - }; - }; - PortfolioController_getAnalytics: { - parameters: { - query?: never; - header?: never; - path: { - id: number; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['AnalyticsEnvelopeDto']; - }; - }; - }; - }; - TBankController_getAccounts: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['BrokerAccountsEnvelopeDto']; - }; - }; - }; - }; - TBankController_getPortfolio: { - parameters: { - query?: never; - header?: never; - path: { - accountId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['BrokerPortfolioEnvelopeDto']; - }; - }; - }; - }; - TBankController_getPositions: { - parameters: { - query?: { - /** @description Cursor for pagination (positionUid) */ - cursor?: string; - limit?: number; - /** @description Filter by instrument type (share, bond, etf, etc.) */ - type?: string; - }; - header?: never; - path: { - accountId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['BrokerPositionsEnvelopeDto']; - }; - }; - }; - }; - TBankController_getOperations: { - parameters: { - query?: { - from?: string; - to?: string; - cursor?: string; - limit?: number; - instrumentId?: string; - operationTypes?: string; - state?: string; - }; - header?: never; - path: { - accountId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['BrokerOperationsEnvelopeDto']; - }; - }; - }; - }; - TBankController_syncOperations: { - parameters: { - query: { - from: string; - to: string; - }; - header?: never; - path: { - accountId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['BrokerOperationSyncEnvelopeDto']; - }; - }; - }; - }; -} +export * from '../shared/api/types'; diff --git a/apps/frontend/src/shared/api/client.test.ts b/apps/frontend/src/shared/api/client.test.ts new file mode 100644 index 0000000..1272a13 --- /dev/null +++ b/apps/frontend/src/shared/api/client.test.ts @@ -0,0 +1,153 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { http, HttpResponse } from 'msw'; +import { server } from '../../test/server'; +import { request, setAccessToken, getAccessToken, setOnUnauthorized } from './client'; + +const API = '/api/v1'; + +beforeEach(() => { + setAccessToken(null); +}); + +describe('request', () => { + it('makes GET request and returns data', async () => { + const result = await request<{ status: string; timestamp: string; uptime: number }>( + '/api/v1/health', + ); + expect(result.data.status).toBe('ok'); + }); + + it('supports the single API envelope shape documented by Swagger', async () => { + server.use( + http.get(`${API}/test-single-envelope`, () => + HttpResponse.json({ + data: { ok: true }, + meta: { fromCache: true, cachedAt: '2026-06-17T00:00:00.000Z' }, + }), + ), + ); + + const result = await request<{ ok: boolean }>('/api/v1/test-single-envelope'); + + expect(result).toEqual({ + data: { ok: true }, + meta: { fromCache: true, cachedAt: '2026-06-17T00:00:00.000Z' }, + }); + }); + + it('includes Authorization header when token is set', async () => { + setAccessToken('test-token'); + let capturedAuth: string | null = null; + server.use( + http.get(`${API}/test-auth`, ({ request }) => { + capturedAuth = request.headers.get('Authorization'); + return HttpResponse.json({ + data: { data: { ok: true }, meta: { fromCache: false, cachedAt: null } }, + }); + }), + ); + await request('/api/v1/test-auth'); + expect(capturedAuth).toBe('Bearer test-token'); + }); + + it('retries on 401 and succeeds after refresh', async () => { + setAccessToken('expired-token'); + let attempts = 0; + server.use( + http.get(`${API}/test-retry`, ({ request }) => { + attempts++; + const auth = request.headers.get('Authorization'); + if (auth === 'Bearer expired-token') { + return new HttpResponse(null, { status: 401 }); + } + return HttpResponse.json({ + data: { data: { ok: true }, meta: { fromCache: false, cachedAt: null } }, + }); + }), + http.post(`${API}/auth/refresh`, () => + HttpResponse.json({ + data: { + data: { + user: { id: 1, email: 'user@test.com', name: null, role: 'user' }, + accessToken: 'new-token', + }, + meta: { fromCache: false, cachedAt: null }, + }, + }), + ), + ); + const result = await request<{ ok: boolean }>('/api/v1/test-retry'); + expect(attempts).toBe(2); + expect(result.data).toEqual({ ok: true }); + expect(getAccessToken()).toBe('new-token'); + }); + + it('throws on persistent 401 and clears token', async () => { + setAccessToken('expired-token'); + let unauthorizedCalled = false; + setOnUnauthorized(() => { + unauthorizedCalled = true; + }); + server.use( + http.get(`${API}/test-fail`, () => new HttpResponse(null, { status: 401 })), + http.post(`${API}/auth/refresh`, () => new HttpResponse(null, { status: 401 })), + ); + await expect(request('/api/v1/test-fail')).rejects.toThrow('Сессия истекла'); + expect(getAccessToken()).toBeNull(); + expect(unauthorizedCalled).toBe(true); + }); + + it('throws on non-ok response with status text', async () => { + server.use( + http.get( + `${API}/test-error`, + () => new HttpResponse('Not found', { status: 404, statusText: 'Not Found' }), + ), + ); + await expect(request('/api/v1/test-error')).rejects.toThrow('Ошибка API: 404'); + }); + + it('sends JSON body for POST requests', async () => { + let capturedBody: string | null = null; + server.use( + http.post(`${API}/test-post`, async ({ request }) => { + capturedBody = await request.text(); + return HttpResponse.json({ + data: { data: { ok: true }, meta: { fromCache: false, cachedAt: null } }, + }); + }), + ); + await request('/api/v1/test-post', undefined, { method: 'POST', body: { foo: 'bar' } }); + expect(capturedBody).toBe(JSON.stringify({ foo: 'bar' })); + }); + + it('does not send auth header when skipAuth is true', async () => { + setAccessToken('test-token'); + let capturedAuth: string | null = null; + server.use( + http.get(`${API}/test-skip`, ({ request }) => { + capturedAuth = request.headers.get('Authorization'); + return HttpResponse.json({ + data: { data: { ok: true }, meta: { fromCache: false, cachedAt: null } }, + }); + }), + ); + await request('/api/v1/test-skip', undefined, { skipAuth: true }); + expect(capturedAuth).toBeNull(); + }); + + it('sets query params correctly', async () => { + let capturedUrl = ''; + server.use( + http.get(`${API}/test-params`, ({ request }) => { + capturedUrl = request.url; + return HttpResponse.json({ + data: { data: { ok: true }, meta: { fromCache: false, cachedAt: null } }, + }); + }), + ); + await request('/api/v1/test-params', { q: 'sber', type: 'share' }); + expect(capturedUrl).toContain('q=sber'); + expect(capturedUrl).toContain('type=share'); + }); +}); diff --git a/apps/frontend/src/shared/api/client.ts b/apps/frontend/src/shared/api/client.ts new file mode 100644 index 0000000..c6f0873 --- /dev/null +++ b/apps/frontend/src/shared/api/client.ts @@ -0,0 +1,232 @@ +import type { + ApiEnvelope, + ApiResponseMeta, + AuthResponse, + ShareResponse, + StockMarketData, + DividendItem, + ShareHistoryItem, + BondResponse, + BondMarketData, + BondHistoryItem, + CandleItem, + SearchResultItem, + HealthResponse, +} from './responses'; + +const BASE = ''; + +let accessToken: string | null = null; +let onUnauthorized: (() => void) | null = null; +let isRefreshing = false; +let refreshPromise: Promise | null = null; + +export function setAccessToken(token: string | null) { + accessToken = token; +} + +export function getAccessToken(): string | null { + return accessToken; +} + +export function setOnUnauthorized(cb: () => void) { + onUnauthorized = cb; +} + +async function refreshTokens(): Promise { + try { + const res = await fetch(`${BASE}/api/v1/auth/refresh`, { + method: 'POST', + credentials: 'include', + }); + if (!res.ok) return false; + const json = await res.json(); + accessToken = normalizeEnvelope(json).data.accessToken; + return true; + } catch { + return false; + } +} + +function normalizeEnvelope(json: unknown): { data: T; meta: ApiResponseMeta } { + const envelope = json as ApiEnvelope; + if ( + envelope.data && + typeof envelope.data === 'object' && + 'data' in envelope.data && + 'meta' in envelope.data + ) { + return envelope.data as { data: T; meta: ApiResponseMeta }; + } + + return { + data: envelope.data as T, + meta: envelope.meta, + }; +} + +async function handleUnauthorized(): Promise { + if (isRefreshing && refreshPromise) { + return refreshPromise; + } + + isRefreshing = true; + refreshPromise = refreshTokens().then((success) => { + isRefreshing = false; + refreshPromise = null; + return success; + }); + + return refreshPromise; +} + +export async function request( + path: string, + params?: Record, + options?: { method?: string; body?: unknown; skipAuth?: boolean }, +): Promise<{ data: T; meta: ApiResponseMeta }> { + const url = new URL(`${BASE}${path}`, window.location.origin); + if (params) { + for (const [k, v] of Object.entries(params)) { + if (v !== undefined) url.searchParams.set(k, v); + } + } + + const headers: Record = {}; + if (!options?.skipAuth && accessToken) { + headers['Authorization'] = `Bearer ${accessToken}`; + } + if (options?.body && !(options.body instanceof FormData)) { + headers['Content-Type'] = 'application/json'; + } + + const fetchOptions: RequestInit = { + headers, + credentials: 'include' as RequestCredentials, + }; + if (options?.method) { + fetchOptions.method = options.method; + } + if (options?.body !== undefined) { + fetchOptions.body = + options.body instanceof FormData ? options.body : JSON.stringify(options.body); + } + + let res = await fetch(url.toString(), fetchOptions); + + if (res.status === 401 && !options?.skipAuth) { + const refreshed = await handleUnauthorized(); + if (refreshed) { + headers['Authorization'] = `Bearer ${accessToken}`; + res = await fetch(url.toString(), { ...fetchOptions, headers }); + } else { + accessToken = null; + onUnauthorized?.(); + throw new Error('Сессия истекла'); + } + } + + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`Ошибка API: ${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`); + } + + const json = await res.json(); + return normalizeEnvelope(json); +} + +export function getHealth(): Promise<{ data: HealthResponse; meta: ApiResponseMeta }> { + return request('/api/v1/health'); +} + +export function searchSecurities( + q: string, + type: 'all' | 'share' | 'bond' = 'all', + limit = 20, +): Promise<{ data: SearchResultItem[]; meta: ApiResponseMeta }> { + return request('/api/v1/securities/search', { + q, + type, + limit: String(limit), + }); +} + +export function getShare(secid: string): Promise<{ data: ShareResponse; meta: ApiResponseMeta }> { + return request(`/api/v1/securities/shares/${encodeURIComponent(secid)}`); +} + +export function getShareMarketData( + secid: string, +): Promise<{ data: StockMarketData; meta: ApiResponseMeta }> { + return request( + `/api/v1/securities/shares/${encodeURIComponent(secid)}/marketdata`, + ); +} + +export function getShareDividends( + secid: string, +): Promise<{ data: DividendItem[]; meta: ApiResponseMeta }> { + return request( + `/api/v1/securities/shares/${encodeURIComponent(secid)}/dividends`, + ); +} + +export function getShareHistory( + secid: string, + from: string, + till: string, +): Promise<{ data: ShareHistoryItem[]; meta: ApiResponseMeta }> { + return request( + `/api/v1/securities/shares/${encodeURIComponent(secid)}/history`, + { from, till }, + ); +} + +export function getBond(secid: string): Promise<{ data: BondResponse; meta: ApiResponseMeta }> { + return request(`/api/v1/securities/bonds/${encodeURIComponent(secid)}`); +} + +export function getBondMarketData( + secid: string, +): Promise<{ data: BondMarketData; meta: ApiResponseMeta }> { + return request( + `/api/v1/securities/bonds/${encodeURIComponent(secid)}/marketdata`, + ); +} + +export function getBondHistory( + secid: string, + from: string, + till: string, +): Promise<{ data: BondHistoryItem[]; meta: ApiResponseMeta }> { + return request( + `/api/v1/securities/bonds/${encodeURIComponent(secid)}/history`, + { from, till }, + ); +} + +export function getShareCandles( + secid: string, + interval: '1h' | '24h', + from: string, + till: string, +): Promise<{ data: CandleItem[]; meta: ApiResponseMeta }> { + return request(`/api/v1/securities/shares/${encodeURIComponent(secid)}/candles`, { + interval, + from, + till, + }); +} + +export function getBondCandles( + secid: string, + interval: '1h' | '24h', + from: string, + till: string, +): Promise<{ data: CandleItem[]; meta: ApiResponseMeta }> { + return request(`/api/v1/securities/bonds/${encodeURIComponent(secid)}/candles`, { + interval, + from, + till, + }); +} diff --git a/apps/frontend/src/shared/api/index.ts b/apps/frontend/src/shared/api/index.ts new file mode 100644 index 0000000..9321a97 --- /dev/null +++ b/apps/frontend/src/shared/api/index.ts @@ -0,0 +1,49 @@ +export { + request, + setAccessToken, + getAccessToken, + setOnUnauthorized, + getHealth, + searchSecurities, + getShare, + getShareMarketData, + getShareDividends, + getShareHistory, + getBond, + getBondMarketData, + getBondHistory, + getShareCandles, + getBondCandles, +} from './client'; +export type { + ApiResponseMeta, + ApiEnvelope, + StockMarketData, + ShareResponse, + DividendItem, + ShareHistoryItem, + BondMarketData, + BondResponse, + BondHistoryItem, + CandleItem, + SearchResultItem, + HealthResponse, + UserResponse, + AuthResponse, + Portfolio, + PositionWithPrice, + PortfolioDetail, + Position, + PortfolioSummary, + AnalyticsResponse, + ScreenerItem, + ScreenerResult, + BrokerMoney, + BrokerAccount, + BrokerPosition, + BrokerPortfolio, + BrokerOperationCategory, + BrokerOperation, + BrokerOperationsPage, + BrokerPositionsPage, +} from './responses'; diff --git a/apps/frontend/src/shared/api/responses.ts b/apps/frontend/src/shared/api/responses.ts new file mode 100644 index 0000000..0dd5305 --- /dev/null +++ b/apps/frontend/src/shared/api/responses.ts @@ -0,0 +1,352 @@ +export interface ApiResponseMeta { + cachedAt: string | null; + fromCache: boolean; +} + +export interface ApiEnvelope { + 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; +} + +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; +} + +export interface DividendItem { + registryCloseDate: string; + value: number; + currency: string; +} + +export interface ShareHistoryItem { + date: string; + open: number; + high: number; + low: number; + close: number; + volume: number; + value: number; +} + +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; +} + +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; +} + +export interface BondHistoryItem { + date: string; + closePrice: number; + yieldClose: number | null; + duration: number | null; +} + +export interface CandleItem { + open: number; + high: number; + low: number; + close: number; + volume: number; + value: number; + begin: string; + end: string; +} + +export interface SearchResultItem { + secid: string; + isin: string; + shortName: string; + type: 'share' | 'bond'; + listLevel: number; + currency: string | null; + price: number | null; +} + +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; +} diff --git a/apps/frontend/src/shared/api/types.ts b/apps/frontend/src/shared/api/types.ts new file mode 100644 index 0000000..c551319 --- /dev/null +++ b/apps/frontend/src/shared/api/types.ts @@ -0,0 +1,1637 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + '/api/v1/health': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Проверка состояния сервиса */ + get: operations['HealthController_check']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/auth/register': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Register new user */ + post: operations['AuthController_register']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/auth/login': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Login with email and password */ + post: operations['AuthController_login']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/auth/refresh': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Refresh access token */ + post: operations['AuthController_refresh']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/auth/logout': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Logout user */ + post: operations['AuthController_logout']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/auth/me': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get current user profile */ + get: operations['AuthController_getProfile']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Update current user profile */ + patch: operations['AuthController_updateProfile']; + trace?: never; + }; + '/api/v1/securities/search': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Поиск по инструментам */ + get: operations['SecuritiesController_search']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/securities/screener': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Фильтр ценных бумаг по параметрам */ + get: operations['SecuritiesController_screener']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/securities/shares/{secid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Получить спецификацию акции */ + get: operations['SharesController_getShare']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/securities/shares/{secid}/marketdata': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Получить рыночные данные акции */ + get: operations['SharesController_getMarketData']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/securities/shares/{secid}/dividends': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Получить дивиденды */ + get: operations['SharesController_getDividends']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/securities/shares/{secid}/history': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Получить дневную историю торгов акции */ + get: operations['SharesController_getHistory']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/securities/bonds/{secid}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Получить спецификацию облигации */ + get: operations['BondsController_getBond']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/securities/bonds/{secid}/marketdata': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Получить рыночные данные облигации */ + get: operations['BondsController_getMarketData']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/securities/bonds/{secid}/history': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Получить дневную историю торгов облигации */ + get: operations['BondsController_getHistory']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/securities/shares/{secid}/candles': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Получить свечи акции */ + get: operations['CandlesController_getShareCandles']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/securities/bonds/{secid}/candles': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Получить свечи облигации */ + get: operations['CandlesController_getBondCandles']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/portfolios': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get all portfolios for current user */ + get: operations['PortfolioController_findAll']; + put?: never; + /** Create a new portfolio */ + post: operations['PortfolioController_create']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/portfolios/{id}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get portfolio details with positions and prices */ + get: operations['PortfolioController_findOne']; + put?: never; + post?: never; + /** Delete portfolio */ + delete: operations['PortfolioController_remove']; + options?: never; + head?: never; + /** Update portfolio */ + patch: operations['PortfolioController_update']; + trace?: never; + }; + '/api/v1/portfolios/{id}/positions': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Add position to portfolio */ + post: operations['PortfolioController_addPosition']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/portfolios/{id}/positions/{positionId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** Remove position from portfolio */ + delete: operations['PortfolioController_removePosition']; + options?: never; + head?: never; + /** Update position */ + patch: operations['PortfolioController_updatePosition']; + trace?: never; + }; + '/api/v1/portfolios/{id}/analytics': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get portfolio analytics with PnL */ + get: operations['PortfolioController_getAnalytics']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/broker/accounts': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get open T-Bank brokerage and IIS accounts */ + get: operations['TBankController_getAccounts']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/broker/accounts/{accountId}/portfolio': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get T-Bank broker account portfolio with cash and positions */ + get: operations['TBankController_getPortfolio']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/broker/accounts/{accountId}/positions': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get paginated T-Bank broker account positions */ + get: operations['TBankController_getPositions']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/broker/accounts/{accountId}/operations': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get paginated T-Bank broker account operations */ + get: operations['TBankController_getOperations']; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/api/v1/broker/accounts/{accountId}/operations/sync': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Synchronize T-Bank broker account operations into local history */ + post: operations['TBankController_syncOperations']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + RegisterDto: { + /** @example user@example.com */ + email: string; + /** @example securePass123 */ + password: string; + /** @example John */ + 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: { + /** @example user@example.com */ + email: string; + /** @example securePass123 */ + password: string; + }; + LogoutDataDto: { + message: string; + }; + AuthLogoutResponseDto: { + data: components['schemas']['LogoutDataDto']; + meta: components['schemas']['AuthResponseMetaDto']; + }; + AuthProfileResponseDto: { + data: components['schemas']['AuthUserDto']; + meta: components['schemas']['AuthResponseMetaDto']; + }; + UpdateProfileDto: { + /** @example John Doe */ + name?: string; + }; + ScreenerItemDto: { + /** @example SBER */ + secid: string; + /** @example Сбербанк */ + shortName: string; + /** @example RU0009029540 */ + isin: string; + /** @enum {string} */ + type: 'share' | 'bond'; + /** @example 322.35 */ + price?: number | null; + /** @example 1.15 */ + change?: number | null; + /** @example 0.36 */ + changePercent?: number | null; + /** @example 1925163 */ + volume: number; + /** @example 1 */ + listLevel: number; + /** @example 6958336818320 */ + capitalization?: number | null; + /** @example 12.71 */ + yieldToMaturity?: number | null; + /** @example 4.5 */ + duration?: number | null; + /** @example 40.64 */ + couponValue?: number | null; + /** @example 8.15 */ + couponPercent?: number | null; + /** @example 29.48 */ + accruedInt?: number | null; + /** @example 2027-02-03 */ + matDate?: string | null; + /** @example ОФЗ-ПД */ + bondType?: string | null; + }; + ScreenerResultDto: { + items: components['schemas']['ScreenerItemDto'][]; + total: number; + page: number; + pageSize: 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: { + id: number; + name: string; + description?: string | null; + /** @default RUB */ + currency: string; + createdAt: string; + updatedAt: string; + /** @description Total market value of all positions */ + totalValue: number; + /** @description Total number of positions */ + positionCount: number; + /** @description Number of share positions */ + shareCount: number; + /** @description Number of bond positions */ + bondCount: number; + }; + PortfolioListEnvelopeDto: { + data: components['schemas']['PortfolioListResponseDto'][]; + meta: components['schemas']['PortfolioResponseMetaDto']; + }; + CreatePortfolioDto: { + /** @example Мой портфель */ + name: string; + /** @example Описание портфеля */ + description?: string; + /** + * @default RUB + * @enum {string} + */ + 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: { + id: number; + /** @example SBER */ + secid: string; + shortName?: string | null; + /** + * @example share + * @enum {string} + */ + type: 'share' | 'bond'; + /** @example 10 */ + quantity: number; + buyPrice?: number | null; + buyDate?: string | null; + notes?: string | null; + tags?: string[] | null; + currentPrice?: number | null; + totalCost?: number | null; + currentValue?: number | null; + weightPercent: number; + pnl?: number | null; + pnlPercent?: number | null; + dividendIncome?: number | null; + totalReturn?: number | null; + totalReturnPercent?: number | null; + 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; + }; + PortfolioSummaryDto: { + totalInvested: number; + totalValue: number; + totalPnl: number; + totalPnlPercent: number | null; + totalDividends: number; + totalReturn: number; + totalReturnPercent: number | null; + positionCount: number; + weightedYield: number | null; + }; + PortfolioDetailResponseDto: { + id: number; + name: string; + description?: string | null; + /** @default RUB */ + currency: string; + createdAt: string; + updatedAt: string; + positions: components['schemas']['PositionWithPriceDto'][]; + totalValue: number; + analytics: components['schemas']['PortfolioSummaryDto']; + }; + PortfolioDetailEnvelopeDto: { + data: components['schemas']['PortfolioDetailResponseDto']; + meta: components['schemas']['PortfolioResponseMetaDto']; + }; + UpdatePortfolioDto: { + /** @example Мой портфель */ + name?: string; + /** @example Обновлённое описание */ + description?: string; + /** + * @default RUB + * @enum {string} + */ + currency: 'RUB' | 'USD' | 'EUR' | 'CNY' | 'KZT' | 'BYN'; + }; + AddPositionDto: { + /** @example SBER */ + secid: string; + /** @example 10 */ + quantity: number; + /** @example 250.5 */ + buyPrice?: number; + /** @example 2026-06-01 */ + buyDate?: string; + /** @example Покупка на дип */ + notes?: string; + /** + * @example [ + * "DIVIDEND", + * "GROWTH" + * ] + */ + tags?: ( + | 'DIVIDEND' + | 'GROWTH' + | 'DEFENSIVE' + | 'SPECULATIVE' + | 'BOND' + | 'ETF' + | 'GOVERNMENT' + | '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: { + /** @example 15 */ + quantity?: number; + /** @example 260 */ + buyPrice?: number; + /** @example 2026-06-15 */ + buyDate?: string; + /** @example Докупка */ + notes?: string; + /** + * @example [ + * "DIVIDEND" + * ] + */ + tags?: ( + | 'DIVIDEND' + | 'GROWTH' + | 'DEFENSIVE' + | 'SPECULATIVE' + | 'BOND' + | 'ETF' + | 'GOVERNMENT' + | 'CASH' + )[]; + }; + AnalyticsResponseDto: { + positions: components['schemas']['PositionWithPriceDto'][]; + summary: components['schemas']['PortfolioSummaryDto']; + }; + AnalyticsEnvelopeDto: { + data: components['schemas']['AnalyticsResponseDto']; + meta: components['schemas']['PortfolioResponseMetaDto']; + }; + BrokerAccountResponseDto: { + id: string; + /** @enum {string} */ + type: 'brokerage' | 'iis'; + name: string; + status: string; + openedAt: Record | null; + accessLevel: Record | null; + }; + BrokerResponseMetaDto: { + cachedAt: Record | null; + fromCache: boolean; + }; + BrokerAccountsEnvelopeDto: { + data: components['schemas']['BrokerAccountResponseDto'][]; + meta: components['schemas']['BrokerResponseMetaDto']; + }; + BrokerPortfolioPositionCountsDto: { + shares: number; + bonds: number; + etf: number; + other: number; + }; + BrokerMoneyDto: { + currency: string; + units: string; + nano: number; + value: number; + }; + BrokerPortfolioTotalsDto: { + shares: components['schemas']['BrokerMoneyDto'] | null; + bonds: components['schemas']['BrokerMoneyDto'] | null; + etf: components['schemas']['BrokerMoneyDto'] | null; + currencies: components['schemas']['BrokerMoneyDto'] | null; + futures: components['schemas']['BrokerMoneyDto'] | null; + options: components['schemas']['BrokerMoneyDto'] | null; + structuredProducts: components['schemas']['BrokerMoneyDto'] | null; + dfa: components['schemas']['BrokerMoneyDto'] | null; + portfolio: components['schemas']['BrokerMoneyDto'] | null; + }; + BrokerPortfolioYieldsDto: { + expectedPercent: Record | null; + daily: components['schemas']['BrokerMoneyDto'] | null; + dailyPercent: Record | null; + }; + BrokerPortfolioResponseDto: { + account: components['schemas']['BrokerAccountResponseDto']; + positionCounts: components['schemas']['BrokerPortfolioPositionCountsDto']; + totals: components['schemas']['BrokerPortfolioTotalsDto']; + yields: components['schemas']['BrokerPortfolioYieldsDto']; + cash: components['schemas']['BrokerMoneyDto'][]; + blockedCash: components['schemas']['BrokerMoneyDto'][]; + asOf: string; + }; + BrokerPortfolioEnvelopeDto: { + data: components['schemas']['BrokerPortfolioResponseDto']; + meta: components['schemas']['BrokerResponseMetaDto']; + }; + BrokerPositionResponseDto: { + figi: Record | null; + instrumentUid: Record | null; + positionUid: Record | null; + ticker: Record | null; + classCode: Record | null; + instrumentType: Record | null; + name: Record | null; + quantity: Record | null; + blockedLots: Record | null; + currentPrice: components['schemas']['BrokerMoneyDto'] | null; + currentValue: components['schemas']['BrokerMoneyDto'] | null; + averagePositionPrice: components['schemas']['BrokerMoneyDto'] | null; + expectedYieldPercent: Record | null; + dailyYield: components['schemas']['BrokerMoneyDto'] | null; + }; + BrokerPositionsPageResponseDto: { + accountId: string; + items: components['schemas']['BrokerPositionResponseDto'][]; + nextCursor: Record | null; + hasNext: boolean; + asOf: string; + }; + BrokerPositionsEnvelopeDto: { + data: components['schemas']['BrokerPositionsPageResponseDto']; + meta: components['schemas']['BrokerResponseMetaDto']; + }; + BrokerOperationResponseDto: { + cursor: Record | null; + accountId: string; + id: Record | null; + parentOperationId: Record | null; + date: Record | null; + type: string; + /** @enum {string} */ + category: 'trade' | 'income' | 'tax' | 'fee' | 'transfer' | 'other'; + description: Record | null; + name: Record | null; + state: Record | null; + instrumentUid: Record | null; + figi: Record | null; + ticker: Record | null; + classCode: Record | null; + instrumentType: Record | null; + payment: components['schemas']['BrokerMoneyDto'] | null; + price: components['schemas']['BrokerMoneyDto'] | null; + commission: components['schemas']['BrokerMoneyDto'] | null; + yield: components['schemas']['BrokerMoneyDto'] | null; + accruedInt: components['schemas']['BrokerMoneyDto'] | null; + quantity: Record | null; + quantityDone: Record | null; + }; + BrokerOperationsPageResponseDto: { + accountId: string; + items: components['schemas']['BrokerOperationResponseDto'][]; + nextCursor: Record | null; + hasNext: boolean; + asOf: string; + }; + BrokerOperationsEnvelopeDto: { + data: components['schemas']['BrokerOperationsPageResponseDto']; + meta: components['schemas']['BrokerResponseMetaDto']; + }; + BrokerOperationSyncResponseDto: { + /** @example 42 */ + upserted: number; + }; + BrokerOperationSyncEnvelopeDto: { + data: components['schemas']['BrokerOperationSyncResponseDto']; + meta: components['schemas']['BrokerResponseMetaDto']; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + HealthController_check: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + AuthController_register: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['RegisterDto']; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AuthTokenResponseDto']; + }; + }; + }; + }; + AuthController_login: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['LoginDto']; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AuthTokenResponseDto']; + }; + }; + }; + }; + AuthController_refresh: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AuthTokenResponseDto']; + }; + }; + }; + }; + AuthController_logout: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AuthLogoutResponseDto']; + }; + }; + }; + }; + AuthController_getProfile: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AuthProfileResponseDto']; + }; + }; + }; + }; + AuthController_updateProfile: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['UpdateProfileDto']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AuthProfileResponseDto']; + }; + }; + }; + }; + SecuritiesController_search: { + parameters: { + query: { + /** @description Поисковый запрос (тикер, название, ISIN) */ + q: string; + type?: 'all' | 'share' | 'bond'; + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + SecuritiesController_screener: { + parameters: { + query: { + type: 'share' | 'bond'; + priceMin?: number; + priceMax?: number; + volumeMin?: number; + listLevel?: number; + changePercentMin?: number; + changePercentMax?: number; + capitalizationMin?: number; + yieldMin?: number; + yieldMax?: number; + durationMin?: number; + durationMax?: number; + couponMin?: number; + couponMax?: number; + couponPercentMin?: number; + couponPercentMax?: number; + maturityBefore?: string; + maturityAfter?: string; + bondType?: string; + sortBy?: string; + sortOrder?: string; + page?: number; + pageSize?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ScreenerResponseDto']; + }; + }; + }; + }; + SharesController_getShare: { + parameters: { + query?: never; + header?: never; + path: { + secid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + SharesController_getMarketData: { + parameters: { + query?: never; + header?: never; + path: { + secid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + SharesController_getDividends: { + parameters: { + query?: never; + header?: never; + path: { + secid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + SharesController_getHistory: { + parameters: { + query: { + from: string; + till: string; + }; + header?: never; + path: { + secid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + BondsController_getBond: { + parameters: { + query?: never; + header?: never; + path: { + secid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + BondsController_getMarketData: { + parameters: { + query?: never; + header?: never; + path: { + secid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + BondsController_getHistory: { + parameters: { + query: { + from: string; + till: string; + }; + header?: never; + path: { + secid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + CandlesController_getShareCandles: { + parameters: { + query: { + interval: '1h' | '24h'; + from: string; + till: string; + }; + header?: never; + path: { + secid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + CandlesController_getBondCandles: { + parameters: { + query: { + interval: '1h' | '24h'; + from: string; + till: string; + }; + header?: never; + path: { + secid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + PortfolioController_findAll: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PortfolioListEnvelopeDto']; + }; + }; + }; + }; + PortfolioController_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['CreatePortfolioDto']; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PortfolioEnvelopeDto']; + }; + }; + }; + }; + PortfolioController_findOne: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PortfolioDetailEnvelopeDto']; + }; + }; + }; + }; + PortfolioController_remove: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data: null; + meta: components['schemas']['PortfolioResponseMetaDto']; + }; + }; + }; + }; + }; + PortfolioController_update: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['UpdatePortfolioDto']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PortfolioEnvelopeDto']; + }; + }; + }; + }; + PortfolioController_addPosition: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['AddPositionDto']; + }; + }; + responses: { + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PositionEnvelopeDto']; + }; + }; + }; + }; + PortfolioController_removePosition: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + positionId: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + data: null; + meta: components['schemas']['PortfolioResponseMetaDto']; + }; + }; + }; + }; + }; + PortfolioController_updatePosition: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + positionId: number; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['UpdatePositionDto']; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['PositionEnvelopeDto']; + }; + }; + }; + }; + PortfolioController_getAnalytics: { + parameters: { + query?: never; + header?: never; + path: { + id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['AnalyticsEnvelopeDto']; + }; + }; + }; + }; + TBankController_getAccounts: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['BrokerAccountsEnvelopeDto']; + }; + }; + }; + }; + TBankController_getPortfolio: { + parameters: { + query?: never; + header?: never; + path: { + accountId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['BrokerPortfolioEnvelopeDto']; + }; + }; + }; + }; + TBankController_getPositions: { + parameters: { + query?: { + /** @description Cursor for pagination (positionUid) */ + cursor?: string; + limit?: number; + /** @description Filter by instrument type (share, bond, etf, etc.) */ + type?: string; + }; + header?: never; + path: { + accountId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['BrokerPositionsEnvelopeDto']; + }; + }; + }; + }; + TBankController_getOperations: { + parameters: { + query?: { + from?: string; + to?: string; + cursor?: string; + limit?: number; + instrumentId?: string; + operationTypes?: string; + state?: string; + }; + header?: never; + path: { + accountId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['BrokerOperationsEnvelopeDto']; + }; + }; + }; + }; + TBankController_syncOperations: { + parameters: { + query: { + from: string; + to: string; + }; + header?: never; + path: { + accountId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['BrokerOperationSyncEnvelopeDto']; + }; + }; + }; + }; +}