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

+7 -1
View File
@@ -19,7 +19,7 @@ import {
} from '../services/shop/produkte.js'
import {
BESTAETIGUNG_TEXT, GEWAEHRLEISTUNG_BILD, gewaehrleistungsHinweis, musterAgb, musterWiderrufsbelehrung, musterWiderrufsformular,
normalisiereRechtstexte, rechtstexteHash, versandUndZahlung, vollstaendig,
normalisiereRechtstexte, pruefeBestaetigbar, rechtstexteHash, versandUndZahlung, vollstaendig,
} from '../services/shop/rechtstexte.js'
import {
BILD_MAX_BYTES, beanspruche, bildLimitErreicht, bildMeta, bildPfad, loescheBild, setzeBildLimitZurueck, speichereBild,
@@ -386,6 +386,8 @@ function rechtstexteAntwort(shop) {
hash,
bestaetigungText: BESTAETIGUNG_TEXT,
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')
}
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 bestaetigt = { texte: { ...entwurf }, hash, at, von }
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_WIDERRUFSFORMULAR = 5000
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 ZAHLUNGSARTEN_REIHENFOLGE = ['card', 'klarna', 'sepa_debit']
@@ -133,7 +135,7 @@ export function musterAgb(e) {
const arten = Array.isArray(e.zahlungsarten) ? e.zahlungsarten : []
const zahlung = zahlungsartenText(e)
const abs = []
abs.push('Muster – bitte prüfen und anpassen')
abs.push(MUSTER_KOPFZEILE)
abs.push('Allgemeine Geschäftsbedingungen')
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)}` : ''}.`
@@ -316,6 +318,40 @@ export function vollstaendig(rechtstexte) {
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. */
export function rechtstexteHash(rechtstexte, einstellungen) {
const r = rechtstexte && typeof rechtstexte === 'object' ? rechtstexte : {}