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', () => {
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' },

View File

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

View File

@ -23,7 +23,7 @@ export class BrokerInstrumentsService {
}
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<
{ idType: string; id: string },
TBankInstrumentResponse

View File

@ -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' }],

View File

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

View File

@ -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({

View File

@ -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<TBankPositionsResponse> {
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),

View File

@ -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');

View File

@ -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<TRequest, TResponse> = (
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';
@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<TRequest, TResponse>(
label: string,
method: GrpcUnary<TRequest, TResponse>,