From 608e5216746b207c493eaea3b0e5aa3393659371 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Wed, 17 Jun 2026 14:43:50 +0300 Subject: [PATCH] feat(frontend): add BrokerPositionsPage types, API, and hook --- apps/frontend/src/api/broker.test.ts | 21 ++++++++++++++++++- apps/frontend/src/api/broker.ts | 14 +++++++++++++ apps/frontend/src/api/responses.ts | 10 ++++++++- apps/frontend/src/hooks/useBrokerPositions.ts | 18 ++++++++++++++++ 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 apps/frontend/src/hooks/useBrokerPositions.ts diff --git a/apps/frontend/src/api/broker.test.ts b/apps/frontend/src/api/broker.test.ts index 68eca40..c0b9119 100644 --- a/apps/frontend/src/api/broker.test.ts +++ b/apps/frontend/src/api/broker.test.ts @@ -1,5 +1,5 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { getBrokerOperations } from './broker'; +import { getBrokerOperations, getBrokerPositions } from './broker'; describe('broker api', () => { afterEach(() => { @@ -24,4 +24,23 @@ describe('broker api', () => { expect.any(Object), ); }); + + it('serializes positions 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 getBrokerPositions('acc-1', { cursor: 'pos-1', limit: 5 }); + + expect(fetch).toHaveBeenCalledWith( + expect.stringContaining('/api/v1/broker/accounts/acc-1/positions?cursor=pos-1&limit=5'), + expect.any(Object), + ); + }); }); diff --git a/apps/frontend/src/api/broker.ts b/apps/frontend/src/api/broker.ts index 4fe91f3..419c633 100644 --- a/apps/frontend/src/api/broker.ts +++ b/apps/frontend/src/api/broker.ts @@ -4,6 +4,7 @@ import type { BrokerAccount, BrokerOperationsPage, BrokerPortfolio, + BrokerPositionsPage, } from './responses'; export type BrokerOperationQuery = { @@ -49,3 +50,16 @@ export function getBrokerOperations( }, ); } + +export function getBrokerPositions( + accountId: string, + query: { cursor?: string; limit?: number } = {}, +): Promise<{ data: BrokerPositionsPage; meta: ApiResponseMeta }> { + return request( + `/api/v1/broker/accounts/${encodeURIComponent(accountId)}/positions`, + { + cursor: query.cursor, + limit: query.limit ? String(query.limit) : undefined, + }, + ); +} diff --git a/apps/frontend/src/api/responses.ts b/apps/frontend/src/api/responses.ts index 3227eec..d69e9f4 100644 --- a/apps/frontend/src/api/responses.ts +++ b/apps/frontend/src/api/responses.ts @@ -299,7 +299,6 @@ export interface BrokerPortfolio { }; cash: BrokerMoney[]; blockedCash: BrokerMoney[]; - positions: BrokerPosition[]; asOf: string; } @@ -314,6 +313,7 @@ export interface BrokerOperation { type: string; category: BrokerOperationCategory; description: string | null; + name: string | null; state: string | null; instrumentUid: string | null; figi: string | null; @@ -336,3 +336,11 @@ export interface BrokerOperationsPage { hasNext: boolean; asOf: string; } + +export interface BrokerPositionsPage { + accountId: string; + items: BrokerPosition[]; + nextCursor: string | null; + hasNext: boolean; + asOf: string; +} diff --git a/apps/frontend/src/hooks/useBrokerPositions.ts b/apps/frontend/src/hooks/useBrokerPositions.ts new file mode 100644 index 0000000..fa56ad4 --- /dev/null +++ b/apps/frontend/src/hooks/useBrokerPositions.ts @@ -0,0 +1,18 @@ +import { keepPreviousData, useQuery } from '@tanstack/react-query'; +import { getBrokerPositions } from '../api/broker'; +import type { BrokerPositionsPage } from '../api/responses'; + +export function useBrokerPositions( + accountId: string | undefined, + query: { cursor?: string; limit?: number } = {}, +) { + return useQuery({ + queryKey: ['broker', 'positions', accountId, query], + enabled: Boolean(accountId), + queryFn: async () => (await getBrokerPositions(accountId!, query)).data, + staleTime: 60_000, + retry: 2, + placeholderData: keepPreviousData, + refetchOnWindowFocus: false, + }); +}