Shop Review B I-5: Bestaetigen nur ohne Platzhalter, Musterkopf und mit Adresse der Widerrufsfunktion

pruefeBestaetigbar listet die Probleme in Du-Form; POST /api/shop/rechtstexte/bestaetigen
antwortet 400 nicht_bestaetigbar mit der Liste, GET und PUT liefern sie als probleme mit.
Der Reiter Rechtliches zeigt die Punkte im Bestaetigungsblock und sperrt den Knopf.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 09:53:17 +00:00
1 parent 1f56b86791
commit 2f4f360a21
6 files changed
+169 -15

No files matched your search

+18 -2
View File
@@ -1,9 +1,11 @@
/* Webshop, Reiter „Rechtliches“ (Plan Block G, Task G6): die Rechtstexte des Haendlers. Er ist Verkaeufer und verantwortlich; WEBklar liefert /* Webshop, Reiter „Rechtliches“ (Plan Block G, Task G6): die Rechtstexte des Haendlers. Er ist Verkaeufer und verantwortlich; WEBklar liefert
Muster und keine Rechtsberatung. Muster und keine Rechtsberatung.
Schnittstelle (server/routes/shop.js): GET /api/shop/rechtstexte?projectId= => { entwurf, bestaetigt: { texte, at, von, aktuell } | null, Schnittstelle (server/routes/shop.js): GET /api/shop/rechtstexte?projectId= => { entwurf, bestaetigt: { texte, at, von, aktuell } | null,
muster: { agb, widerrufsbelehrung, widerrufsformular }, versandUndZahlung, gewaehrleistung: { bild, text }, hash, bestaetigungText, vollstaendig }; muster: { agb, widerrufsbelehrung, widerrufsformular }, versandUndZahlung, gewaehrleistung: { bild, text }, hash, bestaetigungText, vollstaendig,
probleme: [Text] };
PUT /api/shop/rechtstexte { projectId, rechtstexte } speichert den Entwurf (Antwort wie GET, Feldfehler als err.fehler); PUT /api/shop/rechtstexte { projectId, rechtstexte } speichert den Entwurf (Antwort wie GET, Feldfehler als err.fehler);
POST /api/shop/rechtstexte/bestaetigen { projectId, hash } bestaetigt den gespeicherten Entwurf (409 texte_geaendert, 400 unvollstaendig, POST /api/shop/rechtstexte/bestaetigen { projectId, hash } bestaetigt den gespeicherten Entwurf (409 texte_geaendert, 400 unvollstaendig,
400 nicht_bestaetigbar mit probleme: Platzhalter in eckigen Klammern, Musterkopfzeile, Widerrufsbelehrung ohne Adresse der Widerrufsfunktion;
403 in der Kundenansicht). Der Hash kommt aus der letzten Antwort von GET oder PUT; bestaetigt wird nur, was gespeichert ist: bei ungespeicherten 403 in der Kundenansicht). Der Hash kommt aus der letzten Antwort von GET oder PUT; bestaetigt wird nur, was gespeichert ist: bei ungespeicherten
Aenderungen heisst der Knopf „Erst speichern“. Ersetzen eines vorhandenen Textes durch das Muster fragt vorher in der Seite (kein confirm()). Aenderungen heisst der Knopf „Erst speichern“. Ersetzen eines vorhandenen Textes durch das Muster fragt vorher in der Seite (kein confirm()).
render(panel, ctx): ctx = { projectId, nurLesen, zeigeMeldung(text, art), neuLaden(), request } (siehe shop.js). render(panel, ctx): ctx = { projectId, nurLesen, zeigeMeldung(text, art), neuLaden(), request } (siehe shop.js).
@@ -108,8 +110,11 @@ export async function render(panel, ctx = {}) {
const haken = h('input', { type: 'checkbox', 'data-feld': 'bestaetigung' }) const haken = h('input', { type: 'checkbox', 'data-feld': 'bestaetigung' })
const bestaetigen = h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'bestaetigen' }, 'Texte bestätigen') const bestaetigen = h('button', { type: 'button', class: 'kb-btn-primary', 'data-aktion': 'bestaetigen' }, 'Texte bestätigen')
const fehlerBest = h('p', { class: 'sh-fehler', 'data-fehler': 'bestaetigung', role: 'alert' }) const fehlerBest = h('p', { class: 'sh-fehler', 'data-fehler': 'bestaetigung', role: 'alert' })
// Punkte, die das Bestaetigen verhindern (Platzhalter, Musterkopf, fehlende Adresse der Widerrufsfunktion), vom Server
const problemPlatz = h('div', { 'data-probleme-platz': '' })
const bestBlock = h('section', { class: 'kb-panel sh-abschnitt', 'data-block': 'bestaetigen' }, const bestBlock = h('section', { class: 'kb-panel sh-abschnitt', 'data-block': 'bestaetigen' },
h('h3', null, 'Texte bestätigen'), h('h3', null, 'Texte bestätigen'),
problemPlatz,
h('label', { class: 'sh-haken' }, haken, h('span', null, text(daten.bestaetigungText))), h('label', { class: 'sh-haken' }, haken, h('span', null, text(daten.bestaetigungText))),
hinweisBest, hinweisBest,
fehlerBest, fehlerBest,
@@ -135,7 +140,13 @@ export async function render(panel, ctx = {}) {
statusPlatz.replaceChildren(...teile) statusPlatz.replaceChildren(...teile)
const schmutzig = geaendert() const schmutzig = geaendert()
const bereitZumBestaetigen = !schmutzig && daten.vollstaendig === true const probleme = !schmutzig && Array.isArray(daten.probleme) ? daten.probleme.filter((t) => typeof t === 'string' && t !== '') : []
problemPlatz.replaceChildren(...(probleme.length
? [h('div', { class: 'sh-box sh-box-warn', role: 'alert', 'data-probleme': '' },
h('strong', null, 'Das musst du vor dem Bestätigen noch ändern:'),
h('ul', null, probleme.map((t) => h('li', null, t))))]
: []))
const bereitZumBestaetigen = !schmutzig && daten.vollstaendig === true && probleme.length === 0
// schon bestaetigt und nichts geaendert: nichts zu tun // schon bestaetigt und nichts geaendert: nichts zu tun
bestBlock.hidden = Boolean(b?.aktuell) && !schmutzig bestBlock.hidden = Boolean(b?.aktuell) && !schmutzig
bestaetigen.textContent = schmutzig ? 'Erst speichern' : 'Texte bestätigen' bestaetigen.textContent = schmutzig ? 'Erst speichern' : 'Texte bestätigen'
@@ -201,6 +212,11 @@ export async function render(panel, ctx = {}) {
} }
} catch (err) { } catch (err) {
fehlerBest.textContent = err.message fehlerBest.textContent = err.message
// 400 nicht_bestaetigbar: die Liste des Servers statt des allgemeinen Satzes zeigen
if (err?.code === 'nicht_bestaetigbar' && Array.isArray(err.body?.probleme)) {
daten = { ...daten, probleme: err.body.probleme }
fehlerBest.textContent = ''
}
// 409: die Texte haben sich inzwischen geaendert; der neue Stand wird geholt, ungespeicherte Eingaben gibt es hier nicht (sie sperren den Knopf) // 409: die Texte haben sich inzwischen geaendert; der neue Stand wird geholt, ungespeicherte Eingaben gibt es hier nicht (sie sperren den Knopf)
if (err?.status === 409) { if (err?.status === 409) {
try { try {
+7 -1
View File
@@ -19,7 +19,7 @@ import {
} from '../services/shop/produkte.js' } from '../services/shop/produkte.js'
import { import {
BESTAETIGUNG_TEXT, GEWAEHRLEISTUNG_BILD, gewaehrleistungsHinweis, musterAgb, musterWiderrufsbelehrung, musterWiderrufsformular, BESTAETIGUNG_TEXT, GEWAEHRLEISTUNG_BILD, gewaehrleistungsHinweis, musterAgb, musterWiderrufsbelehrung, musterWiderrufsformular,
normalisiereRechtstexte, rechtstexteHash, versandUndZahlung, vollstaendig, normalisiereRechtstexte, pruefeBestaetigbar, rechtstexteHash, versandUndZahlung, vollstaendig,
} from '../services/shop/rechtstexte.js' } from '../services/shop/rechtstexte.js'
import { import {
BILD_MAX_BYTES, beanspruche, bildLimitErreicht, bildMeta, bildPfad, loescheBild, setzeBildLimitZurueck, speichereBild, BILD_MAX_BYTES, beanspruche, bildLimitErreicht, bildMeta, bildPfad, loescheBild, setzeBildLimitZurueck, speichereBild,
@@ -386,6 +386,8 @@ function rechtstexteAntwort(shop) {
hash, hash,
bestaetigungText: BESTAETIGUNG_TEXT, bestaetigungText: BESTAETIGUNG_TEXT,
vollstaendig: vollstaendig(entwurf), vollstaendig: vollstaendig(entwurf),
// Was das Bestaetigen des gespeicherten Entwurfs derzeit verhindert (Platzhalter, Musterkopf, fehlende Adresse der Widerrufsfunktion)
probleme: pruefeBestaetigbar(entwurf, { widerrufUrl: widerrufUrl(shop.siteKey) }),
} }
} }
@@ -426,6 +428,10 @@ router.post('/rechtstexte/bestaetigen', requireSession, nurKunde, limitWrites, r
throw stop(409, 'Die Texte wurden inzwischen geändert. Bitte lies sie noch einmal durch und bestätige dann erneut.', 'texte_geaendert') throw stop(409, 'Die Texte wurden inzwischen geändert. Bitte lies sie noch einmal durch und bestätige dann erneut.', 'texte_geaendert')
} }
if (!vollstaendig(entwurf)) throw stop(400, 'Bitte fülle AGB, Widerrufsbelehrung, Widerrufsformular, Impressum und Datenschutz aus, bevor du bestätigst.') if (!vollstaendig(entwurf)) throw stop(400, 'Bitte fülle AGB, Widerrufsbelehrung, Widerrufsformular, Impressum und Datenschutz aus, bevor du bestätigst.')
const probleme = pruefeBestaetigbar(entwurf, { widerrufUrl: widerrufUrl(aktuell.siteKey) })
if (probleme.length) {
throw Object.assign(stop(400, 'Die Texte können noch nicht bestätigt werden. Bitte behebe zuerst die genannten Punkte.', 'nicht_bestaetigbar'), { body: { code: 'nicht_bestaetigbar', probleme } })
}
const at = new Date().toISOString() const at = new Date().toISOString()
const bestaetigt = { texte: { ...entwurf }, hash, at, von } const bestaetigt = { texte: { ...entwurf }, hash, at, von }
const json = JSON.stringify({ entwurf, bestaetigt }) const json = JSON.stringify({ entwurf, bestaetigt })
+37 -1
View File
@@ -18,6 +18,8 @@ export const MAX_AGB = 30000
export const MAX_WIDERRUFSBELEHRUNG = 20000 export const MAX_WIDERRUFSBELEHRUNG = 20000
export const MAX_WIDERRUFSFORMULAR = 5000 export const MAX_WIDERRUFSFORMULAR = 5000
const MAX_URL = 500 const MAX_URL = 500
/** Erste Zeile des vorbefuellten Muster-AGB: der Haendler muss sie nach der Pruefung loeschen. */
export const MUSTER_KOPFZEILE = 'Muster – bitte prüfen und anpassen'
const LANDNAMEN = { DE: 'Deutschland', AT: 'Österreich' } const LANDNAMEN = { DE: 'Deutschland', AT: 'Österreich' }
const ZAHLUNGSARTEN_REIHENFOLGE = ['card', 'klarna', 'sepa_debit'] const ZAHLUNGSARTEN_REIHENFOLGE = ['card', 'klarna', 'sepa_debit']
@@ -133,7 +135,7 @@ export function musterAgb(e) {
const arten = Array.isArray(e.zahlungsarten) ? e.zahlungsarten : [] const arten = Array.isArray(e.zahlungsarten) ? e.zahlungsarten : []
const zahlung = zahlungsartenText(e) const zahlung = zahlungsartenText(e)
const abs = [] const abs = []
abs.push('Muster – bitte prüfen und anpassen') abs.push(MUSTER_KOPFZEILE)
abs.push('Allgemeine Geschäftsbedingungen') abs.push('Allgemeine Geschäftsbedingungen')
abs.push( abs.push(
`§ 1 Geltung und Anbieter\n\nDiese Allgemeinen Geschäftsbedingungen gelten für alle Bestellungen von Verbrauchern in unserem Online-Shop. Anbieter und Vertragspartner ist ${anbieterZeile(a)}${text(a.ustId) ? `, USt-IdNr.: ${text(a.ustId)}` : ''}.` `§ 1 Geltung und Anbieter\n\nDiese Allgemeinen Geschäftsbedingungen gelten für alle Bestellungen von Verbrauchern in unserem Online-Shop. Anbieter und Vertragspartner ist ${anbieterZeile(a)}${text(a.ustId) ? `, USt-IdNr.: ${text(a.ustId)}` : ''}.`
@@ -316,6 +318,40 @@ export function vollstaendig(rechtstexte) {
return ['agb', 'widerrufsbelehrung', 'widerrufsformular', 'impressumUrl', 'datenschutzUrl'].every((k) => typeof r[k] === 'string' && r[k].trim() !== '') return ['agb', 'widerrufsbelehrung', 'widerrufsformular', 'impressumUrl', 'datenschutzUrl'].every((k) => typeof r[k] === 'string' && r[k].trim() !== '')
} }
const PLATZHALTER_RE = /\[[^\]\n]{2,}\]/g
const TEXT_IN = { agb: 'den AGB', widerrufsbelehrung: 'der Widerrufsbelehrung', widerrufsformular: 'dem Muster-Widerrufsformular' }
/**
* Kann der Haendler diese Texte bestaetigen? Liefert die Probleme in Du-Form (leere Liste = ja): Platzhalter aus dem Muster
* (`[Name]`, `[Anschrift]`, `[E-Mail-Adresse]`, `[Internetadresse der Widerrufsfunktion]` oder jede andere Angabe in eckigen Klammern),
* die Kopfzeile "Muster – bitte prüfen und anpassen" und eine Widerrufsbelehrung ohne die Adresse der Widerrufsfunktion des Shops
* (Art. 246a § 1 Abs. 2 S. 1 Nr. 1 EGBGB: ohne ordnungsgemaesse Belehrung beginnt die Widerrufsfrist nicht).
* @param {object} rechtstexte Entwurf { agb, widerrufsbelehrung, widerrufsformular, ... }
* @param {{ widerrufUrl?: string }} [opts] Adresse der Widerrufsfunktion des Shops
* @returns {string[]}
*/
export function pruefeBestaetigbar(rechtstexte, { widerrufUrl = '' } = {}) {
const r = rechtstexte && typeof rechtstexte === 'object' ? rechtstexte : {}
const probleme = []
for (const [key, wo] of Object.entries(TEXT_IN)) {
const t = typeof r[key] === 'string' ? r[key] : ''
const marken = [...new Set(t.match(PLATZHALTER_RE) ?? [])]
if (marken.length) {
const zeigen = marken.slice(0, 5).map((m) => `„${m}“`).join(', ')
probleme.push(`In ${wo} stehen noch Platzhalter in eckigen Klammern: ${zeigen}${marken.length > 5 ? ' und weitere' : ''}. Ersetze sie durch deine Angaben. Fehlende Angaben ergänzt du in den Einstellungen, danach setzt du das Muster neu ein.`)
}
if (t.includes(MUSTER_KOPFZEILE)) {
probleme.push(`In ${wo} steht noch die Zeile „${MUSTER_KOPFZEILE}“. Prüfe den Text, passe ihn an und lösche die Zeile.`)
}
}
const url = text(widerrufUrl)
// Eine leere Belehrung meldet vollstaendig(); hier nur eine vorhandene ohne die Adresse
if (url && text(r.widerrufsbelehrung) !== '' && !r.widerrufsbelehrung.includes(url)) {
probleme.push(`Die Widerrufsbelehrung muss die Adresse deiner Widerrufsfunktion enthalten: ${url}. Setze das Muster neu ein oder ergänze den Satz „Sie können Ihr Widerrufsrecht auch online unter ${url} ausüben.“`)
}
return probleme
}
/** sha256 (hex) ueber kanonisches JSON der fuenf Felder plus erzeugtem Versand-/Zahlungstext und Gewaehrleistungshinweis. */ /** sha256 (hex) ueber kanonisches JSON der fuenf Felder plus erzeugtem Versand-/Zahlungstext und Gewaehrleistungshinweis. */
export function rechtstexteHash(rechtstexte, einstellungen) { export function rechtstexteHash(rechtstexte, einstellungen) {
const r = rechtstexte && typeof rechtstexte === 'object' ? rechtstexte : {} const r = rechtstexte && typeof rechtstexte === 'object' ? rechtstexte : {}
+34
View File
@@ -12,6 +12,8 @@ import {
vollstaendig, vollstaendig,
rechtstexteHash, rechtstexteHash,
BESTAETIGUNG_TEXT, BESTAETIGUNG_TEXT,
pruefeBestaetigbar,
MUSTER_KOPFZEILE,
} from '../server/services/shop/rechtstexte.js' } from '../server/services/shop/rechtstexte.js'
import { STANDARD_EINSTELLUNGEN } from '../server/services/shop/einstellungen.js' import { STANDARD_EINSTELLUNGEN } from '../server/services/shop/einstellungen.js'
@@ -340,3 +342,35 @@ test('Markdown-Muster und Code teilen die Kernsaetze', () => {
assert.ok(md.includes('Wir tragen die Kosten der Rücksendung der Waren.')) assert.ok(md.includes('Wir tragen die Kosten der Rücksendung der Waren.'))
assert.doesNotMatch(md, /ec\.europa\.eu\/consumers\/odr/) assert.doesNotMatch(md, /ec\.europa\.eu\/consumers\/odr/)
}) })
test('pruefeBestaetigbar I-5: Muster mit Platzhaltern, Kopfzeile oder ohne Adresse der Widerrufsfunktion lässt sich nicht bestätigen', () => {
const leer = JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN))
const roh = { agb: musterAgb(leer), widerrufsbelehrung: musterWiderrufsbelehrung(leer, {}), widerrufsformular: musterWiderrufsformular(leer) }
const p = pruefeBestaetigbar(roh, { widerrufUrl: WIDERRUF_URL })
const alle = p.join('\n')
for (const s of ['[Name]', '[Anschrift]', '[E-Mail-Adresse]', '[Internetadresse der Widerrufsfunktion]', '[Lieferzeit]', MUSTER_KOPFZEILE, WIDERRUF_URL]) assert.ok(alle.includes(s), s)
assert.match(alle, /^In den AGB stehen noch Platzhalter/m)
assert.match(alle, /dem Muster-Widerrufsformular/)
// ausgefuellt, ohne Kopfzeile, mit Adresse: keine Probleme
const e = basis()
const gut = {
agb: musterAgb(e).replace(`${MUSTER_KOPFZEILE}\n\n`, ''),
widerrufsbelehrung: musterWiderrufsbelehrung(e, { widerrufUrl: WIDERRUF_URL }),
widerrufsformular: musterWiderrufsformular(e),
}
assert.deepEqual(pruefeBestaetigbar(gut, { widerrufUrl: WIDERRUF_URL }), [])
// nur die Kopfzeile
assert.equal(pruefeBestaetigbar({ ...gut, agb: musterAgb(e) }, { widerrufUrl: WIDERRUF_URL }).length, 1)
// Belehrung ohne die Adresse
const ohneUrl = pruefeBestaetigbar({ ...gut, widerrufsbelehrung: gut.widerrufsbelehrung.replace(WIDERRUF_URL, 'https://anders.example/') }, { widerrufUrl: WIDERRUF_URL })
assert.equal(ohneUrl.length, 1)
assert.match(ohneUrl[0], /Adresse deiner Widerrufsfunktion enthalten: https:\/\/brot\.example\/shop#widerruf/)
// ohne bekannte Adresse wird sie nicht verlangt; beliebige Angabe in eckigen Klammern zaehlt, einzelne Zeichen nicht
assert.deepEqual(pruefeBestaetigbar(gut, {}), [])
assert.equal(pruefeBestaetigbar({ ...gut, agb: `${gut.agb}\n[Zahlungsarten]` }, { widerrufUrl: WIDERRUF_URL }).length, 1)
assert.deepEqual(pruefeBestaetigbar({ ...gut, agb: `${gut.agb}\nSiehe Anlage [1]` }, { widerrufUrl: WIDERRUF_URL }), [])
// fehlende Texte stoeren hier nicht (das prueft vollstaendig)
assert.doesNotThrow(() => pruefeBestaetigbar(null))
assert.deepEqual(pruefeBestaetigbar({}, { widerrufUrl: WIDERRUF_URL }), [])
})
+37 -11
View File
@@ -61,6 +61,9 @@ const TEXTE = {
datenschutzUrl: 'https://toepferei.example/datenschutz', datenschutzUrl: 'https://toepferei.example/datenschutz',
} }
const ARTIKEL = { name: 'Tasse', preisCent: 1800 } const ARTIKEL = { name: 'Tasse', preisCent: 1800 }
/** Bestaetigungsfaehige Texte: die Belehrung nennt die Adresse der Widerrufsfunktion dieses Shops (I-5). */
const WIDERRUF = (feature) => `${config.publicBaseUrl}/shop/${feature.siteKey}/widerruf`
const texteFuer = (feature) => ({ ...TEXTE, widerrufsbelehrung: `Unsere Widerrufsbelehrung. Widerruf online: ${WIDERRUF(feature)}` })
const q = (projectId = 'pt1') => `projectId=${projectId}` const q = (projectId = 'pt1') => `projectId=${projectId}`
const lesen = (pfad, projectId = 'pt1', opts = { cookie }) => send('GET', `/api/shop/${pfad}${pfad.includes('?') ? '&' : '?'}${q(projectId)}`, opts) const lesen = (pfad, projectId = 'pt1', opts = { cookie }) => send('GET', `/api/shop/${pfad}${pfad.includes('?') ? '&' : '?'}${q(projectId)}`, opts)
@@ -98,7 +101,7 @@ async function machBereit({ mitArtikel = true } = {}) {
const feature = await aktiviere() const feature = await aktiviere()
assert.equal((await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })).status, 200) assert.equal((await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })).status, 200)
if (mitArtikel) assert.equal((await schreiben('POST', 'produkte', { produkt: ARTIKEL })).status, 201) if (mitArtikel) assert.equal((await schreiben('POST', 'produkte', { produkt: ARTIKEL })).status, 201)
assert.equal((await schreiben('PUT', 'rechtstexte', { rechtstexte: TEXTE })).status, 200) assert.equal((await schreiben('PUT', 'rechtstexte', { rechtstexte: texteFuer(feature) })).status, 200)
const hash = (await lesen('rechtstexte')).body.hash const hash = (await lesen('rechtstexte')).body.hash
assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash })).status, 200) assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash })).status, 200)
await updateDocument(config.collections.shopConfig, feature.$id, { await updateDocument(config.collections.shopConfig, feature.$id, {
@@ -447,10 +450,11 @@ test('Rechtstexte GET: Entwurf leer, nichts bestaetigt, Muster, erzeugte Texte,
const res = await lesen('rechtstexte') const res = await lesen('rechtstexte')
assert.equal(res.status, 200) assert.equal(res.status, 200)
const b = res.body const b = res.body
assert.deepEqual(Object.keys(b).sort(), ['bestaetigt', 'bestaetigungText', 'entwurf', 'gewaehrleistung', 'hash', 'muster', 'versandUndZahlung', 'vollstaendig']) assert.deepEqual(Object.keys(b).sort(), ['bestaetigt', 'bestaetigungText', 'entwurf', 'gewaehrleistung', 'hash', 'muster', 'probleme', 'versandUndZahlung', 'vollstaendig'])
assert.deepEqual(b.entwurf, { agb: '', widerrufsbelehrung: '', widerrufsformular: '', impressumUrl: '', datenschutzUrl: '' }) assert.deepEqual(b.entwurf, { agb: '', widerrufsbelehrung: '', widerrufsformular: '', impressumUrl: '', datenschutzUrl: '' })
assert.equal(b.bestaetigt, null) assert.equal(b.bestaetigt, null)
assert.equal(b.vollstaendig, false) assert.equal(b.vollstaendig, false)
assert.deepEqual(b.probleme, [], 'leerer Entwurf: nur vollstaendig meldet etwas')
assert.match(b.hash, /^[0-9a-f]{64}$/) assert.match(b.hash, /^[0-9a-f]{64}$/)
assert.equal(b.hash, rechtstexteHash(b.entwurf, (await lesen('einstellungen')).body.einstellungen)) assert.equal(b.hash, rechtstexteHash(b.entwurf, (await lesen('einstellungen')).body.einstellungen))
for (const k of ['agb', 'widerrufsbelehrung', 'widerrufsformular']) assert.ok(b.muster[k].length > 100, k) for (const k of ['agb', 'widerrufsbelehrung', 'widerrufsformular']) assert.ok(b.muster[k].length > 100, k)
@@ -485,13 +489,13 @@ test('Rechtstexte PUT: speichert den Entwurf, normalisiert, Fehler 400; Hash aen
test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400, sonst Kopie mit Zeit, Person und Hash', async () => { test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400, sonst Kopie mit Zeit, Person und Hash', async () => {
const feature = await aktiviere() const feature = await aktiviere()
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN }) await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })
const unvollstaendig = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...TEXTE, datenschutzUrl: '' } }) const unvollstaendig = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...texteFuer(feature), datenschutzUrl: '' } })
const hashUnvollstaendig = unvollstaendig.body.hash const hashUnvollstaendig = unvollstaendig.body.hash
const leer = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: hashUnvollstaendig }) const leer = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: hashUnvollstaendig })
assert.equal(leer.status, 400) assert.equal(leer.status, 400)
assert.equal(shopConfigDoc(feature).rechtstexteHash || '', '') assert.equal(shopConfigDoc(feature).rechtstexteHash || '', '')
const voll = await schreiben('PUT', 'rechtstexte', { rechtstexte: TEXTE }) const voll = await schreiben('PUT', 'rechtstexte', { rechtstexte: texteFuer(feature) })
const falsch = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: 'abc' }) const falsch = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: 'abc' })
assert.deepEqual([falsch.status, falsch.body.code], [409, 'texte_geaendert']) assert.deepEqual([falsch.status, falsch.body.code], [409, 'texte_geaendert'])
assert.match(falsch.body.error, /^Die Texte wurden inzwischen geändert/) assert.match(falsch.body.error, /^Die Texte wurden inzwischen geändert/)
@@ -500,7 +504,7 @@ test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400,
const ok = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: voll.body.hash }) const ok = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: voll.body.hash })
assert.equal(ok.status, 200) assert.equal(ok.status, 200)
assert.deepEqual(ok.body.bestaetigt.texte, TEXTE) assert.deepEqual(ok.body.bestaetigt.texte, texteFuer(feature))
assert.equal(ok.body.bestaetigt.von, `${customers.c1.name} <${customers.c1.email}>`) assert.equal(ok.body.bestaetigt.von, `${customers.c1.name} <${customers.c1.email}>`)
assert.equal(ok.body.bestaetigt.aktuell, true) assert.equal(ok.body.bestaetigt.aktuell, true)
assert.ok(Math.abs(Date.now() - Date.parse(ok.body.bestaetigt.at)) < 10_000) assert.ok(Math.abs(Date.now() - Date.parse(ok.body.bestaetigt.at)) < 10_000)
@@ -509,13 +513,13 @@ test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400,
assert.equal(doc.rechtstexteBestaetigtVon, `${customers.c1.name} <${customers.c1.email}>`) assert.equal(doc.rechtstexteBestaetigtVon, `${customers.c1.name} <${customers.c1.email}>`)
assert.equal(new Date(doc.rechtstexteBestaetigtAt).toISOString(), new Date(ok.body.bestaetigt.at).toISOString()) assert.equal(new Date(doc.rechtstexteBestaetigtAt).toISOString(), new Date(ok.body.bestaetigt.at).toISOString())
const gespeichert = JSON.parse(doc.rechtstexte) const gespeichert = JSON.parse(doc.rechtstexte)
assert.deepEqual(gespeichert.entwurf, TEXTE) assert.deepEqual(gespeichert.entwurf, texteFuer(feature))
assert.deepEqual([gespeichert.bestaetigt.texte, gespeichert.bestaetigt.hash], [TEXTE, voll.body.hash]) assert.deepEqual([gespeichert.bestaetigt.texte, gespeichert.bestaetigt.hash], [texteFuer(feature), voll.body.hash])
assert.equal((await shopPerSiteKey(feature.siteKey)).shop.rechtstexte.bestaetigt.hash, voll.body.hash, 'Speicher wurde geleert') assert.equal((await shopPerSiteKey(feature.siteKey)).shop.rechtstexte.bestaetigt.hash, voll.body.hash, 'Speicher wurde geleert')
// Entwurf aendern: die bestaetigte Fassung bleibt, ist aber nicht mehr aktuell; Bereitschaft verlangt neue Bestaetigung // Entwurf aendern: die bestaetigte Fassung bleibt, ist aber nicht mehr aktuell; Bereitschaft verlangt neue Bestaetigung
const geaendert = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...TEXTE, agb: 'Geaenderte AGB' } }) const geaendert = await schreiben('PUT', 'rechtstexte', { rechtstexte: { ...texteFuer(feature), agb: 'Geaenderte AGB' } })
assert.deepEqual(geaendert.body.bestaetigt.texte, TEXTE) assert.deepEqual(geaendert.body.bestaetigt.texte, texteFuer(feature))
assert.equal(geaendert.body.bestaetigt.aktuell, false) assert.equal(geaendert.body.bestaetigt.aktuell, false)
assert.equal(geaendert.body.entwurf.agb, 'Geaenderte AGB') assert.equal(geaendert.body.entwurf.agb, 'Geaenderte AGB')
const stand = (await lesen('status')).body.bereitschaft.punkte.find((p) => p.id === 'rechtstexte') const stand = (await lesen('status')).body.bereitschaft.punkte.find((p) => p.id === 'rechtstexte')
@@ -524,9 +528,9 @@ test('Rechtstexte bestaetigen: falscher oder alter Hash 409, unvollstaendig 400,
}) })
test('Rechtstexte: Hash haengt auch an den Einstellungen (Versand und Zahlung), eine Aenderung dort verlangt neue Bestaetigung', async () => { test('Rechtstexte: Hash haengt auch an den Einstellungen (Versand und Zahlung), eine Aenderung dort verlangt neue Bestaetigung', async () => {
await aktiviere() const feature = await aktiviere()
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN }) await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })
await schreiben('PUT', 'rechtstexte', { rechtstexte: TEXTE }) await schreiben('PUT', 'rechtstexte', { rechtstexte: texteFuer(feature) })
const alt = (await lesen('rechtstexte')).body.hash const alt = (await lesen('rechtstexte')).body.hash
await schreiben('PUT', 'einstellungen', { einstellungen: { ...EINSTELLUNGEN, versand: { ...EINSTELLUNGEN.versand, kostenCent: { DE: 690 } } } }) await schreiben('PUT', 'einstellungen', { einstellungen: { ...EINSTELLUNGEN, versand: { ...EINSTELLUNGEN.versand, kostenCent: { DE: 690 } } } })
const neu = (await lesen('rechtstexte')).body.hash const neu = (await lesen('rechtstexte')).body.hash
@@ -535,6 +539,28 @@ test('Rechtstexte: Hash haengt auch an den Einstellungen (Versand und Zahlung),
assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash: neu })).status, 200) assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash: neu })).status, 200)
}) })
test('Rechtstexte bestaetigen I-5: Platzhalter, Musterkopf und fehlende Adresse der Widerrufsfunktion ⇒ 400 nicht_bestaetigbar mit Liste; GET zeigt dieselben Probleme', async () => {
const feature = await aktiviere()
await schreiben('PUT', 'einstellungen', { einstellungen: EINSTELLUNGEN })
const schlecht = { ...texteFuer(feature), agb: 'Muster – bitte prüfen und anpassen\n\nAGB von [Name], [Anschrift]', widerrufsbelehrung: 'Unsere Widerrufsbelehrung ohne Adresse.' }
const put = await schreiben('PUT', 'rechtstexte', { rechtstexte: schlecht })
assert.equal(put.status, 200)
assert.equal(put.body.probleme.length, 3)
assert.deepEqual((await lesen('rechtstexte')).body.probleme, put.body.probleme)
const res = await schreiben('POST', 'rechtstexte/bestaetigen', { hash: put.body.hash })
assert.equal(res.status, 400)
assert.equal(res.body.code, 'nicht_bestaetigbar')
assert.deepEqual(res.body.probleme, put.body.probleme)
assert.match(res.body.probleme.join('\n'), /„\[Name\]“, „\[Anschrift\]“/)
assert.match(res.body.probleme.join('\n'), /Muster – bitte prüfen und anpassen/)
assert.ok(res.body.probleme.join('\n').includes(WIDERRUF(feature)))
assert.equal(shopConfigDoc(feature).rechtstexteHash || '', '', 'nichts bestaetigt')
// behoben ⇒ ohne Probleme und bestaetigbar
const gut = await schreiben('PUT', 'rechtstexte', { rechtstexte: texteFuer(feature) })
assert.deepEqual(gut.body.probleme, [])
assert.equal((await schreiben('POST', 'rechtstexte/bestaetigen', { hash: gut.body.hash })).status, 200)
})
// ---------------------------------------------------------------- Oeffnen // ---------------------------------------------------------------- Oeffnen
test('Oeffnen: nur wenn bereit (sonst 409 mit Punkten), Schliessen immer, Speicher geleert, Eingabe geprueft', async () => { test('Oeffnen: nur wenn bereit (sonst 409 mit Punkten), Schliessen immer, Speicher geleert, Eingabe geprueft', async () => {
+36
View File
@@ -257,6 +257,42 @@ test('ungespeicherte Änderungen sperren das Bestätigen: „Erst speichern“,
} }
}) })
test('I-5: Probleme des Servers (Platzhalter, Musterkopf, Adresse der Widerrufsfunktion) stehen im Bestätigungsblock und sperren den Knopf', async () => {
const probleme = ['In den AGB stehen noch Platzhalter in eckigen Klammern: „[Name]“.', 'Die Widerrufsbelehrung muss die Adresse deiner Widerrufsfunktion enthalten: https://x.example/w.']
const fx = startFx({ get: () => antwort(VOLL, { hash: 'h1', probleme }), post: () => ({ body: antwort(VOLL) }) })
try {
const { root } = await oeffne(fx)
const box = root.querySelector('[data-probleme]')
assert.ok(box, 'Liste sichtbar')
assert.deepEqual([...box.querySelectorAll('li')].map((li) => li.textContent), probleme)
await fx.klick(haken(root))
assert.equal(knopf(root, 'bestaetigen').disabled, true, 'mit Problemen gesperrt')
await fx.klick(knopf(root, 'bestaetigen'))
assert.equal(sendet(fx, 'POST').length, 0)
} finally {
fx.restore()
}
})
test('I-5: lehnt der Server mit nicht_bestaetigbar ab, zeigt der Reiter dessen Liste', async () => {
const probleme = ['In den AGB steht noch die Zeile „Muster – bitte prüfen und anpassen“. Prüfe den Text, passe ihn an und lösche die Zeile.']
const fx = startFx({
get: () => antwort(VOLL, { hash: 'h1' }),
post: () => ({ status: 400, body: { error: 'Die Texte können noch nicht bestätigt werden. Bitte behebe zuerst die genannten Punkte.', code: 'nicht_bestaetigbar', probleme } }),
})
try {
const { root } = await oeffne(fx)
assert.equal(root.querySelector('[data-probleme]'), null)
await fx.klick(haken(root))
await fx.klick(knopf(root, 'bestaetigen'))
assert.equal(sendet(fx, 'POST').length, 1)
assert.deepEqual([...root.querySelector('[data-probleme]').querySelectorAll('li')].map((li) => li.textContent), probleme)
assert.equal(knopf(root, 'bestaetigen').disabled, true)
} finally {
fx.restore()
}
})
test('unvollständige Texte lassen sich nicht bestätigen und sagen warum', mitDom({ get: () => antwort({ ...VOLL, datenschutzUrl: '' }) }, async (fx) => { test('unvollständige Texte lassen sich nicht bestätigen und sagen warum', mitDom({ get: () => antwort({ ...VOLL, datenschutzUrl: '' }) }, async (fx) => {
const { root } = await oeffne(fx) const { root } = await oeffne(fx)
await fx.klick(haken(root)) await fx.klick(haken(root))