codex/backend-architecture-refactor #48

Merged
ksv741 merged 9 commits from codex/backend-architecture-refactor into main 2026-06-25 22:14:39 +03:00
9 changed files with 66 additions and 18 deletions
Showing only changes of commit fe938b2706 - Show all commits

View File

@ -4,7 +4,7 @@ import { CacheService } from '../../cache/cache.service';
describe('BrokerAccountsService', () => { describe('BrokerAccountsService', () => {
const client = { const client = {
getServiceClient: vi.fn(), getUsersClient: vi.fn(),
callUnary: vi.fn(), callUnary: vi.fn(),
} as unknown as TBankClientService; } as unknown as TBankClientService;
const cache = { const cache = {
@ -23,7 +23,7 @@ describe('BrokerAccountsService', () => {
cachedAt: '2026-06-16T02:30:00.000Z', 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({ vi.mocked(client.callUnary).mockResolvedValue({
accounts: [ accounts: [
{ id: '1', type: 'ACCOUNT_TYPE_TINKOFF', name: 'Broker', status: 'ACCOUNT_STATUS_OPEN' }, { id: '1', type: 'ACCOUNT_TYPE_TINKOFF', name: 'Broker', status: 'ACCOUNT_STATUS_OPEN' },

View File

@ -32,9 +32,9 @@ export class BrokerAccountsService {
} }
private async fetchAccounts(): Promise<BrokerAccount[]> { private async fetchAccounts(): Promise<BrokerAccount[]> {
const usersClient = this.tbankClient.getServiceClient('UsersService') as any; const usersClient = this.tbankClient.getUsersClient();
const response = await this.tbankClient.callUnary< const response = await this.tbankClient.callUnary<
Record<string, string>, { status: string },
TBankAccountsResponse TBankAccountsResponse
>( >(
'UsersService/GetAccounts', 'UsersService/GetAccounts',

View File

@ -23,7 +23,7 @@ export class BrokerInstrumentsService {
} }
private async fetchByUid(instrumentUid: string): Promise<TBankInstrument | null> { private async fetchByUid(instrumentUid: string): Promise<TBankInstrument | null> {
const instrumentsClient = this.tbankClient.getServiceClient('InstrumentsService') as any; const instrumentsClient = this.tbankClient.getInstrumentsClient();
const response = await this.tbankClient.callUnary< const response = await this.tbankClient.callUnary<
{ idType: string; id: string }, { idType: string; id: string },
TBankInstrumentResponse TBankInstrumentResponse

View File

@ -6,7 +6,7 @@ import { TBankClientService } from './tbank-client.service';
describe('BrokerOperationsService', () => { describe('BrokerOperationsService', () => {
const accounts = { findById: vi.fn() } as unknown as BrokerAccountsService; 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; const cache = { getOrFetch: vi.fn() } as unknown as CacheService;
beforeEach(() => { beforeEach(() => {
@ -36,7 +36,7 @@ describe('BrokerOperationsService', () => {
cachedAt: null, 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({ vi.mocked(client.callUnary).mockResolvedValue({
hasNext: false, hasNext: false,
items: [{ cursor: 'c1', brokerAccountId: 'acc-1', type: 'OPERATION_TYPE_BUY' }], items: [{ cursor: 'c1', brokerAccountId: 'acc-1', type: 'OPERATION_TYPE_BUY' }],

View File

@ -69,7 +69,7 @@ export class BrokerOperationsService {
accountId: string, accountId: string,
request: Record<string, unknown>, request: Record<string, unknown>,
): Promise<BrokerOperationsPage> { ): Promise<BrokerOperationsPage> {
const operationsClient = this.tbankClient.getServiceClient('OperationsService') as any; const operationsClient = this.tbankClient.getOperationsClient();
const response = await this.tbankClient.callUnary< const response = await this.tbankClient.callUnary<
Record<string, unknown>, Record<string, unknown>,
TBankOperationsByCursorResponse TBankOperationsByCursorResponse

View File

@ -8,7 +8,7 @@ import { TBankClientService } from './tbank-client.service';
describe('BrokerPortfolioService', () => { describe('BrokerPortfolioService', () => {
const accounts = { findById: vi.fn() } as unknown as BrokerAccountsService; const accounts = { findById: vi.fn() } as unknown as BrokerAccountsService;
const instruments = { findByInstrumentUid: vi.fn() } as unknown as BrokerInstrumentsService; 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; const cache = { getOrFetch: vi.fn() } as unknown as CacheService;
beforeEach(() => { beforeEach(() => {
@ -38,7 +38,7 @@ describe('BrokerPortfolioService', () => {
cachedAt: null, cachedAt: null,
}), }),
); );
vi.mocked(client.getServiceClient).mockReturnValue({ vi.mocked(client.getOperationsClient).mockReturnValue({
getPortfolio: vi.fn(), getPortfolio: vi.fn(),
getPositions: vi.fn(), getPositions: vi.fn(),
} as any); } as any);
@ -104,7 +104,7 @@ describe('BrokerPortfolioService', () => {
it('returns first page of positions', async () => { it('returns first page of positions', async () => {
mockAccount(); mockAccount();
mockCache(); mockCache();
vi.mocked(client.getServiceClient).mockReturnValue({ vi.mocked(client.getOperationsClient).mockReturnValue({
getPortfolio: vi.fn(), getPortfolio: vi.fn(),
} as any); } as any);
vi.mocked(client.callUnary).mockResolvedValueOnce({ vi.mocked(client.callUnary).mockResolvedValueOnce({
@ -139,7 +139,7 @@ describe('BrokerPortfolioService', () => {
it('paginates using cursor', async () => { it('paginates using cursor', async () => {
mockAccount(); mockAccount();
mockCache(); mockCache();
vi.mocked(client.getServiceClient).mockReturnValue({ vi.mocked(client.getOperationsClient).mockReturnValue({
getPortfolio: vi.fn(), getPortfolio: vi.fn(),
} as any); } as any);
vi.mocked(client.callUnary).mockResolvedValueOnce({ vi.mocked(client.callUnary).mockResolvedValueOnce({
@ -179,7 +179,7 @@ describe('BrokerPortfolioService', () => {
it('returns last page with hasNext=false', async () => { it('returns last page with hasNext=false', async () => {
mockAccount(); mockAccount();
mockCache(); mockCache();
vi.mocked(client.getServiceClient).mockReturnValue({ vi.mocked(client.getOperationsClient).mockReturnValue({
getPortfolio: vi.fn(), getPortfolio: vi.fn(),
} as any); } as any);
vi.mocked(client.callUnary).mockResolvedValueOnce({ vi.mocked(client.callUnary).mockResolvedValueOnce({
@ -206,7 +206,7 @@ describe('BrokerPortfolioService', () => {
it('caches positions with cursor/limit/type in key and tbankPositionsTtl', async () => { it('caches positions with cursor/limit/type in key and tbankPositionsTtl', async () => {
mockAccount(); mockAccount();
mockCache(); mockCache();
vi.mocked(client.getServiceClient).mockReturnValue({ vi.mocked(client.getOperationsClient).mockReturnValue({
getPortfolio: vi.fn(), getPortfolio: vi.fn(),
} as any); } as any);
vi.mocked(client.callUnary).mockResolvedValueOnce({ vi.mocked(client.callUnary).mockResolvedValueOnce({
@ -229,7 +229,7 @@ describe('BrokerPortfolioService', () => {
it('filters by instrument type and caches with type in key', async () => { it('filters by instrument type and caches with type in key', async () => {
mockAccount(); mockAccount();
mockCache(); mockCache();
vi.mocked(client.getServiceClient).mockReturnValue({ vi.mocked(client.getOperationsClient).mockReturnValue({
getPortfolio: vi.fn(), getPortfolio: vi.fn(),
} as any); } as any);
vi.mocked(client.callUnary).mockResolvedValueOnce({ vi.mocked(client.callUnary).mockResolvedValueOnce({
@ -279,7 +279,7 @@ describe('BrokerPortfolioService', () => {
it('returns empty items when type filter matches nothing', async () => { it('returns empty items when type filter matches nothing', async () => {
mockAccount(); mockAccount();
mockCache(); mockCache();
vi.mocked(client.getServiceClient).mockReturnValue({ vi.mocked(client.getOperationsClient).mockReturnValue({
getPortfolio: vi.fn(), getPortfolio: vi.fn(),
} as any); } as any);
vi.mocked(client.callUnary).mockResolvedValueOnce({ vi.mocked(client.callUnary).mockResolvedValueOnce({

View File

@ -120,7 +120,7 @@ export class BrokerPortfolioService {
'tbank:raw-portfolio', 'tbank:raw-portfolio',
[accountId], [accountId],
async () => { async () => {
const operationsClient = this.tbankClient.getServiceClient('OperationsService') as any; const operationsClient = this.tbankClient.getOperationsClient();
return this.tbankClient.callUnary< return this.tbankClient.callUnary<
{ accountId: string; currency: string }, { accountId: string; currency: string },
TBankPortfolioResponse TBankPortfolioResponse
@ -139,7 +139,7 @@ export class BrokerPortfolioService {
} }
private async fetchPositions(accountId: string): Promise<TBankPositionsResponse> { private async fetchPositions(accountId: string): Promise<TBankPositionsResponse> {
const operationsClient = this.tbankClient.getServiceClient('OperationsService') as any; const operationsClient = this.tbankClient.getOperationsClient();
return this.tbankClient.callUnary<{ accountId: string }, TBankPositionsResponse>( return this.tbankClient.callUnary<{ accountId: string }, TBankPositionsResponse>(
'OperationsService/GetPositions', 'OperationsService/GetPositions',
operationsClient.getPositions.bind(operationsClient), operationsClient.getPositions.bind(operationsClient),

View File

@ -44,6 +44,16 @@ describe('TBankClientService', () => {
expect(() => service.getServiceClient('UsersService')).not.toThrow(); 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', () => { it('creates grpc SSL credentials with configured custom CA certificate', () => {
const caPath = join(mkdtempSync(join(tmpdir(), 'tbank-ca-')), 'root.pem'); const caPath = join(mkdtempSync(join(tmpdir(), 'tbank-ca-')), 'root.pem');
writeFileSync(caPath, '-----BEGIN CERTIFICATE-----\ntest-ca\n-----END CERTIFICATE-----\n'); writeFileSync(caPath, '-----BEGIN CERTIFICATE-----\ntest-ca\n-----END CERTIFICATE-----\n');

View File

@ -1,6 +1,13 @@
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { TBankNotConfiguredException, TBankApiException } from '../../../common/exceptions/tbank-api.exception'; import { TBankNotConfiguredException, TBankApiException } from '../../../common/exceptions/tbank-api.exception';
import type {
TBankAccountsResponse,
TBankInstrumentResponse,
TBankOperationsByCursorResponse,
TBankPortfolioResponse,
TBankPositionsResponse,
} from '../types/tbank-proto.types';
import { import {
CallOptions, CallOptions,
ChannelCredentials, ChannelCredentials,
@ -26,6 +33,25 @@ type GrpcUnary<TRequest, TResponse> = (
type GrpcServiceConstructor = new (address: string, credentials: ChannelCredentials) => Client; 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<TBankAccountsRequest, TBankAccountsResponse>;
};
export type TBankOperationsClient = Client & {
getPortfolio: GrpcUnary<TBankPortfolioRequest, TBankPortfolioResponse>;
getPositions: GrpcUnary<TBankPositionsRequest, TBankPositionsResponse>;
getOperationsByCursor: GrpcUnary<Record<string, unknown>, TBankOperationsByCursorResponse>;
};
export type TBankInstrumentsClient = Client & {
getInstrumentBy: GrpcUnary<TBankInstrumentRequest, TBankInstrumentResponse>;
};
type QueueName = 'operations' | 'instruments' | 'users'; type QueueName = 'operations' | 'instruments' | 'users';
@Injectable() @Injectable()
@ -120,6 +146,18 @@ export class TBankClientService {
return client; 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<TRequest, TResponse>( async callUnary<TRequest, TResponse>(
label: string, label: string,
method: GrpcUnary<TRequest, TResponse>, method: GrpcUnary<TRequest, TResponse>,