feat: use money for upcoming event projections
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { MoneyDto } from './portfolio.dto';
|
||||||
|
|
||||||
export class UpcomingEventDto {
|
export class UpcomingEventDto {
|
||||||
@ApiProperty({ enum: ['coupon', 'dividend', 'maturity', 'offer'] })
|
@ApiProperty({ enum: ['coupon', 'dividend', 'maturity', 'offer'] })
|
||||||
@@ -22,11 +23,8 @@ export class UpcomingEventDto {
|
|||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|
||||||
@ApiProperty({ type: String, nullable: true })
|
@ApiProperty({ type: MoneyDto, nullable: true })
|
||||||
currency: string | null;
|
projectedAmount: MoneyDto | null;
|
||||||
|
|
||||||
@ApiProperty({ type: String, nullable: true })
|
|
||||||
amount: string | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpcomingEventsDto {
|
export class UpcomingEventsDto {
|
||||||
|
|||||||
@@ -94,9 +94,17 @@ describe('UpcomingEventsService', () => {
|
|||||||
|
|
||||||
await expect(service.get('user', 'account')).resolves.toMatchObject({
|
await expect(service.get('user', 'account')).resolves.toMatchObject({
|
||||||
events: [
|
events: [
|
||||||
{ type: 'coupon', amount: '7.5', dateKind: 'event' },
|
{
|
||||||
{ type: 'dividend', amount: '2.5', dateKind: 'registry' },
|
type: 'coupon',
|
||||||
{ type: 'offer', amount: null },
|
projectedAmount: { currency: 'RUB', value: '7.5' },
|
||||||
|
dateKind: 'event',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'dividend',
|
||||||
|
projectedAmount: { currency: 'RUB', value: '2.5' },
|
||||||
|
dateKind: 'registry',
|
||||||
|
},
|
||||||
|
{ type: 'offer', projectedAmount: null },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -135,7 +143,9 @@ describe('UpcomingEventsService', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await expect(service.get('user', 'account')).resolves.toMatchObject({
|
await expect(service.get('user', 'account')).resolves.toMatchObject({
|
||||||
events: [{ ticker: 'SBER', amount: '1' }],
|
events: [
|
||||||
|
{ ticker: 'SBER', projectedAmount: { currency: 'RUB', value: '1' } },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
expect(tbank.getInstrumentByFigi).toHaveBeenCalledWith('token', 'S');
|
expect(tbank.getInstrumentByFigi).toHaveBeenCalledWith('token', 'S');
|
||||||
});
|
});
|
||||||
@@ -170,7 +180,39 @@ describe('UpcomingEventsService', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
await expect(service.get('user', 'account')).resolves.toMatchObject({
|
await expect(service.get('user', 'account')).resolves.toMatchObject({
|
||||||
events: [{ type: 'dividend', amount: '2' }],
|
events: [
|
||||||
|
{
|
||||||
|
type: 'dividend',
|
||||||
|
projectedAmount: { currency: 'RUB', value: '2' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not invent projected money when the source omits currency', async () => {
|
||||||
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
tbank.getPortfolio.mockResolvedValue({
|
||||||
|
positions: [
|
||||||
|
{
|
||||||
|
figi: 'S',
|
||||||
|
ticker: 'SBER',
|
||||||
|
instrumentType: 'share',
|
||||||
|
quantity: { units: 1 },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
tbank.getDividends.mockResolvedValue([
|
||||||
|
{
|
||||||
|
dividendNet: { units: 1, nano: 0 },
|
||||||
|
recordDate: {
|
||||||
|
seconds: String(Math.floor(Date.parse(`${today}T00:00:00Z`) / 1000)),
|
||||||
|
},
|
||||||
|
dividendType: 'Regular Cash',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(service.get('user', 'account')).resolves.toMatchObject({
|
||||||
|
events: [{ type: 'dividend', projectedAmount: null }],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { RequestContextService } from '../common/logging/request-context.service
|
|||||||
import { StructuredLogger } from '../common/logging/structured-logger.service';
|
import { StructuredLogger } from '../common/logging/structured-logger.service';
|
||||||
import { decimal, formatDecimal, isZero, multiply } from './decimal';
|
import { decimal, formatDecimal, isZero, multiply } from './decimal';
|
||||||
import { MoexBondEvent, MoexClient } from './moex.client';
|
import { MoexBondEvent, MoexClient } from './moex.client';
|
||||||
|
import { MoneyDto } from './portfolio.dto';
|
||||||
import { TbankAccessService } from './tbank-access.service';
|
import { TbankAccessService } from './tbank-access.service';
|
||||||
import {
|
import {
|
||||||
TbankClient,
|
TbankClient,
|
||||||
@@ -184,9 +185,12 @@ export class UpcomingEventsService {
|
|||||||
.toISOString()
|
.toISOString()
|
||||||
.slice(0, 10),
|
.slice(0, 10),
|
||||||
dateKind: 'registry',
|
dateKind: 'registry',
|
||||||
currency: event.dividendNet?.currency ?? null,
|
projectedAmount: event.dividendNet
|
||||||
amount: event.dividendNet
|
? projectedMoney(
|
||||||
? formatDecimal(multiply(quantity, decimal(event.dividendNet)))
|
quantity,
|
||||||
|
formatDecimal(decimal(event.dividendNet)),
|
||||||
|
event.dividendNet.currency,
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -204,8 +208,10 @@ export class UpcomingEventsService {
|
|||||||
type: event.type,
|
type: event.type,
|
||||||
date: event.date,
|
date: event.date,
|
||||||
dateKind: 'event',
|
dateKind: 'event',
|
||||||
currency: 'currency' in event ? event.currency : null,
|
projectedAmount:
|
||||||
amount: 'value' in event ? amount(quantity, event.value) : null,
|
'value' in event
|
||||||
|
? projectedMoney(quantity, event.value, event.currency)
|
||||||
|
: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -226,8 +232,17 @@ function dateWindow() {
|
|||||||
return { from, to: end.toISOString().slice(0, 10) };
|
return { from, to: end.toISOString().slice(0, 10) };
|
||||||
}
|
}
|
||||||
|
|
||||||
function amount(quantity: ReturnType<typeof decimal>, value: string): string {
|
function projectedMoney(
|
||||||
return formatDecimal(multiply(quantity, decimalFromString(value)));
|
quantity: ReturnType<typeof decimal>,
|
||||||
|
value: string,
|
||||||
|
currency: string | undefined,
|
||||||
|
): MoneyDto | null {
|
||||||
|
const normalizedCurrency = currency?.trim().toUpperCase();
|
||||||
|
if (!normalizedCurrency) return null;
|
||||||
|
return {
|
||||||
|
currency: normalizedCurrency,
|
||||||
|
value: formatDecimal(multiply(quantity, decimalFromString(value))),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function decimalFromString(value: string) {
|
function decimalFromString(value: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user