feat: add broker frontend api hooks
This commit is contained in:
parent
b848d256d1
commit
50bff3dbe7
27
apps/frontend/src/api/broker.test.ts
Normal file
27
apps/frontend/src/api/broker.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { getBrokerOperations } from './broker';
|
||||
|
||||
describe('broker api', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('serializes operations query parameters', async () => {
|
||||
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: {
|
||||
data: { accountId: 'acc-1', items: [], nextCursor: null, hasNext: false, asOf: 'now' },
|
||||
meta: { fromCache: false, cachedAt: null },
|
||||
},
|
||||
}),
|
||||
} as Response);
|
||||
|
||||
await getBrokerOperations('acc-1', { cursor: 'c1', limit: 50 });
|
||||
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
expect.stringContaining('/api/v1/broker/accounts/acc-1/operations?cursor=c1&limit=50'),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
});
|
||||
51
apps/frontend/src/api/broker.ts
Normal file
51
apps/frontend/src/api/broker.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { request } from './client';
|
||||
import type {
|
||||
ApiResponseMeta,
|
||||
BrokerAccount,
|
||||
BrokerOperationsPage,
|
||||
BrokerPortfolio,
|
||||
} from './responses';
|
||||
|
||||
export type BrokerOperationQuery = {
|
||||
from?: string;
|
||||
to?: string;
|
||||
cursor?: string;
|
||||
limit?: number;
|
||||
instrumentId?: string;
|
||||
operationTypes?: string;
|
||||
state?: string;
|
||||
};
|
||||
|
||||
export function getBrokerAccounts(): Promise<{
|
||||
data: BrokerAccount[];
|
||||
meta: ApiResponseMeta;
|
||||
}> {
|
||||
return request<BrokerAccount[]>('/api/v1/broker/accounts');
|
||||
}
|
||||
|
||||
export function getBrokerPortfolio(accountId: string): Promise<{
|
||||
data: BrokerPortfolio;
|
||||
meta: ApiResponseMeta;
|
||||
}> {
|
||||
return request<BrokerPortfolio>(
|
||||
`/api/v1/broker/accounts/${encodeURIComponent(accountId)}/portfolio`,
|
||||
);
|
||||
}
|
||||
|
||||
export function getBrokerOperations(
|
||||
accountId: string,
|
||||
query: BrokerOperationQuery = {},
|
||||
): Promise<{ data: BrokerOperationsPage; meta: ApiResponseMeta }> {
|
||||
return request<BrokerOperationsPage>(
|
||||
`/api/v1/broker/accounts/${encodeURIComponent(accountId)}/operations`,
|
||||
{
|
||||
from: query.from,
|
||||
to: query.to,
|
||||
cursor: query.cursor,
|
||||
limit: query.limit ? String(query.limit) : undefined,
|
||||
instrumentId: query.instrumentId,
|
||||
operationTypes: query.operationTypes,
|
||||
state: query.state,
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -245,3 +245,94 @@ export interface ScreenerResult {
|
||||
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;
|
||||
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[];
|
||||
positions: BrokerPosition[];
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
@ -383,6 +383,57 @@ export interface paths {
|
||||
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}/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;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
@ -689,6 +740,109 @@ export interface components {
|
||||
data: components['schemas']['AnalyticsResponseDto'];
|
||||
meta: components['schemas']['PortfolioResponseMetaDto'];
|
||||
};
|
||||
BrokerAccountResponseDto: {
|
||||
id: string;
|
||||
/** @enum {string} */
|
||||
type: 'brokerage' | 'iis';
|
||||
name: string;
|
||||
status: string;
|
||||
openedAt: Record<string, never> | null;
|
||||
accessLevel: Record<string, never> | null;
|
||||
};
|
||||
BrokerResponseMetaDto: {
|
||||
cachedAt: Record<string, never> | null;
|
||||
fromCache: boolean;
|
||||
};
|
||||
BrokerAccountsEnvelopeDto: {
|
||||
data: components['schemas']['BrokerAccountResponseDto'][];
|
||||
meta: components['schemas']['BrokerResponseMetaDto'];
|
||||
};
|
||||
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<string, never> | null;
|
||||
daily: components['schemas']['BrokerMoneyDto'] | null;
|
||||
dailyPercent: Record<string, never> | null;
|
||||
};
|
||||
BrokerPositionResponseDto: {
|
||||
figi: Record<string, never> | null;
|
||||
instrumentUid: Record<string, never> | null;
|
||||
positionUid: Record<string, never> | null;
|
||||
ticker: Record<string, never> | null;
|
||||
classCode: Record<string, never> | null;
|
||||
instrumentType: Record<string, never> | null;
|
||||
name: Record<string, never> | null;
|
||||
quantity: Record<string, never> | null;
|
||||
blockedLots: Record<string, never> | null;
|
||||
currentPrice: components['schemas']['BrokerMoneyDto'] | null;
|
||||
currentValue: components['schemas']['BrokerMoneyDto'] | null;
|
||||
averagePositionPrice: components['schemas']['BrokerMoneyDto'] | null;
|
||||
expectedYieldPercent: Record<string, never> | null;
|
||||
dailyYield: components['schemas']['BrokerMoneyDto'] | null;
|
||||
};
|
||||
BrokerPortfolioResponseDto: {
|
||||
account: components['schemas']['BrokerAccountResponseDto'];
|
||||
totals: components['schemas']['BrokerPortfolioTotalsDto'];
|
||||
yields: components['schemas']['BrokerPortfolioYieldsDto'];
|
||||
cash: components['schemas']['BrokerMoneyDto'][];
|
||||
blockedCash: components['schemas']['BrokerMoneyDto'][];
|
||||
positions: components['schemas']['BrokerPositionResponseDto'][];
|
||||
asOf: string;
|
||||
};
|
||||
BrokerPortfolioEnvelopeDto: {
|
||||
data: components['schemas']['BrokerPortfolioResponseDto'];
|
||||
meta: components['schemas']['BrokerResponseMetaDto'];
|
||||
};
|
||||
BrokerOperationResponseDto: {
|
||||
cursor: Record<string, never> | null;
|
||||
accountId: string;
|
||||
id: Record<string, never> | null;
|
||||
parentOperationId: Record<string, never> | null;
|
||||
date: Record<string, never> | null;
|
||||
type: string;
|
||||
/** @enum {string} */
|
||||
category: 'trade' | 'income' | 'tax' | 'fee' | 'transfer' | 'other';
|
||||
description: Record<string, never> | null;
|
||||
state: Record<string, never> | null;
|
||||
instrumentUid: Record<string, never> | null;
|
||||
figi: Record<string, never> | null;
|
||||
ticker: Record<string, never> | null;
|
||||
classCode: Record<string, never> | null;
|
||||
instrumentType: Record<string, never> | 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<string, never> | null;
|
||||
quantityDone: Record<string, never> | null;
|
||||
};
|
||||
BrokerOperationsPageResponseDto: {
|
||||
accountId: string;
|
||||
items: components['schemas']['BrokerOperationResponseDto'][];
|
||||
nextCursor: Record<string, never> | null;
|
||||
hasNext: boolean;
|
||||
asOf: string;
|
||||
};
|
||||
BrokerOperationsEnvelopeDto: {
|
||||
data: components['schemas']['BrokerOperationsPageResponseDto'];
|
||||
meta: components['schemas']['BrokerResponseMetaDto'];
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
@ -1300,4 +1454,73 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
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_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'];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
41
apps/frontend/src/hooks/useBrokerAccounts.test.tsx
Normal file
41
apps/frontend/src/hooks/useBrokerAccounts.test.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { getBrokerAccounts } from '../api/broker';
|
||||
import { useBrokerAccounts } from './useBrokerAccounts';
|
||||
|
||||
vi.mock('../api/broker', () => ({
|
||||
getBrokerAccounts: vi.fn(),
|
||||
}));
|
||||
|
||||
function createWrapper() {
|
||||
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
|
||||
return function Wrapper({ children }: { children: ReactNode }) {
|
||||
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
|
||||
};
|
||||
}
|
||||
|
||||
describe('useBrokerAccounts', () => {
|
||||
it('returns broker accounts from API', async () => {
|
||||
vi.mocked(getBrokerAccounts).mockResolvedValue({
|
||||
data: [
|
||||
{
|
||||
id: 'acc-1',
|
||||
type: 'brokerage',
|
||||
name: 'Broker',
|
||||
status: 'ACCOUNT_STATUS_OPEN',
|
||||
openedAt: null,
|
||||
accessLevel: null,
|
||||
},
|
||||
],
|
||||
meta: { fromCache: false, cachedAt: null },
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useBrokerAccounts(), { wrapper: createWrapper() });
|
||||
|
||||
await waitFor(() => expect(result.current.isSuccess).toBe(true));
|
||||
expect(result.current.data?.[0].name).toBe('Broker');
|
||||
});
|
||||
});
|
||||
13
apps/frontend/src/hooks/useBrokerAccounts.ts
Normal file
13
apps/frontend/src/hooks/useBrokerAccounts.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getBrokerAccounts } from '../api/broker';
|
||||
import type { BrokerAccount } from '../api/responses';
|
||||
|
||||
export function useBrokerAccounts() {
|
||||
return useQuery<BrokerAccount[]>({
|
||||
queryKey: ['broker', 'accounts'],
|
||||
queryFn: async () => (await getBrokerAccounts()).data,
|
||||
staleTime: 3_600_000,
|
||||
retry: 2,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
17
apps/frontend/src/hooks/useBrokerOperations.ts
Normal file
17
apps/frontend/src/hooks/useBrokerOperations.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getBrokerOperations, type BrokerOperationQuery } from '../api/broker';
|
||||
import type { BrokerOperationsPage } from '../api/responses';
|
||||
|
||||
export function useBrokerOperations(
|
||||
accountId: string | undefined,
|
||||
query: BrokerOperationQuery = {},
|
||||
) {
|
||||
return useQuery<BrokerOperationsPage>({
|
||||
queryKey: ['broker', 'operations', accountId, query],
|
||||
enabled: Boolean(accountId),
|
||||
queryFn: async () => (await getBrokerOperations(accountId!, query)).data,
|
||||
staleTime: 300_000,
|
||||
retry: 2,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
14
apps/frontend/src/hooks/useBrokerPortfolio.ts
Normal file
14
apps/frontend/src/hooks/useBrokerPortfolio.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getBrokerPortfolio } from '../api/broker';
|
||||
import type { BrokerPortfolio } from '../api/responses';
|
||||
|
||||
export function useBrokerPortfolio(accountId: string | undefined) {
|
||||
return useQuery<BrokerPortfolio>({
|
||||
queryKey: ['broker', 'portfolio', accountId],
|
||||
enabled: Boolean(accountId),
|
||||
queryFn: async () => (await getBrokerPortfolio(accountId!)).data,
|
||||
staleTime: 60_000,
|
||||
retry: 2,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user