From e680ea32660be6dbfb7fe1d056366d31c6fbd442 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 14:16:09 +0000 Subject: [PATCH] =?UTF-8?q?AB-Kern:=20Admin-Routen=20zum=20Senden,=20Erinn?= =?UTF-8?q?ern,=20f=C3=BCr=20neue=20Fassungen,=20zum=20Stornieren=20und=20?= =?UTF-8?q?zum=20Zuordnen=20eines=20Projekts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- server/routes/admin/auftraege.js | 48 +++++++++++++++++++ test/auftraegeRoutesAb.test.js | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) diff --git a/server/routes/admin/auftraege.js b/server/routes/admin/auftraege.js index 52dadb6..f9f7577 100644 --- a/server/routes/admin/auftraege.js +++ b/server/routes/admin/auftraege.js @@ -94,4 +94,52 @@ router.get('/:id/pdf', async (req, res) => { } }) +/** Antwort der Aktionen, die eine Mail auslösen: { auftrag, mailFehler }. */ +const mitMail = ({ auftrag, mailFehler }) => ({ auftrag: auftragFuerTeam(auftrag), mailFehler }) + +router.post('/:id/senden', async (req, res) => { + try { + await ladeMitSchalter(req.params.id) + return res.json(mitMail(await sendeAuftrag(req.params.id, { von: vonAdmin(req) }))) + } catch (err) { + return sendeFehler(res, 'Auftragsbestätigung senden', err) + } +}) + +router.post('/:id/erinnern', async (req, res) => { + try { + await ladeMitSchalter(req.params.id) + return res.json(mitMail(await erinnereAuftrag(req.params.id))) + } catch (err) { + return sendeFehler(res, 'Auftragsbestätigung erinnern', err) + } +}) + +router.post('/:id/neue-fassung', async (req, res) => { + try { + await ladeMitSchalter(req.params.id) + return res.json({ auftrag: auftragFuerTeam(await neueFassung(req.params.id, { von: vonAdmin(req) })) }) + } catch (err) { + return sendeFehler(res, 'Neue Fassung anlegen', err) + } +}) + +router.post('/:id/stornieren', async (req, res) => { + try { + await ladeMitSchalter(req.params.id) + return res.json({ auftrag: auftragFuerTeam(await storniereAuftrag(req.params.id)) }) + } catch (err) { + return sendeFehler(res, 'Auftragsbestätigung stornieren', err) + } +}) + +router.post('/:id/projekt', async (req, res) => { + try { + await ladeMitSchalter(req.params.id) + return res.json({ auftrag: auftragFuerTeam(await ordneProjektZu(req.params.id, req.body?.projektId)) }) + } catch (err) { + return sendeFehler(res, 'Projekt zuordnen', err) + } +}) + export default router diff --git a/test/auftraegeRoutesAb.test.js b/test/auftraegeRoutesAb.test.js index 3fbcf5c..947f6eb 100644 --- a/test/auftraegeRoutesAb.test.js +++ b/test/auftraegeRoutesAb.test.js @@ -125,3 +125,85 @@ test('Unerwartete Fehler: 500 mit fester Meldung, nichts von Appwrite gelangt an const r = await api('GET', `/${a.id}`) assert.deepEqual([r.status, r.body], [500, { error: 'Interner Fehler.' }]) }) + +test('POST senden: 200 ohne Token und Dateipfad; Kundendaten-, PDF- und Statusfehler mit festen Meldungen; Mail-Fehler als mailFehler: true', async () => { + mock.method(console, 'error', () => {}) + const a = await entwurfAnlegen() + aw.seed(config.collections.customers, kundeDoc({ email: '' })) + let r = await api('POST', `/${a.id}/senden`) + assert.deepEqual([r.status, r.body], [400, { error: MELDUNG.keineMail }]) + aw.seed(config.collections.customers, kundeDoc({ street: '' })) + r = await api('POST', `/${a.id}/senden`) + assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.adresse }]) + aw.seed(config.collections.customers, kundeDoc()) + kontext = baueDeps({ pdfFehler: new Error('pdfkit') }) + setzeDepsFuerTests(kontext.deps) + r = await api('POST', `/${a.id}/senden`) + assert.deepEqual([r.status, r.body], [500, { error: MELDUNG.pdf }]) + kontext = baueDeps({ mailFehler: new Error('smtp') }) + setzeDepsFuerTests(kontext.deps) + r = await api('POST', `/${a.id}/senden`) + assert.equal(r.status, 200) + assert.equal(r.body.mailFehler, true) + assert.deepEqual([r.body.auftrag.status, r.body.auftrag.mailStatus, r.body.auftrag.pdfVorhanden], ['versendet', 'fehler', true]) + assert.match(r.body.auftrag.nummer, /^AB-\d{4}-0001$/) + for (const feld of ['tokenHash', 'pdfDatei', 'bestaetigtPdfSha256']) assert.equal(feld in r.body.auftrag, false, feld) + r = await api('POST', `/${a.id}/senden`) + assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.nurEntwurfSenden }]) +}) + +test('POST erinnern, neue-fassung, stornieren: Antworten, Folgezustände und 409 bei falschem Status', async () => { + const a = await entwurfAnlegen() + let r = await api('POST', `/${a.id}/erinnern`) + assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.keineErinnerung }]) + r = await api('POST', `/${a.id}/senden`) + assert.deepEqual([r.status, r.body.mailFehler, r.body.auftrag.status], [200, false, 'versendet']) + r = await api('POST', `/${a.id}/erinnern`) + assert.deepEqual([r.status, r.body.mailFehler, r.body.auftrag.status], [200, false, 'versendet']) + assert.equal(kontext.calls.mail.at(-1).erinnerung, true) + r = await api('POST', `/${a.id}/neue-fassung`) + assert.deepEqual([r.status, r.body.auftrag.status, r.body.auftrag.ersetztId], [200, 'entwurf', a.id]) + r = await api('POST', `/${a.id}/neue-fassung`) + assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.schonEntwurf }]) + r = await api('POST', `/${a.id}/stornieren`) + assert.deepEqual([r.status, r.body.auftrag.status], [200, 'storniert']) + r = await api('POST', `/${a.id}/stornieren`) + assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.keinStorno }]) +}) + +test('POST projekt: ordnet einer AB ohne Projekt das Projekt zu (200, AuftragTeam); 400 ohne oder mit fremdem Projekt, 404 bei unbekannter AB, 409 bei vorhandenem Projekt oder falschem Status', async () => { + const a = await entwurfAnlegen({ projektId: '', titel: 'Beratung ohne Projekt' }) + await api('POST', `/${a.id}/senden`) + let r = await api('POST', `/${a.id}/projekt`, {}) + assert.deepEqual([r.status, r.body], [400, { error: MELDUNG.projektFehlt }]) + r = await api('POST', `/${a.id}/projekt`) + assert.deepEqual([r.status, r.body], [400, { error: MELDUNG.projektFehlt }]) + r = await api('POST', `/${a.id}/projekt`, { projektId: FREMD.projekt }) + assert.deepEqual([r.status, r.body], [400, { error: MELDUNG.projektFremd }]) + r = await api('POST', '/a_gibtsnicht/projekt', { projektId: PROJEKT_ID }) + assert.deepEqual([r.status, r.body], [404, { error: MELDUNG.auftragFehlt }]) + assert.equal((await getAuftrag(a.id)).projektId, '') // die abgelehnten Aufrufe haben nichts geändert + r = await api('POST', `/${a.id}/projekt`, { projektId: PROJEKT_ID }) + assert.equal(r.status, 200) + assert.deepEqual([r.body.auftrag.projektId, r.body.auftrag.zeitquelle, r.body.auftrag.status], [PROJEKT_ID, [TICKET_ID], 'versendet']) + for (const feld of ['tokenHash', 'pdfDatei', 'bestaetigtPdfSha256']) assert.equal(feld in r.body.auftrag, false, feld) + r = await api('POST', `/${a.id}/projekt`, { projektId: PROJEKT_ID }) + assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.schonProjekt }]) + const b = await entwurfAnlegen({ projektId: '', titel: 'Zweite Beratung ohne Projekt' }) + await api('POST', `/${b.id}/senden`) + await api('POST', `/${b.id}/stornieren`) + r = await api('POST', `/${b.id}/projekt`, { projektId: PROJEKT_ID }) + assert.deepEqual([r.status, r.body], [409, { error: MELDUNG.keinProjekt }]) +}) + +test('Schalter aus: auch senden, erinnern, neue-fassung, stornieren und projekt antworten 404 mit der festen Meldung', async () => { + const a = await entwurfAnlegen({ projektId: '', titel: 'Beratung ohne Projekt' }) + config.auftraege.mode = 'off' + for (const aktion of ['senden', 'erinnern', 'neue-fassung', 'stornieren', 'projekt']) { + const r = await api('POST', `/${a.id}/${aktion}`, aktion === 'projekt' ? { projektId: PROJEKT_ID } : undefined) + assert.deepEqual([r.status, r.body], [404, { error: SCHALTER_AUS_MELDUNG }], aktion) + } + const danach = await getAuftrag(a.id) + assert.deepEqual([danach.status, danach.projektId], ['entwurf', '']) + assert.equal(kontext.calls.pdf.length + kontext.calls.mail.length, 0) +})