diff --git a/server/services/auftraege/pdf.js b/server/services/auftraege/pdf.js index 654bcff..0db6914 100644 --- a/server/services/auftraege/pdf.js +++ b/server/services/auftraege/pdf.js @@ -404,10 +404,25 @@ function zeichneFuss(doc, daten, nummer, gesamt) { schreibe(doc, `${b.seite} ${nummer} ${b.von} ${gesamt}`, RECHTS - 70, y + 8, STIL.fussLabel, { width: 70, align: 'right', lineBreak: false }) } +function zeichneWasserzeichen(doc, daten) { + const stil = { schrift: 'semibold', groesse: 110, lineGap: 0, farbe: FARBEN.entwurf } + setzeStil(doc, stil) + const breite = doc.widthOfString(daten.beschriftung.entwurf) + const mitteX = SEITE.breite / 2 + const mitteY = SEITE.hoehe / 2 + doc.save() + doc.fillOpacity(0.07) + doc.rotate(-35, { origin: [mitteX, mitteY] }) + doc.text(daten.beschriftung.entwurf, mitteX - breite / 2, mitteY - (stil.groesse * 1.2) / 2, { lineBreak: false }) + doc.restore() + schreibe(doc, daten.beschriftung.entwurfHinweis, SEITE.links, 22, { schrift: 'semibold', groesse: 8, lineGap: 0, farbe: FARBEN.entwurf }, { width: INHALT_BREITE, align: 'center', lineBreak: false }) +} + /** Zeichnet alle Seiten; die erste Seite legt pdfkit selbst an. */ function zeichneAuftrag(doc, daten, seiten, assets) { seiten.forEach((seite, index) => { if (index > 0) doc.addPage() + if (daten.vorschau) zeichneWasserzeichen(doc, daten) if (index > 0) zeichneFolgeKopf(doc, daten, assets) for (const posten of seite.posten) zeichneAtom(doc, daten, posten.atom, posten.y, assets) zeichneFuss(doc, daten, seite.nummer, seiten.length) @@ -416,9 +431,17 @@ function zeichneAuftrag(doc, daten, seiten, assets) { // ===== 5. Ablauf ===== +async function ladeErsetzte(id) { + const { getAuftrag } = await import('./auftragStore.js') + const alt = await getAuftrag(id) + if (!alt?.nummer || !alt.gesendetAm) throw new AuftragError(409, 'Die ersetzte Auftragsbestätigung wurde nicht gefunden.') + return { nummer: alt.nummer, datum: berlinDate(new Date(alt.gesendetAm)) } +} + /** Das AB-PDF (A4) als Buffer. */ -export async function erzeugeAuftragPdf(auftrag, { settings, vorschau = false, jetzt = new Date() } = {}) { - const daten = bereinigeTexte(auftragPdfDaten(auftrag, { settings, vorschau, jetzt })) +export async function erzeugeAuftragPdf(auftrag, { settings, vorschau = false, jetzt = new Date(), ersetzt } = {}) { + const alt = ersetzt !== undefined ? ersetzt : auftrag.ersetztId ? await ladeErsetzte(auftrag.ersetztId) : null + const daten = bereinigeTexte(auftragPdfDaten(auftrag, { settings, vorschau, jetzt, ersetzt: alt })) const assets = ladeSchriften() const doc = new PDFDocument({ size: 'A4', margins: { top: 0, bottom: 0, left: 0, right: 0 }, lang: 'de-DE', displayTitle: true, diff --git a/test/auftraegePdf.test.js b/test/auftraegePdf.test.js index 54b8a2e..955621e 100644 --- a/test/auftraegePdf.test.js +++ b/test/auftraegePdf.test.js @@ -128,3 +128,20 @@ 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' })))) +}) diff --git a/test/auftraegePdfErsetzt.test.js b/test/auftraegePdfErsetzt.test.js new file mode 100644 index 0000000..306e3a1 --- /dev/null +++ b/test/auftraegePdfErsetzt.test.js @@ -0,0 +1,35 @@ +import { test, after } from 'node:test' +import assert from 'node:assert/strict' +import { startAppwriteMock } from './support/appwriteMock.js' +import { schemaKeys } from './support/schema.js' +import { useTestEnv } from './support/testEnv.js' +import { beispielAuftrag, beispielSettings, leseSeiten } from './auftraegePdfHilfe.js' + +// Die alte AB liegt im Appwrite-Nachbau (Schema aus L0); erzeugeAuftragPdf liest sie über getAuftrag aus auftragStore.js +const aw = await startAppwriteMock({ schema: { auftraege: schemaKeys('auftraege') } }) +after(() => aw.close()) +useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' }) +const { erzeugeAuftragPdf } = await import('../server/services/auftraege/pdf.js') +const { AB_TEXTE } = await import('../server/services/auftraege/texte.js') +const { AuftragError } = await import('../server/services/auftraege/fehler.js') + +const optionen = { settings: beispielSettings, jetzt: new Date('2026-10-06T10:00:00.000Z') } +const ohneLeerraum = (text) => text.replace(/\s+/g, '') +const text = (pdf) => ohneLeerraum(leseSeiten(pdf).flat().join('')) +const hinweis = ohneLeerraum(AB_TEXTE.ersetzt({ nummer: 'AB-2026-0003', datum: '01.10.2026' })) + +aw.seed('auftraege', { $id: 'alt1', kundenId: 'k1', status: 'versendet', nummer: 'AB-2026-0003', gesendetAm: '2026-10-01T09:00:00.000+00:00' }) + +test('ersetztId ohne Option: die alte AB wird gelesen, ihre Nummer und ihr Berliner Datum stehen im PDF', async () => { + const pdf = await erzeugeAuftragPdf(beispielAuftrag({ ersetztId: 'alt1' }), optionen) + assert.ok(text(pdf).includes(hinweis)) +}) + +test('ersetztId verweist auf eine unbekannte AB: AuftragError 409 statt PDF ohne den Hinweis', async () => { + await assert.rejects(erzeugeAuftragPdf(beispielAuftrag({ ersetztId: 'gibtsnicht' }), optionen), (e) => e instanceof AuftragError && e.status === 409) +}) + +test('ersetzt: null unterdrückt Hinweis und Lesen, auch bei gesetztem ersetztId', async () => { + const pdf = await erzeugeAuftragPdf(beispielAuftrag({ ersetztId: 'gibtsnicht' }), { ...optionen, ersetzt: null }) + assert.equal(text(pdf).includes(hinweis), false) +})