import { test } from 'node:test' import assert from 'node:assert/strict' import { PdfDoc } from '../server/services/unterkunft/pdf.js' import { rechnungPdf } from '../server/services/unterkunft/rechnungPdf.js' // Buchungskalender Task 19: PDF-Writer und Rechnungs-PDF const latin = (b) => b.toString('latin1') function inhalt(extra = {}) { return { aussteller: { firma: 'Haus Seeblick', inhaber: 'Anna Muster', strasse: 'Seestr. 1', plz: '88662', ort: 'Ueberlingen', email: 'a@x.de', telefon: '0123', steuernummer: '12/345/67890', ustId: '', iban: 'DE02 1203 0000 0000 2020 51', bic: 'BYLADEM1001', bank: 'DKB' }, empfaenger: { name: 'Max Gast', firma: '', strasse: 'Weg 2', plz: '10115', ort: 'Berlin', land: 'Deutschland' }, unterkunft: { name: 'Ferienwohnung Süd' }, von: '2026-11-12', bis: '2026-11-15', naechte: 3, personen: 2, posten: [{ text: 'Übernachtung (Frühstück), 3 Nächte', menge: 3, einzelCent: 9000, ustProzent: 7 }, { text: 'Endreinigung', menge: 1, einzelCent: 5000, ustProzent: 19 }], summen: { bruttoCent: 32000, nettoCent: 29000, ust: [{ satz: 7, nettoCent: 25234, steuerCent: 1766 }, { satz: 19, nettoCent: 4202, steuerCent: 798 }], steuerart: 'regel' }, steuerart: 'regel', zahlart: 'vorort', bezugNummer: '', anfrageNummer: 'A-1', ...extra, } } const kleinInhalt = () => inhalt({ steuerart: 'kleinunternehmer', posten: [{ text: 'Übernachtung, 3 Nächte', menge: 3, einzelCent: 9000, ustProzent: 0 }], summen: { bruttoCent: 27000, nettoCent: 27000, ust: [], steuerart: 'kleinunternehmer' }, }) test('PdfDoc: Struktur, xref, Kopf und Ende', () => { const d = new PdfDoc() d.text(50, 50, 'Hallo') const b = d.toBuffer() const s = latin(b) assert.ok(s.startsWith('%PDF-1.4')) assert.ok(s.endsWith('%%EOF\n')) const start = Number(/startxref\n(\d+)\n/.exec(s)[1]) assert.equal(s.slice(start, start + 4), 'xref') const eintraege = s.slice(start).split('\n').filter((z) => /^\d{10} 00000 n $/.test(z)) assert.ok(eintraege.length >= 6) eintraege.forEach((z, k) => { const off = Number(z.slice(0, 10)) assert.ok(s.slice(off).startsWith(`${k + 1} 0 obj`), `Objekt ${k + 1}`) }) }) test('PdfDoc: CP1252, Euro, Escaping, Minus', () => { const d = new PdfDoc() d.text(10, 10, 'Übernachtung ß 5 € −3 (a) \\ ☃') const s = latin(d.toBuffer()) assert.ok(s.includes('\xdcbernachtung \xdf 5 \x80 -3 \\(a\\) \\\\ ?')) assert.ok(s.includes('/Encoding /WinAnsiEncoding')) }) test('PdfDoc: Breiten und Umbruch', () => { const d = new PdfDoc() assert.equal(Math.round(d.breite('Hello', 10) * 100), 2278) assert.ok(d.breite('Hello', 10, true) > d.breite('Hello', 10)) const z = d.umbrechen('eins zwei drei vier fünf sechs', 60, 10) assert.ok(z.length > 1) z.forEach((x) => assert.ok(d.breite(x, 10) <= 60)) assert.ok(d.umbrechen('Donaudampfschifffahrtsgesellschaft', 50, 10).length > 1) assert.ok(d.breite('x', 10) > 0 && d.breite('€', 10) > 0) }) test('Rechnung: Regelbesteuerung', () => { const s = latin(rechnungPdf(inhalt(), { art: 'rechnung', nummer: 'RE-2026-0001', datum: '2026-11-01' })) assert.ok(s.includes('RE-2026-0001')) assert.ok(s.includes('Enthaltene USt 7 %')) assert.ok(s.includes('Nettobetrag 19 %')) assert.ok(s.includes('12.11.2026')) assert.ok(s.includes('Beherbergung Ferienwohnung S\xfcd')) assert.ok(s.includes('Zahlung vor Ort')) assert.ok(!s.includes('Gem\xe4\xdf \xa7 19 UStG')) assert.ok(s.includes('Steuernummer 12/345/67890')) assert.ok(s.includes('IBAN')) }) test('Rechnung: Kleinunternehmer und Online-Zahlung', () => { const s = latin(rechnungPdf({ ...kleinInhalt(), zahlart: 'online' }, { art: 'rechnung', nummer: 'RE-2026-0002', datum: '2026-11-01' })) assert.ok(s.includes('Gem\xe4\xdf \xa7 19 UStG wird keine Umsatzsteuer berechnet.')) assert.ok(s.includes('Online-Zahlung')) assert.ok(!s.includes('Enthaltene USt')) }) test('Storno: Titel mit Bezug, negative Betraege', () => { const neg = kleinInhalt() neg.posten = neg.posten.map((p) => ({ ...p, einzelCent: -p.einzelCent })) neg.summen = { ...neg.summen, bruttoCent: -27000, nettoCent: -27000 } neg.bezugNummer = 'RE-2026-0002' const s = latin(rechnungPdf(neg, { art: 'storno', nummer: 'RE-2026-0003', datum: '2026-11-02' })) assert.ok(s.includes('Stornorechnung zu RE-2026-0002')) assert.ok(s.includes('-270,00 \x80')) }) test('Viele Posten: Seitenumbruch', () => { const posten = Array.from({ length: 40 }, (_, k) => ({ text: `Posten ${k + 1}`, menge: 1, einzelCent: 1000, ustProzent: 0 })) const s = latin(rechnungPdf({ ...kleinInhalt(), posten }, { art: 'rechnung', nummer: 'RE-2026-0004', datum: '2026-11-01' })) assert.ok(s.includes('/Count 2')) assert.ok(s.includes('Seite 2 von 2')) }) test('Deterministisch', () => { const o = { art: 'rechnung', nummer: 'RE-2026-0001', datum: '2026-11-01' } assert.ok(rechnungPdf(inhalt(), o).equals(rechnungPdf(inhalt(), o))) }) test('Beispiel-PDF schreiben (nur wenn Zielordner existiert)', async () => { const { mkdirSync, writeFileSync } = await import('node:fs') const dir = '/root/reports/buchungskalender-2026-10-07' if (process.env.UK_BEISPIEL_PDF !== '1') return mkdirSync(dir, { recursive: true }) writeFileSync(`${dir}/beispiel-rechnung.pdf`, rechnungPdf(inhalt(), { art: 'rechnung', nummer: 'RE-2026-0001', datum: '2026-11-01' })) })