diff --git a/server/services/auftraege/auftraege.js b/server/services/auftraege/auftraege.js index 1f469fb..a6999b6 100644 --- a/server/services/auftraege/auftraege.js +++ b/server/services/auftraege/auftraege.js @@ -7,7 +7,9 @@ */ import { config } from '../../config.js' import { getDocument, listDocuments, Query, updateDocument } from '../appwriteAdmin.js' +import { getSettings } from '../finance/settings.js' import { withLocks } from '../keyedLock.js' +import { kundeSnapshot } from './auftragSicht.js' import { createAuftrag, deleteAuftrag, getAuftrag, updateAuftrag } from './auftragStore.js' import { AuftragError } from './fehler.js' import { @@ -303,3 +305,51 @@ export async function loescheEntwurf(id) { await deleteAuftrag(id) }) } + +// --------------------------------------------------------------------------------------------------------------------- +// Abhängigkeiten (PDF, Token, Mail) und Vorschau-PDF +// --------------------------------------------------------------------------------------------------------------------- + +/** Ins Protokoll nur Art, Status und Code, nie die Meldung (sie kann Adressen oder Pfade enthalten). */ +function logFehler(was, err) { + console.error(`[auftraege] ${was} fehlgeschlagen:`, err?.type || err?.name || 'Error', err?.status ?? err?.statusCode ?? '', err?.code ?? '') +} + +// PDF, Token und Mail entstehen in L4/L5 parallel: Standard ist ein dynamischer Import beim Aufruf, Tests übergeben Attrappen +const STANDARD_QUELLEN = { erzeugeAuftragPdf: './pdf.js', neuerToken: './token.js', sendeAuftragMail: './mails.js' } +let depsFuerTests = null + +/** Nur für Tests: Ersatz für die Standard-Abhängigkeiten (null stellt die dynamischen Importe wieder her). */ +export function setzeDepsFuerTests(deps) { + depsFuerTests = deps +} + +async function ladeDeps(deps, namen) { + const quelle = { ...depsFuerTests, ...deps } + const geladen = {} + for (const name of namen) geladen[name] = quelle[name] ?? (await import(STANDARD_QUELLEN[name]))[name] + return geladen +} + +/** Erzeugt das PDF; ein AuftragError der PDF-Funktion (z. B. 409 „Entwurf unvollständig“) bleibt erhalten. */ +async function pdfErzeugen(d, auftrag, optionen) { + let pdf + try { + pdf = await d.erzeugeAuftragPdf(auftrag, optionen) + if (!Buffer.isBuffer(pdf) || pdf.length === 0) throw new Error('leeres PDF') + } catch (err) { + if (err instanceof AuftragError) throw err + logFehler('PDF-Erzeugung', err) + throw new AuftragError(500, 'Das PDF konnte nicht erzeugt werden.') + } + return pdf +} + +export async function vorschauPdf(id, { deps } = {}) { + const d = await ladeDeps(deps, ['erzeugeAuftragPdf']) + const auftrag = await ladeAuftrag(id) + if (auftrag.status !== 'entwurf') throw new AuftragError(409, 'Eine Vorschau gibt es nur für Entwürfe.') + const kundeDoc = await ladeKunde(auftrag.kundenId) + const { steuernummer, bankDetailsText } = await getSettings() + return pdfErzeugen(d, { ...auftrag, nummer: 'Entwurf', kunde: kundeSnapshot(kundeDoc) }, { settings: { steuernummer, bankDetailsText }, vorschau: true }) +} diff --git a/test/auftraegeAbVorschau.test.js b/test/auftraegeAbVorschau.test.js new file mode 100644 index 0000000..b264385 --- /dev/null +++ b/test/auftraegeAbVorschau.test.js @@ -0,0 +1,53 @@ +import { test, after, beforeEach, mock } from 'node:test' +import assert from 'node:assert/strict' +import { starteUmgebung, VON, MELDUNG, festpreisEingabe, kundeDoc, baueDeps, abgelehnt, dateienIn } from './helpers/auftraegeAb.js' + +const umg = await starteUmgebung() +const { aw, config, uploadDir } = umg +after(() => umg.schliesse()) +beforeEach(umg.zuruecksetzen) +const { getAuftrag, updateAuftrag } = await import('../server/services/auftraege/auftragStore.js') +const { kundeSnapshot } = await import('../server/services/auftraege/auftragSicht.js') +const { AuftragError } = await import('../server/services/auftraege/fehler.js') +const { legeEntwurfAn, vorschauPdf, setzeDepsFuerTests } = await import('../server/services/auftraege/auftraege.js') +beforeEach(() => setzeDepsFuerTests(null)) + +test('vorschauPdf: Entwurf mit „Entwurf“ als Nummer, aktuellem Kunden und den Einstellungen – ohne etwas zu speichern', async () => { + aw.seed(config.collections.financeSettings, { $id: 'main', steuernummer: '12/345/67890', bankDetailsText: 'IBAN DE00 0000 0000 0000 0000 00' }) + const entwurf = await legeEntwurfAn(festpreisEingabe(), { von: VON }) + aw.seed(config.collections.customers, kundeDoc({ street: 'Neue Straße 5' })) // der Kunde ändert sich nach dem Entwurf + const { deps, calls } = baueDeps() + assert.deepEqual(await vorschauPdf(entwurf.id, { deps }), Buffer.from('%PDF-1.4 Entwurf')) + assert.equal(calls.pdf.length, 1) + const { auftrag, optionen } = calls.pdf[0] + assert.deepEqual([auftrag.id, auftrag.nummer], [entwurf.id, 'Entwurf']) + assert.deepEqual(auftrag.kunde, kundeSnapshot(kundeDoc({ street: 'Neue Straße 5' }))) + assert.equal(auftrag.kunde.strasse, 'Neue Straße 5') + assert.deepEqual(optionen, { settings: { steuernummer: '12/345/67890', bankDetailsText: 'IBAN DE00 0000 0000 0000 0000 00' }, vorschau: true }) + assert.equal(calls.token.length + calls.mail.length, 0) + assert.deepEqual(dateienIn(uploadDir), []) + const danach = await getAuftrag(entwurf.id) + assert.deepEqual([danach.status, danach.nummer, danach.kunde], ['entwurf', '', null]) +}) + +test('vorschauPdf: nur Entwürfe (409), unbekannt (404); wirft die PDF-Erzeugung oder liefert sie nichts, gibt es 500 mit fester Meldung; ein AuftragError der PDF-Funktion bleibt erhalten', async () => { + mock.method(console, 'error', () => {}) + const entwurf = await legeEntwurfAn(festpreisEingabe(), { von: VON }) + await abgelehnt(vorschauPdf('a_gibtsnicht', { deps: baueDeps().deps }), 404, MELDUNG.auftragFehlt) + await abgelehnt(vorschauPdf(entwurf.id, { deps: baueDeps({ pdfFehler: new Error('pdfkit kaputt') }).deps }), 500, MELDUNG.pdf) + await abgelehnt(vorschauPdf(entwurf.id, { deps: baueDeps({ pdf: Buffer.alloc(0) }).deps }), 500, MELDUNG.pdf) + const unvollstaendig = new AuftragError(409, 'Entwurf unvollständig: Anschrift fehlt.') + await abgelehnt(vorschauPdf(entwurf.id, { deps: baueDeps({ pdfFehler: unvollstaendig }).deps }), 409, 'Entwurf unvollständig: Anschrift fehlt.') + await updateAuftrag(entwurf.id, { status: 'versendet' }) + await abgelehnt(vorschauPdf(entwurf.id, { deps: baueDeps().deps }), 409, 'Eine Vorschau gibt es nur für Entwürfe.') +}) + +test('deps: ohne Angabe gilt der Ersatz aus setzeDepsFuerTests, ausdrückliche Angaben haben Vorrang, nötig ist nur die benutzte Funktion', async () => { + const entwurf = await legeEntwurfAn(festpreisEingabe(), { von: VON }) + const ersatz = baueDeps({ pdf: Buffer.from('%PDF-1.4 ersatz') }) + const eigene = baueDeps({ pdf: Buffer.from('%PDF-1.4 eigene') }) + setzeDepsFuerTests(ersatz.deps) + assert.deepEqual(await vorschauPdf(entwurf.id), Buffer.from('%PDF-1.4 ersatz')) + assert.deepEqual(await vorschauPdf(entwurf.id, { deps: { erzeugeAuftragPdf: eigene.deps.erzeugeAuftragPdf } }), Buffer.from('%PDF-1.4 eigene')) + assert.deepEqual([ersatz.calls.pdf.length, eigene.calls.pdf.length], [1, 1]) +})