diff --git a/src/tbank/upcoming-events.dto.ts b/src/tbank/upcoming-events.dto.ts index 7bfe46b..0f04e13 100644 --- a/src/tbank/upcoming-events.dto.ts +++ b/src/tbank/upcoming-events.dto.ts @@ -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 { diff --git a/src/tbank/upcoming-events.service.spec.ts b/src/tbank/upcoming-events.service.spec.ts index e0ef4a1..97be051 100644 --- a/src/tbank/upcoming-events.service.spec.ts +++ b/src/tbank/upcoming-events.service.spec.ts @@ -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 }], }); }); diff --git a/src/tbank/upcoming-events.service.ts b/src/tbank/upcoming-events.service.ts index 8566a4f..b21f40f 100644 --- a/src/tbank/upcoming-events.service.ts +++ b/src/tbank/upcoming-events.service.ts @@ -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, value: string): string { - return formatDecimal(multiply(quantity, decimalFromString(value))); +function projectedMoney( + quantity: ReturnType, + 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) {