diff --git a/src/modules/investments/income/income-analytics.service.spec.ts b/src/modules/investments/income/income-analytics.service.spec.ts index 0aef960..03a76dc 100644 --- a/src/modules/investments/income/income-analytics.service.spec.ts +++ b/src/modules/investments/income/income-analytics.service.spec.ts @@ -189,6 +189,49 @@ describe('IncomeAnalyticsService', () => { ).toEqual(['10.25', '-0.25']); }); + it('assigns signed payments to Moscow days and retains empty buckets', async () => { + seed([ + { + ...entry('before', 'OPERATION_TYPE_COUPON', '0.1', 'RUB'), + occurredAt: '2026-01-14T20:59:59.999Z', + }, + { + ...entry('after', 'OPERATION_TYPE_COUPON', '0.2', 'RUB'), + occurredAt: '2026-01-14T21:00:00.000Z', + }, + { + ...entry('reversal', 'OPERATION_TYPE_DIVIDEND', '-0.1', 'RUB'), + occurredAt: '2026-01-15T20:59:59.999Z', + }, + ]); + const response = await service.analytics( + 'user', + 'account', + '2026-01-14', + '2026-01-16', + ); + expect(response.data?.buckets).toEqual([ + { + period: { from: '2026-01-14', to: '2026-01-14' }, + totalsByCurrency: [expect.objectContaining({ + total: { currency: 'RUB', value: '0.1' }, + })], + }, + { + period: { from: '2026-01-15', to: '2026-01-15' }, + totalsByCurrency: [expect.objectContaining({ + coupon: { currency: 'RUB', value: '0.2' }, + dividend: { currency: 'RUB', value: '-0.1' }, + total: { currency: 'RUB', value: '0.1' }, + })], + }, + { + period: { from: '2026-01-16', to: '2026-01-16' }, + totalsByCurrency: [], + }, + ]); + }); + it('aggregates only exact coupon and dividend payments by source currency', async () => { operations.collectRanges.mockImplementation( ( diff --git a/src/modules/investments/income/income-analytics.service.ts b/src/modules/investments/income/income-analytics.service.ts index 480e37c..fad1401 100644 --- a/src/modules/investments/income/income-analytics.service.ts +++ b/src/modules/investments/income/income-analytics.service.ts @@ -232,13 +232,16 @@ function buckets( granularity: IncomeBucketGranularity, entries: BrokerageOperationDto[], ) { + const datedEntries = entries.map((entry) => ({ + entry, + date: moscowDate(new Date(entry.occurredAt)), + })); return rangeBuckets(range, granularity).map((period) => ({ period, totalsByCurrency: totals( - entries.filter((entry) => { - const date = moscowDate(new Date(entry.occurredAt)); - return date >= period.from && date <= period.to; - }), + datedEntries + .filter(({ date }) => date >= period.from && date <= period.to) + .map(({ entry }) => entry), ), })); }