Files
Webklar-Kundenbereich/test/auftraegeWiderruf.test.js

186 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
})