feat(income): add account income analytics API

This commit is contained in:
2026-08-28 01:25:52 +03:00
parent 924f60f653
commit 7ba4da0d1b
13 changed files with 997 additions and 49 deletions
+48
View File
@@ -0,0 +1,48 @@
import { BadRequestException } from '@nestjs/common';
import {
dateOnlyBounds,
planIncomePeriod,
rangeBuckets,
} from './income-period';
describe('income period planning', () => {
afterEach(() => jest.useRealTimers());
it('uses the Moscow current month and validates strict inclusive ranges', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-01-31T22:30:00.000Z'));
expect(planIncomePeriod(undefined, undefined)).toMatchObject({
selected: { from: '2026-02-01', to: '2026-02-01' },
});
for (const [from, to] of [
['2026-02-30', '2026-03-01'],
['2026-03-02', '2026-03-01'],
['2026-01-01', '2027-01-02'],
['2026-01-01', '2026-04-01'],
])
expect(() => planIncomePeriod(from, to)).toThrow(BadRequestException);
});
it('uses a full previous month, skips impossible year-ago dates, and keeps empty buckets', () => {
const fullMonth = planIncomePeriod('2026-03-01', '2026-03-31');
expect(fullMonth.previous).toEqual({
from: '2026-02-01',
to: '2026-02-28',
});
expect(planIncomePeriod('2024-02-29', '2024-02-29').yearAgo).toBeNull();
expect(
rangeBuckets({ from: '2026-01-01', to: '2026-02-15' }, 'week'),
).toHaveLength(7);
});
it('does not send the unfinished Moscow day as a future instant', () => {
jest.useFakeTimers();
jest.setSystemTime(new Date('2026-08-27T21:44:18.510Z'));
expect(dateOnlyBounds({ from: '2026-08-01', to: '2026-08-28' })).toEqual({
from: new Date('2026-07-31T21:00:00.000Z'),
to: new Date('2026-08-27T21:44:18.510Z'),
});
});
});