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:
1 parent
5693f37d5f
commit
172d9f1778
6 files changed
+63
-9
No files matched your search
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
|
||||
Reference in new issue
Block a user