feat(portfolio): expose asset allocation by currency
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import type { Server } from 'node:http';
|
||||
import request from 'supertest';
|
||||
import { AppModule } from '../src/app.module';
|
||||
import { configureApp } from '../src/main';
|
||||
import { TbankAccessService } from '../src/tbank/tbank-access.service';
|
||||
import { TbankClient } from '../src/tbank/tbank.client';
|
||||
import { PortfolioService } from '../src/tbank/portfolio.service';
|
||||
|
||||
describe('Portfolio structure HTTP contract', () => {
|
||||
let app: INestApplication<Server>;
|
||||
const context = {
|
||||
token: 'fixture',
|
||||
generation: 'fixture',
|
||||
accounts: [
|
||||
{ id: 'a', name: 'A' },
|
||||
{ id: 'b', name: 'B' },
|
||||
],
|
||||
};
|
||||
const tbank = {
|
||||
getPortfolio: jest.fn(),
|
||||
getPositions: jest.fn(),
|
||||
getInstrumentByFigi: jest.fn().mockResolvedValue(null),
|
||||
getInstrumentByUid: jest.fn().mockResolvedValue(null),
|
||||
};
|
||||
const access = {
|
||||
withConnection: (_user: string, action: (value: unknown) => unknown) =>
|
||||
action(context),
|
||||
withSupportedAccounts: (
|
||||
_user: string,
|
||||
action: (value: unknown) => unknown,
|
||||
) => action(context),
|
||||
withAccountForConnection: (
|
||||
_context: unknown,
|
||||
_id: string,
|
||||
action: () => unknown,
|
||||
) => action(),
|
||||
};
|
||||
const token = new JwtService().sign(
|
||||
{ sub: 'structure-user', type: 'access' },
|
||||
{ secret: process.env.JWT_SECRET },
|
||||
);
|
||||
|
||||
beforeAll(async () => {
|
||||
const module = await Test.createTestingModule({ imports: [AppModule] })
|
||||
.overrideProvider(TbankAccessService)
|
||||
.useValue(access)
|
||||
.overrideProvider(TbankClient)
|
||||
.useValue(tbank)
|
||||
.compile();
|
||||
app = module.createNestApplication();
|
||||
configureApp(app);
|
||||
await app.init();
|
||||
});
|
||||
beforeEach(() => {
|
||||
app.get(PortfolioService).invalidate('structure-user');
|
||||
tbank.getPortfolio.mockResolvedValue({
|
||||
positions: [
|
||||
{
|
||||
instrumentType: 'share',
|
||||
quantity: { units: 2 },
|
||||
currentPrice: { currency: 'RUB', units: 30 },
|
||||
},
|
||||
],
|
||||
});
|
||||
tbank.getPositions.mockResolvedValue({
|
||||
money: [{ currency: 'RUB', units: 40 }],
|
||||
});
|
||||
});
|
||||
afterAll(() => app.close());
|
||||
|
||||
it.each(['a', 'all'])(
|
||||
'serializes exact structure including cash for %s',
|
||||
async (id) => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/api/v1/brokerage-accounts/${id}/portfolio`)
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
expect(response.body).toMatchObject({
|
||||
structure: {
|
||||
unvaluedCategories: [],
|
||||
currencies: [
|
||||
{
|
||||
currency: 'RUB',
|
||||
totalValue: id === 'all' ? '200' : '100',
|
||||
hasNegativeValues: false,
|
||||
categories: [
|
||||
{
|
||||
type: 'STOCK',
|
||||
value: id === 'all' ? '120' : '60',
|
||||
sharePercent: '60',
|
||||
},
|
||||
{ type: 'BOND', value: '0', sharePercent: '0' },
|
||||
{ type: 'OTHER', value: '0', sharePercent: '0' },
|
||||
{
|
||||
type: 'CASH',
|
||||
value: id === 'all' ? '80' : '40',
|
||||
sharePercent: '40',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('retains unknown valuations on the wire', async () => {
|
||||
tbank.getPortfolio.mockResolvedValue({
|
||||
positions: [{ instrumentType: 'bond', quantity: { units: 1 } }],
|
||||
});
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/api/v1/brokerage-accounts/a/portfolio')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
expect(response.body).toMatchObject({
|
||||
structure: {
|
||||
unvaluedCategories: [{ type: 'BOND', count: 1 }],
|
||||
currencies: [
|
||||
{
|
||||
categories: [
|
||||
{ sharePercent: null },
|
||||
{ sharePercent: null },
|
||||
{ sharePercent: null },
|
||||
{ value: '40', sharePercent: null },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps a negative cash constituent visible to aggregate eligibility after netting', async () => {
|
||||
tbank.getPositions.mockImplementation((_token: string, id: string) => ({
|
||||
money: [{ currency: 'RUB', units: id === 'a' ? -10 : 40 }],
|
||||
}));
|
||||
const response = await request(app.getHttpServer())
|
||||
.get('/api/v1/brokerage-accounts/all/portfolio')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.expect(200);
|
||||
expect(response.body).toMatchObject({
|
||||
structure: {
|
||||
currencies: [
|
||||
{
|
||||
hasNegativeValues: true,
|
||||
totalValue: '150',
|
||||
categories: [
|
||||
{ value: '120', sharePercent: null },
|
||||
{ sharePercent: null },
|
||||
{ sharePercent: null },
|
||||
{ value: '30', sharePercent: null },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user