Files
website/src/lib/server/money-tracker.test.ts

120 lines
4.9 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import {
cents,
csvDate,
importCents,
parseCsv,
parseTransactionImport,
planImport
} from './money-tracker';
describe('transaction import parsing', () => {
it('handles quoted commas and semicolon-separated European amounts', () => {
expect(
parseCsv('date;account;description;amount\n23/09/2026;Cash;"Food; drinks";-12,50')[1]
).toEqual(['23/09/2026', 'Cash', 'Food; drinks', '-12,50']);
expect(
parseCsv('date,account,description,amount\n2026-09-23,Cash,"Food, drinks",-12.50')[1][2]
).toBe('Food, drinks');
expect(csvDate('23/09/2026')).toBe('2026-09-23');
expect(cents('-12,50')).toBe(-1250);
});
it('rejects invalid dates and amounts rather than altering them', () => {
expect(csvDate('31/02/2026')).toBeNull();
expect(csvDate('Sat 1 Aug 2025')).toBeNull();
expect(cents('1.234,56')).toBeUndefined();
});
it('accepts the spreadsheet export with tabs, spacer rows, named dates and euro amounts', () => {
const rows = parseCsv(
'date\taccount\tdescription\tamount\n' +
'Fri 1 Aug 2025\tDaily\tStart balance\t€123.45\n' +
'Fri 1 Aug 2025\tBusiness\tStart balance\t-€12.34\n' +
'Fri 1 Aug 2025\tShared\tStart balance\t€1,234.56\n' +
'\t\t\t\n' +
'Sat 2 Aug 2025\tShared\tExample bill\t-€1,234.50\n'
);
expect(rows).toHaveLength(5);
expect(rows[4]).toEqual(['Sat 2 Aug 2025', 'Shared', 'Example bill', '-€1,234.50']);
expect(csvDate(rows[1][0])).toBe('2025-08-01');
expect(csvDate(rows[4][0])).toBe('2025-08-02');
expect(rows.slice(1).map((row) => importCents(row[3]))).toEqual([
12345, -1234, 123456, -123450
]);
expect(
parseCsv(
'date,account,description,amount\n"Fri 1 Aug 2025",Shared,Start balance,"€1,234.56"'
)[1]
).toEqual(['Fri 1 Aug 2025', 'Shared', 'Start balance', '€1,234.56']);
expect(
parseCsv('date,account,description,amount\nFri 1 Aug 2025,Shared,Start balance,€1,234.56')[1]
).toHaveLength(5);
expect(
parseTransactionImport(
'date\taccount\tdescription\tamount\n' +
'Sat 2 Aug 2025\tShared\tExample bill\t-€1,234.50\n' +
'\t\t\t\n' +
'Fri 1 Aug 2025\tDaily\tStart balance\t€123.45\n' +
'Fri 1 Aug 2025\tBusiness\tStart balance\t-€12.34\n'
).map((row) => [row.date, row.account, row.amountCents])
).toEqual([
['2025-08-01', 'Daily', 12345],
['2025-08-01', 'Business', -1234],
['2025-08-02', 'Shared', -123450]
]);
expect(() =>
parseTransactionImport(
'date,account,description,amount\nFri 1 Aug 2025,Shared,Start balance,€1,234.56'
)
).toThrow('Quote values containing commas');
});
});
describe('revolut statements and import dedupe', () => {
const statement =
'Type,Product,Started Date,Completed Date,Description,Amount,Fee,Currency,State,Balance\n' +
'CARD_PAYMENT,Current,2026-10-01 10:00:00,2026-10-02 09:00:00,Albert Heijn,-12.34,0.00,EUR,COMPLETED,100.00\n' +
'TOPUP,Current,2026-10-03 10:00:00,,Pending top-up,50.00,0.00,EUR,PENDING,\n' +
'TRANSFER,Current,2026-10-04 10:00:00,2026-10-04 10:00:01,To Joint,-100.00,0.50,EUR,COMPLETED,0.00\n' +
'CARD_PAYMENT,Current,2026-10-05 10:00:00,2026-10-05 11:00:00,Coffee,-3.00,0.00,USD,COMPLETED,0.00\n';
it('keeps completed EUR rows, uses the completed date and deducts fees', () => {
const rows = parseTransactionImport(statement, 'Revolut');
expect(rows.map((row) => [row.date, row.account, row.description, row.amountCents])).toEqual([
['2026-10-02', 'Revolut', 'Albert Heijn', -1234],
['2026-10-04', 'Revolut', 'To Joint', -10050]
]);
expect(rows[0].externalId).toMatch(/^csv:[0-9a-f]{32}$/);
expect(parseTransactionImport(statement, 'Revolut')[0].externalId).toBe(rows[0].externalId);
expect(() => parseTransactionImport(statement)).toThrow('Choose which account');
});
it('gives identical rows in one file distinct, stable ids', () => {
const file =
'date,account,description,amount\n2026-10-01,Cash,Kiosk,-2\n2026-10-01,Cash,Kiosk,-2\n';
const [a, b] = parseTransactionImport(file);
expect(a.externalId).not.toBe(b.externalId);
expect(parseTransactionImport(file).map((row) => row.externalId)).toEqual([
a.externalId,
b.externalId
]);
});
it('skips rows already imported or already entered by hand (±1 day)', () => {
const rows = [
{ accountId: 1, date: '2026-10-02', amountCents: -1234, externalId: 'csv:a' },
{ accountId: 1, date: '2026-10-02', amountCents: -1234, externalId: 'csv:b' },
{ accountId: 1, date: '2026-10-04', amountCents: -500, externalId: 'csv:c' },
{ accountId: 2, date: '2026-10-04', amountCents: -500, externalId: 'csv:d' }
];
const existing = [
{ id: 10, accountId: 1, date: '2026-10-01', amountCents: -1234, externalId: null },
{ id: 11, accountId: 1, date: '2026-10-04', amountCents: -500, externalId: 'csv:c' }
];
const { insert, duplicates } = planImport(rows, existing);
expect(insert.map((row) => row.externalId)).toEqual(['csv:b', 'csv:d']);
expect(duplicates).toBe(2);
});
});