diff --git a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts index a04288a..097d559 100644 --- a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts +++ b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts @@ -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([ diff --git a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts index 3ce5886..6382d25 100644 --- a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts +++ b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts @@ -62,10 +62,49 @@ export function buildRows(team: Team, from: string, to: string): CashboxExportRo }); } +export interface CashboxReceivableRow { + date: string; + type: string; + who: string; + note: string; + amount: number; +} + +const RECEIVABLE_TYPES = new Set(['fine', 'levy', 'fee']); + +export function buildReceivableRows(team: Team, from: string, to: string): CashboxReceivableRow[] { + const fromTime = new Date(`${from}T00:00:00.000Z`).getTime(); + const toTime = new Date(`${to}T23:59:59.999Z`).getTime(); + + const raw: CashboxReceivableRow[] = []; + for (const player of team.players ?? []) { + for (const transaction of player.transactions ?? []) { + if (!transaction.type || !RECEIVABLE_TYPES.has(transaction.type.name)) continue; + raw.push({ + date: transaction.date, + type: transaction.type.name, + who: `${player.firstName} ${player.lastName}`, + note: transaction.note, + amount: Number(transaction.amount), + }); + } + } + + return raw + .filter((row) => { + const time = new Date(row.date).getTime(); + return time >= fromTime && time <= toTime; + }) + .sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0)); +} + const TYPE_LABELS: Record = { payment: 'Zahlung', credit: 'Guthaben', expense: 'Ausgabe', + fine: 'Strafe', + levy: 'Umlage', + fee: 'Gebühr', }; function formatGermanAmount(value: number): string {