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
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.
163 lines
4.3 KiB
TypeScript
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 },
|
|
],
|
|
});
|
|
});
|
|
});
|