diff --git a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts new file mode 100644 index 0000000..d3b3f3b --- /dev/null +++ b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts @@ -0,0 +1,75 @@ +import { 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([]); + }); +}); diff --git a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts new file mode 100644 index 0000000..0ca5776 --- /dev/null +++ b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts @@ -0,0 +1,61 @@ +import { Team } from 'src/teams/entities/team.entity'; + +export interface CashboxExportRow { + date: string; + type: string; + who: string; + note: string; + amount: number; + runningTotal: number; +} + +interface RawRow { + date: string; + type: string; + who: string; + note: string; + amount: number; +} + +export function buildRows(team: Team, from: string, to: string): CashboxExportRow[] { + const fromTime = new Date(`${from}T00:00:00.000Z`).getTime(); + const toTime = new Date(`${to}T23:59:59.999Z`).getTime(); + + const raw: RawRow[] = []; + + for (const transaction of team.transactions ?? []) { + raw.push({ + date: transaction.date, + type: transaction.type.name, + who: 'Teamkasse', + note: transaction.note, + amount: Number(transaction.amount), + }); + } + + for (const player of team.players ?? []) { + for (const transaction of player.transactions ?? []) { + if (transaction.type.name !== 'payment') continue; + raw.push({ + date: transaction.date, + type: transaction.type.name, + who: `${player.firstName} ${player.lastName}`, + note: transaction.note, + amount: Number(transaction.amount), + }); + } + } + + const filtered = 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)); + + let runningTotal = 0; + return filtered.map((row) => { + runningTotal += row.amount; + return { ...row, runningTotal }; + }); +}