feat: expose tbank broker accounts
This commit is contained in:
parent
4ef2f8d05e
commit
ddeab01b9e
@ -10,6 +10,7 @@ import { CandlesModule } from './modules/candles/candles.module';
|
|||||||
import { PortfolioModule } from './modules/portfolio/portfolio.module';
|
import { PortfolioModule } from './modules/portfolio/portfolio.module';
|
||||||
import { PrismaModule } from './modules/prisma/prisma.module';
|
import { PrismaModule } from './modules/prisma/prisma.module';
|
||||||
import { AuthModule } from './modules/auth/auth.module';
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
|
import { TBankModule } from './modules/tbank/tbank.module';
|
||||||
import configuration from './config/configuration';
|
import configuration from './config/configuration';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -25,6 +26,7 @@ import configuration from './config/configuration';
|
|||||||
BondsModule,
|
BondsModule,
|
||||||
CandlesModule,
|
CandlesModule,
|
||||||
PortfolioModule,
|
PortfolioModule,
|
||||||
|
TBankModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
export class BrokerAccountResponseDto {
|
||||||
|
@ApiProperty()
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ enum: ['brokerage', 'iis'] })
|
||||||
|
type!: 'brokerage' | 'iis';
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
name!: string;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
status!: string;
|
||||||
|
|
||||||
|
@ApiProperty({ nullable: true })
|
||||||
|
openedAt!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty({ nullable: true })
|
||||||
|
accessLevel!: string | null;
|
||||||
|
}
|
||||||
18
apps/backend/src/modules/tbank/dto/broker-envelope.dto.ts
Normal file
18
apps/backend/src/modules/tbank/dto/broker-envelope.dto.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { BrokerAccountResponseDto } from './broker-account-response.dto';
|
||||||
|
|
||||||
|
export class BrokerResponseMetaDto {
|
||||||
|
@ApiProperty({ nullable: true })
|
||||||
|
cachedAt!: string | null;
|
||||||
|
|
||||||
|
@ApiProperty()
|
||||||
|
fromCache!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BrokerAccountsEnvelopeDto {
|
||||||
|
@ApiProperty({ type: [BrokerAccountResponseDto] })
|
||||||
|
data!: BrokerAccountResponseDto[];
|
||||||
|
|
||||||
|
@ApiProperty({ type: BrokerResponseMetaDto })
|
||||||
|
meta!: BrokerResponseMetaDto;
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
import { BrokerAccountsService } from './broker-accounts.service';
|
||||||
|
import { TBankClientService } from './tbank-client.service';
|
||||||
|
import { CacheService } from '../../cache/cache.service';
|
||||||
|
|
||||||
|
describe('BrokerAccountsService', () => {
|
||||||
|
const client = {
|
||||||
|
getServiceClient: vi.fn(),
|
||||||
|
callUnary: vi.fn(),
|
||||||
|
} as unknown as TBankClientService;
|
||||||
|
const cache = {
|
||||||
|
getOrFetch: vi.fn(),
|
||||||
|
} as unknown as CacheService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns only open brokerage and IIS accounts from cache wrapper', async () => {
|
||||||
|
vi.mocked(cache.getOrFetch).mockImplementation(
|
||||||
|
async (_prefix: string, _parts: string[], fetchFn: () => Promise<unknown>) => ({
|
||||||
|
data: await fetchFn(),
|
||||||
|
fromCache: false,
|
||||||
|
cachedAt: '2026-06-16T02:30:00.000Z',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(client.getServiceClient).mockReturnValue({ getAccounts: vi.fn() } as any);
|
||||||
|
vi.mocked(client.callUnary).mockResolvedValue({
|
||||||
|
accounts: [
|
||||||
|
{ id: '1', type: 'ACCOUNT_TYPE_TINKOFF', name: 'Broker', status: 'ACCOUNT_STATUS_OPEN' },
|
||||||
|
{ id: '2', type: 'ACCOUNT_TYPE_TINKOFF_IIS', name: 'IIS', status: 'ACCOUNT_STATUS_OPEN' },
|
||||||
|
{ id: '3', type: 'ACCOUNT_TYPE_INVEST_BOX', name: 'Box', status: 'ACCOUNT_STATUS_OPEN' },
|
||||||
|
{ id: '4', type: 'ACCOUNT_TYPE_TINKOFF', name: 'Closed', status: 'ACCOUNT_STATUS_CLOSED' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = new BrokerAccountsService(client, cache);
|
||||||
|
const result = await service.findAll();
|
||||||
|
|
||||||
|
expect(result.data).toHaveLength(2);
|
||||||
|
expect(result.data.map((account) => account.type)).toEqual(['brokerage', 'iis']);
|
||||||
|
expect(result.meta.fromCache).toBe(false);
|
||||||
|
expect(cache.getOrFetch).toHaveBeenCalledWith(
|
||||||
|
'tbank:accounts',
|
||||||
|
['open-brokerage-iis'],
|
||||||
|
expect.any(Function),
|
||||||
|
'tbankAccountsTtl',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { CacheService } from '../../cache/cache.service';
|
||||||
|
import { isSupportedBrokerAccount, mapAccount } from '../mappers/account.mapper';
|
||||||
|
import { TBANK_CACHE_KEYS } from '../tbank.config';
|
||||||
|
import type { BrokerAccount } from '../types/broker.types';
|
||||||
|
import type { TBankAccountsResponse } from '../types/tbank-proto.types';
|
||||||
|
import { TBankClientService } from './tbank-client.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BrokerAccountsService {
|
||||||
|
constructor(
|
||||||
|
private readonly tbankClient: TBankClientService,
|
||||||
|
private readonly cacheService: CacheService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findAll(): Promise<{
|
||||||
|
data: BrokerAccount[];
|
||||||
|
meta: { fromCache: boolean; cachedAt: string | null };
|
||||||
|
}> {
|
||||||
|
const result = await this.cacheService.getOrFetch(
|
||||||
|
TBANK_CACHE_KEYS.accounts,
|
||||||
|
['open-brokerage-iis'],
|
||||||
|
() => this.fetchAccounts(),
|
||||||
|
'tbankAccountsTtl',
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: result.data,
|
||||||
|
meta: { fromCache: result.fromCache, cachedAt: result.cachedAt },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(accountId: string): Promise<BrokerAccount | null> {
|
||||||
|
const accounts = await this.findAll();
|
||||||
|
|
||||||
|
return accounts.data.find((account) => account.id === accountId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchAccounts(): Promise<BrokerAccount[]> {
|
||||||
|
const usersClient = this.tbankClient.getServiceClient('UsersService') as any;
|
||||||
|
const response = await this.tbankClient.callUnary<
|
||||||
|
Record<string, string>,
|
||||||
|
TBankAccountsResponse
|
||||||
|
>('UsersService/GetAccounts', usersClient.getAccounts.bind(usersClient), {
|
||||||
|
status: 'ACCOUNT_STATUS_OPEN',
|
||||||
|
});
|
||||||
|
|
||||||
|
return (response.accounts ?? []).filter(isSupportedBrokerAccount).map(mapAccount);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,15 @@
|
|||||||
import { ServiceUnavailableException } from '@nestjs/common';
|
import { ServiceUnavailableException } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { Metadata, status } from '@grpc/grpc-js';
|
import { ClientUnaryCall, Metadata, ServiceError, status } from '@grpc/grpc-js';
|
||||||
import { TBankClientService } from './tbank-client.service';
|
import { TBankClientService } from './tbank-client.service';
|
||||||
|
|
||||||
describe('TBankClientService', () => {
|
describe('TBankClientService', () => {
|
||||||
|
const unaryCall = {
|
||||||
|
cancel: vi.fn(),
|
||||||
|
getPeer: vi.fn(),
|
||||||
|
getAuthContext: vi.fn(),
|
||||||
|
} as unknown as ClientUnaryCall;
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
get: vi.fn((key: string, fallback?: unknown) => {
|
get: vi.fn((key: string, fallback?: unknown) => {
|
||||||
const values: Record<string, unknown> = {
|
const values: Record<string, unknown> = {
|
||||||
@ -42,6 +48,7 @@ describe('TBankClientService', () => {
|
|||||||
'UsersService/GetAccounts',
|
'UsersService/GetAccounts',
|
||||||
(_request, _metadata, _options, callback) => {
|
(_request, _metadata, _options, callback) => {
|
||||||
callback(null, {});
|
callback(null, {});
|
||||||
|
return unaryCall;
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
@ -52,8 +59,9 @@ describe('TBankClientService', () => {
|
|||||||
const service = new TBankClientService(config);
|
const service = new TBankClientService(config);
|
||||||
const error = Object.assign(new Error('Too many requests'), {
|
const error = Object.assign(new Error('Too many requests'), {
|
||||||
code: status.RESOURCE_EXHAUSTED,
|
code: status.RESOURCE_EXHAUSTED,
|
||||||
|
details: 'Too many requests',
|
||||||
metadata: new Metadata(),
|
metadata: new Metadata(),
|
||||||
});
|
}) as ServiceError;
|
||||||
error.metadata.set('x-tracking-id', 'tracking-1');
|
error.metadata.set('x-tracking-id', 'tracking-1');
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
@ -61,6 +69,7 @@ describe('TBankClientService', () => {
|
|||||||
'OperationsService/GetPortfolio',
|
'OperationsService/GetPortfolio',
|
||||||
(_request, _metadata, _options, callback) => {
|
(_request, _metadata, _options, callback) => {
|
||||||
callback(error, null);
|
callback(error, null);
|
||||||
|
return unaryCall;
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
|
|||||||
18
apps/backend/src/modules/tbank/tbank.controller.ts
Normal file
18
apps/backend/src/modules/tbank/tbank.controller.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { BrokerAccountsEnvelopeDto } from './dto/broker-envelope.dto';
|
||||||
|
import { BrokerAccountsService } from './services/broker-accounts.service';
|
||||||
|
|
||||||
|
@ApiTags('Broker')
|
||||||
|
@ApiBearerAuth()
|
||||||
|
@Controller('broker')
|
||||||
|
export class TBankController {
|
||||||
|
constructor(private readonly brokerAccountsService: BrokerAccountsService) {}
|
||||||
|
|
||||||
|
@Get('accounts')
|
||||||
|
@ApiOperation({ summary: 'Get open T-Bank brokerage and IIS accounts' })
|
||||||
|
@ApiOkResponse({ type: BrokerAccountsEnvelopeDto })
|
||||||
|
async getAccounts() {
|
||||||
|
return this.brokerAccountsService.findAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TBankController } from './tbank.controller';
|
||||||
|
import { BrokerAccountsService } from './services/broker-accounts.service';
|
||||||
import { TBankClientService } from './services/tbank-client.service';
|
import { TBankClientService } from './services/tbank-client.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [TBankClientService],
|
controllers: [TBankController],
|
||||||
exports: [TBankClientService],
|
providers: [TBankClientService, BrokerAccountsService],
|
||||||
|
exports: [TBankClientService, BrokerAccountsService],
|
||||||
})
|
})
|
||||||
export class TBankModule {}
|
export class TBankModule {}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user