From 731bc8c05e564099093ca8731948b85ef6263132 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 14:12:33 +0000 Subject: [PATCH] =?UTF-8?q?AB-PDF:=20Zahlungsplan,=20Termine,=20AGB-Satz,?= =?UTF-8?q?=20Privatperson-Seiten,=20Fu=C3=9F=20und=20Vorschau=20in=20den?= =?UTF-8?q?=20PDF-Daten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- server/services/auftraege/pdf.js | 22 ++++++++-- test/auftraegePdfDaten.test.js | 73 ++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 3 deletions(-) diff --git a/server/services/auftraege/pdf.js b/server/services/auftraege/pdf.js index 5d10632..235b941 100644 --- a/server/services/auftraege/pdf.js +++ b/server/services/auftraege/pdf.js @@ -3,9 +3,9 @@ import * as fontkitModul from 'fontkit' import { euroDe } from '../finance/format.js' import { berlinDate, formatDe } from '../finance/dates.js' import { AuftragError } from './fehler.js' -import { AUFTRAG_ARTEN, AUFTRAG_WEGE, ZAHLUNGSPLAN_ARTEN } from './konstanten.js' -import { AB_TEXTE, ABSENDER } from './texte.js' -import { aufwandText } from './auftragSicht.js' +import { AGB_FASSUNG, AUFTRAG_ARTEN, AUFTRAG_WEGE, ZAHLUNGSPLAN_ARTEN } from './konstanten.js' +import { AB_TEXTE, ABSENDER, WIDERRUFSBELEHRUNG_PROJEKT, WIDERRUFSFORMULAR } from './texte.js' +import { aufwandText, zahlungsplanText } from './auftragSicht.js' /** * PDF der Auftragsbestätigung (A4, pdfkit): Schriften und Zeichenabdeckung, Daten (alle Texte fertig formatiert), @@ -127,6 +127,13 @@ export function auftragPdfDaten(auftrag, { settings = {}, vorschau = false, jetz const b = PDF_BESCHRIFTUNG const steuernummer = String(settings.steuernummer ?? '').trim() const festpreis = auftrag.art === 'festpreis' + const privat = auftrag.kunde.kundenart === 'privat' + const termine = AB_TEXTE.termine({ start: auftrag.startAm ? formatDe(auftrag.startAm) : null, fertig: auftrag.fertigBis ? formatDe(auftrag.fertigBis) : null, privat }) + const abschnitte = [ + { id: 'zahlung', ueberschrift: b.zahlung, text: zahlungsplanText(auftrag) }, + ...(termine ? [{ id: 'termine', ueberschrift: b.termine, text: termine }] : []), + { id: 'mitwirkung', ueberschrift: b.mitwirkung, text: AB_TEXTE.mitwirkung }, + ] return { vorschau, beschriftung: b, @@ -154,6 +161,15 @@ export function auftragPdfDaten(auftrag, { settings = {}, vorschau = false, jetz } : { art: 'aufwand', ueberschrift: b.leistung, positionen: [], gesamt: null, aufwandText: aufwandText(auftrag) }, kleinunternehmer: AB_TEXTE.kleinunternehmer, + abschnitte, + agb: AB_TEXTE.agb({ fassung: formatDe(auftrag.agbFassung || AGB_FASSUNG) }), + ersetzt: ersetzt ? AB_TEXTE.ersetzt({ nummer: ersetzt.nummer, datum: formatDe(ersetzt.datum) }) : null, + gruss: AB_TEXTE.gruss, + widerruf: privat ? { belehrung: WIDERRUFSBELEHRUNG_PROJEKT, formular: WIDERRUFSFORMULAR } : null, + fuss: { + bank: zeilen(settings.bankDetailsText), + kontakt: [`${ABSENDER.name} · vertreten durch ${ABSENDER.vertretenDurch}`, `${b.telefon} ${ABSENDER.telefon} · ${ABSENDER.email} · ${ABSENDER.web}`], + }, } } diff --git a/test/auftraegePdfDaten.test.js b/test/auftraegePdfDaten.test.js index fdc2351..d2a616c 100644 --- a/test/auftraegePdfDaten.test.js +++ b/test/auftraegePdfDaten.test.js @@ -97,3 +97,76 @@ test('Fehlende Pflichtangaben und falsche Summe: AuftragError 409 mit Klartext s assert.throws(() => daten(extra, { vorschau: true }), (e) => e instanceof AuftragError && e.status === 409 && muster.test(e.message), JSON.stringify(extra)) } }) +test('Zahlungsplan Anzahlung nennt Anzahlung und Rest mit Beträgen und das Zahlungsziel', () => { + const auftrag = beispielAuftrag() + const text = daten().abschnitte.find((a) => a.id === 'zahlung').text + assert.equal(text, zahlungsplanText(auftrag)) + assert.equal(text.split(euroDe(40000)).length - 1, 2) + assert.ok(text.includes('50') && text.includes('14')) + const nachAufwand = beispielAuftrag(aufwandAuftrag) + assert.equal(auftragPdfDaten(nachAufwand, { settings: beispielSettings, jetzt: JETZT }).abschnitte[0].text, zahlungsplanText(nachAufwand)) + const dreissig = beispielAuftrag({ zahlungsplan: { art: 'anzahlung', prozent: 30, text: '' }, zahlungszielTage: 30 }) + const text30 = auftragPdfDaten(dreissig, { settings: beispielSettings, jetzt: JETZT }).abschnitte[0].text + assert.ok(text30.includes(euroDe(24000)) && text30.includes(euroDe(56000)) && text30.includes('30')) +}) + +test('Abschnitte: Zahlung, Termine (nur mit Datum), Mitwirkung und Abnahme; AGB-Satz mit Fassung', () => { + const d = daten() + assert.deepEqual(d.abschnitte.map((a) => [a.id, a.ueberschrift]), [['zahlung', 'Zahlung'], ['termine', 'Termine'], ['mitwirkung', 'Mitwirkung und Abnahme']]) + assert.equal(d.abschnitte[1].text, AB_TEXTE.termine({ start: '12.10.2026', fertig: '15.11.2026', privat: false })) + assert.equal(d.abschnitte[2].text, AB_TEXTE.mitwirkung) + assert.equal(d.agb, AB_TEXTE.agb({ fassung: '30.09.2026' })) + assert.equal(daten({ startAm: '', fertigBis: '' }).abschnitte.some((a) => a.id === 'termine'), false) + assert.equal(daten({ agbFassung: '' }).agb, AB_TEXTE.agb({ fassung: '30.09.2026' }), 'ohne agbFassung gilt AGB_FASSUNG') + assert.equal(daten().gruss, AB_TEXTE.gruss) +}) + +test('Termine: Privatpersonen bekommen den Satz zum Beginn nach Ablauf der Widerrufsfrist, auch ohne Daten; Unternehmen nicht', () => { + const privat = { kunde: { ...beispielAuftrag().kunde, firma: '', kundenart: 'privat' } } + const termine = (d) => d.abschnitte.find((a) => a.id === 'termine')?.text + const mitDaten = termine(daten(privat)) + assert.equal(mitDaten, AB_TEXTE.termine({ start: '12.10.2026', fertig: '15.11.2026', privat: true })) + assert.ok(mitDaten.includes(FRUEHBEGINN_SATZ) && mitDaten.includes('12.10.2026') && mitDaten.includes('15.11.2026')) + const ohneDaten = termine(daten({ ...privat, startAm: '', fertigBis: '' })) + assert.equal(ohneDaten, AB_TEXTE.termine({ start: null, fertig: null, privat: true })) + assert.ok(ohneDaten.includes(FRUEHBEGINN_SATZ), 'der Abschnitt „Termine“ steht auch ohne Start- und Fertigdatum') + assert.equal(termine(daten()).includes(FRUEHBEGINN_SATZ), false) + assert.equal(termine(daten({ startAm: '', fertigBis: '' })), undefined, 'Unternehmen ohne Daten: kein Abschnitt') +}) + +test('Privatperson: Widerrufsbelehrung und Muster-Widerrufsformular gehören dazu, bei Unternehmen nicht', () => { + assert.equal(daten().widerruf, null) + const privat = { kunde: { ...beispielAuftrag().kunde, firma: '', kundenart: 'privat' } } + const d = daten(privat) + assert.deepEqual(d.widerruf, { belehrung: WIDERRUFSBELEHRUNG_PROJEKT, formular: WIDERRUFSFORMULAR }) + assert.deepEqual(d.empfaenger, ['Anna Müller', 'Hauptstraße 12', '67722 Winnweiler']) +}) + +test('Ersetzte AB: Hinweis mit Nummer und Datum nur, wenn ersetzt übergeben wird', () => { + assert.equal(daten().ersetzt, null) + assert.equal(daten({}, { ersetzt: { nummer: 'AB-2026-0003', datum: '2026-10-01' } }).ersetzt, AB_TEXTE.ersetzt({ nummer: 'AB-2026-0003', datum: '01.10.2026' })) +}) + +test('Fuß: Bankverbindung zeilenweise, Kontakt; leere Angaben lassen ihre Zeilen weg', () => { + const d = daten() + assert.deepEqual(d.fuss.bank, ['Finom Bank', 'IBAN DE12 3456 7890 1234 5678 90', 'BIC FNOMDEB2']) + assert.deepEqual(d.fuss.kontakt, ['WEBklar GbR · vertreten durch Kenso Grimm und Justin Klein', 'Telefon 0176 23726355 · support@webklar.com · webklar.com']) + const leer = auftragPdfDaten(beispielAuftrag({ kunde: { ...beispielAuftrag().kunde, kundennummer: '' } }), { settings: {}, jetzt: JETZT }) + assert.deepEqual(leer.fuss.bank, []) + assert.equal(leer.kundennummer, '') + assert.equal(leer.absender.zeilen.at(-1), 'webklar.com', 'ohne Steuernummer entfällt die Zeile') + assert.equal(auftragPdfDaten(beispielAuftrag(), { jetzt: JETZT }).fuss.bank.length, 0, 'ohne settings') +}) + +test('Wächter: alle Texte, die ins PDF kommen, haben ein Glyph in Figtree (die Ersetzung greift dort nie ein)', () => { + const kunde = beispielAuftrag().kunde + const faelle = [ + daten({ kunde: { ...kunde, kundenart: 'privat' } }, { ersetzt: { nummer: 'AB-2026-0003', datum: '2026-10-01' } }), + daten({ kunde: { ...kunde, kundenart: 'privat' }, startAm: '', fertigBis: '' }), + daten({ zahlungsplan: { art: 'nach_abnahme', prozent: null, text: '' } }), + daten({ zahlungsplan: { art: 'frei', prozent: null, text: 'Zahlung in drei Raten.' } }), + daten(aufwandAuftrag), + daten({ ...aufwandAuftrag, schaetzungMinuten: null }), + ] + for (const d of faelle) assert.deepEqual(bereinigeTexte(d), d) +})