Files
backend/src/modules/banking/bank-cards/domain/bank-card.calculations.spec.ts
T
ksv741 4796d9d3f0
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Successful in 11m9s
Conventional commits / commits (pull_request) Successful in 28s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 10m38s
fix: harden auth and investment synchronization
Resolve A01-A10 and C01-C07 from the backend audit.
Preserve auth failures, forced candle reconciliation and forecast freshness.
Validate MOEX responses and nested HTTP errors; remove unused legacy paths.

Verified: 209 unit tests, 262 SQLite e2e tests on Linux, OpenAPI and durability.
Record the remaining macOS native SQLite abort in the audit.
2026-09-27 14:09:05 +03:00

163 lines
4.3 KiB
TypeScript

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 },
],
});
});
});