import { buildCsv, buildPdf, buildReceivableRows, buildRows } from './cashbox-export.utils'; describe('buildRows', () => { const team = (overrides: Partial<{ transactions: any[]; players: any[] }> = {}) => ({ id: 5, name: 'Team A', alias: 'team-a', transactions: [], players: [], ...overrides, }); it('includes team-wallet credit and expense rows as "Teamkasse"', () => { const rows = buildRows( team({ transactions: [ { date: '2026-08-05T00:00:00.000Z', amount: 100, note: 'Sponsoring', type: { name: 'credit' } }, { date: '2026-08-10T00:00:00.000Z', amount: -20, note: 'Bälle', type: { name: 'expense' } }, ], }) as any, '2026-08-01', '2026-08-31', ); expect(rows).toEqual([ { date: '2026-08-05T00:00:00.000Z', type: 'credit', who: 'Teamkasse', note: 'Sponsoring', amount: 100, runningTotal: 100 }, { date: '2026-08-10T00:00:00.000Z', type: 'expense', who: 'Teamkasse', note: 'Bälle', amount: -20, runningTotal: 80 }, ]); }); it('includes only "payment" player transactions, excluding fee/levy/fine', () => { const rows = buildRows( team({ players: [ { firstName: 'Alex', lastName: 'Muster', transactions: [ { date: '2026-08-03T00:00:00.000Z', amount: 10, note: 'Bar bezahlt', type: { name: 'payment' } }, { date: '2026-08-04T00:00:00.000Z', amount: 15, note: 'Monatsbeitrag', type: { name: 'fee' } }, ], }, ], }) as any, '2026-08-01', '2026-08-31', ); expect(rows).toEqual([ { date: '2026-08-03T00:00:00.000Z', type: 'payment', who: 'Alex Muster', note: 'Bar bezahlt', amount: 10, runningTotal: 10 }, ]); }); it('excludes rows outside the [from, to] range and sorts the rest chronologically', () => { const rows = buildRows( team({ transactions: [ { date: '2026-07-31T23:59:00.000Z', amount: 5, note: 'zu früh', type: { name: 'credit' } }, { date: '2026-09-01T00:00:01.000Z', amount: 5, note: 'zu spät', type: { name: 'credit' } }, { date: '2026-08-20T00:00:00.000Z', amount: 5, note: 'zweitens', type: { name: 'credit' } }, { date: '2026-08-01T00:00:00.000Z', amount: 5, note: 'erstens', type: { name: 'credit' } }, ], }) as any, '2026-08-01', '2026-08-31', ); expect(rows.map((row) => row.note)).toEqual(['erstens', 'zweitens']); }); it('returns an empty array when nothing falls in range', () => { const rows = buildRows(team() as any, '2026-08-01', '2026-08-31'); expect(rows).toEqual([]); }); it('skips transactions with null type and includes valid ones', () => { const rows = buildRows( team({ transactions: [ { date: '2026-08-05T00:00:00.000Z', amount: 100, note: 'Valid credit', type: { name: 'credit' } }, { date: '2026-08-06T00:00:00.000Z', amount: 50, note: 'Null type team wallet', type: null }, ], players: [ { firstName: 'Bob', lastName: 'Smith', transactions: [ { date: '2026-08-07T00:00:00.000Z', amount: 20, note: 'Valid payment', type: { name: 'payment' } }, { date: '2026-08-08T00:00:00.000Z', amount: 30, note: 'Null type player', type: null }, ], }, ], }) as any, '2026-08-01', '2026-08-31', ); expect(rows).toEqual([ { date: '2026-08-05T00:00:00.000Z', type: 'credit', who: 'Teamkasse', note: 'Valid credit', amount: 100, runningTotal: 100 }, { date: '2026-08-07T00:00:00.000Z', type: 'payment', who: 'Bob Smith', note: 'Valid payment', amount: 20, runningTotal: 120 }, ]); }); }); describe('buildReceivableRows', () => { const team = (overrides: Partial<{ players: any[] }> = {}) => ({ id: 5, name: 'Team A', alias: 'team-a', players: [], ...overrides, }); it('includes fine, levy and fee player transactions, excluding payment and credit', () => { const rows = buildReceivableRows( team({ players: [ { firstName: 'Alex', lastName: 'Muster', transactions: [ { date: '2026-08-03T00:00:00.000Z', amount: 10, note: 'Bar bezahlt', type: { name: 'payment' } }, { date: '2026-08-04T00:00:00.000Z', amount: 15, note: 'Monatsbeitrag', type: { name: 'fee' } }, { date: '2026-08-05T00:00:00.000Z', amount: 5, note: 'Zu spät', type: { name: 'fine' } }, { date: '2026-08-06T00:00:00.000Z', amount: 20, note: 'Umlage Trikots', type: { name: 'levy' } }, ], }, ], }) as any, '2026-08-01', '2026-08-31', ); expect(rows).toEqual([ { date: '2026-08-04T00:00:00.000Z', type: 'fee', who: 'Alex Muster', note: 'Monatsbeitrag', amount: 15 }, { date: '2026-08-05T00:00:00.000Z', type: 'fine', who: 'Alex Muster', note: 'Zu spät', amount: 5 }, { date: '2026-08-06T00:00:00.000Z', type: 'levy', who: 'Alex Muster', note: 'Umlage Trikots', amount: 20 }, ]); }); it('excludes rows outside the [from, to] range and sorts the rest chronologically', () => { const rows = buildReceivableRows( team({ players: [ { firstName: 'Bob', lastName: 'Smith', transactions: [ { date: '2026-07-31T23:59:00.000Z', amount: 5, note: 'zu früh', type: { name: 'fine' } }, { date: '2026-09-01T00:00:01.000Z', amount: 5, note: 'zu spät', type: { name: 'fine' } }, { date: '2026-08-20T00:00:00.000Z', amount: 5, note: 'zweitens', type: { name: 'fee' } }, { date: '2026-08-01T00:00:00.000Z', amount: 5, note: 'erstens', type: { name: 'levy' } }, ], }, ], }) as any, '2026-08-01', '2026-08-31', ); expect(rows.map((row) => row.note)).toEqual(['erstens', 'zweitens']); }); it('skips transactions with null type and returns an empty array when nothing matches', () => { const rows = buildReceivableRows( team({ players: [ { firstName: 'Carla', lastName: 'Beispiel', transactions: [ { date: '2026-08-05T00:00:00.000Z', amount: 10, note: 'Null type', type: null }, { date: '2026-08-06T00:00:00.000Z', amount: 10, note: 'Zahlung', type: { name: 'payment' } }, ], }, ], }) as any, '2026-08-01', '2026-08-31', ); expect(rows).toEqual([]); }); }); describe('buildCsv', () => { it('renders the header and formatted rows with German decimals', () => { const csv = buildCsv([ { date: '2026-08-05T00:00:00.000Z', type: 'payment', who: 'Alex Muster', note: 'Bar bezahlt', amount: 10, runningTotal: 10 }, { date: '2026-08-10T00:00:00.000Z', type: 'expense', who: 'Teamkasse', note: 'Bälle', amount: -20.5, runningTotal: -10.5 }, ]); expect(csv).toBe( 'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\n' + '2026-08-05;Zahlung;Alex Muster;Bar bezahlt;10,00;10,00\r\n' + '2026-08-10;Ausgabe;Teamkasse;Bälle;-20,50;-10,50', ); }); it('quotes notes containing a semicolon and escapes embedded quotes', () => { const csv = buildCsv([ { date: '2026-08-05T00:00:00.000Z', type: 'credit', who: 'Teamkasse', note: 'Spende; "danke"', amount: 5, runningTotal: 5 }, ]); expect(csv).toBe( 'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\n' + '2026-08-05;Guthaben;Teamkasse;"Spende; ""danke""";5,00;5,00', ); }); it('shows a placeholder row when there are no bookings', () => { const csv = buildCsv([]); expect(csv).toBe( 'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\nKeine Buchungen im gewählten Zeitraum', ); }); it('formats negative-zero as positive zero', () => { const csv = buildCsv([ { date: '2026-08-05T00:00:00.000Z', type: 'expense', who: 'Teamkasse', note: 'Test', amount: -0.001, runningTotal: 0 }, ]); expect(csv).toBe( 'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\n' + '2026-08-05;Ausgabe;Teamkasse;Test;0,00;0,00', ); }); it('rounds half-cent boundaries correctly', () => { const csv = buildCsv([ { date: '2026-08-05T00:00:00.000Z', type: 'payment', who: 'Alex Muster', note: 'Test', amount: 1.005, runningTotal: 1.005 }, ]); expect(csv).toBe( 'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\n' + '2026-08-05;Zahlung;Alex Muster;Test;1,01;1,01', ); }); it('rounds negative half-cent boundaries correctly', () => { const csv = buildCsv([ { date: '2026-08-05T00:00:00.000Z', type: 'expense', who: 'Teamkasse', note: 'Test', amount: -1.005, runningTotal: -1.005 }, ]); expect(csv).toBe( 'Datum;Typ;Wer;Notiz;Betrag;Periodensaldo\r\n' + '2026-08-05;Ausgabe;Teamkasse;Test;-1,01;-1,01', ); }); }); describe('buildPdf', () => { it('produces a non-empty valid PDF buffer', async () => { const buffer = await buildPdf( { name: 'Team A' } as any, [ { date: '2026-08-05T00:00:00.000Z', type: 'payment', who: 'Alex Muster', note: 'Bar bezahlt', amount: 10, runningTotal: 10 }, ], '2026-08-01', '2026-08-31', ); expect(Buffer.isBuffer(buffer)).toBe(true); expect(buffer.length).toBeGreaterThan(100); expect(buffer.subarray(0, 5).toString('utf-8')).toBe('%PDF-'); }); it('still produces a valid PDF when there are no rows', async () => { const buffer = await buildPdf({ name: 'Team A' } as any, [], '2026-08-01', '2026-08-31'); expect(buffer.subarray(0, 5).toString('utf-8')).toBe('%PDF-'); }); });