AB-PDF: Bausteine der Auftragsbestätigung mit Höhen und Zusammenhalt

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 14:13:02 +00:00
1 parent 4706f232b5
commit f71e3e8345
2 files changed
+125

No files matched your search

+68
View File
@@ -74,3 +74,71 @@ test('paginiere: Kette größer als eine Seite wird zwischen Bausteinen getrennt
assert.deepEqual(lage(paginiere(kette, SEITEN)), [['a@0'], ['b@20'], ['c@20']])
assert.deepEqual(lage(paginiere([baustein('a', 50), baustein('riesig', 500), baustein('b', 50)], SEITEN)), [['a@0'], ['riesig@20'], ['b@20']])
})
// Messfunktion ohne pdfkit: 10 pt je angefangene 60 Zeichen
const messe = (text) => 10 * Math.max(1, Math.ceil(String(text).length / 60))
const bausteine = (extra = {}, optionen = {}) => baueAtome(auftragPdfDaten(beispielAuftrag(extra), { settings: beispielSettings, ...optionen }), messe)
const ids = (atome) => atome.map((a) => a.id)
test('baueAtome Festpreis: Reihenfolge, Zeilenhöhen und Zusammenhalt der Tabelle', () => {
const atome = bausteine()
assert.deepEqual(ids(atome), [
'briefkopf', 'einleitung', 'leistung', 'tabellenkopf', 'zeile-1', 'zeile-2', 'summe', 'kleinunternehmer',
'zahlung-kopf', 'zahlung', 'termine-kopf', 'termine', 'mitwirkung-kopf', 'mitwirkung', 'agb', 'gruss',
])
const nach = (id) => atome.find((a) => a.id === id)
assert.equal(nach('briefkopf').typ, 'briefkopf')
assert.ok(nach('briefkopf').hoehe > 150)
assert.equal(nach('briefkopf').masse.infoZeilen.length, 3)
assert.equal(nach('zeile-1').hoehe, 10 + 12, 'eine Zeile Text plus 2 × 6 pt Rand')
assert.deepEqual([nach('leistung'), nach('tabellenkopf'), nach('zeile-1'), nach('zeile-2'), nach('summe'), nach('kleinunternehmer')].map((a) => a.haengtAnNaechstem), [true, true, false, true, true, false])
assert.equal(nach('zeile-1').kopf, nach('tabellenkopf'))
assert.equal(nach('zeile-2').kopf, nach('tabellenkopf'))
assert.equal(nach('summe').label, 'Gesamtpreis')
assert.ok(atome.every((a) => a.hoehe > 0 && Number.isFinite(a.hoehe)))
})
test('baueAtome Aufwand, ersetzt und Privatperson: nur die passenden Bausteine', () => {
const aufwand = bausteine({ art: 'aufwand', positionen: [], summeCents: null, stundensatzCents: 10000, schaetzungMinuten: 600, zahlungsplan: { art: 'monatlich', prozent: null, text: '' } })
assert.ok(ids(aufwand).includes('aufwand') && !ids(aufwand).includes('tabellenkopf') && !ids(aufwand).includes('summe'))
assert.equal(aufwand.find((a) => a.id === 'aufwand').kasten, true)
const mitErsetzt = bausteine({}, { ersetzt: { nummer: 'AB-2026-0003', datum: '2026-10-01' } })
assert.ok(ids(mitErsetzt).indexOf('ersetzt') > ids(mitErsetzt).indexOf('agb') && ids(mitErsetzt).indexOf('ersetzt') < ids(mitErsetzt).indexOf('gruss'))
assert.equal(ids(bausteine()).some((id) => /^(widerruf|formular)/.test(id)), false)
const privat = bausteine({ kunde: { ...beispielAuftrag().kunde, kundenart: 'privat' } })
const titel = privat.find((a) => a.id === 'widerruf-titel')
assert.equal(titel.umbruchDavor, true)
assert.ok(ids(privat).indexOf('formular-titel') > ids(privat).indexOf('widerruf-titel'))
assert.equal(privat.find((a) => a.id === 'formular-titel').haengtAnNaechstem, true)
assert.equal(privat.at(-1).haengtAnNaechstem, false)
})
test('20 Positionen mit je 200 Zeichen ohne Leerzeichen: alle Zeilen da, mehrere Seiten, Tabellenkopf auf jeder Folgeseite', () => {
const positionen = Array.from({ length: 20 }, (_, i) => ({ beschreibung: 'X'.repeat(200), menge: i + 1, einzelpreisCents: 1000 }))
const summe = positionen.reduce((s, p) => s + p.menge * p.einzelpreisCents, 0)
const seiten = paginiere(bausteine({ positionen, summeCents: summe }), BEREICHE)
assert.ok(seiten.length >= 2)
const zeilen = seiten.flatMap((s) => s.posten.filter((p) => p.atom.typ === 'zeile').map((p) => p.atom.id))
assert.deepEqual(zeilen, Array.from({ length: 20 }, (_, i) => `zeile-${i + 1}`))
for (const seite of seiten.slice(1)) {
const ersteZeile = seite.posten.findIndex((p) => p.atom.typ === 'zeile')
if (ersteZeile >= 0) assert.equal(seite.posten[0].atom.typ, 'tabellenkopf', `Seite ${seite.nummer} beginnt mit dem Tabellenkopf`)
}
const letzte = seiten.at(-1).posten.map((p) => p.atom.id)
assert.ok(letzte.indexOf('summe') > letzte.indexOf('zeile-20'), 'Summe steht bei der letzten Zeile')
for (const seite of seiten) for (const p of seite.posten) assert.ok(p.y + p.atom.hoehe <= seite.unten + 1e-9)
})
test('lange Widerrufsbelehrung: beginnt auf einer neuen Seite, das Formular bleibt zusammen', () => {
const absatz = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(12)
const d = auftragPdfDaten(beispielAuftrag({ kunde: { ...beispielAuftrag().kunde, kundenart: 'privat' } }), { settings: beispielSettings })
d.widerruf = { ...d.widerruf, belehrung: { titel: 'Widerrufsbelehrung', abschnitte: Array.from({ length: 6 }, (_, i) => ({ ueberschrift: `Abschnitt ${i + 1}`, absaetze: [absatz, absatz] })) } }
const seiten = paginiere(baueAtome(d, messe), BEREICHE)
const seiteVon = (id) => seiten.find((s) => s.posten.some((p) => p.atom.id === id)).nummer
assert.ok(seiten.length >= 4)
assert.ok(seiteVon('widerruf-titel') >= 2)
assert.equal(seiten[seiteVon('widerruf-titel') - 1].posten.find((p) => !p.wiederholt).atom.id, 'widerruf-titel', 'Belehrung beginnt oben auf einer neuen Seite')
const formular = seiten.flatMap((s) => s.posten.filter((p) => p.atom.id.startsWith('formular')).map(() => s.nummer))
assert.ok(formular.length >= 2 && new Set(formular).size === 1, 'alle Zeilen des Formulars stehen auf derselben Seite')
for (const seite of seiten) for (const p of seite.posten) assert.ok(p.y + p.atom.hoehe <= seite.unten + 1e-9)
})