Shop P1b: Bestellnummer im Widerruf freiwillig, ohne Nummer Bestelldatum und Artikel in der Erklärung (§ 356a BGB)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:18:14 +00:00
1 parent 5693f37d5f
commit 172d9f1778
6 files changed
+63 -9

No files matched your search

+2 -1
View File
@@ -1597,7 +1597,7 @@
var refs = {}
form.appendChild(feld(refs, { name: 'name', label: 'Ihr Name', typ: 'text', wert: w.name, pflicht: true, autocomplete: 'name', maxlength: 100, onInput: function (v) { w.name = v } }))
form.appendChild(feld(refs, { name: 'email', label: 'E-Mail-Adresse der Bestellung', typ: 'email', wert: w.email, pflicht: true, autocomplete: 'email', maxlength: 254, onInput: function (v) { w.email = v } }))
form.appendChild(feld(refs, { name: 'bestellnummer', label: 'Bestellnummer', typ: 'text', wert: w.bestellnummer, pflicht: false, maxlength: 40, onInput: function (v) { w.bestellnummer = v } }))
form.appendChild(feld(refs, { name: 'bestellnummer', label: 'Bestellnummer (falls zur Hand)', typ: 'text', wert: w.bestellnummer, pflicht: false, maxlength: 40, hinweis: 'Ohne Bestellnummer: Nennen Sie bitte Bestelldatum und Artikel im Feld „Erklärung“, damit wir Ihren Vertrag zuordnen können.', onInput: function (v) { w.bestellnummer = v } }))
form.appendChild(feld(refs, { name: 'nachricht', label: 'Erklärung', typ: 'textarea', wert: w.nachricht, pflicht: false, maxlength: 1000, hinweis: 'Zum Beispiel, welche Artikel Sie widerrufen. Ohne Angabe widerrufen Sie die gesamte Bestellung.', onInput: function (v) { w.nachricht = v } }))
form.appendChild(honigfeld())
var fehlerBox = el('div', 'wks-fehlerbox')
@@ -1615,6 +1615,7 @@
var f = {}
if (!w.name.trim()) f.name = 'Bitte geben Sie Ihren Namen an.'
if (!EMAIL_FORM.test(w.email.trim())) f.email = 'Bitte geben Sie eine gültige E-Mail-Adresse an.'
if (!w.bestellnummer.trim() && !w.nachricht.trim()) f.nachricht = 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.'
fehlerBox.textContent = ''
zeigeFeldfehler(refs, f)
if (Object.keys(f).length) return
+6 -5
View File
@@ -10,8 +10,8 @@ import { anHaendler, anKaeufer, haendlerK26 } from './mails.js'
/**
* Widerrufe von Kaeufern (§ 356a BGB; Spezifikation 2026-10-05-webshop-design.md Abschnitt 5.4; Plan Task E5).
*
* Der Kaeufer erklaert den Widerruf ueber die oeffentliche Widerrufsseite des Shops (Name, Bestellnummer als Vertragsbezeichnung,
* E-Mail als Kontakt, optionale Nachricht). erfasseWiderruf speichert die Erklaerung (shopWiderrufe), haengt sie an die passende
* Der Kaeufer erklaert den Widerruf ueber die oeffentliche Widerrufsseite des Shops (Name, E-Mail als Kontakt, Bestellnummer als
* freiwillige Vertragsbezeichnung; ohne Nummer ist die Nachricht mit Bestelldatum und Artikel Pflicht). erfasseWiderruf speichert die Erklaerung (shopWiderrufe), haengt sie an die passende
* Bestellung (gleiche Nummer und gleiche E-Mail) und bestaetigt den Eingang sofort mit G12 (Datum und Uhrzeit, Berliner Zeit, auf
* dem dauerhaften Datentraeger E-Mail). K26 informiert den Haendler. Mailfehler lassen die Anfrage nie scheitern: dann bleibt
* bestaetigungAt leer. Die Bestellnummer muss nicht existieren (der Widerruf ist trotzdem wirksam erklaert).
@@ -69,11 +69,12 @@ export function normalisiereWiderruf(eingabe) {
else if (email.length > 254 || !EMAIL_RE.test(email)) f.email = 'Bitte geben Sie eine gültige E-Mail-Adresse an.'
const nummer = einzeilig(roh.bestellnummer).toUpperCase()
if (nummer === '') f.bestellnummer = 'Bitte geben Sie die Bestellnummer an.'
else if (!NUMMER_RE.test(nummer)) f.bestellnummer = 'Die Bestellnummer darf nur aus Buchstaben, Ziffern und Bindestrichen bestehen und höchstens 32 Zeichen lang sein.'
// Die Bestellnummer ist freiwillig (§ 356a BGB: der Kaeufer hat sie nicht immer zur Hand); ohne sie muss die Erklaerung den Vertrag benennen
if (nummer !== '' && !NUMMER_RE.test(nummer)) f.bestellnummer = 'Die Bestellnummer darf nur aus Buchstaben, Ziffern und Bindestrichen bestehen und höchstens 32 Zeichen lang sein.'
const nachricht = mehrzeilig(roh.nachricht)
if (nachricht.length > 2000) f.nachricht = 'Die Nachricht darf höchstens 2000 Zeichen lang sein.'
else if (nummer === '' && nachricht === '') f.nachricht = 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.'
if (Object.keys(f).length > 0) return { fehler: f }
return { widerruf: { name, email, bestellnummer: nummer, nachricht } }
@@ -131,7 +132,7 @@ export async function erfasseWiderruf(ctx, eingabe, { now = new Date(), ip } = {
if (geprueft.fehler) throw Object.assign(fehler(400, Object.values(geprueft.fehler)[0]), { fehler: geprueft.fehler })
const { name, email, bestellnummer, nachricht } = geprueft.widerruf
const order = await passendeBestellung(shopId, bestellnummer, email)
const order = bestellnummer === '' ? null : await passendeBestellung(shopId, bestellnummer, email)
const doc = await createDocument(col(), {
shopId,
+10 -1
View File
@@ -292,10 +292,19 @@ test('kein Objekt als Rumpf: 400; zu groß: 413', async () => {
assert.equal(gross.status, 413)
})
test('Ohne Bestellnummer: 400 fehler.nachricht, solange die Erklärung fehlt; mit Erklärung 200', async () => {
const leer = await widerruf(koerper({ bestellnummer: '', nachricht: '' }))
assert.equal(leer.status, 400)
assert.deepEqual(leer.body.fehler, { nachricht: 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.' })
const ok = await widerruf(koerper({ bestellnummer: '', nachricht: 'Bestellt am 01.10., Tasse blau' }))
assert.equal(ok.status, 200)
assert.equal(ok.body.ok, true)
})
test('Prüfung: 400 mit fehler je Feld, nichts gespeichert', async () => {
const res = await widerruf(koerper({ name: 'M', email: 'keine-adresse', bestellnummer: '', nachricht: 'x'.repeat(2001) }))
assert.equal(res.status, 400)
assert.deepEqual(Object.keys(res.body.fehler).sort(), ['bestellnummer', 'email', 'nachricht', 'name'])
assert.deepEqual(Object.keys(res.body.fehler).sort(), ['email', 'nachricht', 'name'])
for (const t of Object.values(res.body.fehler)) assert.match(t, /Bitte|darf/)
assert.equal(res.body.code, undefined)
assert.equal(kopf(res, 'access-control-allow-origin'), O_LIVE)
+7
View File
@@ -74,6 +74,13 @@ test('Anfragen ohne Cookies und nur an die Shop-Schnittstelle, eine Stelle fuer
assert.equal([...OHNE_KOMMENTARE.matchAll(/request\('POST', '\/bestellung'/g)].length, 1, 'genau eine Stelle legt Bestellungen an')
})
test('Widerruf: Bestellnummer freiwillig mit Hinweis auf Bestelldatum und Artikel (§ 356a BGB)', () => {
assert.ok(QUELLE.includes("label: 'Bestellnummer (falls zur Hand)'"))
assert.ok(QUELLE.includes('Ohne Bestellnummer: Nennen Sie bitte Bestelldatum und Artikel im Feld „Erklärung“, damit wir Ihren Vertrag zuordnen können.'))
assert.ok(QUELLE.includes('Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.'), 'Pruefung im Formular ohne Nummer')
assert.ok(!/label: 'Bestellnummer'/.test(QUELLE))
})
test('Pflichttexte: Knopf der Kasse, Widerrufsfunktion, Gewaehrleistungsbild, Stripe-Hinweis, Fuss, Kopfzeile, geschlossen', () => {
for (const noetig of [
"'Zahlungspflichtig bestellen'", "'Vertrag widerrufen'", "'Widerruf bestätigen'", "'Weiter'",
+11
View File
@@ -252,6 +252,17 @@ test('G12: ohne passende Bestellung, an die Adresse der Erklärung, mit Datum un
assert.deepEqual(warnLog(), [])
})
test('G12 und K26 ohne Bestellnummer: „–“ statt leerer Stelle', async () => {
const erg = await anKaeufer('G12', ctxMit(), null, { an: 'kaeufer@example.com', bookingId: 'w_widerruf02', name: 'Kai Käufer', bestellnummer: '', eingang: JETZT, nachricht: 'Bestellt am 01.10., Tasse blau' }, JETZT)
assert.equal(erg.ok, true)
const k = haendlerK26(ctxMit(), { name: 'Kai', email: 'kai@example.com', bestellnummer: '', nachricht: 'Bestellt am 01.10., Tasse blau', eingang: JETZT })
assert.equal(k.values.bestellnummer, '–')
assert.equal((await anHaendler('K26', ctxMit(), k.values, k.blocks)).ok, true)
await warteAufVersand()
assert.match(sink.sent[0].text, /Bestellnummer: –/)
assert.match(sink.sent[1].text, /Bestellnummer: –/)
})
test('G13: erstatteter Betrag in Euro, Grund nur wenn angegeben; ohne Betrag { ok: false }', async () => {
assert.equal((await anKaeufer('G13', ctxMit(), bestellung(), { betragCent: 1250, grund: 'Artikel beschädigt' }, JETZT)).ok, true)
assert.equal((await anKaeufer('G13', ctxMit(), bestellung({ $id: 'o_zwei' }), { betragCent: 4140 }, JETZT)).ok, true)
+27 -2
View File
@@ -111,9 +111,34 @@ test('normalisiereWiderruf: gültige Eingabe wird bereinigt (E-Mail klein, Beste
assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: 'X' }).widerruf.nachricht, '', 'Nachricht ist freiwillig')
})
test('normalisiereWiderruf: Bestellnummer freiwillig; ohne Nummer ist die Erklärung Pflicht (§ 356a BGB)', () => {
const ohneNummer = normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: ' ', nachricht: 'Bestellt am 01.10., Tasse blau' })
assert.deepEqual(ohneNummer, { widerruf: { name: 'Ab', email: 'a@b.de', bestellnummer: '', nachricht: 'Bestellt am 01.10., Tasse blau' } })
assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de' }).fehler.nachricht, 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.')
assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', nachricht: ' \n ' }).fehler.nachricht, 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.')
assert.equal(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: '26-K7QM2X' }).fehler, undefined, 'mit Nummer bleibt die Erklärung freiwillig')
assert.ok(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: 'a b', nachricht: 'x' }).fehler.bestellnummer, 'gegebene Nummer: gleiche Formatregel')
})
test('erfasseWiderruf ohne Bestellnummer: gespeichert ohne Zuordnung, G12 und K26 mit „–“', async () => {
aw.seed('shopOrders', bestellung())
const erg = await erfasseWiderruf(ctxMit(), { ...EINGABE, bestellnummer: '', nachricht: 'Bestellt am 01.10., Tasse blau' }, { now: JETZT, ip: '203.0.113.77' })
assert.equal(erg.ok, true)
const d = widerrufe().find((x) => x.$id === erg.id)
assert.equal(d.bestellnummer, '')
assert.equal(d.orderId, '')
await warteAufVersand()
assert.equal(outbox().filter((e) => e.art === 'G12').length, 1)
assert.equal(JSON.parse(aw.docs('shopOrders').get('o_bestellung01').bestellung).verlauf.length, 1, 'ohne Nummer keine Zuordnung zum Verlauf')
await assert.rejects(
() => erfasseWiderruf(ctxMit(), { ...EINGABE, bestellnummer: '', nachricht: '' }, { now: JETZT, ip: '203.0.113.78' }),
(err) => err.status === 400 && err.fehler.nachricht === 'Bitte nennen Sie Bestelldatum und Artikel, damit wir Ihren Vertrag zuordnen können.',
)
})
test('normalisiereWiderruf: Fehler je Feld in Sie-Form', () => {
const grenzen = normalisiereWiderruf({ name: 'M', email: 'keine-adresse', bestellnummer: '', nachricht: 'x'.repeat(2001) })
assert.deepEqual(Object.keys(grenzen.fehler).sort(), ['bestellnummer', 'email', 'nachricht', 'name'])
assert.deepEqual(Object.keys(grenzen.fehler).sort(), ['email', 'nachricht', 'name'], 'leere Bestellnummer ist erlaubt')
for (const text of Object.values(grenzen.fehler)) assert.match(text, /Bitte|darf/)
assert.equal(normalisiereWiderruf({ name: 'x'.repeat(201), email: 'a@b.de', bestellnummer: '1' }).fehler.name.includes('200'), true)
assert.ok(normalisiereWiderruf({ name: 'Ab', email: 'a@b.de', bestellnummer: 'x'.repeat(33) }).fehler.bestellnummer)
@@ -124,7 +149,7 @@ test('normalisiereWiderruf: Fehler je Feld in Sie-Form', () => {
for (const email of ['a@b.de, c@d.de', 'a@b.de;c@d.de', 'Max <a@b.de>', 'a@b.de\nc@d.de', 'a b@c.de', 'a@b']) {
assert.ok(normalisiereWiderruf({ name: 'Ab', email, bestellnummer: '1' }).fehler?.email, email)
}
assert.deepEqual(Object.keys(normalisiereWiderruf(null).fehler).sort(), ['bestellnummer', 'email', 'name'])
assert.deepEqual(Object.keys(normalisiereWiderruf(null).fehler).sort(), ['email', 'nachricht', 'name'])
assert.ok(normalisiereWiderruf({ name: 5, email: {}, bestellnummer: [] }).fehler.name, 'falsche Typen sind leer')
})