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({ from: '2026-09-01', to: '2026-09-30' }, 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({ from: '2026-07-01', to: '2026-07-31' }, 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({ from: '2026-09-01', to: '2026-09-30' }, [ operation, operation, ]), ).toEqual({ expenses: total, refunds: 0n, net: total, categories: [ { categoryId: 'food', expenses: total, refunds: 0n, net: total }, ], }); }); });