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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import type { CandleItem } from '@/shared/api/responses'
import type { CandleItem } from '@/shared/api'
import { getBondCandles } from '../api/bondApi'
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 type { ApiResponseMeta, BrokerAccount, BrokerPortfolio } from '@/shared/api/responses'
export type BrokerOperationQuery = {
from?: string

View File

@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'
import type { BrokerPortfolio } from '@/shared/api/responses'
import type { BrokerPortfolio } from '@/shared/api'
import { aggregateBrokerAccounts } from '../model/brokerAccountsOverview'
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 {
shares: number

View File

@ -1,5 +1,5 @@
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'
export function useBrokerAccountPortfolios(accounts: BrokerAccount[]) {

View File

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

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import type { BrokerPortfolio } from '@/shared/api/responses'
import type { BrokerPortfolio } from '@/shared/api'
import { getBrokerPortfolio } from '../api/brokerAccountApi'
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 type { ApiResponseMeta, BrokerEventsData } from '@/shared/api/responses'
export type BrokerEventsQuery = {
from: string

View File

@ -1,5 +1,5 @@
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'
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 type { ApiResponseMeta, BrokerOperationsPage } from '@/shared/api/responses'
export type BrokerOperationQuery = {
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'

View File

@ -1,5 +1,5 @@
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'
export function useBrokerOperations(

View File

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

View File

@ -1,5 +1,5 @@
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'
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'

View File

@ -5,7 +5,7 @@ import {
getBrokerOperationTypeLabel,
isBrokerOperationType,
} from '@/entities/broker-operation'
import type { BrokerOperation, BrokerPosition } from '@/shared/api/responses'
import type { BrokerOperation, BrokerPosition } from '@/shared/api'
import { getBrokerInstrumentPath, getBrokerPositionGroup } from './brokerDisplay'
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'

View File

@ -1,5 +1,5 @@
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'
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 type {
AnalyticsResponse,
Portfolio,
PortfolioDetail,
Position,
} from '@/shared/api/responses'
export function getPortfolios(): Promise<{
data: Portfolio[]

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
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'
export function usePositionMutations(portfolioId: number) {

View File

@ -1,5 +1,5 @@
import type { SearchResultItem } from '@/shared/api'
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) {
return request<SearchResultItem[]>('/api/v1/securities/search', {

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import type { SearchResultItem } from '@/shared/api/responses'
import type { SearchResultItem } from '@/shared/api'
import { searchSecurities } from '../api/searchApi'
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 type { AuthResponse, UserResponse } from '@/shared/api/responses'
import { setAccessToken } from './tokenManager'
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 type { AuthResponse } from '@/shared/api/responses'
let accessToken: string | null = null
let onUnauthorized: (() => void) | null = null

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import type { CandleItem } from '@/shared/api/responses'
import type { CandleItem } from '@/shared/api'
import { getShareCandles } from '../api/stockApi'
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 type { DividendItem } from '@/shared/api/responses'
import type { DividendItem } from '@/shared/api'
import { getShareDividends } from '../api/stockApi'
export function useStockDividends(secid: string) {

View File

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

View File

@ -1,5 +1,5 @@
import { Link } from '@tanstack/react-router'
import type { ScreenerResult } from '@/shared/api/responses'
import type { ScreenerResult } from '@/shared/api'
interface Props {
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 type {
AnalyticsResponse,
ApiEnvelope,
ApiResponseMeta,
AuthResponse,
BondHistoryItem,
BondMarketData,
BondResponse,
BrokerAccount,
BrokerEventItem,
BrokerEventsData,
BrokerEventsSummary,
BrokerMoney,
BrokerOperation,
BrokerOperationCategory,
BrokerOperationsPage,
BrokerPortfolio,
BrokerPosition,
BrokerPositionsPage,
CandleItem,
DividendItem,
HealthResponse,
Portfolio,
PortfolioDetail,
PortfolioSummary,
Position,
PositionWithPrice,
ScreenerItem,
ScreenerResult,
SearchResultItem,
ShareHistoryItem,
ShareResponse,
StockMarketData,
UserResponse,
} from './responses'
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
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 {
return new Intl.NumberFormat('ru-RU', {

View File

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

View File

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

View File

@ -1,10 +1,5 @@
import { HttpResponse, http } from 'msw'
import type {
BondResponse,
CandleItem,
SearchResultItem,
ShareResponse,
} from '@/shared/api/responses'
import type { BondResponse, CandleItem, SearchResultItem, ShareResponse } from '@/shared/api'
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 {
bond: BondResponse

View File

@ -1,6 +1,6 @@
import { Link } from '@tanstack/react-router'
import { useState } from 'react'
import type { PositionWithPrice } from '@/shared/api/responses'
import type { PositionWithPrice } from '@/shared/api'
interface Props {
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'
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 { Link } from '@tanstack/react-router'
import { buildBrokerAllocation } from '@/entities/broker-position'
import type { BrokerAccount, BrokerPortfolio } from '@/shared/api/responses'
import type { BrokerAccount, BrokerPortfolio } from '@/shared/api'
import {
formatBrokerDate,
formatBrokerMoney,

View File

@ -1,7 +1,7 @@
import { Text } from '@moex-vibe/design-system'
import { Box } from '@mui/material'
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'
const RADIUS = 44

View File

@ -8,7 +8,7 @@ import {
getBrokerOperationTypeLabel,
} from '@/entities/broker-operation'
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 { TableSkeleton } from '@/shared/ui/TableSkeleton'

View File

@ -1,7 +1,7 @@
import { Text } from '@moex-vibe/design-system'
import { Box } from '@mui/material'
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'
function allocationPercent(value: BrokerMoney | null, total: BrokerMoney | null) {

View File

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

View File

@ -1,6 +1,6 @@
import { Button, Heading, Skeleton, Text } from '@moex-vibe/design-system'
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 { PositionTicker } from './PositionTicker'

View File

@ -1,7 +1,7 @@
import { Box } from '@mui/material'
import { Link } from '@tanstack/react-router'
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 }) {
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 {
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 }) {
const formatRub = (val: number | null) =>

View File

@ -1,5 +1,5 @@
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'
export function PortfolioCard({ portfolio }: { portfolio: Portfolio }) {

View File

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

View File

@ -1,4 +1,4 @@
import type { PositionWithPrice } from '@/shared/api/responses'
import type { PositionWithPrice } from '@/shared/api'
interface AllocationChartProps {
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'
export function PortfolioSummary({ portfolio }: { portfolio: PortfolioDetail }) {

View File

@ -1,6 +1,6 @@
import { Link } from '@tanstack/react-router'
import { useState } from 'react'
import type { PositionWithPrice } from '@/shared/api/responses'
import type { PositionWithPrice } from '@/shared/api'
interface Props {
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'
interface Props {

View File

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

View File

@ -47,10 +47,14 @@
### 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`
3. Перенести normalizeEnvelope (ky-версию) в `shared/api/kyClient.ts`
4. Прогнать `npm run build`
3. Переключить все импорты с `@/shared/api/responses` на `@/shared/api`
4. Перенести normalizeEnvelope (ky-версию) в `shared/api/kyClient.ts`
5. Прогнать `npm run build`
### Phase 4 — MSW Browser
*Низкий риск, handlers готовы*

View File

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