From 3beba876d63284d8e26a27e70797315a7c188f86 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 14:16:45 +0000 Subject: [PATCH] =?UTF-8?q?Auftr=C3=A4ge=20L5:=20Widerruf=20eines=20Projek?= =?UTF-8?q?tauftrags=20auf=20der=20Widerrufsseite=20(nur=20Privatpersonen?= =?UTF-8?q?=20in=20der=20Frist,=20Nummer=20optional)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- server/routes/abo.js | 74 ++++++++++--- test/auftraegeWiderruf.test.js | 185 +++++++++++++++++++++++++++++++++ 2 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 test/auftraegeWiderruf.test.js diff --git a/server/routes/abo.js b/server/routes/abo.js index 894c5c3..74a083c 100644 --- a/server/routes/abo.js +++ b/server/routes/abo.js @@ -5,6 +5,8 @@ import { getSessionCustomerId, requireSession } from '../middleware/session.js' import { getCustomerByEmail, getDocument, updateDocument } from '../services/appwriteAdmin.js' import { eventIdFor, listContractEvents, openWithdrawals, recordContractEvent, updateContractEvent } from '../services/contractEvents.js' import { lateStartAt, withdrawalDeadline } from '../services/deadlines.js' +import { findAuftragByNummer } from '../services/auftraege/auftragStore.js' +import { widerrufeAuftrag } from '../services/auftraege/bestaetigung.js' import { getLedger } from '../services/hourLedger.js' import { sendAdminNotice, sendCustomerMail } from '../services/mailer.js' import { changeStorage, previewStorageChange, recordPaidStorage, storageTaskKind } from '../services/mailStorage.js' @@ -727,7 +729,8 @@ export function publicRateLimitState() { // Auswahl im Formular: Vertrag (Kuendigung) und Bestellung (Widerruf), Standard jeweils der erste Wert const CONTRACTS = ['abo', 'zusatzspeicher'] -const ORDERS = ['abschluss', 'wechsel', 'zusatzspeicher', 'funktion', 'termine', 'shop', 'zeiten'] +const ORDERS = ['abschluss', 'wechsel', 'zusatzspeicher', 'funktion', 'termine', 'shop', 'zeiten', 'projekt'] +const AB_NUMMER = /^AB-\d{4}-\d{4}$/ // Bestellung einer Funktion -> Funktionsschluessel im Register ('funktion' meint die News-Pinnwand; jede Funktion hat ihre eigene Frist) const FEATURE_OF_ORDER = { funktion: 'news', termine: 'termine', shop: 'shop', zeiten: 'zeiten' } // Bestellung -> Ereignistyp mit Widerrufsfrist (F03); beim Zusatzspeicher zaehlt nur ein Plus (openWithdrawals) @@ -735,7 +738,7 @@ const ORDER_EVENT_TYPE = { abschluss: 'abschluss', wechsel: 'wechsel', zusatzspe /** * Erklaerung aus dem Formular: null ohne Name oder gueltige E-Mail; { error } bei einer unzulaessigen Paketzahl. - * contract 'abo' | 'zusatzspeicher', order 'abschluss' | 'wechsel' | 'zusatzspeicher' | 'funktion' | 'termine' | 'shop' | 'zeiten'; packages (nur beim + * contract 'abo' | 'zusatzspeicher', order 'abschluss' | 'wechsel' | 'zusatzspeicher' | 'funktion' | 'termine' | 'shop' | 'zeiten' | 'projekt'; packages (nur beim * Zusatzspeicher) ganze Zahl von 1 bis maxUnits, null heisst alle gebuchten (Rechtsrunde 28.09.2026). */ function validDeclaration(body) { @@ -764,6 +767,7 @@ function validDeclaration(body) { contract, order, packages, + abNummer: String(body?.abNummer || '').trim().toUpperCase().slice(0, 20), } } @@ -804,7 +808,8 @@ function contractLabel(contract, sub) { } /** Beschriftung der widerrufenen Bestellung fuer K04 (an den Kunden) und T09 (intern). */ -function orderLabel(order, sub, { customer = false } = {}) { +function orderLabel(order, sub, { customer = false, abNummer = '' } = {}) { + if (order === 'projekt') return AB_NUMMER.test(abNummer) ? `Projektauftrag (Auftragsbestätigung ${abNummer})` : 'Projektauftrag (Auftragsbestätigung)' if (order === 'wechsel') return 'Wechsel ins Wartungs-Abo' if (order === 'zusatzspeicher') return 'E-Mail-Zusatzspeicher' if (order === 'funktion') return 'Funktion (z. B. News-Pinnwand)' @@ -1147,6 +1152,37 @@ async function withdrawFeature(d, receivedAt, featureKey) { return { customer, orders, deadline, lines, inTime: Boolean(deadline) && receivedAt <= deadline } } +/** Projektauftrag (Aufträge und Rechnungen, Spec 2026-10-06 Abschnitt 2): Nummer und E-Mail-Adresse müssen zur gesendeten AB passen. Ohne gültige Nummer wird nichts nachgeschlagen. Wirft bei Datenbankfehlern. */ +async function withdrawProjekt(d, receivedAt) { + const auftrag = AB_NUMMER.test(d.abNummer) ? await findAuftragByNummer(d.abNummer) : null + const adressen = [auftrag?.gesendetAn, auftrag?.kunde?.email].map((adresse) => String(adresse || '').trim().toLowerCase()) + if (!auftrag || !adressen.includes(d.email.toLowerCase())) return { auftrag: null, ergebnis: 'ungeprueft', fristEnde: null } + return widerrufeAuftrag(auftrag, { jetzt: receivedAt }) +} + +const PROJEKT_EINGANG = { widerrufen: 'RECHTZEITIG', nach_frist: 'NACH Fristende, vor einer Ablehnung bitte prüfen', bereits_widerrufen: 'bereits widerrufen' } + +/** Werte für die Zeilen „Ergebnis“, „Widerrufsfrist bis“ und „Eingang“ im Block projekt von T09 (nur das Team sieht sie). */ +function projektWerte({ ergebnis, auftrag, fristEnde }, abNummer) { + const ergebnisText = { + widerrufen: () => `Auftragsbestätigung auf „widerrufen“ gesetzt${auftrag.fruehbeginn ? '; Frühbeginn war verlangt: Wertersatz prüfen (§ 357a Abs. 2 BGB)' : ''}`, + bereits_widerrufen: () => 'war schon widerrufen, nichts geändert', + kein_widerrufsrecht: () => 'Unternehmen: kein gesetzliches Widerrufsrecht, nichts geändert', + nach_frist: () => 'nichts geändert', + nicht_widerrufbar: () => `Status „${auftrag.status}“, nichts geändert, bitte prüfen`, + fehler: () => 'ungeprüft: Prüfung fehlgeschlagen (Datenbank), bitte von Hand prüfen', + }[ergebnis] + return { + ereignisse: ergebnisText + ? ergebnisText() + : AB_NUMMER.test(abNummer) + ? `ungeprüft: keine Auftragsbestätigung zu Nummer „${abNummer}“ und dieser E-Mail-Adresse gefunden, nichts geändert` + : 'ungeprüft: keine gültige AB-Nummer angegeben, nichts geändert, bitte von Hand zuordnen', + fristende: ergebnis === 'kein_widerrufsrecht' ? 'entfällt (Unternehmen)' : fristEnde ? `${fmtDate(fristEnde)}, 24 Uhr` : 'keine Frist gefunden', + rechtzeitig: PROJEKT_EINGANG[ergebnis] ?? 'ungeprüft', + } +} + /** Zeile je widerrufenem Ereignis fuer T09: ID, Inhalt (Speicher: Menge vorher -> nachher), Bestelldatum. */ function describeWithdrawn(events) { return events @@ -1168,10 +1204,14 @@ router.post('/public/withdraw', async (req, res) => { // Funktion (News, Terminplaner oder Webshop): kein Abo, keine Vertragsereignisse; nur Nachschlagen fuer das Team (withdrawFeature) const featureKey = FEATURE_OF_ORDER[d.order] ?? null const feature = featureKey !== null + const projekt = d.order === 'projekt' + let pr = { auftrag: null, ergebnis: 'ungeprueft', fristEnde: null } const funktion = feature ? featureConfig(featureKey) : null let fn = { customer: null, orders: [], deadline: null, lines: '', inTime: false } try { - if (feature) { + if (projekt) { + pr = await withdrawProjekt(d, receivedAt) + } else if (feature) { fn = await withdrawFeature(d, receivedAt, featureKey) result = { ...result, deadline: fn.deadline, inTime: fn.inTime, late: !fn.inTime && Boolean(fn.deadline) } } else { @@ -1183,8 +1223,11 @@ router.post('/public/withdraw', async (req, res) => { } } catch (err) { console.error('[abo] Widerruf nicht verarbeitbar:', err.message) + if (projekt) pr = { auftrag: null, ergebnis: 'fehler', fristEnde: null } } const { inTime } = result + const projektAngaben = projekt ? projektWerte(pr, d.abNummer) : null + const produktText = projekt ? 'Projektauftrag' : feature ? funktion.label.name : productLabelOf(sub) // Fuer T09: warum sich die Frist nicht pruefen liess const pruefhinweis = result.unreadable ? 'Ereignisse nicht lesbar – bitte von Hand prüfen' : result.startMissing ? 'Abo-Beginn fehlt' : '' @@ -1192,8 +1235,8 @@ router.post('/public/withdraw', async (req, res) => { template: 'K04', values: { name: d.name, - bestellung: orderLabel(d.order, sub, { customer: true }), - produkt: feature ? funktion.label.name : productLabelOf(sub), + bestellung: orderLabel(d.order, sub, { customer: true, abNummer: d.abNummer }), + produkt: produktText, eingang_zeitpunkt: fmtDateTime(receivedAt), email: d.email, kundennummer: d.customerNumber, @@ -1204,7 +1247,9 @@ router.post('/public/withdraw', async (req, res) => { wechsel: inTime && d.order === 'wechsel', zusatzspeicher: inTime && d.order === 'zusatzspeicher', funktion: feature, - pruefen: !inTime && !feature, + pruefen: !inTime && !feature && !projekt, + projekt, + projekt_unternehmen: projekt && pr.ergebnis === 'kein_widerrufsrecht', }, }).catch((e) => console.error('[abo] Widerrufsbestaetigung fehlgeschlagen:', e.message)) @@ -1215,13 +1260,15 @@ router.post('/public/withdraw', async (req, res) => { email: d.email, name: d.name, kundennummer: d.customerNumber, - bestellung: orderLabel(d.order, sub), - produkt: feature ? funktion.label.name : productLabelOf(sub), + bestellung: orderLabel(d.order, sub, { abNummer: d.abNummer }), + produkt: produktText, preis: feature ? funktion.priceLabel : '', stripe_abo_id: sub?.stripeSubscriptionId || '', abo_beginn: fmtDate(sub?.startedAt), - fristende: result.deadline ? `${fmtDate(result.deadline)}, 24 Uhr` : 'keine Frist gefunden', - rechtzeitig: feature + fristende: projekt ? projektAngaben.fristende : result.deadline ? `${fmtDate(result.deadline)}, 24 Uhr` : 'keine Frist gefunden', + rechtzeitig: projekt + ? projektAngaben.rechtzeitig + : feature ? !fn.customer ? 'ungeprüft (kein Kunde zu dieser E-Mail-Adresse gefunden)' : !fn.orders.length @@ -1242,14 +1289,14 @@ router.post('/public/withdraw', async (req, res) => { : result.deadline ? 'ungeprüft (keine offene Frist: Bestellung gescheitert)' : 'ungeprüft (keine Bestellung dieser Art gefunden)', - ereignisse: feature ? fn.lines : describeWithdrawn(result.withdrawn), + ereignisse: projekt ? projektAngaben.ereignisse : feature ? fn.lines : describeWithdrawn(result.withdrawn), pruefhinweis, }, blocks: { kundennummer: Boolean(d.customerNumber), abo_gefunden: Boolean(sub), auf_kuendigung_gesetzt: result.canceled, - kein_abo_gefunden: !sub && !feature, + kein_abo_gefunden: !sub && !feature && !projekt, kuendigung_fehlgeschlagen: result.cancelFailed, abschluss: inTime && d.order === 'abschluss', wechsel: inTime && d.order === 'wechsel', @@ -1261,6 +1308,7 @@ router.post('/public/withdraw', async (req, res) => { abschluss_verspaetet: Boolean(sub) && d.order === 'abschluss' && result.late && !result.alreadyWithdrawn, nicht_pruefbar: Boolean(sub) && Boolean(pruefhinweis), bereits_widerrufen: Boolean(sub) && result.alreadyWithdrawn, + projekt, }, }) diff --git a/test/auftraegeWiderruf.test.js b/test/auftraegeWiderruf.test.js new file mode 100644 index 0000000..088106c --- /dev/null +++ b/test/auftraegeWiderruf.test.js @@ -0,0 +1,185 @@ +import { test, after, beforeEach, afterEach, mock } from 'node:test' +import assert from 'node:assert/strict' +import { startAppwriteMock } from './support/appwriteMock.js' +import { startFakeStripe } from './support/fakeStripe.js' +import { useTestEnv } from './support/testEnv.js' +import { mailSink } from './support/mailSink.js' +import { AUFTRAEGE_DATETIMES, AUFTRAEGE_SCHEMA, TOKEN, abDokument, kunde } from './auftraegeBestaetigungHilfe.js' + +// Widerruf eines Projektauftrags über /api/abo/public/withdraw (order = 'projekt', abNummer optional); K04 und T09 mit dem Block projekt +process.env.AUFTRAEGE_ENABLED = 'on' +const aw = await startAppwriteMock({ schema: AUFTRAEGE_SCHEMA, datetimes: AUFTRAEGE_DATETIMES, defaultLimit: 25 }) +const st = await startFakeStripe() +useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url }) +const { config } = await import('../server/config.js') +const { startTestApp, post, send } = await import('./support/testApp.js') +const { auftragToDoc, getAuftrag } = await import('../server/services/auftraege/auftragStore.js') +const { withdrawalDeadline } = await import('../server/services/deadlines.js') +const { setMailTransportForTests } = await import('../server/services/mailer.js') +const { default: aboRoutes, resetPublicRateLimit } = await import('../server/routes/abo.js') +const { default: tokenRoutes, resetAuftragRoutesForTests } = await import('../server/routes/auftrag.js') +const app = await startTestApp({ '/api/abo': aboRoutes, '/api/auftrag': tokenRoutes }) +const sink = mailSink() +setMailTransportForTests(sink) +after(() => { + setMailTransportForTests(null) + return Promise.all([app.close(), aw.close(), st.close()]) +}) + +const vor = (tage) => new Date(Date.now() - tage * 86_400_000).toISOString() +const tag = (zeit) => new Date(zeit).toLocaleDateString('de-DE', { timeZone: 'Europe/Berlin', day: '2-digit', month: '2-digit', year: 'numeric' }) +const seed = (over = {}) => aw.seed('auftraege', { $id: 'ab1', ...auftragToDoc(abDokument(over)) }) +let protokoll +beforeEach(() => { + resetPublicRateLimit() + resetAuftragRoutesForTests() + sink.sent.length = 0 + config.auftraege.mode = 'on' + protokoll = mock.method(console, 'error', () => {}) +}) +afterEach(() => mock.restoreAll()) +const widerruf = (body = {}) => post('/api/abo/public/withdraw', { name: 'Erika Muster', email: 'k@example.invalid', order: 'projekt', abNummer: 'AB-2026-0001', ...body }) +const k04 = () => sink.sent.find((mail) => mail.subject.startsWith('Eingangsbestätigung Ihres Widerrufs')) +const t09 = () => sink.sent.find((mail) => mail.subject.startsWith('[Abo] WIDERRUF eingegangen')) +const zeile = (mail, anfang) => mail.text.split('\n').find((z) => z.startsWith(anfang)) +const LABEL = 'Projektauftrag (Auftragsbestätigung AB-2026-0001)' +const OHNE_NUMMER = 'Projektauftrag (Auftragsbestätigung)' +const UNTERNEHMEN_HINWEIS = 'Für Verträge mit Unternehmen besteht kein gesetzliches Widerrufsrecht.' +const KEINE_NUMMER = 'ungeprüft: keine gültige AB-Nummer angegeben, nichts geändert, bitte von Hand zuordnen' +const pruefeT09 = (ergebnis, frist, eingang) => { + assert.equal(zeile(t09(), 'Ergebnis: '), `Ergebnis: ${ergebnis}`) + assert.equal(zeile(t09(), 'Widerrufsfrist bis: '), `Widerrufsfrist bis: ${frist}`) + assert.equal(zeile(t09(), 'Eingang: '), `Eingang: ${eingang}`) +} + +test('Privatperson innerhalb der Frist: AB widerrufen, Antwort wie immer, K04 und T09 mit Block projekt; kein Stripe', async () => { + const gesendetAm = vor(3) + seed({ gesendetAm }) + const stripeVorher = st.calls.length + const res = await widerruf({ customerNumber: 'KD-9' }) + assert.equal(res.status, 200, JSON.stringify(res.body)) + assert.deepEqual(Object.keys(res.body).sort(), ['ok', 'receivedAt']) + assert.equal(st.calls.length, stripeVorher, 'kein Stripe-Aufruf') + const ab = await getAuftrag('ab1') + assert.equal(ab.status, 'widerrufen') + assert.equal(new Date(ab.widerrufenAm).getTime(), new Date(res.body.receivedAt).getTime()) + // K04 an die Privatperson: Vertragsart, „wir prüfen“, keine Zusagen, kein Hinweis für Unternehmen + assert.equal(k04().subject, 'Eingangsbestätigung Ihres Widerrufs – WEBklar Projektauftrag') + assert.deepEqual(k04().to.map((a) => a.address), ['k@example.invalid']) + assert.ok(k04().text.includes(`Widerruf der Bestellung „${LABEL}“ am `), k04().text) + assert.equal(zeile(k04(), 'Bestellung: '), `Bestellung: ${LABEL}`) + assert.ok(k04().text.includes('Wir prüfen Ihren Widerruf des Projektauftrags und melden uns umgehend bei Ihnen.')) + assert.ok(k04().text.includes('Kundennummer: KD-9')) + assert.doesNotMatch(k04().text, /Unternehmen|Abbuchungen|Wir prüfen, ob für diese Bestellung/) + // T09 ans Team: Vertragsart, Ergebnis, Frist, Eingang + assert.equal(t09().subject, '[Abo] WIDERRUF eingegangen: k@example.invalid') + assert.equal(zeile(t09(), 'Bestellung: '), `Bestellung: ${LABEL}`) + assert.equal(zeile(t09(), 'Abo: '), 'Abo: Projektauftrag') + pruefeT09('Auftragsbestätigung auf „widerrufen“ gesetzt', `${tag(withdrawalDeadline(gesendetAm).endsAt)}, 24 Uhr`, 'RECHTZEITIG') + assert.doesNotMatch(t09().text, /KEIN aktives Abo|Stripe-Abo/) +}) + +test('bestätigte AB mit verlangtem Frühbeginn: wird widerrufen, T09 mahnt die Prüfung des Wertersatzes an', async () => { + seed({ gesendetAm: vor(5), status: 'bestaetigt', bestaetigtAm: vor(4), bestaetigtUeber: 'link', fruehbeginn: true }) + assert.equal((await widerruf()).status, 200) + const ab = await getAuftrag('ab1') + assert.equal(ab.status, 'widerrufen') + assert.notEqual(ab.bestaetigtAm, null) + assert.equal(zeile(t09(), 'Ergebnis: '), 'Ergebnis: Auftragsbestätigung auf „widerrufen“ gesetzt; Frühbeginn war verlangt: Wertersatz prüfen (§ 357a Abs. 2 BGB)') +}) + +test('Unternehmen: kein gesetzliches Widerrufsrecht – AB bleibt unverändert, K04 weist darauf hin, T09 sagt es dem Team', async () => { + seed({ gesendetAm: vor(3), kunde: kunde('unternehmen') }) + assert.equal((await widerruf()).status, 200) + assert.equal((await getAuftrag('ab1')).status, 'versendet') + assert.ok(k04().text.includes(`Wir prüfen Ihren Widerruf des Projektauftrags und melden uns umgehend bei Ihnen. ${UNTERNEHMEN_HINWEIS}`), k04().text) + pruefeT09('Unternehmen: kein gesetzliches Widerrufsrecht, nichts geändert', 'entfällt (Unternehmen)', 'ungeprüft') +}) + +test('nach Fristende: nichts geändert, T09 nennt das Fristende und bittet um Prüfung; K04 ohne Unternehmen-Hinweis', async () => { + const gesendetAm = vor(20) + seed({ gesendetAm }) + assert.equal((await widerruf()).status, 200) + assert.equal((await getAuftrag('ab1')).status, 'versendet') + pruefeT09('nichts geändert', `${tag(withdrawalDeadline(gesendetAm).endsAt)}, 24 Uhr`, 'NACH Fristende, vor einer Ablehnung bitte prüfen') + assert.ok(k04().text.includes('Wir prüfen Ihren Widerruf des Projektauftrags')) + assert.ok(!k04().text.includes(UNTERNEHMEN_HINWEIS)) +}) + +test('ohne, mit ungültiger oder falscher Nummer und mit fremder E-Mail-Adresse: dieselbe Antwort, nichts geändert, K04 und T09 gehen trotzdem raus (ungeprüft)', async () => { + const faelle = [ + [{ abNummer: 'AB-2026-0002' }, 'Projektauftrag (Auftragsbestätigung AB-2026-0002)', 'ungeprüft: keine Auftragsbestätigung zu Nummer „AB-2026-0002“ und dieser E-Mail-Adresse gefunden, nichts geändert'], + [{ email: 'fremd@example.invalid' }, LABEL, 'ungeprüft: keine Auftragsbestätigung zu Nummer „AB-2026-0001“ und dieser E-Mail-Adresse gefunden, nichts geändert'], + [{ abNummer: 'AB-2026-1' }, OHNE_NUMMER, KEINE_NUMMER], + [{ abNummer: '' }, OHNE_NUMMER, KEINE_NUMMER], + [{ abNummer: undefined }, OHNE_NUMMER, KEINE_NUMMER], + [{ abNummer: 'irgendwas' }, OHNE_NUMMER, KEINE_NUMMER], + ] + seed({ gesendetAm: vor(3) }) + for (const [body, label, ergebnis] of faelle) { + sink.sent.length = 0 + resetPublicRateLimit() + const res = await widerruf(body) + const was = JSON.stringify(body) + assert.equal(res.status, 200, was) + assert.deepEqual(Object.keys(res.body).sort(), ['ok', 'receivedAt'], was) + assert.equal((await getAuftrag('ab1')).status, 'versendet', was) + assert.equal(zeile(k04(), 'Bestellung: '), `Bestellung: ${label}`, was) + assert.equal(zeile(t09(), 'Bestellung: '), `Bestellung: ${label}`, was) + pruefeT09(ergebnis, 'keine Frist gefunden', 'ungeprüft') + assert.ok(k04().text.includes('Wir prüfen Ihren Widerruf des Projektauftrags und melden uns umgehend bei Ihnen.'), was) + assert.ok(!k04().text.includes(UNTERNEHMEN_HINWEIS), was) + } +}) + +test('Groß-/Kleinschreibung und Leerzeichen bei Nummer und E-Mail-Adresse spielen keine Rolle', async () => { + seed({ gesendetAm: vor(3) }) + assert.equal((await widerruf({ abNummer: ' ab-2026-0001 ', email: 'K@Example.INVALID' })).status, 200) + assert.equal((await getAuftrag('ab1')).status, 'widerrufen') + assert.equal(zeile(t09(), 'Bestellung: '), `Bestellung: ${LABEL}`) +}) + +test('bereits widerrufen, abgerechnet, ersetzt, storniert: nichts geändert, T09 sagt warum', async () => { + const faelle = [ + ['widerrufen', 'war schon widerrufen, nichts geändert', 'bereits widerrufen'], + ['abgerechnet', 'Status „abgerechnet“, nichts geändert, bitte prüfen', 'ungeprüft'], + ['ersetzt', 'Status „ersetzt“, nichts geändert, bitte prüfen', 'ungeprüft'], + ['storniert', 'Status „storniert“, nichts geändert, bitte prüfen', 'ungeprüft'], + ] + for (const [status, ergebnis, eingang] of faelle) { + const gesendetAm = vor(3) + seed({ gesendetAm, status, widerrufenAm: status === 'widerrufen' ? vor(1) : null }) + sink.sent.length = 0 + resetPublicRateLimit() + assert.equal((await widerruf()).status, 200, status) + pruefeT09(ergebnis, `${tag(withdrawalDeadline(gesendetAm).endsAt)}, 24 Uhr`, eingang) + assert.equal((await getAuftrag('ab1')).status, status, status) + } +}) + +test('nach dem Widerruf bestätigt der Link nichts mehr: 410 mit Grund', async () => { + seed({ gesendetAm: vor(3) }) + assert.equal((await widerruf()).status, 200) + const lesen = await send('GET', `/api/auftrag/${TOKEN}`) + assert.deepEqual([lesen.status, lesen.body], [410, { error: 'Diese Auftragsbestätigung wurde widerrufen.' }]) + assert.equal((await send('POST', `/api/auftrag/${TOKEN}/bestaetigen`, { body: { bestaetigt: true } })).status, 410) + assert.equal((await getAuftrag('ab1')).bestaetigtAm, null) +}) + +test('Schalter aus: der Widerruf wird trotzdem verarbeitet (Verbraucherrecht), der Link antwortet wie bei unbekannter AB', async () => { + seed({ gesendetAm: vor(3) }) + config.auftraege.mode = 'off' + assert.equal((await widerruf()).status, 200) + assert.equal((await getAuftrag('ab1')).status, 'widerrufen') + assert.equal((await send('GET', `/api/auftrag/${TOKEN}`)).status, 404) +}) + +test('Datenbankfehler bei der Prüfung: Antwort 200, nichts geändert, T09 meldet die fehlgeschlagene Prüfung, K04 geht raus', async () => { + seed({ gesendetAm: vor(3) }) + aw.failNext({ collection: 'auftraege', method: 'GET', status: 503 }) + assert.equal((await widerruf()).status, 200) + assert.equal((await getAuftrag('ab1')).status, 'versendet') + pruefeT09('ungeprüft: Prüfung fehlgeschlagen (Datenbank), bitte von Hand prüfen', 'keine Frist gefunden', 'ungeprüft') + assert.ok(k04().text.includes('Wir prüfen Ihren Widerruf des Projektauftrags')) + assert.ok(protokoll.mock.callCount() > 0) +})