refactor: organize backend by business responsibility

This commit is contained in:
2026-09-13 13:52:57 +03:00
parent 7fde3ece6e
commit 8ba005346d
145 changed files with 3027 additions and 2346 deletions
@@ -0,0 +1,153 @@
import {
bankBalance,
bankSummary,
isCalendarDate,
moscowToday,
} from './bank-card.calculations';
describe('bank card calculations', () => {
it('uses Moscow midnight for today and the current month', () => {
expect(moscowToday(new Date('2026-08-31T20:59:59.999Z'))).toBe(
'2026-08-31',
);
expect(moscowToday(new Date('2026-08-31T21:00:00.000Z'))).toBe(
'2026-09-01',
);
});
it.each([
'0001-01-01',
'9999-12-31',
'2024-02-29',
'2000-02-29',
'2026-09-12',
])('accepts calendar date %s', (value) => {
expect(isCalendarDate(value)).toBe(true);
});
it.each([
'0000-01-01',
'10000-01-01',
'2026-02-29',
'1900-02-29',
'2026-04-31',
'2026-00-01',
'2026-13-01',
'2026-01-00',
'2026-01-32',
'2026-1-01',
'2026-09-12\n',
'2026-09-12T00:00:00Z',
'',
])('rejects invalid calendar date %s', (value) => {
expect(isCalendarDate(value)).toBe(false);
});
it('folds expenses, topups, refunds and both transfer sides exactly', () => {
const operations = [
{ kind: 'expense', cardId: 'a', targetCardId: null, amountKopecks: 10n },
{ kind: 'expense', cardId: 'a', targetCardId: null, amountKopecks: 20n },
{ kind: 'topup', cardId: 'a', targetCardId: null, amountKopecks: 15n },
{ kind: 'refund', cardId: 'a', targetCardId: null, amountKopecks: 5n },
{ kind: 'transfer', cardId: 'a', targetCardId: 'b', amountKopecks: 120n },
{ kind: 'expense', cardId: 'c', targetCardId: null, amountKopecks: 300n },
];
expect(bankBalance('a', 100n, operations.slice(0, 2))).toBe(70n);
expect(bankBalance('a', 100n, operations)).toBe(-30n);
expect(bankBalance('b', 0n, operations)).toBe(120n);
expect(bankBalance('unused', -1n, operations)).toBe(-1n);
});
it('does not bound computed balances by the storage range', () => {
expect(
bankBalance('a', 2n ** 63n - 1n, [
{ kind: 'topup', cardId: 'a', targetCardId: null, amountKopecks: 1n },
]),
).toBe(2n ** 63n);
expect(
bankBalance('a', -(2n ** 63n), [
{ kind: 'expense', cardId: 'a', targetCardId: null, amountKopecks: 1n },
]),
).toBe(-(2n ** 63n) - 1n);
});
it('counts refunds in their own month and inherited category, excluding transfers and topups', () => {
const operations = [
{
kind: 'expense',
date: '2026-08-31',
categoryId: 'food',
amountKopecks: 1000n,
},
{
kind: 'refund',
date: '2026-09-01',
categoryId: 'food',
amountKopecks: 500n,
},
{
kind: 'expense',
date: '2026-09-30',
categoryId: 'food',
amountKopecks: 100n,
},
{
kind: 'expense',
date: '2026-09-03',
categoryId: 'travel',
amountKopecks: 20n,
},
{
kind: 'transfer',
date: '2026-09-03',
categoryId: null,
amountKopecks: 2000n,
},
{
kind: 'topup',
date: '2026-09-03',
categoryId: null,
amountKopecks: 3000n,
},
{
kind: 'expense',
date: '2026-10-01',
categoryId: 'travel',
amountKopecks: 5000n,
},
];
expect(bankSummary('2026-09', operations)).toEqual({
expenses: 120n,
refunds: 500n,
net: -380n,
categories: [
{ categoryId: 'food', expenses: 100n, refunds: 500n, net: -400n },
{ categoryId: 'travel', expenses: 20n, refunds: 0n, net: 20n },
],
});
expect(bankSummary('2026-07', operations)).toEqual({
expenses: 0n,
refunds: 0n,
net: 0n,
categories: [],
});
});
it('keeps summary sums beyond signed 64-bit exact', () => {
const operation = {
kind: 'expense',
date: '2026-09-01',
categoryId: 'food',
amountKopecks: 2n ** 63n - 1n,
};
const total = 2n * (2n ** 63n - 1n);
expect(bankSummary('2026-09', [operation, operation])).toEqual({
expenses: total,
refunds: 0n,
net: total,
categories: [
{ categoryId: 'food', expenses: total, refunds: 0n, net: total },
],
});
});
});