feat: complete Phase 3 API type unification, delete stale test file

- Delete shared/api/responses.ts, move type re-exports to index.ts
- Update all 55+ imports from shared/api/responses to shared/api
- Delete stale client.test.ts (tested deleted client.ts)
- Run biome checks and fix import ordering
- Update tasks.md and plan.md to reflect actual approach
This commit is contained in:
Sergey Krylov 2026-06-23 20:24:16 +03:00
parent 063e80c375
commit 9932278b64
65 changed files with 136 additions and 341 deletions

View File

@ -6,8 +6,8 @@ import {
handleUnauthorized, handleUnauthorized,
setOnUnauthorized, setOnUnauthorized,
} from '@/entities/session/api/tokenManager' } from '@/entities/session/api/tokenManager'
import type { UserResponse } from '@/shared/api'
import { configureKyAuth } from '@/shared/api/kyClient' import { configureKyAuth } from '@/shared/api/kyClient'
import type { UserResponse } from '@/shared/api/responses'
export function SessionProvider({ children }: { children: ReactNode }) { export function SessionProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<UserResponse | null>(null) const [user, setUser] = useState<UserResponse | null>(null)

View File

@ -1,11 +1,11 @@
import { request } from '@/shared/api/kyClient'
import type { import type {
ApiResponseMeta, ApiResponseMeta,
BondHistoryItem, BondHistoryItem,
BondMarketData, BondMarketData,
BondResponse, BondResponse,
CandleItem, CandleItem,
} from '@/shared/api/responses' } from '@/shared/api'
import { request } from '@/shared/api/kyClient'
export function getBond(secid: string): Promise<{ data: BondResponse; meta: ApiResponseMeta }> { export function getBond(secid: string): Promise<{ data: BondResponse; meta: ApiResponseMeta }> {
return request<BondResponse>(`/api/v1/securities/bonds/${encodeURIComponent(secid)}`) return request<BondResponse>(`/api/v1/securities/bonds/${encodeURIComponent(secid)}`)

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { BondResponse } from '@/shared/api/responses' import type { BondResponse } from '@/shared/api'
import { getBond } from '../api/bondApi' import { getBond } from '../api/bondApi'
export function useBond(secid: string) { export function useBond(secid: string) {

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { CandleItem } from '@/shared/api/responses' import type { CandleItem } from '@/shared/api'
import { getBondCandles } from '../api/bondApi' import { getBondCandles } from '../api/bondApi'
export function useBondCandles(secid: string, interval: '1h' | '24h', from: string, till: string) { export function useBondCandles(secid: string, interval: '1h' | '24h', from: string, till: string) {

View File

@ -1,5 +1,5 @@
import type { ApiResponseMeta, BrokerAccount, BrokerPortfolio } from '@/shared/api'
import { request } from '@/shared/api/kyClient' import { request } from '@/shared/api/kyClient'
import type { ApiResponseMeta, BrokerAccount, BrokerPortfolio } from '@/shared/api/responses'
export type BrokerOperationQuery = { export type BrokerOperationQuery = {
from?: string from?: string

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import type { BrokerPortfolio } from '@/shared/api/responses' import type { BrokerPortfolio } from '@/shared/api'
import { aggregateBrokerAccounts } from '../model/brokerAccountsOverview' import { aggregateBrokerAccounts } from '../model/brokerAccountsOverview'
function portfolio( function portfolio(

View File

@ -1,4 +1,4 @@
import type { BrokerMoney, BrokerPortfolio } from '@/shared/api/responses' import type { BrokerMoney, BrokerPortfolio } from '@/shared/api'
export interface BrokerCurrencyAllocationSummary { export interface BrokerCurrencyAllocationSummary {
shares: number shares: number

View File

@ -1,5 +1,5 @@
import { useQueries } from '@tanstack/react-query' import { useQueries } from '@tanstack/react-query'
import type { BrokerAccount, BrokerPortfolio } from '@/shared/api/responses' import type { BrokerAccount, BrokerPortfolio } from '@/shared/api'
import { getBrokerPortfolio } from '../api/brokerAccountApi' import { getBrokerPortfolio } from '../api/brokerAccountApi'
export function useBrokerAccountPortfolios(accounts: BrokerAccount[]) { export function useBrokerAccountPortfolios(accounts: BrokerAccount[]) {

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { BrokerAccount } from '@/shared/api/responses' import type { BrokerAccount } from '@/shared/api'
import { getBrokerAccounts } from '../api/brokerAccountApi' import { getBrokerAccounts } from '../api/brokerAccountApi'
export function useBrokerAccounts() { export function useBrokerAccounts() {

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { BrokerPortfolio } from '@/shared/api/responses' import type { BrokerPortfolio } from '@/shared/api'
import { getBrokerPortfolio } from '../api/brokerAccountApi' import { getBrokerPortfolio } from '../api/brokerAccountApi'
export function useBrokerPortfolio(accountId: string | undefined) { export function useBrokerPortfolio(accountId: string | undefined) {

View File

@ -1,5 +1,5 @@
import type { ApiResponseMeta, BrokerEventsData } from '@/shared/api'
import { request } from '@/shared/api/kyClient' import { request } from '@/shared/api/kyClient'
import type { ApiResponseMeta, BrokerEventsData } from '@/shared/api/responses'
export type BrokerEventsQuery = { export type BrokerEventsQuery = {
from: string from: string

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { BrokerEventsData } from '@/shared/api/responses' import type { BrokerEventsData } from '@/shared/api'
import { type BrokerEventsQuery, getBrokerEvents } from '../api/brokerEventApi' import { type BrokerEventsQuery, getBrokerEvents } from '../api/brokerEventApi'
export function useBrokerEvents(accountId: string | undefined, query: BrokerEventsQuery) { export function useBrokerEvents(accountId: string | undefined, query: BrokerEventsQuery) {

View File

@ -1,5 +1,5 @@
import type { ApiResponseMeta, BrokerOperationsPage } from '@/shared/api'
import { request } from '@/shared/api/kyClient' import { request } from '@/shared/api/kyClient'
import type { ApiResponseMeta, BrokerOperationsPage } from '@/shared/api/responses'
export type BrokerOperationQuery = { export type BrokerOperationQuery = {
from?: string from?: string

View File

@ -1,4 +1,4 @@
import type { BrokerOperation } from '@/shared/api/responses' import type { BrokerOperation } from '@/shared/api'
export type BrokerOperationImpact = 'adds' | 'reduces' | 'neutral' | 'unknown' export type BrokerOperationImpact = 'adds' | 'reduces' | 'neutral' | 'unknown'

View File

@ -1,5 +1,5 @@
import { keepPreviousData, useQuery } from '@tanstack/react-query' import { keepPreviousData, useQuery } from '@tanstack/react-query'
import type { BrokerOperationsPage } from '@/shared/api/responses' import type { BrokerOperationsPage } from '@/shared/api'
import { type BrokerOperationQuery, getBrokerOperations } from '../api/brokerOperationApi' import { type BrokerOperationQuery, getBrokerOperations } from '../api/brokerOperationApi'
export function useBrokerOperations( export function useBrokerOperations(

View File

@ -1,5 +1,5 @@
import type { ApiResponseMeta, BrokerPositionsPage } from '@/shared/api'
import { request } from '@/shared/api/kyClient' import { request } from '@/shared/api/kyClient'
import type { ApiResponseMeta, BrokerPositionsPage } from '@/shared/api/responses'
export function getBrokerPositions( export function getBrokerPositions(
accountId: string, accountId: string,

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import type { BrokerMoney, BrokerPortfolio } from '@/shared/api/responses' import type { BrokerMoney, BrokerPortfolio } from '@/shared/api'
import { buildBrokerAllocation } from './brokerAllocation' import { buildBrokerAllocation } from './brokerAllocation'
function money(value: number): BrokerMoney { function money(value: number): BrokerMoney {

View File

@ -1,4 +1,4 @@
import type { BrokerPortfolio } from '@/shared/api/responses' import type { BrokerPortfolio } from '@/shared/api'
export type BrokerAllocationKey = 'shares' | 'bonds' | 'etf' | 'cash' | 'other' export type BrokerAllocationKey = 'shares' | 'bonds' | 'etf' | 'cash' | 'other'

View File

@ -5,7 +5,7 @@ import {
getBrokerOperationTypeLabel, getBrokerOperationTypeLabel,
isBrokerOperationType, isBrokerOperationType,
} from '@/entities/broker-operation' } from '@/entities/broker-operation'
import type { BrokerOperation, BrokerPosition } from '@/shared/api/responses' import type { BrokerOperation, BrokerPosition } from '@/shared/api'
import { getBrokerInstrumentPath, getBrokerPositionGroup } from './brokerDisplay' import { getBrokerInstrumentPath, getBrokerPositionGroup } from './brokerDisplay'
function position(input: Partial<BrokerPosition>): BrokerPosition { function position(input: Partial<BrokerPosition>): BrokerPosition {

View File

@ -1,4 +1,4 @@
import type { BrokerPosition } from '@/shared/api/responses' import type { BrokerPosition } from '@/shared/api'
export type BrokerPositionGroup = 'shares' | 'bonds' | 'other' export type BrokerPositionGroup = 'shares' | 'bonds' | 'other'

View File

@ -1,5 +1,5 @@
import { keepPreviousData, useQuery } from '@tanstack/react-query' import { keepPreviousData, useQuery } from '@tanstack/react-query'
import type { BrokerPositionsPage } from '@/shared/api/responses' import type { BrokerPositionsPage } from '@/shared/api'
import { getBrokerPositions } from '../api/brokerPositionApi' import { getBrokerPositions } from '../api/brokerPositionApi'
export function useBrokerPositions( export function useBrokerPositions(

View File

@ -1,10 +1,5 @@
import type { AnalyticsResponse, Portfolio, PortfolioDetail, Position } from '@/shared/api'
import { request } from '@/shared/api/kyClient' import { request } from '@/shared/api/kyClient'
import type {
AnalyticsResponse,
Portfolio,
PortfolioDetail,
Position,
} from '@/shared/api/responses'
export function getPortfolios(): Promise<{ export function getPortfolios(): Promise<{
data: Portfolio[] data: Portfolio[]

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { PortfolioDetail } from '@/shared/api/responses' import type { PortfolioDetail } from '@/shared/api'
import { getPortfolio } from '../api/portfolioApi' import { getPortfolio } from '../api/portfolioApi'
export function usePortfolio(id: number) { export function usePortfolio(id: number) {

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { AnalyticsResponse } from '@/shared/api/responses' import type { AnalyticsResponse } from '@/shared/api'
import { getPortfolioAnalytics } from '../api/portfolioApi' import { getPortfolioAnalytics } from '../api/portfolioApi'
export function usePortfolioAnalytics(portfolioId: number) { export function usePortfolioAnalytics(portfolioId: number) {

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { Portfolio } from '@/shared/api/responses' import type { Portfolio } from '@/shared/api'
import { getPortfolios } from '../api/portfolioApi' import { getPortfolios } from '../api/portfolioApi'
export function usePortfolios() { export function usePortfolios() {

View File

@ -1,5 +1,5 @@
import { useMutation, useQueryClient } from '@tanstack/react-query' import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { PortfolioDetail } from '@/shared/api/responses' import type { PortfolioDetail } from '@/shared/api'
import { addPosition, removePosition, updatePosition } from '../api/portfolioApi' import { addPosition, removePosition, updatePosition } from '../api/portfolioApi'
export function usePositionMutations(portfolioId: number) { export function usePositionMutations(portfolioId: number) {

View File

@ -1,5 +1,5 @@
import type { SearchResultItem } from '@/shared/api'
import { request } from '@/shared/api/kyClient' import { request } from '@/shared/api/kyClient'
import type { SearchResultItem } from '@/shared/api/responses'
export function searchSecurities(q: string, type: 'all' | 'share' | 'bond' = 'all', limit = 20) { export function searchSecurities(q: string, type: 'all' | 'share' | 'bond' = 'all', limit = 20) {
return request<SearchResultItem[]>('/api/v1/securities/search', { return request<SearchResultItem[]>('/api/v1/securities/search', {

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { SearchResultItem } from '@/shared/api/responses' import type { SearchResultItem } from '@/shared/api'
import { searchSecurities } from '../api/searchApi' import { searchSecurities } from '../api/searchApi'
export function useSearch(query: string) { export function useSearch(query: string) {

View File

@ -1,5 +1,5 @@
import type { AuthResponse, UserResponse } from '@/shared/api'
import { request } from '@/shared/api/kyClient' import { request } from '@/shared/api/kyClient'
import type { AuthResponse, UserResponse } from '@/shared/api/responses'
import { setAccessToken } from './tokenManager' import { setAccessToken } from './tokenManager'
export async function login(email: string, password: string) { export async function login(email: string, password: string) {

View File

@ -1,5 +1,5 @@
import type { AuthResponse } from '@/shared/api'
import { normalizeEnvelope } from '@/shared/api/kyClient' import { normalizeEnvelope } from '@/shared/api/kyClient'
import type { AuthResponse } from '@/shared/api/responses'
let accessToken: string | null = null let accessToken: string | null = null
let onUnauthorized: (() => void) | null = null let onUnauthorized: (() => void) | null = null

View File

@ -1,5 +1,5 @@
import { create } from 'zustand' import { create } from 'zustand'
import type { UserResponse } from '@/shared/api/responses' import type { UserResponse } from '@/shared/api'
interface SessionState { interface SessionState {
user: UserResponse | null user: UserResponse | null

View File

@ -1,4 +1,3 @@
import { request } from '@/shared/api/kyClient'
import type { import type {
ApiResponseMeta, ApiResponseMeta,
CandleItem, CandleItem,
@ -6,7 +5,8 @@ import type {
ShareHistoryItem, ShareHistoryItem,
ShareResponse, ShareResponse,
StockMarketData, StockMarketData,
} from '@/shared/api/responses' } from '@/shared/api'
import { request } from '@/shared/api/kyClient'
export function getShare(secid: string): Promise<{ data: ShareResponse; meta: ApiResponseMeta }> { export function getShare(secid: string): Promise<{ data: ShareResponse; meta: ApiResponseMeta }> {
return request<ShareResponse>(`/api/v1/securities/shares/${encodeURIComponent(secid)}`) return request<ShareResponse>(`/api/v1/securities/shares/${encodeURIComponent(secid)}`)

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { ShareResponse } from '@/shared/api/responses' import type { ShareResponse } from '@/shared/api'
import { getShare } from '../api/stockApi' import { getShare } from '../api/stockApi'
export function useStock(secid: string) { export function useStock(secid: string) {

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { CandleItem } from '@/shared/api/responses' import type { CandleItem } from '@/shared/api'
import { getShareCandles } from '../api/stockApi' import { getShareCandles } from '../api/stockApi'
export function useStockCandles(secid: string, interval: '1h' | '24h', from: string, till: string) { export function useStockCandles(secid: string, interval: '1h' | '24h', from: string, till: string) {

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import type { DividendItem } from '@/shared/api/responses' import type { DividendItem } from '@/shared/api'
import { getShareDividends } from '../api/stockApi' import { getShareDividends } from '../api/stockApi'
export function useStockDividends(secid: string) { export function useStockDividends(secid: string) {

View File

@ -1,5 +1,5 @@
import type { ScreenerResult } from '@/shared/api'
import { request } from '@/shared/api/kyClient' import { request } from '@/shared/api/kyClient'
import type { ScreenerResult } from '@/shared/api/responses'
export interface ScreenerQuery { export interface ScreenerQuery {
type: 'share' | 'bond' type: 'share' | 'bond'

View File

@ -1,5 +1,5 @@
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import type { ScreenerResult } from '@/shared/api/responses' import type { ScreenerResult } from '@/shared/api'
interface Props { interface Props {
result: ScreenerResult result: ScreenerResult

View File

@ -1,163 +0,0 @@
import { HttpResponse, http } from 'msw'
import { beforeEach, describe, expect, it } from 'vitest'
import {
getAccessToken,
handleUnauthorized,
setAccessToken,
setOnUnauthorized,
} from '@/entities/session/api/tokenManager'
import { server } from '@/shared/lib/test/server'
import { configureKyAuth, request } from './kyClient'
const API = '/api/v1'
beforeEach(() => {
setAccessToken(null)
configureKyAuth({
getAccessToken,
handleUnauthorized,
})
})
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')
})
})

View File

@ -1,36 +1,62 @@
import type { components } from './types'
export { configureKyAuth, getHealth, request } from './kyClient' export { configureKyAuth, getHealth, request } from './kyClient'
export type {
AnalyticsResponse, export type ApiResponseMeta = components['schemas']['ApiResponseMeta']
ApiEnvelope,
ApiResponseMeta, export interface ApiEnvelope<T> {
AuthResponse, data: T
BondHistoryItem, meta: ApiResponseMeta
BondMarketData, }
BondResponse,
BrokerAccount, // Auth
BrokerEventItem, export type AuthResponse = components['schemas']['AuthTokenDataDto']
BrokerEventsData, export type UserResponse = components['schemas']['AuthUserDto']
BrokerEventsSummary,
BrokerMoney, // Shares
BrokerOperation, export type ShareResponse = components['schemas']['ShareResponseDto']
BrokerOperationCategory, export type StockMarketData = components['schemas']['StockMarketDataDto']
BrokerOperationsPage, export type DividendItem = components['schemas']['DividendItemDto']
BrokerPortfolio, export type ShareHistoryItem = components['schemas']['HistoryItemDto']
BrokerPosition,
BrokerPositionsPage, // Bonds
CandleItem, export type BondResponse = components['schemas']['BondResponseDto']
DividendItem, export type BondMarketData = components['schemas']['BondMarketDataDto']
HealthResponse, export type BondHistoryItem = components['schemas']['BondHistoryItemDto']
Portfolio,
PortfolioDetail, // Candles
PortfolioSummary, export type CandleItem = components['schemas']['CandleItemDto']
Position,
PositionWithPrice, // Search
ScreenerItem, export type SearchResultItem = components['schemas']['SearchResultItemDto']
ScreenerResult,
SearchResultItem, // Screener
ShareHistoryItem, export type ScreenerResult = components['schemas']['ScreenerResultDto']
ShareResponse, export type ScreenerItem = components['schemas']['ScreenerItemDto']
StockMarketData,
UserResponse, // Portfolio
} from './responses' export type Portfolio = components['schemas']['PortfolioListResponseDto']
export type PortfolioDetail = components['schemas']['PortfolioDetailResponseDto']
export type Position = components['schemas']['PositionResponseDto']
export type PositionWithPrice = components['schemas']['PositionWithPriceDto']
export type PortfolioSummary = components['schemas']['PortfolioSummaryDto']
export type AnalyticsResponse = components['schemas']['AnalyticsResponseDto']
// Health
export type HealthResponse = components['schemas']['HealthResponseDto']
// Broker
export type BrokerAccount = components['schemas']['BrokerAccountResponseDto']
export type BrokerPortfolio = components['schemas']['BrokerPortfolioResponseDto']
export type BrokerMoney = components['schemas']['BrokerMoneyDto']
export type BrokerPosition = components['schemas']['BrokerPositionResponseDto']
export type BrokerOperation = components['schemas']['BrokerOperationResponseDto']
export type BrokerOperationsPage = components['schemas']['BrokerOperationsPageResponseDto']
export type BrokerPositionsPage = components['schemas']['BrokerPositionsPageResponseDto']
export type BrokerOperationCategory =
components['schemas']['BrokerOperationResponseDto']['category']
// Broker events
export type BrokerEventItem = components['schemas']['BrokerEventItemDto']
export type BrokerEventsData = components['schemas']['BrokerEventsDataDto']
export type BrokerEventsSummary = components['schemas']['BrokerEventsSummaryDto']

View File

@ -1,62 +0,0 @@
import type { components } from './types'
// Re-exported from openapi-typescript codegen with friendly names
export type ApiResponseMeta = components['schemas']['ApiResponseMeta']
export interface ApiEnvelope<T> {
data: T
meta: ApiResponseMeta
}
// Auth
export type AuthResponse = components['schemas']['AuthTokenDataDto']
export type UserResponse = components['schemas']['AuthUserDto']
// Shares
export type ShareResponse = components['schemas']['ShareResponseDto']
export type StockMarketData = components['schemas']['StockMarketDataDto']
export type DividendItem = components['schemas']['DividendItemDto']
export type ShareHistoryItem = components['schemas']['HistoryItemDto']
// Bonds
export type BondResponse = components['schemas']['BondResponseDto']
export type BondMarketData = components['schemas']['BondMarketDataDto']
export type BondHistoryItem = components['schemas']['BondHistoryItemDto']
// Candles
export type CandleItem = components['schemas']['CandleItemDto']
// Search
export type SearchResultItem = components['schemas']['SearchResultItemDto']
// Screener
export type ScreenerResult = components['schemas']['ScreenerResultDto']
export type ScreenerItem = components['schemas']['ScreenerItemDto']
// Portfolio
export type Portfolio = components['schemas']['PortfolioListResponseDto']
export type PortfolioDetail = components['schemas']['PortfolioDetailResponseDto']
export type Position = components['schemas']['PositionResponseDto']
export type PositionWithPrice = components['schemas']['PositionWithPriceDto']
export type PortfolioSummary = components['schemas']['PortfolioSummaryDto']
export type AnalyticsResponse = components['schemas']['AnalyticsResponseDto']
// Health
export type HealthResponse = components['schemas']['HealthResponseDto']
// Broker — some nullable fields still have Record<string, never> from backend
export type BrokerAccount = components['schemas']['BrokerAccountResponseDto']
export type BrokerPortfolio = components['schemas']['BrokerPortfolioResponseDto']
export type BrokerMoney = components['schemas']['BrokerMoneyDto']
export type BrokerPosition = components['schemas']['BrokerPositionResponseDto']
export type BrokerOperation = components['schemas']['BrokerOperationResponseDto']
export type BrokerOperationsPage = components['schemas']['BrokerOperationsPageResponseDto']
export type BrokerPositionsPage = components['schemas']['BrokerPositionsPageResponseDto']
export type BrokerOperationCategory =
components['schemas']['BrokerOperationResponseDto']['category']
// Broker events
export type BrokerEventItem = components['schemas']['BrokerEventItemDto']
export type BrokerEventsData = components['schemas']['BrokerEventsDataDto']
export type BrokerEventsSummary = components['schemas']['BrokerEventsSummaryDto']

View File

@ -1,4 +1,4 @@
import type { BrokerMoney } from '@/shared/api/responses' import type { BrokerMoney } from '@/shared/api'
export function formatBrokerCurrencyValue(currency: string, value: number): string { export function formatBrokerCurrencyValue(currency: string, value: number): string {
return new Intl.NumberFormat('ru-RU', { return new Intl.NumberFormat('ru-RU', {

View File

@ -1,5 +1,5 @@
import { createContext } from 'react' import { createContext } from 'react'
import type { UserResponse } from '@/shared/api/responses' import type { UserResponse } from '@/shared/api'
export interface SessionContextValue { export interface SessionContextValue {
user: UserResponse | null user: UserResponse | null

View File

@ -8,7 +8,7 @@ import type {
ShareResponse, ShareResponse,
StockMarketData, StockMarketData,
UserResponse, UserResponse,
} from '@/shared/api/responses' } from '@/shared/api'
export function createMockMarketData(overrides: Partial<StockMarketData> = {}): StockMarketData { export function createMockMarketData(overrides: Partial<StockMarketData> = {}): StockMarketData {
return { return {

View File

@ -1,10 +1,5 @@
import { HttpResponse, http } from 'msw' import { HttpResponse, http } from 'msw'
import type { import type { BondResponse, CandleItem, SearchResultItem, ShareResponse } from '@/shared/api'
BondResponse,
CandleItem,
SearchResultItem,
ShareResponse,
} from '@/shared/api/responses'
const API = '/api/v1' const API = '/api/v1'

View File

@ -1,4 +1,4 @@
import type { BondResponse } from '@/shared/api/responses' import type { BondResponse } from '@/shared/api'
interface BondDetailsProps { interface BondDetailsProps {
bond: BondResponse bond: BondResponse

View File

@ -1,6 +1,6 @@
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import { useState } from 'react' import { useState } from 'react'
import type { PositionWithPrice } from '@/shared/api/responses' import type { PositionWithPrice } from '@/shared/api'
interface Props { interface Props {
position: PositionWithPrice position: PositionWithPrice

View File

@ -1,4 +1,4 @@
import type { PositionWithPrice } from '@/shared/api/responses' import type { PositionWithPrice } from '@/shared/api'
import { BondPositionRow } from './BondPositionRow' import { BondPositionRow } from './BondPositionRow'
interface Props { interface Props {

View File

@ -2,7 +2,7 @@ import { Alert, Button, Heading, Skeleton, Text } from '@moex-vibe/design-system
import { Box } from '@mui/material' import { Box } from '@mui/material'
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import { buildBrokerAllocation } from '@/entities/broker-position' import { buildBrokerAllocation } from '@/entities/broker-position'
import type { BrokerAccount, BrokerPortfolio } from '@/shared/api/responses' import type { BrokerAccount, BrokerPortfolio } from '@/shared/api'
import { import {
formatBrokerDate, formatBrokerDate,
formatBrokerMoney, formatBrokerMoney,

View File

@ -1,7 +1,7 @@
import { Text } from '@moex-vibe/design-system' import { Text } from '@moex-vibe/design-system'
import { Box } from '@mui/material' import { Box } from '@mui/material'
import { buildBrokerAllocation } from '@/entities/broker-position' import { buildBrokerAllocation } from '@/entities/broker-position'
import type { BrokerPortfolio } from '@/shared/api/responses' import type { BrokerPortfolio } from '@/shared/api'
import { formatBrokerCurrencyValue } from '@/shared/lib/formatters' import { formatBrokerCurrencyValue } from '@/shared/lib/formatters'
const RADIUS = 44 const RADIUS = 44

View File

@ -8,7 +8,7 @@ import {
getBrokerOperationTypeLabel, getBrokerOperationTypeLabel,
} from '@/entities/broker-operation' } from '@/entities/broker-operation'
import { getBrokerInstrumentPath } from '@/entities/broker-position' import { getBrokerInstrumentPath } from '@/entities/broker-position'
import type { BrokerOperation, BrokerOperationsPage } from '@/shared/api/responses' import type { BrokerOperation, BrokerOperationsPage } from '@/shared/api'
import { formatBrokerSignedMoney } from '@/shared/lib/formatters' import { formatBrokerSignedMoney } from '@/shared/lib/formatters'
import { TableSkeleton } from '@/shared/ui/TableSkeleton' import { TableSkeleton } from '@/shared/ui/TableSkeleton'

View File

@ -1,7 +1,7 @@
import { Text } from '@moex-vibe/design-system' import { Text } from '@moex-vibe/design-system'
import { Box } from '@mui/material' import { Box } from '@mui/material'
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import type { BrokerMoney, BrokerPortfolio } from '@/shared/api/responses' import type { BrokerMoney, BrokerPortfolio } from '@/shared/api'
import { formatBrokerMoney, pluralize } from '@/shared/lib/formatters' import { formatBrokerMoney, pluralize } from '@/shared/lib/formatters'
function allocationPercent(value: BrokerMoney | null, total: BrokerMoney | null) { function allocationPercent(value: BrokerMoney | null, total: BrokerMoney | null) {

View File

@ -1,6 +1,6 @@
import { Text } from '@moex-vibe/design-system' import { Text } from '@moex-vibe/design-system'
import { Box } from '@mui/material' import { Box } from '@mui/material'
import type { BrokerPortfolio } from '@/shared/api/responses' import type { BrokerPortfolio } from '@/shared/api'
import { import {
formatBrokerMoney as formatMoney, formatBrokerMoney as formatMoney,
formatBrokerPercent as formatPercent, formatBrokerPercent as formatPercent,

View File

@ -1,6 +1,6 @@
import { Button, Heading, Skeleton, Text } from '@moex-vibe/design-system' import { Button, Heading, Skeleton, Text } from '@moex-vibe/design-system'
import { Box } from '@mui/material' import { Box } from '@mui/material'
import type { BrokerPositionsPage as BrokerPositionsPageData } from '@/shared/api/responses' import type { BrokerPositionsPage as BrokerPositionsPageData } from '@/shared/api'
import { formatBrokerMoney as formatMoney } from '@/shared/lib/formatters' import { formatBrokerMoney as formatMoney } from '@/shared/lib/formatters'
import { PositionTicker } from './PositionTicker' import { PositionTicker } from './PositionTicker'

View File

@ -1,7 +1,7 @@
import { Box } from '@mui/material' import { Box } from '@mui/material'
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import { getBrokerInstrumentPath } from '@/entities/broker-position' import { getBrokerInstrumentPath } from '@/entities/broker-position'
import type { BrokerPosition } from '@/shared/api/responses' import type { BrokerPosition } from '@/shared/api'
export function PositionTicker({ position }: { position: BrokerPosition }) { export function PositionTicker({ position }: { position: BrokerPosition }) {
const label = position.ticker || position.figi || '-' const label = position.ticker || position.figi || '-'

View File

@ -1,4 +1,4 @@
import type { DividendItem } from '@/shared/api/responses' import type { DividendItem } from '@/shared/api'
interface DividendsTableProps { interface DividendsTableProps {
dividends: DividendItem[] dividends: DividendItem[]

View File

@ -1,4 +1,4 @@
import type { PortfolioSummary } from '@/shared/api/responses' import type { PortfolioSummary } from '@/shared/api'
export function AnalyticsSummary({ summary }: { summary: PortfolioSummary }) { export function AnalyticsSummary({ summary }: { summary: PortfolioSummary }) {
const formatRub = (val: number | null) => const formatRub = (val: number | null) =>

View File

@ -1,5 +1,5 @@
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import type { Portfolio } from '@/shared/api/responses' import type { Portfolio } from '@/shared/api'
import { pluralize } from '@/shared/lib/formatters' import { pluralize } from '@/shared/lib/formatters'
export function PortfolioCard({ portfolio }: { portfolio: Portfolio }) { export function PortfolioCard({ portfolio }: { portfolio: Portfolio }) {

View File

@ -1,5 +1,5 @@
import { useState } from 'react' import { useState } from 'react'
import type { Portfolio } from '@/shared/api/responses' import type { Portfolio } from '@/shared/api'
interface Props { interface Props {
initial?: Portfolio initial?: Portfolio

View File

@ -1,4 +1,4 @@
import type { PositionWithPrice } from '@/shared/api/responses' import type { PositionWithPrice } from '@/shared/api'
interface AllocationChartProps { interface AllocationChartProps {
positions: PositionWithPrice[] positions: PositionWithPrice[]

View File

@ -1,4 +1,4 @@
import type { PortfolioDetail } from '@/shared/api/responses' import type { PortfolioDetail } from '@/shared/api'
import { AllocationChart } from './AllocationChart' import { AllocationChart } from './AllocationChart'
export function PortfolioSummary({ portfolio }: { portfolio: PortfolioDetail }) { export function PortfolioSummary({ portfolio }: { portfolio: PortfolioDetail }) {

View File

@ -1,6 +1,6 @@
import { Link } from '@tanstack/react-router' import { Link } from '@tanstack/react-router'
import { useState } from 'react' import { useState } from 'react'
import type { PositionWithPrice } from '@/shared/api/responses' import type { PositionWithPrice } from '@/shared/api'
interface Props { interface Props {
position: PositionWithPrice position: PositionWithPrice

View File

@ -1,4 +1,4 @@
import type { PositionWithPrice } from '@/shared/api/responses' import type { PositionWithPrice } from '@/shared/api'
import { SharePositionRow } from './SharePositionRow' import { SharePositionRow } from './SharePositionRow'
interface Props { interface Props {

View File

@ -1,4 +1,4 @@
import type { ShareResponse } from '@/shared/api/responses' import type { ShareResponse } from '@/shared/api'
interface StockDetailsProps { interface StockDetailsProps {
stock: ShareResponse stock: ShareResponse

View File

@ -47,10 +47,14 @@
### Phase 3 — Unify API Types ### Phase 3 — Unify API Types
*Низкий риск* *Низкий риск*
1. Убедиться, что все entity API импортируют из `types.ts` (codegen) > **Фактический подход: ре-экспорты в index.ts вместо types.ts.**
> В процессе реализации выяснилось, что direct-импорты из `types.ts` ведут к многословным `components['schemas']['XxxDto']` в 60+ файлах. Принято решение перенести friendly-name ре-экспорты из `responses.ts` в `index.ts`. Таким образом `responses.ts` удалён, а единая точка входа — `@/shared/api`.
1. Перенести friendly-name type-алиасы из `responses.ts` в `shared/api/index.ts`
2. Удалить `shared/api/responses.ts` 2. Удалить `shared/api/responses.ts`
3. Перенести normalizeEnvelope (ky-версию) в `shared/api/kyClient.ts` 3. Переключить все импорты с `@/shared/api/responses` на `@/shared/api`
4. Прогнать `npm run build` 4. Перенести normalizeEnvelope (ky-версию) в `shared/api/kyClient.ts`
5. Прогнать `npm run build`
### Phase 4 — MSW Browser ### Phase 4 — MSW Browser
*Низкий риск, handlers готовы* *Низкий риск, handlers готовы*

View File

@ -21,9 +21,9 @@
## Phase 2: Biome Migration ## Phase 2: Biome Migration
- [ ] Research: проверить Biome plugin system на поддержку FSD/import-no-restricted-paths - [x] Research: проверить Biome plugin system на поддержку FSD/import-no-restricted-paths — не поддерживается, ESLint оставлен для FSD
- [x] Установить `@biomejs/biome` (devDependency) - [x] Установить `@biomejs/biome` (devDependency)
- [ ] Запустить `npx @biomejs/biome migrate eslint --write` - [x] Запустить `npx @biomejs/biome migrate eslint --write` — 0 правил мигрировано, FSD-правила несовместимы, конфиг откачен
- [x] Создать и настроить `biome.json` под проект - [x] Создать и настроить `biome.json` под проект
- [x] ESLint оставлен только для FSD-правил (`.eslintrc.cjs`) - [x] ESLint оставлен только для FSD-правил (`.eslintrc.cjs`)
- [x] Удалить зависимость prettier (ESLint core, @typescript-eslint/*, eslint-plugin-* оставлены для FSD-правил) - [x] Удалить зависимость prettier (ESLint core, @typescript-eslint/*, eslint-plugin-* оставлены для FSD-правил)
@ -37,11 +37,11 @@
## Phase 3: Unify API Types ## Phase 3: Unify API Types
- [ ] Аудит импортов: entity API используют `types.ts` (codegen) или `responses.ts`? - [x] Аудит импортов: entity API используют `types.ts` (codegen) или `responses.ts`?
- [ ] Переключить все импорты с `responses.ts` на `types.ts` (если типы есть в codegen) - [x] Переключить все импорты с `responses.ts` на `@/shared/api` (типы перенесены в `index.ts` вместо рукописного файла)
- [ ] Удалить `shared/api/responses.ts` (после переключения) - [x] Удалить `shared/api/responses.ts` (после переключения)
- [x] normalizeEnvelope перенесён в `shared/api/kyClient.ts` - [x] normalizeEnvelope перенесён в `shared/api/kyClient.ts`
- [ ] `npm run build` — сборка проходит - [x] `npm run build` — сборка проходит
## Phase 4: MSW Browser ## Phase 4: MSW Browser