Files
Webklar-Kundenbereich/test/auftraegePdfDaten.test.js
T

173 lines
11 KiB
JavaScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { useTestEnv } from './support/testEnv.js'
import { beispielAuftrag, beispielSettings } from './auftraegePdfHilfe.js'
useTestEnv({ appwriteUrl: 'http://127.0.0.1:1', stripeUrl: 'http://127.0.0.1:1' })
const { auftragPdfDaten, PDF_BESCHRIFTUNG, bereinigeTexte } = await import('../server/services/auftraege/pdf.js')
const { AB_TEXTE, WIDERRUFSBELEHRUNG_PROJEKT, WIDERRUFSFORMULAR } = await import('../server/services/auftraege/texte.js')
const { zahlungsplanText, aufwandText } = await import('../server/services/auftraege/auftragSicht.js')
const { AuftragError } = await import('../server/services/auftraege/fehler.js')
const { euroDe } = await import('../server/services/finance/format.js')
const JETZT = new Date('2026-10-06T10:00:00.000Z')
const NBSP = String.fromCodePoint(0xa0)
const FRUEHBEGINN_SATZ = 'Ohne Ihr ausdrückliches Verlangen beginnen wir erst nach Ablauf der Widerrufsfrist.' // V7: AB_TEXTE.termine bei Privatpersonen
const daten = (extra = {}, optionen = {}) => auftragPdfDaten(beispielAuftrag(extra), { settings: beispielSettings, jetzt: JETZT, ...optionen })
const aufwandAuftrag = { art: 'aufwand', positionen: [], summeCents: null, stundensatzCents: 10000, schaetzungMinuten: 600, zahlungsplan: { art: 'monatlich', prozent: null, text: '' } }
test('euroDe liefert Beträge mit geschütztem Leerzeichen vor dem €-Zeichen (Grundlage aller Betragsvergleiche hier)', () => {
assert.equal(euroDe(60000), `600,00${NBSP}€`)
})
test('Festpreis: Kopf, Anschrift, Angaben und Positionen sind fertig formatiert', () => {
const d = daten()
assert.equal(d.vorschau, false)
assert.equal(d.titel, AB_TEXTE.titel)
assert.equal(d.nummer, 'AB-2026-0007')
assert.equal(d.datum, '06.10.2026')
assert.equal(d.kundennummer, 'K-1042')
assert.equal(d.projekt, 'Neue Website Müller Bäckerei')
assert.deepEqual(d.absender.zeilen, [
'WEBklar GbR', 'Am Schwimmbad 10', '67722 Winnweiler', 'Telefon 0176 23726355', 'support@webklar.com', 'webklar.com', 'Steuernummer 12/345/67890',
])
assert.equal(d.absender.rueckzeile, 'WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler')
assert.deepEqual(d.empfaenger, ['Müller Bäckerei GmbH', 'Anna Müller', 'Hauptstraße 12', '67722 Winnweiler'])
assert.equal(d.einleitung, AB_TEXTE.einleitung({ datum: '05.10.2026', weg: 'telefonisch' }))
assert.ok(d.einleitung.includes('05.10.2026') && d.einleitung.includes('telefonisch'))
assert.equal(d.leistung.art, 'festpreis')
assert.equal(d.leistung.ueberschrift, 'Leistungsumfang')
assert.deepEqual(d.leistung.positionen, [
{ nr: '1', beschreibung: 'Gestaltung und Umsetzung der Website (5 Seiten)', menge: '1', einzelpreis: euroDe(60000), summe: euroDe(60000) },
{ nr: '2', beschreibung: 'Kontaktformular mit E-Mail-Versand', menge: '2', einzelpreis: euroDe(10000), summe: euroDe(20000) },
])
assert.deepEqual(d.leistung.gesamt, { label: 'Gesamtpreis', betrag: euroDe(80000) })
assert.equal(d.leistung.aufwandText, null)
assert.equal(d.kleinunternehmer, 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.')
assert.deepEqual(PDF_BESCHRIFTUNG.spalten, ['Pos.', 'Beschreibung', 'Menge', 'Einzelpreis', 'Summe'])
})
test('Datum der AB ist der Berliner Tag von gesendetAm, ohne gesendetAm der heutige Berliner Tag', () => {
assert.equal(daten({ gesendetAm: '2026-10-06T22:30:00.000Z' }).datum, '07.10.2026')
assert.equal(daten({ gesendetAm: '2026-10-06T21:30:00+00:00' }).datum, '06.10.2026')
assert.equal(daten({ gesendetAm: null }, { jetzt: new Date('2026-12-31T23:30:00.000Z') }).datum, '01.01.2027')
})
test('Aufwand: Aufwandsregel statt Positionen, keine Gesamtsumme', () => {
const auftrag = beispielAuftrag(aufwandAuftrag)
const d = auftragPdfDaten(auftrag, { settings: beispielSettings, jetzt: JETZT })
assert.equal(d.leistung.art, 'aufwand')
assert.deepEqual(d.leistung.positionen, [])
assert.equal(d.leistung.gesamt, null)
assert.equal(d.leistung.aufwandText, aufwandText(auftrag))
assert.ok(d.leistung.aufwandText.includes(euroDe(10000)))
assert.equal(d.kleinunternehmer, AB_TEXTE.kleinunternehmer)
})
test('Vorschau: Nummer „Entwurf“, heutiges Datum, Kennzeichnung; ohne Vorschau braucht die AB eine Nummer', () => {
const entwurf = { nummer: '', gesendetAm: null }
const d = daten(entwurf, { vorschau: true })
assert.equal(d.vorschau, true)
assert.equal(d.nummer, 'Entwurf')
assert.equal(d.datum, '06.10.2026')
assert.equal(d.beschriftung.entwurf, 'ENTWURF')
assert.throws(() => daten(entwurf), (e) => e instanceof AuftragError && e.status === 409 && /Nummer/.test(e.message))
})
test('Anschrift: Firma und Name nur einmal, Land nur außerhalb Deutschlands, leere Teile entfallen', () => {
const kunde = (extra) => ({ ...beispielAuftrag().kunde, ...extra })
assert.deepEqual(daten({ kunde: kunde({ firma: 'Anna Müller' }) }).empfaenger, ['Anna Müller', 'Hauptstraße 12', '67722 Winnweiler'])
assert.deepEqual(daten({ kunde: kunde({ land: 'Österreich', plz: '1010', ort: 'Wien' }) }).empfaenger, ['Müller Bäckerei GmbH', 'Anna Müller', 'Hauptstraße 12', '1010 Wien', 'Österreich'])
assert.deepEqual(daten({ kunde: kunde({ firma: '', land: '', strasse: '' }) }).empfaenger, ['Anna Müller', '67722 Winnweiler'])
})
test('Fehlende Pflichtangaben und falsche Summe: AuftragError 409 mit Klartext statt kaputtem PDF', () => {
const faelle = [
[{ kunde: null }, /Kundendaten/],
[{ titel: ' ' }, /Titel/],
[{ auftragVom: '' }, /Datum der Absprache/],
[{ auftragWeg: 'brieftaube' }, /Weg der Absprache/],
[{ art: 'pauschal' }, /Art des Auftrags/],
[{ zahlungsplan: null }, /Zahlungsplan/],
[{ positionen: [], summeCents: 0 }, /Positionen/],
[{ summeCents: 79999 }, /Summe der Positionen/],
[{ ...aufwandAuftrag, stundensatzCents: null }, /Stundensatz/],
]
for (const [extra, muster] of faelle) {
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)
})