From fe938b27067469fcad09b9b6399189dd486c2bd6 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Thu, 25 Jun 2026 21:55:23 +0300 Subject: [PATCH] refactor: localize T-Bank gRPC as any casts behind typed facade methods --- .../services/broker-accounts.service.spec.ts | 4 +- .../tbank/services/broker-accounts.service.ts | 4 +- .../services/broker-instruments.service.ts | 2 +- .../broker-operations.service.spec.ts | 4 +- .../services/broker-operations.service.ts | 2 +- .../services/broker-portfolio.service.spec.ts | 16 ++++---- .../services/broker-portfolio.service.ts | 4 +- .../services/tbank-client.service.spec.ts | 10 +++++ .../tbank/services/tbank-client.service.ts | 38 +++++++++++++++++++ 9 files changed, 66 insertions(+), 18 deletions(-) diff --git a/apps/backend/src/modules/tbank/services/broker-accounts.service.spec.ts b/apps/backend/src/modules/tbank/services/broker-accounts.service.spec.ts index dc63349..ecfc8e2 100644 --- a/apps/backend/src/modules/tbank/services/broker-accounts.service.spec.ts +++ b/apps/backend/src/modules/tbank/services/broker-accounts.service.spec.ts @@ -4,7 +4,7 @@ import { CacheService } from '../../cache/cache.service'; describe('BrokerAccountsService', () => { const client = { - getServiceClient: vi.fn(), + getUsersClient: vi.fn(), callUnary: vi.fn(), } as unknown as TBankClientService; const cache = { @@ -23,7 +23,7 @@ describe('BrokerAccountsService', () => { cachedAt: '2026-06-16T02:30:00.000Z', }), ); - vi.mocked(client.getServiceClient).mockReturnValue({ getAccounts: vi.fn() } as any); + vi.mocked(client.getUsersClient).mockReturnValue({ getAccounts: vi.fn() } as any); vi.mocked(client.callUnary).mockResolvedValue({ accounts: [ { id: '1', type: 'ACCOUNT_TYPE_TINKOFF', name: 'Broker', status: 'ACCOUNT_STATUS_OPEN' }, diff --git a/apps/backend/src/modules/tbank/services/broker-accounts.service.ts b/apps/backend/src/modules/tbank/services/broker-accounts.service.ts index 4c6191d..8c2c918 100644 --- a/apps/backend/src/modules/tbank/services/broker-accounts.service.ts +++ b/apps/backend/src/modules/tbank/services/broker-accounts.service.ts @@ -32,9 +32,9 @@ export class BrokerAccountsService { } private async fetchAccounts(): Promise { - const usersClient = this.tbankClient.getServiceClient('UsersService') as any; + const usersClient = this.tbankClient.getUsersClient(); const response = await this.tbankClient.callUnary< - Record, + { status: string }, TBankAccountsResponse >( 'UsersService/GetAccounts', diff --git a/apps/backend/src/modules/tbank/services/broker-instruments.service.ts b/apps/backend/src/modules/tbank/services/broker-instruments.service.ts index be65b9f..2679575 100644 --- a/apps/backend/src/modules/tbank/services/broker-instruments.service.ts +++ b/apps/backend/src/modules/tbank/services/broker-instruments.service.ts @@ -23,7 +23,7 @@ export class BrokerInstrumentsService { } private async fetchByUid(instrumentUid: string): Promise { - const instrumentsClient = this.tbankClient.getServiceClient('InstrumentsService') as any; + const instrumentsClient = this.tbankClient.getInstrumentsClient(); const response = await this.tbankClient.callUnary< { idType: string; id: string }, TBankInstrumentResponse diff --git a/apps/backend/src/modules/tbank/services/broker-operations.service.spec.ts b/apps/backend/src/modules/tbank/services/broker-operations.service.spec.ts index f36b405..f05d356 100644 --- a/apps/backend/src/modules/tbank/services/broker-operations.service.spec.ts +++ b/apps/backend/src/modules/tbank/services/broker-operations.service.spec.ts @@ -6,7 +6,7 @@ import { TBankClientService } from './tbank-client.service'; describe('BrokerOperationsService', () => { const accounts = { findById: vi.fn() } as unknown as BrokerAccountsService; - const client = { getServiceClient: vi.fn(), callUnary: vi.fn() } as unknown as TBankClientService; + const client = { getOperationsClient: vi.fn(), callUnary: vi.fn() } as unknown as TBankClientService; const cache = { getOrFetch: vi.fn() } as unknown as CacheService; beforeEach(() => { @@ -36,7 +36,7 @@ describe('BrokerOperationsService', () => { cachedAt: null, }), ); - vi.mocked(client.getServiceClient).mockReturnValue({ getOperationsByCursor: vi.fn() } as any); + vi.mocked(client.getOperationsClient).mockReturnValue({ getOperationsByCursor: vi.fn() } as any); vi.mocked(client.callUnary).mockResolvedValue({ hasNext: false, items: [{ cursor: 'c1', brokerAccountId: 'acc-1', type: 'OPERATION_TYPE_BUY' }], diff --git a/apps/backend/src/modules/tbank/services/broker-operations.service.ts b/apps/backend/src/modules/tbank/services/broker-operations.service.ts index f5ea5f1..d182c36 100644 --- a/apps/backend/src/modules/tbank/services/broker-operations.service.ts +++ b/apps/backend/src/modules/tbank/services/broker-operations.service.ts @@ -69,7 +69,7 @@ export class BrokerOperationsService { accountId: string, request: Record, ): Promise { - const operationsClient = this.tbankClient.getServiceClient('OperationsService') as any; + const operationsClient = this.tbankClient.getOperationsClient(); const response = await this.tbankClient.callUnary< Record, TBankOperationsByCursorResponse diff --git a/apps/backend/src/modules/tbank/services/broker-portfolio.service.spec.ts b/apps/backend/src/modules/tbank/services/broker-portfolio.service.spec.ts index e9b52f8..ec8dd55 100644 --- a/apps/backend/src/modules/tbank/services/broker-portfolio.service.spec.ts +++ b/apps/backend/src/modules/tbank/services/broker-portfolio.service.spec.ts @@ -8,7 +8,7 @@ import { TBankClientService } from './tbank-client.service'; describe('BrokerPortfolioService', () => { const accounts = { findById: vi.fn() } as unknown as BrokerAccountsService; const instruments = { findByInstrumentUid: vi.fn() } as unknown as BrokerInstrumentsService; - const client = { getServiceClient: vi.fn(), callUnary: vi.fn() } as unknown as TBankClientService; + const client = { getOperationsClient: vi.fn(), callUnary: vi.fn() } as unknown as TBankClientService; const cache = { getOrFetch: vi.fn() } as unknown as CacheService; beforeEach(() => { @@ -38,7 +38,7 @@ describe('BrokerPortfolioService', () => { cachedAt: null, }), ); - vi.mocked(client.getServiceClient).mockReturnValue({ + vi.mocked(client.getOperationsClient).mockReturnValue({ getPortfolio: vi.fn(), getPositions: vi.fn(), } as any); @@ -104,7 +104,7 @@ describe('BrokerPortfolioService', () => { it('returns first page of positions', async () => { mockAccount(); mockCache(); - vi.mocked(client.getServiceClient).mockReturnValue({ + vi.mocked(client.getOperationsClient).mockReturnValue({ getPortfolio: vi.fn(), } as any); vi.mocked(client.callUnary).mockResolvedValueOnce({ @@ -139,7 +139,7 @@ describe('BrokerPortfolioService', () => { it('paginates using cursor', async () => { mockAccount(); mockCache(); - vi.mocked(client.getServiceClient).mockReturnValue({ + vi.mocked(client.getOperationsClient).mockReturnValue({ getPortfolio: vi.fn(), } as any); vi.mocked(client.callUnary).mockResolvedValueOnce({ @@ -179,7 +179,7 @@ describe('BrokerPortfolioService', () => { it('returns last page with hasNext=false', async () => { mockAccount(); mockCache(); - vi.mocked(client.getServiceClient).mockReturnValue({ + vi.mocked(client.getOperationsClient).mockReturnValue({ getPortfolio: vi.fn(), } as any); vi.mocked(client.callUnary).mockResolvedValueOnce({ @@ -206,7 +206,7 @@ describe('BrokerPortfolioService', () => { it('caches positions with cursor/limit/type in key and tbankPositionsTtl', async () => { mockAccount(); mockCache(); - vi.mocked(client.getServiceClient).mockReturnValue({ + vi.mocked(client.getOperationsClient).mockReturnValue({ getPortfolio: vi.fn(), } as any); vi.mocked(client.callUnary).mockResolvedValueOnce({ @@ -229,7 +229,7 @@ describe('BrokerPortfolioService', () => { it('filters by instrument type and caches with type in key', async () => { mockAccount(); mockCache(); - vi.mocked(client.getServiceClient).mockReturnValue({ + vi.mocked(client.getOperationsClient).mockReturnValue({ getPortfolio: vi.fn(), } as any); vi.mocked(client.callUnary).mockResolvedValueOnce({ @@ -279,7 +279,7 @@ describe('BrokerPortfolioService', () => { it('returns empty items when type filter matches nothing', async () => { mockAccount(); mockCache(); - vi.mocked(client.getServiceClient).mockReturnValue({ + vi.mocked(client.getOperationsClient).mockReturnValue({ getPortfolio: vi.fn(), } as any); vi.mocked(client.callUnary).mockResolvedValueOnce({ diff --git a/apps/backend/src/modules/tbank/services/broker-portfolio.service.ts b/apps/backend/src/modules/tbank/services/broker-portfolio.service.ts index 45244f9..89796f0 100644 --- a/apps/backend/src/modules/tbank/services/broker-portfolio.service.ts +++ b/apps/backend/src/modules/tbank/services/broker-portfolio.service.ts @@ -120,7 +120,7 @@ export class BrokerPortfolioService { 'tbank:raw-portfolio', [accountId], async () => { - const operationsClient = this.tbankClient.getServiceClient('OperationsService') as any; + const operationsClient = this.tbankClient.getOperationsClient(); return this.tbankClient.callUnary< { accountId: string; currency: string }, TBankPortfolioResponse @@ -139,7 +139,7 @@ export class BrokerPortfolioService { } private async fetchPositions(accountId: string): Promise { - const operationsClient = this.tbankClient.getServiceClient('OperationsService') as any; + const operationsClient = this.tbankClient.getOperationsClient(); return this.tbankClient.callUnary<{ accountId: string }, TBankPositionsResponse>( 'OperationsService/GetPositions', operationsClient.getPositions.bind(operationsClient), diff --git a/apps/backend/src/modules/tbank/services/tbank-client.service.spec.ts b/apps/backend/src/modules/tbank/services/tbank-client.service.spec.ts index 953b0cb..1f7474c 100644 --- a/apps/backend/src/modules/tbank/services/tbank-client.service.spec.ts +++ b/apps/backend/src/modules/tbank/services/tbank-client.service.spec.ts @@ -44,6 +44,16 @@ describe('TBankClientService', () => { expect(() => service.getServiceClient('UsersService')).not.toThrow(); }); + it('exposes typed service-client facades for broker services', () => { + const service = new TBankClientService(config); + + expect(service.getUsersClient()).toHaveProperty('getAccounts'); + expect(service.getOperationsClient()).toHaveProperty('getPortfolio'); + expect(service.getOperationsClient()).toHaveProperty('getPositions'); + expect(service.getOperationsClient()).toHaveProperty('getOperationsByCursor'); + expect(service.getInstrumentsClient()).toHaveProperty('getInstrumentBy'); + }); + it('creates grpc SSL credentials with configured custom CA certificate', () => { const caPath = join(mkdtempSync(join(tmpdir(), 'tbank-ca-')), 'root.pem'); writeFileSync(caPath, '-----BEGIN CERTIFICATE-----\ntest-ca\n-----END CERTIFICATE-----\n'); diff --git a/apps/backend/src/modules/tbank/services/tbank-client.service.ts b/apps/backend/src/modules/tbank/services/tbank-client.service.ts index dea60f3..abc7b39 100644 --- a/apps/backend/src/modules/tbank/services/tbank-client.service.ts +++ b/apps/backend/src/modules/tbank/services/tbank-client.service.ts @@ -1,6 +1,13 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { TBankNotConfiguredException, TBankApiException } from '../../../common/exceptions/tbank-api.exception'; +import type { + TBankAccountsResponse, + TBankInstrumentResponse, + TBankOperationsByCursorResponse, + TBankPortfolioResponse, + TBankPositionsResponse, +} from '../types/tbank-proto.types'; import { CallOptions, ChannelCredentials, @@ -26,6 +33,25 @@ type GrpcUnary = ( type GrpcServiceConstructor = new (address: string, credentials: ChannelCredentials) => Client; +type TBankAccountsRequest = { status: string }; +type TBankPortfolioRequest = { accountId: string; currency: string }; +type TBankPositionsRequest = { accountId: string }; +type TBankInstrumentRequest = { idType: string; id: string }; + +export type TBankUsersClient = Client & { + getAccounts: GrpcUnary; +}; + +export type TBankOperationsClient = Client & { + getPortfolio: GrpcUnary; + getPositions: GrpcUnary; + getOperationsByCursor: GrpcUnary, TBankOperationsByCursorResponse>; +}; + +export type TBankInstrumentsClient = Client & { + getInstrumentBy: GrpcUnary; +}; + type QueueName = 'operations' | 'instruments' | 'users'; @Injectable() @@ -120,6 +146,18 @@ export class TBankClientService { return client; } + getUsersClient(): TBankUsersClient { + return this.getServiceClient('UsersService') as TBankUsersClient; + } + + getOperationsClient(): TBankOperationsClient { + return this.getServiceClient('OperationsService') as TBankOperationsClient; + } + + getInstrumentsClient(): TBankInstrumentsClient { + return this.getServiceClient('InstrumentsService') as TBankInstrumentsClient; + } + async callUnary( label: string, method: GrpcUnary,