feat: add buildReceivableRows for fine/levy/fee entries

Cashbox export previously only saw payment transactions. This adds
the query for fine/levy/fee entries (Forderungen) that the redesigned
PDF report will show in a separate section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Bastian Wagner
2026-08-04 10:22:59 +02:00
parent 0bad154971
commit 7b499b361f
2 changed files with 121 additions and 1 deletions

View File

@@ -1,4 +1,4 @@
import { buildCsv, buildPdf, buildRows } from './cashbox-export.utils';
import { buildCsv, buildPdf, buildReceivableRows, buildRows } from './cashbox-export.utils';
describe('buildRows', () => {
const team = (overrides: Partial<{ transactions: any[]; players: any[] }> = {}) => ({
@@ -102,6 +102,87 @@ describe('buildRows', () => {
});
});
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([