From 4185efb83af9b14e341279985e29cf1c7a5eb0f5 Mon Sep 17 00:00:00 2001 From: Bastian Wagner Date: Tue, 4 Aug 2026 13:26:45 +0200 Subject: [PATCH] fix: negate expense amounts and stop blank trailing pages in cashbox PDF export Expenses were stored as positive amounts (DB convention) and buildRows() never negated them, so they were added to the running budget total instead of subtracted. Negate expense amounts for team-wallet transactions, mirroring the existing signedFlowAmount() convention in teams.service.ts. Separately, addFooters() placed footer text inside the reserved bottom margin without an explicit height option, which made pdfkit's LineWrapper treat every footer draw as overflowing the page and call continueOnNewPage() twice per page - inflating page counts 3x with blank trailing pages. Bounding the footer text to its own small height box prevents pdfkit's automatic pagination from firing. Co-Authored-By: Claude Sonnet 5 --- .../cashbox-export.utils.spec.ts | 26 +++++++++++++++++-- .../cashbox-export/cashbox-export.utils.ts | 5 +++- 2 files changed, 28 insertions(+), 3 deletions(-) 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 af2fbe6..028c08b 100644 --- a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts +++ b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.spec.ts @@ -1,5 +1,11 @@ import { buildCsv, buildPdf, buildReceivableRows, buildRows } from './cashbox-export.utils'; +function pdfPageCount(buffer: Buffer): number { + const match = buffer.toString('latin1').match(/\/Type\s*\/Pages[\s\S]{0,80}?\/Count\s+(\d+)/); + if (!match) throw new Error('Could not find page count in PDF buffer'); + return Number(match[1]); +} + describe('buildRows', () => { const team = (overrides: Partial<{ transactions: any[]; players: any[] }> = {}) => ({ id: 5, @@ -10,12 +16,15 @@ describe('buildRows', () => { ...overrides, }); - it('includes team-wallet credit and expense rows as "Teamkasse"', () => { + it('includes team-wallet credit rows as-is and negates expense rows so they reduce the budget', () => { + // DB stores TeamWalletTransaction.amount as a positive number even for + // expenses (see team-wallet-transaction.entity.ts setBalance()); buildRows + // must negate expenses itself so they subtract from the running total. 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' } }, + { date: '2026-08-10T00:00:00.000Z', amount: 20, note: 'Bälle', type: { name: 'expense' } }, ], }) as any, '2026-08-01', @@ -297,6 +306,19 @@ describe('buildPdf', () => { 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-'); + expect(pdfPageCount(buffer)).toBe(1); + }); + + it('does not append blank trailing pages when content fits on a single page', async () => { + const buffer = await buildPdf( + { name: 'Team A' } as any, + [cashRow], + [receivableRow], + '2026-08-01', + '2026-08-31', + ); + + expect(pdfPageCount(buffer)).toBe(1); }); it('paginates correctly and stays a valid PDF for many rows', async () => { diff --git a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts index 0b8d872..8f1a060 100644 --- a/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts +++ b/myteamwallet_backend/src/cashbox-export/cashbox-export.utils.ts @@ -26,12 +26,13 @@ export function buildRows(team: Team, from: string, to: string): CashboxExportRo for (const transaction of team.transactions ?? []) { if (!transaction.type) continue; + const amount = Number(transaction.amount); raw.push({ date: transaction.date, type: transaction.type.name, who: 'Teamkasse', note: transaction.note, - amount: Number(transaction.amount), + amount: transaction.type.name === 'expense' ? -amount : amount, }); } @@ -318,6 +319,7 @@ function addFooters(doc: PDFKit.PDFDocument, teamName: string): void { .fillColor(COLORS.footerText) .text(`${teamName} – Kassenbuch-Report, erstellt am ${generatedAt}`, PAGE_MARGIN, footerY, { width: doc.page.width - PAGE_MARGIN * 2 - 60, + height: 20, lineBreak: false, }); doc @@ -325,6 +327,7 @@ function addFooters(doc: PDFKit.PDFDocument, teamName: string): void { .fillColor(COLORS.footerText) .text(`Seite ${i - range.start + 1} von ${range.count}`, doc.page.width - PAGE_MARGIN - 60, footerY, { width: 60, + height: 20, align: 'right', lineBreak: false, });