feat: use money for upcoming event projections

This commit is contained in:
2026-08-25 22:30:08 +03:00
parent 130b303ec1
commit f1df104462
3 changed files with 72 additions and 17 deletions
+3 -5
View File
@@ -1,4 +1,5 @@
import { ApiProperty } from '@nestjs/swagger';
import { MoneyDto } from './portfolio.dto';
export class UpcomingEventDto {
@ApiProperty({ enum: ['coupon', 'dividend', 'maturity', 'offer'] })
@@ -22,11 +23,8 @@ export class UpcomingEventDto {
@ApiProperty()
displayName: string;
@ApiProperty({ type: String, nullable: true })
currency: string | null;
@ApiProperty({ type: String, nullable: true })
amount: string | null;
@ApiProperty({ type: MoneyDto, nullable: true })
projectedAmount: MoneyDto | null;
}
export class UpcomingEventsDto {
+47 -5
View File
@@ -94,9 +94,17 @@ describe('UpcomingEventsService', () => {
await expect(service.get('user', 'account')).resolves.toMatchObject({
events: [
{ type: 'coupon', amount: '7.5', dateKind: 'event' },
{ type: 'dividend', amount: '2.5', dateKind: 'registry' },
{ type: 'offer', amount: null },
{
type: 'coupon',
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({
events: [{ ticker: 'SBER', amount: '1' }],
events: [
{ ticker: 'SBER', projectedAmount: { currency: 'RUB', value: '1' } },
],
});
expect(tbank.getInstrumentByFigi).toHaveBeenCalledWith('token', 'S');
});
@@ -170,7 +180,39 @@ describe('UpcomingEventsService', () => {
]);
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 }],
});
});
+22 -7
View File
@@ -3,6 +3,7 @@ import { RequestContextService } from '../common/logging/request-context.service
import { StructuredLogger } from '../common/logging/structured-logger.service';
import { decimal, formatDecimal, isZero, multiply } from './decimal';
import { MoexBondEvent, MoexClient } from './moex.client';
import { MoneyDto } from './portfolio.dto';
import { TbankAccessService } from './tbank-access.service';
import {
TbankClient,
@@ -184,9 +185,12 @@ export class UpcomingEventsService {
.toISOString()
.slice(0, 10),
dateKind: 'registry',
currency: event.dividendNet?.currency ?? null,
amount: event.dividendNet
? formatDecimal(multiply(quantity, decimal(event.dividendNet)))
projectedAmount: event.dividendNet
? projectedMoney(
quantity,
formatDecimal(decimal(event.dividendNet)),
event.dividendNet.currency,
)
: null,
};
}
@@ -204,8 +208,10 @@ export class UpcomingEventsService {
type: event.type,
date: event.date,
dateKind: 'event',
currency: 'currency' in event ? event.currency : null,
amount: 'value' in event ? amount(quantity, event.value) : null,
projectedAmount:
'value' in event
? projectedMoney(quantity, event.value, event.currency)
: null,
};
}
}
@@ -226,8 +232,17 @@ function dateWindow() {
return { from, to: end.toISOString().slice(0, 10) };
}
function amount(quantity: ReturnType<typeof decimal>, value: string): string {
return formatDecimal(multiply(quantity, decimalFromString(value)));
function projectedMoney(
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) {