Files

148 lines
8.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { useTestEnv } from './support/testEnv.js'
import { alleTexte, beispielAuftrag, beispielSettings, leseSeiten, seitenZahl } from './auftraegePdfHilfe.js'
useTestEnv({ appwriteUrl: 'http://127.0.0.1:1', stripeUrl: 'http://127.0.0.1:1' })
const { auftragPdfDaten, erzeugeAuftragPdf, bereinigeTexte } = await import('../server/services/auftraege/pdf.js')
const { AB_TEXTE, WIDERRUFSBELEHRUNG_PROJEKT, WIDERRUFSFORMULAR } = await import('../server/services/auftraege/texte.js')
const { 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 cp = (...codes) => String.fromCodePoint(...codes)
const FRUEHBEGINN_SATZ = 'Ohne Ihr ausdrückliches Verlangen beginnen wir erst nach Ablauf der Widerrufsfrist.' // V7: AB_TEXTE.termine bei Privatpersonen
const optionen = (extra = {}) => ({ settings: beispielSettings, jetzt: JETZT, ...extra })
const ohneLeerraum = (text) => text.replace(/\s+/g, '')
const gesamtText = (pdf) => leseSeiten(pdf).flat().join('\n')
const privat = { kunde: { ...beispielAuftrag().kunde, firma: '', kundenart: 'privat' } }
const aufwand = { art: 'aufwand', positionen: [], summeCents: null, stundensatzCents: 10000, schaetzungMinuten: 600, zahlungsplan: { art: 'monatlich', prozent: null, text: '' } }
/** Alles, was in den Daten steht und nicht nur Beschriftung oder Schlüssel ist, muss im PDF-Text vorkommen. */
function pruefeAlleTexteImPdf(pdf, auftrag, extra = {}) {
const daten = bereinigeTexte(auftragPdfDaten(auftrag, optionen(extra)))
const imPdf = ohneLeerraum(gesamtText(pdf))
const erwartet = alleTexte({ ...daten, beschriftung: null })
assert.ok(erwartet.length > 20)
for (const text of erwartet) assert.ok(imPdf.includes(ohneLeerraum(text)), `fehlt im PDF: ${text.slice(0, 70)}`)
}
test('Festpreis-AB: gültiges A4-PDF, jede Angabe aus den Daten steht im Text', async () => {
const auftrag = beispielAuftrag()
const pdf = await erzeugeAuftragPdf(auftrag, optionen())
assert.ok(Buffer.isBuffer(pdf))
assert.equal(pdf.subarray(0, 5).toString('latin1'), '%PDF-')
assert.equal(pdf.subarray(-6).toString('latin1'), '%%EOF\n')
assert.match(pdf.toString('latin1'), /\/MediaBox \[0 0 595\.28 841\.89\]/)
const n = seitenZahl(pdf)
assert.ok(n >= 1)
assert.equal(leseSeiten(pdf).length, n)
pruefeAlleTexteImPdf(pdf, auftrag)
const text = gesamtText(pdf)
for (const erwartet of ['AB-2026-0007', '06.10.2026', 'K-1042', 'Steuernummer 12/345/67890', euroDe(60000), euroDe(80000), `Seite 1 von ${n}`]) {
assert.ok(text.includes(erwartet), erwartet)
}
assert.equal(text.includes('ENTWURF'), false)
assert.equal(text.includes('\u{FFFD}'), false)
})
test('Aufwand-AB: Aufwandsregel statt Tabelle', async () => {
const auftrag = beispielAuftrag(aufwand)
const pdf = await erzeugeAuftragPdf(auftrag, optionen())
pruefeAlleTexteImPdf(pdf, auftrag)
const text = gesamtText(pdf)
assert.ok(ohneLeerraum(text).includes(ohneLeerraum(aufwandText(auftrag))))
assert.equal(text.includes('Einzelpreis'), false)
assert.equal(text.includes('Gesamtpreis'), false)
})
test('Privatperson: Widerrufsbelehrung und Muster-Widerrufsformular auf eigenen Seiten nach der AB', async () => {
const auftrag = beispielAuftrag(privat)
const pdf = await erzeugeAuftragPdf(auftrag, optionen())
const seiten = leseSeiten(pdf)
const texte = seiten.map((zeilen) => zeilen.join('\n'))
const index = seiten.findIndex((zeilen) => zeilen.includes(WIDERRUFSBELEHRUNG_PROJEKT.titel))
assert.ok(index >= 1, 'die Belehrung steht nicht auf Seite 1')
assert.equal(seiten[index][2], WIDERRUFSBELEHRUNG_PROJEKT.titel, 'sie beginnt oben, direkt unter dem Seitenkopf (WEBklar, Titel und Nummer)')
assert.ok(texte.slice(index).join('\n').includes(WIDERRUFSFORMULAR.titel))
assert.ok(ohneLeerraum(texte.join('')).includes(ohneLeerraum(FRUEHBEGINN_SATZ)), 'Satz zum Beginn nach Ablauf der Widerrufsfrist')
pruefeAlleTexteImPdf(pdf, auftrag)
texte.forEach((text, i) => assert.ok(text.includes(`Seite ${i + 1} von ${texte.length}`)))
const unternehmen = gesamtText(await erzeugeAuftragPdf(beispielAuftrag(), optionen()))
assert.equal(unternehmen.includes(WIDERRUFSFORMULAR.titel), false)
assert.equal(ohneLeerraum(unternehmen).includes(ohneLeerraum(FRUEHBEGINN_SATZ)), false, 'bei Unternehmen steht der Satz nicht im PDF')
})
test('Umlaute, ß, €, Minuszeichen, Gedankenstrich und Anführungszeichen kommen lesbar im PDF an', async () => {
const name = `Jörg Müßig${cp(0x2212)}Åström`
const titel = `Übergabe „Größe“ ${cp(0x2013)} 3${cp(0xa0)}€ ${cp(0x2212)}5${cp(0xa0)}€`
const auftrag = beispielAuftrag({ titel, kunde: { ...beispielAuftrag().kunde, name, firma: '' } })
const pdf = await erzeugeAuftragPdf(auftrag, optionen())
const text = gesamtText(pdf)
assert.ok(text.includes(name), name)
assert.ok(ohneLeerraum(text).includes(ohneLeerraum(titel)), titel)
assert.equal(text.includes('\u{FFFD}'), false)
})
test('Zeichen ohne Glyph werden ersetzt statt als Kästchen gedruckt', async () => {
const auftrag = beispielAuftrag({ kunde: { ...beispielAuftrag().kunde, firma: '', name: `${cp(0x411, 0x43e, 0x440)} Łukasz`, strasse: `Hauptstraße${cp(0x202f)}12` } })
const text = gesamtText(await erzeugeAuftragPdf(auftrag, optionen()))
assert.ok(text.includes('??? Łukasz'))
assert.ok(text.includes('Hauptstraße 12'))
assert.equal(text.includes('\u{FFFD}'), false)
})
test('20 Positionen mit 200 Zeichen ohne Leerzeichen: mehrere Seiten, Tabellenkopf auf jeder Seite mit Positionen, nichts geht verloren', async () => {
const baustein = 'ÄÖÜäöü߀' + cp(0x2212)
const positionen = Array.from({ length: 20 }, (_, i) => ({ beschreibung: `P${i + 1}${baustein.repeat(30)}`.slice(0, 200), menge: i + 1, einzelpreisCents: 1000 + i }))
const summeCents = positionen.reduce((s, p) => s + p.menge * p.einzelpreisCents, 0)
const auftrag = beispielAuftrag({ positionen, summeCents, zahlungsplan: { art: 'nach_abnahme', prozent: null, text: '' } })
const pdf = await erzeugeAuftragPdf(auftrag, optionen())
const n = seitenZahl(pdf)
const seiten = leseSeiten(pdf).map((zeilen) => zeilen.join('\n'))
assert.ok(n >= 2, `Seiten: ${n}`)
assert.equal(seiten.length, n)
const alles = ohneLeerraum(seiten.join('\n'))
for (const p of positionen) assert.ok(alles.includes(ohneLeerraum(p.beschreibung)), p.beschreibung.slice(0, 12))
assert.ok(alles.includes(ohneLeerraum(euroDe(summeCents))), 'Gesamtpreis')
seiten.forEach((text, i) => {
if (/P\d+/.test(text)) assert.ok(text.includes('Beschreibung') && text.includes('Einzelpreis'), `Seite ${i + 1} ohne Tabellenkopf`)
assert.ok(text.includes(`Seite ${i + 1} von ${n}`))
assert.equal(text.includes('\u{FFFD}'), false)
})
assert.ok(seiten.find((text) => text.includes('P20')).includes('Gesamtpreis'), 'Summe steht auf der Seite der letzten Position')
})
test('mehrere PDFs gleichzeitig: Schriften sind geteilt, nichts vermischt sich', async () => {
const nummern = [1, 2, 3, 4, 5].map((i) => `AB-2026-000${i}`)
const pdfs = await Promise.all(nummern.map((nummer) => erzeugeAuftragPdf(beispielAuftrag({ nummer }), optionen())))
pdfs.forEach((pdf, i) => {
const text = gesamtText(pdf)
assert.ok(text.includes(nummern[i]))
assert.equal(nummern.filter((n) => text.includes(n)).length, 1)
})
})
test('unvollständige AB: AuftragError 409 statt halbem PDF', async () => {
await assert.rejects(erzeugeAuftragPdf(beispielAuftrag({ titel: '' }), optionen()), (e) => e instanceof AuftragError && e.status === 409)
await assert.rejects(erzeugeAuftragPdf(beispielAuftrag({ kunde: null }), optionen({ vorschau: true })), (e) => e instanceof AuftragError && e.status === 409)
})
test('Vorschau: ENTWURF und Hinweis auf jeder Seite, Nummer „Entwurf“', async () => {
const pdf = await erzeugeAuftragPdf(beispielAuftrag({ nummer: '', gesendetAm: null }), optionen({ vorschau: true }))
const seiten = leseSeiten(pdf)
assert.ok(seiten.length >= 1)
for (const zeilen of seiten) {
const text = zeilen.join('\n')
assert.ok(text.includes('ENTWURF'))
assert.ok(text.includes('Entwurf – Vorschau, noch keine gültige Auftragsbestätigung'))
}
assert.ok(gesamtText(pdf).includes('Entwurf'))
})
test('ersetzte AB: Hinweis mit Nummer und Datum der alten AB im PDF', async () => {
const pdf = await erzeugeAuftragPdf(beispielAuftrag(), optionen({ ersetzt: { nummer: 'AB-2026-0003', datum: '2026-10-01' } }))
assert.ok(ohneLeerraum(gesamtText(pdf)).includes(ohneLeerraum(AB_TEXTE.ersetzt({ nummer: 'AB-2026-0003', datum: '01.10.2026' }))))
})