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:
@@ -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([
|
||||
|
||||
@@ -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<string, string> = {
|
||||
payment: 'Zahlung',
|
||||
credit: 'Guthaben',
|
||||
expense: 'Ausgabe',
|
||||
fine: 'Strafe',
|
||||
levy: 'Umlage',
|
||||
fee: 'Gebühr',
|
||||
};
|
||||
|
||||
function formatGermanAmount(value: number): string {
|
||||
|
||||
Reference in New Issue
Block a user