feat(tbank): add totalFees/totalTaxesPaid, portfolio history endpoint, and category filtering
- Add totalFees and totalTaxesPaid to BrokerAnalyticsDto and service - Add GET /accounts/:accountId/portfolio/history endpoint with estimated v1 read-model - Add categories query param to operations endpoint for filtering by category - Update tests and module registration
This commit is contained in:
parent
c4b93f9f0c
commit
ff0e11b303
@ -19,6 +19,12 @@ export class BrokerAnalyticsDto {
|
||||
@ApiProperty()
|
||||
totalReceived!: number;
|
||||
|
||||
@ApiProperty()
|
||||
totalFees!: number;
|
||||
|
||||
@ApiProperty()
|
||||
totalTaxesPaid!: number;
|
||||
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
totalReturnPercent!: number | null;
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import { BrokerOperationsPageResponseDto } from './broker-operation-response.dto
|
||||
import { BrokerPositionsPageResponseDto } from './broker-positions-page-response.dto';
|
||||
import { BrokerPortfolioResponseDto } from './broker-portfolio-response.dto';
|
||||
import { BrokerAnalyticsDto } from './broker-analytics-response.dto';
|
||||
import { BrokerPortfolioHistoryDataDto } from './broker-portfolio-history-response.dto';
|
||||
|
||||
export class BrokerAccountsEnvelopeDto {
|
||||
@ApiProperty({ type: [BrokerAccountResponseDto] })
|
||||
@ -63,3 +64,11 @@ export class BrokerEventsEnvelopeDto {
|
||||
@ApiProperty({ type: ApiResponseMeta })
|
||||
meta!: ApiResponseMeta;
|
||||
}
|
||||
|
||||
export class BrokerPortfolioHistoryEnvelopeDto {
|
||||
@ApiProperty({ type: BrokerPortfolioHistoryDataDto })
|
||||
data!: BrokerPortfolioHistoryDataDto;
|
||||
|
||||
@ApiProperty({ type: ApiResponseMeta })
|
||||
meta!: ApiResponseMeta;
|
||||
}
|
||||
|
||||
@ -40,4 +40,9 @@ export class BrokerOperationQueryDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
state?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Comma-separated category filter: trade,income,tax,fee,transfer,other' })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
categories?: string;
|
||||
}
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { BrokerMoneyDto } from './broker-money.dto';
|
||||
|
||||
export class BrokerPortfolioHistoryPointDto {
|
||||
@ApiProperty()
|
||||
month!: string;
|
||||
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto })
|
||||
value!: BrokerMoneyDto;
|
||||
}
|
||||
|
||||
export class BrokerPortfolioHistoryDataDto {
|
||||
@ApiProperty()
|
||||
accountId!: string;
|
||||
|
||||
@ApiProperty({ type: [BrokerPortfolioHistoryPointDto] })
|
||||
points!: BrokerPortfolioHistoryPointDto[];
|
||||
|
||||
@ApiProperty()
|
||||
asOf!: string;
|
||||
}
|
||||
@ -59,6 +59,8 @@ describe('BrokerAnalyticsService', () => {
|
||||
totalCoupons: 0,
|
||||
totalReceived: 0,
|
||||
totalReturnPercent: null,
|
||||
totalFees: 0,
|
||||
totalTaxesPaid: 0,
|
||||
currency: 'RUB',
|
||||
});
|
||||
});
|
||||
@ -177,17 +179,62 @@ describe('BrokerAnalyticsService', () => {
|
||||
expect(prisma.brokerOperation.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
accountId: 'acc-1',
|
||||
type: { in: expect.any(Array) },
|
||||
payment: { not: null },
|
||||
OR: [
|
||||
{ state: 'OPERATION_STATE_EXECUTED' },
|
||||
{ state: null },
|
||||
],
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{ type: { in: expect.any(Array) } },
|
||||
{ category: { in: ['fee', 'tax'] } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
select: { type: true, payment: true },
|
||||
select: { type: true, payment: true, category: true },
|
||||
});
|
||||
});
|
||||
|
||||
it('aggregates fee and tax categories from executed operations', async () => {
|
||||
vi.mocked(accounts.findById).mockResolvedValue(acc1);
|
||||
mockCachePassthrough();
|
||||
vi.mocked(prisma.brokerOperation.findMany).mockResolvedValue([
|
||||
{ type: 'OPERATION_TYPE_SERVICE_FEE', payment: JSON.stringify({ value: -100, currency: 'RUB' }), state: 'OPERATION_STATE_EXECUTED', category: 'fee' },
|
||||
{ type: 'OPERATION_TYPE_BROKER_FEE', payment: JSON.stringify({ value: -50, currency: 'RUB' }), state: 'OPERATION_STATE_EXECUTED', category: 'fee' },
|
||||
{ type: 'OPERATION_TYPE_TAX', payment: JSON.stringify({ value: -200, currency: 'RUB' }), state: 'OPERATION_STATE_EXECUTED', category: 'tax' },
|
||||
{ type: 'OPERATION_TYPE_DIVIDEND_TAX', payment: JSON.stringify({ value: -30, currency: 'RUB' }), state: 'OPERATION_STATE_EXECUTED', category: 'tax' },
|
||||
makeOp('OPERATION_TYPE_INPUT', 1000),
|
||||
] as any);
|
||||
|
||||
const service = new BrokerAnalyticsService(prisma, accounts, cache);
|
||||
const result = await service.getAnalytics('acc-1');
|
||||
|
||||
expect(result.data.totalDeposits).toBe(1000);
|
||||
expect(result.data.totalFees).toBe(150);
|
||||
expect(result.data.totalTaxesPaid).toBe(230);
|
||||
expect(result.data.netInvested).toBe(1000);
|
||||
});
|
||||
|
||||
it('includes fee/tax category operations in the query alongside ANALYTICS_TYPES', async () => {
|
||||
vi.mocked(accounts.findById).mockResolvedValue(acc1);
|
||||
mockCachePassthrough();
|
||||
|
||||
const service = new BrokerAnalyticsService(prisma, accounts, cache);
|
||||
await service.getAnalytics('acc-1');
|
||||
|
||||
const whereArg = vi.mocked(prisma.brokerOperation.findMany).mock.calls[0][0]!.where as any;
|
||||
expect(whereArg.accountId).toBe('acc-1');
|
||||
expect(whereArg.payment).toEqual({ not: null });
|
||||
const orConditions = whereArg.AND[0].OR;
|
||||
expect(orConditions).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ category: { in: ['fee', 'tax'] } },
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('rounds all monetary values to 2 decimal places', async () => {
|
||||
vi.mocked(accounts.findById).mockResolvedValue(acc1);
|
||||
mockCachePassthrough();
|
||||
@ -216,6 +263,8 @@ describe('BrokerAnalyticsService', () => {
|
||||
totalCoupons: 0,
|
||||
totalReceived: 0,
|
||||
totalReturnPercent: null,
|
||||
totalFees: 0,
|
||||
totalTaxesPaid: 0,
|
||||
currency: 'RUB',
|
||||
},
|
||||
fromCache: true,
|
||||
|
||||
@ -61,20 +61,29 @@ export class BrokerAnalyticsService {
|
||||
const operations = await this.prisma.brokerOperation.findMany({
|
||||
where: {
|
||||
accountId,
|
||||
type: { in: Array.from(ANALYTICS_TYPES) },
|
||||
payment: { not: null },
|
||||
OR: [
|
||||
{ state: 'OPERATION_STATE_EXECUTED' },
|
||||
{ state: null },
|
||||
],
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{ type: { in: Array.from(ANALYTICS_TYPES) } },
|
||||
{ category: { in: ['fee', 'tax'] } },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
select: { type: true, payment: true },
|
||||
select: { type: true, payment: true, category: true },
|
||||
});
|
||||
|
||||
let totalDeposits = 0;
|
||||
let totalWithdrawn = 0;
|
||||
let totalDividends = 0;
|
||||
let totalCoupons = 0;
|
||||
let totalFees = 0;
|
||||
let totalTaxesPaid = 0;
|
||||
|
||||
for (const op of operations) {
|
||||
let value = 0;
|
||||
@ -85,7 +94,11 @@ export class BrokerAnalyticsService {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (DEPOSIT_TYPES.has(op.type)) {
|
||||
if (op.category === 'fee') {
|
||||
totalFees += Math.abs(value);
|
||||
} else if (op.category === 'tax') {
|
||||
totalTaxesPaid += Math.abs(value);
|
||||
} else if (DEPOSIT_TYPES.has(op.type)) {
|
||||
totalDeposits += value;
|
||||
} else if (WITHDRAWAL_TYPES.has(op.type)) {
|
||||
totalWithdrawn += Math.abs(value);
|
||||
@ -108,6 +121,8 @@ export class BrokerAnalyticsService {
|
||||
totalDividends: Math.round(totalDividends * 100) / 100,
|
||||
totalCoupons: Math.round(totalCoupons * 100) / 100,
|
||||
totalReceived: Math.round(totalReceived * 100) / 100,
|
||||
totalFees: Math.round(totalFees * 100) / 100,
|
||||
totalTaxesPaid: Math.round(totalTaxesPaid * 100) / 100,
|
||||
totalReturnPercent,
|
||||
currency: 'RUB',
|
||||
};
|
||||
|
||||
@ -5,7 +5,7 @@ import type { BrokerOperationQueryDto } from '../dto/broker-operation-query.dto'
|
||||
import { mapOperationsPage } from '../mappers/operation.mapper';
|
||||
import { TBANK_CACHE_KEYS } from '../tbank.config';
|
||||
import { EntityNotFoundException } from '../../../common/exceptions/entity-not-found.exception';
|
||||
import type { BrokerOperationsPage } from '../types/broker.types';
|
||||
import type { BrokerOperation, BrokerOperationsPage } from '../types/broker.types';
|
||||
import type { TBankOperationsByCursorResponse } from '../types/tbank-proto.types';
|
||||
import { BrokerAccountsService } from './broker-accounts.service';
|
||||
import { TBankClientService } from './tbank-client.service';
|
||||
@ -33,6 +33,19 @@ export class BrokerOperationsService {
|
||||
'tbankOperationsTtl',
|
||||
);
|
||||
|
||||
if (query.categories) {
|
||||
const allowedCategories = query.categories
|
||||
.split(',')
|
||||
.map((c) => c.trim() as BrokerOperation['category'])
|
||||
.filter((c) => ['trade', 'income', 'tax', 'fee', 'transfer', 'other'].includes(c));
|
||||
|
||||
if (allowedCategories.length > 0) {
|
||||
result.data.items = result.data.items.filter((item) =>
|
||||
allowedCategories.includes(item.category),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new ApiEnvelopePayload(result.data, result.fromCache, result.cachedAt);
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CacheService } from '../../cache/cache.service';
|
||||
import { TBANK_CACHE_KEYS } from '../tbank.config';
|
||||
import { EntityNotFoundException } from '../../../common/exceptions/entity-not-found.exception';
|
||||
import { ApiEnvelopePayload } from '../../../common/dto/api-response.dto';
|
||||
import { BrokerAccountsService } from './broker-accounts.service';
|
||||
import { BrokerPortfolioService } from './broker-portfolio.service';
|
||||
import type { BrokerPortfolioHistoryDataDto, BrokerPortfolioHistoryPointDto } from '../dto/broker-portfolio-history-response.dto';
|
||||
import type { BrokerMoney } from '../types/broker.types';
|
||||
|
||||
const RUSSIAN_MONTHS = [
|
||||
'Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн',
|
||||
'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек',
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class BrokerPortfolioHistoryService {
|
||||
constructor(
|
||||
private readonly accountsService: BrokerAccountsService,
|
||||
private readonly portfolioService: BrokerPortfolioService,
|
||||
private readonly cacheService: CacheService,
|
||||
) {}
|
||||
|
||||
async getHistory(
|
||||
accountId: string,
|
||||
months: number,
|
||||
): Promise<ApiEnvelopePayload<BrokerPortfolioHistoryDataDto>> {
|
||||
const account = await this.accountsService.findById(accountId);
|
||||
if (!account) throw new EntityNotFoundException('BrokerAccount', accountId);
|
||||
|
||||
const result = await this.cacheService.getOrFetch(
|
||||
TBANK_CACHE_KEYS.portfolio,
|
||||
[accountId, 'history', String(months)],
|
||||
() => this.computeHistory(accountId, months),
|
||||
'tbankPortfolioTtl',
|
||||
);
|
||||
|
||||
return new ApiEnvelopePayload(result.data, result.fromCache, result.cachedAt);
|
||||
}
|
||||
|
||||
private async computeHistory(
|
||||
accountId: string,
|
||||
months: number,
|
||||
): Promise<BrokerPortfolioHistoryDataDto> {
|
||||
const portfolioEnvelope = await this.portfolioService.getPortfolio(accountId);
|
||||
const currentValue = portfolioEnvelope.data.totals.portfolio;
|
||||
|
||||
const defaultMoney: BrokerMoney = currentValue ?? {
|
||||
currency: 'RUB',
|
||||
units: '0',
|
||||
nano: 0,
|
||||
value: 0,
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
const points: BrokerPortfolioHistoryPointDto[] = [];
|
||||
|
||||
for (let i = months - 1; i >= 0; i--) {
|
||||
const d = new Date(now.getFullYear(), now.getMonth() - i, 1);
|
||||
const month = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
|
||||
const label = RUSSIAN_MONTHS[d.getMonth()];
|
||||
|
||||
points.push({ month, label, value: { ...defaultMoney } });
|
||||
}
|
||||
|
||||
return {
|
||||
accountId,
|
||||
points,
|
||||
asOf: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import { BrokerAnalyticsService } from './services/broker-analytics.service';
|
||||
import { BrokerEventsService } from './services/broker-events.service';
|
||||
import { BrokerOperationSyncService } from './services/broker-operation-sync.service';
|
||||
import { BrokerOperationsService } from './services/broker-operations.service';
|
||||
import { BrokerPortfolioHistoryService } from './services/broker-portfolio-history.service';
|
||||
import { BrokerPortfolioService } from './services/broker-portfolio.service';
|
||||
|
||||
describe('TBankController', () => {
|
||||
@ -15,6 +16,7 @@ describe('TBankController', () => {
|
||||
const events = { getEvents: vi.fn() } as unknown as BrokerEventsService;
|
||||
const operations = { getOperations: vi.fn() } as unknown as BrokerOperationsService;
|
||||
const sync = { syncAccount: vi.fn() } as unknown as BrokerOperationSyncService;
|
||||
const portfolioHistory = { getHistory: vi.fn() } as unknown as BrokerPortfolioHistoryService;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@ -42,7 +44,7 @@ describe('TBankController', () => {
|
||||
),
|
||||
);
|
||||
|
||||
const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics);
|
||||
const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics, portfolioHistory);
|
||||
const response = await controller.getAccounts();
|
||||
|
||||
expect(response).toBeInstanceOf(ApiEnvelopePayload);
|
||||
@ -54,7 +56,7 @@ describe('TBankController', () => {
|
||||
it('exposes a sync trigger for durable operation history', async () => {
|
||||
vi.mocked(sync.syncAccount).mockResolvedValueOnce({ upserted: 2 });
|
||||
|
||||
const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics);
|
||||
const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics, portfolioHistory);
|
||||
const response = await controller.syncOperations('acc-1', {
|
||||
from: '2026-06-01T00:00:00.000Z',
|
||||
to: '2026-06-17T00:00:00.000Z',
|
||||
@ -76,13 +78,15 @@ describe('TBankController', () => {
|
||||
totalCoupons: 50,
|
||||
totalReceived: 200,
|
||||
totalReturnPercent: 25,
|
||||
totalFees: 0,
|
||||
totalTaxesPaid: 0,
|
||||
currency: 'RUB',
|
||||
};
|
||||
vi.mocked(analytics.getAnalytics).mockResolvedValueOnce(
|
||||
new ApiEnvelopePayload(analyticsData, false, null),
|
||||
);
|
||||
|
||||
const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics);
|
||||
const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics, portfolioHistory);
|
||||
const response = await controller.getAnalytics('acc-1');
|
||||
|
||||
expect(analytics.getAnalytics).toHaveBeenCalledWith('acc-1');
|
||||
@ -92,6 +96,67 @@ describe('TBankController', () => {
|
||||
expect(response.cachedAt).toBeNull();
|
||||
});
|
||||
|
||||
it('returns portfolio history with correct shape and points', async () => {
|
||||
const historyData = {
|
||||
accountId: 'acc-1',
|
||||
points: [
|
||||
{ month: '2026-01', label: 'Янв', value: { currency: 'RUB', units: '100000', nano: 0, value: 100000 } },
|
||||
{ month: '2026-02', label: 'Фев', value: { currency: 'RUB', units: '100000', nano: 0, value: 100000 } },
|
||||
],
|
||||
asOf: '2026-06-22T00:00:00.000Z',
|
||||
};
|
||||
vi.mocked(portfolioHistory.getHistory).mockResolvedValueOnce(
|
||||
new ApiEnvelopePayload(historyData, false, '2026-06-22T00:00:00.000Z'),
|
||||
);
|
||||
|
||||
const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics, portfolioHistory);
|
||||
const response = await controller.getPortfolioHistory('acc-1', 6);
|
||||
|
||||
expect(portfolioHistory.getHistory).toHaveBeenCalledWith('acc-1', 6);
|
||||
expect(response).toBeInstanceOf(ApiEnvelopePayload);
|
||||
expect(response.data).toEqual(historyData);
|
||||
expect(response.data.points).toHaveLength(2);
|
||||
expect(response.data.points[0].month).toBe('2026-01');
|
||||
expect(response.data.points[0].label).toBe('Янв');
|
||||
});
|
||||
|
||||
it('passes categories filter query to operations service', async () => {
|
||||
const pageData = {
|
||||
accountId: 'acc-1',
|
||||
items: [],
|
||||
nextCursor: null,
|
||||
hasNext: false,
|
||||
asOf: '2026-06-22T00:00:00.000Z',
|
||||
};
|
||||
vi.mocked(operations.getOperations).mockResolvedValueOnce(
|
||||
new ApiEnvelopePayload(pageData, false, null),
|
||||
);
|
||||
|
||||
const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics, portfolioHistory);
|
||||
const query = { from: '2026-06-01', to: '2026-06-30', categories: 'fee,tax' };
|
||||
const response = await controller.getOperations('acc-1', query);
|
||||
|
||||
expect(operations.getOperations).toHaveBeenCalledWith('acc-1', query);
|
||||
expect(response.data.accountId).toBe('acc-1');
|
||||
});
|
||||
|
||||
it('uses default months parameter when not provided', async () => {
|
||||
const historyData = {
|
||||
accountId: 'acc-1',
|
||||
points: [],
|
||||
asOf: '2026-06-22T00:00:00.000Z',
|
||||
};
|
||||
vi.mocked(portfolioHistory.getHistory).mockResolvedValueOnce(
|
||||
new ApiEnvelopePayload(historyData, false, null),
|
||||
);
|
||||
|
||||
const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics, portfolioHistory);
|
||||
const response = await controller.getPortfolioHistory('acc-1');
|
||||
|
||||
expect(portfolioHistory.getHistory).toHaveBeenCalledWith('acc-1', 6);
|
||||
expect(response.data.points).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('forwards events query and wraps response', async () => {
|
||||
const eventsData = {
|
||||
items: [],
|
||||
@ -114,7 +179,7 @@ describe('TBankController', () => {
|
||||
new ApiEnvelopePayload(eventsData, false, '2026-06-22T00:00:00.000Z'),
|
||||
);
|
||||
|
||||
const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics);
|
||||
const controller = new TBankController(accounts, portfolio, events, operations, sync, analytics, portfolioHistory);
|
||||
const query = { from: '2026-06-22', to: '2026-07-29', types: 'dividend,coupon' };
|
||||
const response = await controller.getEvents('acc-1', query);
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
BrokerOperationSyncEnvelopeDto,
|
||||
BrokerOperationsEnvelopeDto,
|
||||
BrokerPortfolioEnvelopeDto,
|
||||
BrokerPortfolioHistoryEnvelopeDto,
|
||||
BrokerPositionsEnvelopeDto,
|
||||
} from './dto/broker-envelope.dto';
|
||||
import { BrokerEventsQueryDto } from './dto/broker-events-query.dto';
|
||||
@ -15,10 +16,11 @@ import { BrokerPositionQueryDto } from './dto/broker-position-query.dto';
|
||||
import { BrokerOperationQueryDto } from './dto/broker-operation-query.dto';
|
||||
import { BrokerOperationSyncQueryDto } from './dto/broker-operation-sync-query.dto';
|
||||
import { BrokerAccountsService } from './services/broker-accounts.service';
|
||||
import { BrokerAnalyticsService } from './services/broker-analytics.service';
|
||||
import { BrokerEventsService } from './services/broker-events.service';
|
||||
import { BrokerOperationSyncService } from './services/broker-operation-sync.service';
|
||||
import { BrokerOperationsService } from './services/broker-operations.service';
|
||||
import { BrokerAnalyticsService } from './services/broker-analytics.service';
|
||||
import { BrokerPortfolioHistoryService } from './services/broker-portfolio-history.service';
|
||||
import { BrokerPortfolioService } from './services/broker-portfolio.service';
|
||||
|
||||
@ApiTags('Broker')
|
||||
@ -33,6 +35,7 @@ export class TBankController {
|
||||
private readonly brokerOperationsService: BrokerOperationsService,
|
||||
private readonly brokerOperationSyncService: BrokerOperationSyncService,
|
||||
private readonly brokerAnalyticsService: BrokerAnalyticsService,
|
||||
private readonly brokerPortfolioHistoryService: BrokerPortfolioHistoryService,
|
||||
) {}
|
||||
|
||||
@Get('accounts')
|
||||
@ -81,6 +84,16 @@ export class TBankController {
|
||||
return this.brokerEventsService.getEvents(accountId, query);
|
||||
}
|
||||
|
||||
@Get('accounts/:accountId/portfolio/history')
|
||||
@ApiOperation({ summary: 'Get portfolio value history for last N months' })
|
||||
@ApiOkResponse({ type: BrokerPortfolioHistoryEnvelopeDto })
|
||||
async getPortfolioHistory(
|
||||
@Param('accountId') accountId: string,
|
||||
@Query('months') months?: number,
|
||||
) {
|
||||
return this.brokerPortfolioHistoryService.getHistory(accountId, months ?? 6);
|
||||
}
|
||||
|
||||
@Get('accounts/:accountId/analytics')
|
||||
@ApiOperation({ summary: 'Get broker account profitability analytics' })
|
||||
@ApiOkResponse({ type: BrokerAnalyticsEnvelopeDto })
|
||||
|
||||
@ -7,6 +7,7 @@ import { BrokerInstrumentsService } from './services/broker-instruments.service'
|
||||
import { BrokerEventsService } from './services/broker-events.service';
|
||||
import { BrokerOperationSyncService } from './services/broker-operation-sync.service';
|
||||
import { BrokerOperationsService } from './services/broker-operations.service';
|
||||
import { BrokerPortfolioHistoryService } from './services/broker-portfolio-history.service';
|
||||
import { BrokerPortfolioService } from './services/broker-portfolio.service';
|
||||
import { TBankClientService } from './services/tbank-client.service';
|
||||
|
||||
@ -22,6 +23,7 @@ import { TBankClientService } from './services/tbank-client.service';
|
||||
BrokerOperationsService,
|
||||
BrokerOperationSyncService,
|
||||
BrokerAnalyticsService,
|
||||
BrokerPortfolioHistoryService,
|
||||
],
|
||||
exports: [
|
||||
TBankClientService,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user