148 lines
8.7 KiB
JavaScript
148 lines
8.7 KiB
JavaScript
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' }))))
|
||
})
|