codex/backend-architecture-refactor #48
@ -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' },
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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' }],
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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({
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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>,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user