Shop C5: Muster-Rechtstexte (Widerrufsbelehrung, Formular, AGB), Versand/Zahlung, Gewaehrleistungshinweis, Normalisierung und Hash

Widerrufsbelehrung nach Anlage 1 (Stand 2026, mit Hinweis auf die Widerrufsfunktion), Formular nach Anlage 2,
Muster-AGB ohne OS-Plattform, erzeugter Versand-/Zahlungstext, Gewaehrleistungshinweis (Wortlaut zu pruefen),
Muster-Markdown fuer die Kanzlei und Tests inkl. Abgleich Markdown/Code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 08:40:23 +00:00
1 parent bea9f3cfba
commit 0063c04d8a
3 files changed
+773

No files matched your search

+150
View File
@@ -0,0 +1,150 @@
# Shop: Muster-Rechtstexte (Widerrufsbelehrung, Widerrufsformular, AGB)
**Muster – keine Rechtsberatung.** Diese Texte sind Muster, die das Kundenportal im Webshop vorbefüllt (`server/services/shop/rechtstexte.js`, dort mit den Angaben aus den Shop-Einstellungen). Der Händler prüft sie, passt sie an und bestätigt sie selbst; WEBklar berät nicht rechtlich. Diese Datei enthält dieselben Texte mit Platzhaltern in eckigen Klammern zur Prüfung durch eine Kanzlei (Plan 2026-10-05, Task C5; Offene Punkte 5 und 9 der Recherche).
Stand: 2026-10-05.
## Quellen und Fundstellen
- Widerrufsbelehrung: Anlage 1 zu Art. 246a § 1 Abs. 2 S. 2 EGBGB, Fassung 2026 mit dem Baustein zur Widerrufsfunktion (https://www.gesetze-im-internet.de/bgbeg/BJNR006049896.html). Ein richtig ausgefülltes Muster erfüllt die Informationspflicht (Art. 246a § 1 Abs. 2 S. 3 EGBGB).
- Elektronische Widerrufsfunktion: § 356a BGB (seit 19.06.2026), Hinweispflicht in der Belehrung nach Art. 246a § 1 Abs. 2 S. 1 Nr. 1 EGBGB.
- Widerrufsformular: Anlage 2 zu Art. 246a § 1 Abs. 2 S. 1 Nr. 1 EGBGB.
- Ausschluss und vorzeitiges Erlöschen: § 312g Abs. 2 Nr. 1, 2, 3 BGB; Information nach Art. 246a § 1 Abs. 3 EGBGB.
- Gewährleistungshinweis: Art. 246a § 1 Abs. 1 S. 1 Nr. 11 EGBGB i. V. m. Durchführungsverordnung (EU) 2025/1960 Anhang I (seit 27.09.2026).
- Streitbeilegung: § 36 VSBG. Die EU-Plattform zur Online-Streitbeilegung wurde am 20.07.2025 eingestellt; es gibt keinen Link darauf.
- Button und Vertragsschluss: § 312j Abs. 3 und 4 BGB, § 312i BGB. Recherche: `/root/reports/webshop-2026-10-05/recherche-recht-stripe.md` (B.1 bis B.10).
## Hinweise zur Prüfung (nicht Teil der Texte)
- Fristbeginn: Die Belehrung enthält beide gesetzlichen Alternativen (eine Ware bzw. die Waren; mehrere Waren einer Bestellung, getrennt geliefert: die letzte Ware), weil der Warenkorb beides erzeugen kann. Teilsendungen und regelmäßige Lieferung sind nicht vorgesehen.
- Rücksendekosten: Standard ist „Käufer trägt“. Variante „Händler trägt“ steht unten.
- Abholung: Bietet der Shop Abholung an, kommt der Satz „Haben Sie die Waren bei uns abgeholt …“ hinzu.
- Ausschlussgründe: Der letzte Absatz der Belehrung nennt § 312g Abs. 2 Nr. 1, 2 und 3 BGB in allgemeiner Form. Ob und für welche Waren er zutrifft, entscheidet der Händler; Einordnung von Lieferung und Abholung von Speisen (§ 312g Abs. 2 Nr. 9, § 312 Abs. 2 Nr. 8 BGB) ist offen (Recherche, Offene Punkte 6).
- Wortlaut des Gewährleistungshinweises: im Code als „Wortlaut prüfen (Kanzlei)“ markiert; der amtliche deutsche Wortlaut von Anhang I ist einzusetzen.
- Widerrufsfunktion: Erreichbarkeit ohne Login bei Gastbestellungen, Identifikation der Bestellung und Zeitstempel mit der Kanzlei abstimmen (Recherche, Offene Punkte 7).
## Widerrufsbelehrung
Widerrufsbelehrung
Widerrufsrecht
Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.
Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die Waren in Besitz genommen haben bzw. hat.
Haben Sie mehrere Waren im Rahmen einer einheitlichen Bestellung bestellt und werden diese getrennt geliefert, beträgt die Widerrufsfrist vierzehn Tage ab dem Tag, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die letzte Ware in Besitz genommen haben bzw. hat.
Um Ihr Widerrufsrecht auszuüben, müssen Sie uns ([Name des Händlers], [Straße Nr.], [PLZ] [Ort], E-Mail: [E-Mail-Adresse]) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.
Sie können Ihr Widerrufsrecht auch online unter [Internetadresse der Widerrufsfunktion im Shop] ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.
Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.
Folgen des Widerrufs
Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.
Wir können die Rückzahlung verweigern, bis wir die Waren wieder zurückerhalten haben oder bis Sie den Nachweis erbracht haben, dass Sie die Waren zurückgesandt haben, je nachdem, welches der frühere Zeitpunkt ist.
Sie haben die Waren unverzüglich und in jedem Fall spätestens binnen vierzehn Tagen ab dem Tag, an dem Sie uns über den Widerruf dieses Vertrags unterrichten, an uns ([Name des Händlers], [Straße Nr.], [PLZ] [Ort]) zurückzusenden oder zu übergeben. Die Frist ist gewahrt, wenn Sie die Waren vor Ablauf der Frist von vierzehn Tagen absenden.
Sie tragen die unmittelbaren Kosten der Rücksendung der Waren.
Sie müssen für einen etwaigen Wertverlust der Waren nur aufkommen, wenn dieser Wertverlust auf einen zur Prüfung der Beschaffenheit, Eigenschaften und Funktionsweise der Waren nicht notwendigen Umgang mit ihnen zurückzuführen ist.
Ausschluss bzw. vorzeitiges Erlöschen des Widerrufsrechts
Das Widerrufsrecht besteht nicht bei Verträgen zur Lieferung von Waren, die nicht vorgefertigt sind und für deren Herstellung eine individuelle Auswahl oder Bestimmung durch den Verbraucher maßgeblich ist oder die eindeutig auf die persönlichen Bedürfnisse des Verbrauchers zugeschnitten sind (§ 312g Abs. 2 Nr. 1 BGB), z. B. Waren mit Wunschaufdruck oder Maßanfertigungen.
Das Widerrufsrecht besteht nicht bei Verträgen zur Lieferung von Waren, die schnell verderben können oder deren Verfallsdatum schnell überschritten würde (§ 312g Abs. 2 Nr. 2 BGB), z. B. frische Lebensmittel.
Das Widerrufsrecht erlischt vorzeitig bei Verträgen zur Lieferung versiegelter Waren, die aus Gründen des Gesundheitsschutzes oder der Hygiene nicht zur Rückgabe geeignet sind, wenn ihre Versiegelung nach der Lieferung entfernt wurde (§ 312g Abs. 2 Nr. 3 BGB).
Ende der Widerrufsbelehrung
### Variante: Händler trägt die Rücksendekosten
Statt „Sie tragen die unmittelbaren Kosten der Rücksendung der Waren.“ steht:
Wir tragen die Kosten der Rücksendung der Waren.
### Variante: Abholung angeboten
Nach dem Absatz zur Rücksendung folgt zusätzlich:
Haben Sie die Waren bei uns abgeholt, übergeben Sie diese zur Rückgabe an unserer Anschrift: [Abholadresse].
## Widerrufsformular
Muster-Widerrufsformular
(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)
An [Name des Händlers], [Straße Nr.], [PLZ] [Ort], E-Mail: [E-Mail-Adresse]:
Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)
– Bestellt am (*)/erhalten am (*)
– Name des/der Verbraucher(s)
– Anschrift des/der Verbraucher(s)
– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)
– Datum
(*) Unzutreffendes streichen.
## Allgemeine Geschäftsbedingungen
Die Kopfzeile „Muster – bitte prüfen und anpassen“ steht im vorbefüllten Text und wird vom Händler nach der Prüfung entfernt. Die Zahlungsarten, Länder und Abholung werden aus den Shop-Einstellungen eingesetzt; die Platzhalter unten zeigen den Standardfall.
Muster – bitte prüfen und anpassen
Allgemeine Geschäftsbedingungen
§ 1 Geltung und Anbieter
Diese Allgemeinen Geschäftsbedingungen gelten für alle Bestellungen von Verbrauchern in unserem Online-Shop. Anbieter und Vertragspartner ist [Name des Händlers], [Straße Nr.], [PLZ] [Ort], E-Mail: [E-Mail-Adresse].
§ 2 Vertragsschluss
Die Darstellung der Waren im Shop ist kein verbindliches Angebot. Mit dem Klick auf die Schaltfläche „Zahlungspflichtig bestellen“ geben Sie eine verbindliche Bestellung ab. Wir bestätigen den Eingang der Bestellung unverzüglich per E-Mail. Der Vertrag kommt zustande, sobald wir Ihnen nach erfolgreicher Zahlung die Bestellbestätigung per E-Mail senden. Bestellungen, die nicht bezahlt werden, werden nach Ablauf der Zahlungsfrist verworfen.
§ 3 Preise und Versandkosten
Es gelten die zum Zeitpunkt der Bestellung im Shop angegebenen Preise in Euro. Alle Preise enthalten die gesetzliche Umsatzsteuer. Versandkosten und etwaige weitere Kosten werden vor Abgabe der Bestellung gesondert ausgewiesen und richten sich nach dem Zielland. Einzelheiten finden Sie unter „Versand & Zahlung“.
§ 4 Zahlung
Die Zahlung erfolgt über den Zahlungsdienstleister Stripe. Es stehen folgende Zahlungsarten zur Verfügung: Kreditkarte, Apple Pay, Google Pay, Klarna, SEPA-Lastschrift. Bei SEPA-Lastschrift versenden wir die Ware erst nach Eingang der Zahlung.
§ 5 Lieferung und Abholung
Wir liefern in folgende Länder: Deutschland. Die Lieferzeit beträgt [Lieferzeit], gerechnet ab Zahlungseingang. Ist eine Ware nicht verfügbar, informieren wir Sie unverzüglich.
§ 6 Eigentumsvorbehalt
Die Waren bleiben bis zur vollständigen Bezahlung unser Eigentum.
§ 7 Widerrufsrecht
Verbrauchern steht ein gesetzliches Widerrufsrecht zu. Einzelheiten ergeben sich aus der Widerrufsbelehrung, die Sie unter „Widerrufsbelehrung“ im Shop finden.
§ 8 Gewährleistung
Es gelten die gesetzlichen Mängelhaftungsrechte. Der Hinweis auf das gesetzliche Gewährleistungsrecht steht vor Abgabe der Bestellung im Shop.
§ 9 Vertragstext und Sprache
Wir speichern den Vertragstext nach Vertragsschluss nicht für Sie abrufbar. Ihre Bestelldaten, diese Allgemeinen Geschäftsbedingungen und die Widerrufsbelehrung erhalten Sie mit der Bestellbestätigung per E-Mail; bitte bewahren Sie diese E-Mail auf. Die Vertragssprache ist Deutsch.
§ 10 Streitbeilegung
[Name des Händlers] ist nicht bereit und nicht verpflichtet, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen (§ 36 VSBG).
Bei Kleinunternehmern steht in § 3 statt „Alle Preise enthalten die gesetzliche Umsatzsteuer.“ der Satz „Gemäß § 19 UStG wird keine Umsatzsteuer berechnet und ausgewiesen.“
## Hinweis auf das gesetzliche Gewährleistungsrecht (Kasse und Bestellbestätigung)
Wortlaut prüfen (Kanzlei): sinngemäße Fassung, bis der amtliche Wortlaut von Anhang I eingesetzt ist.
Gesetzliches Gewährleistungsrecht
Für Waren besteht ein gesetzliches Gewährleistungsrecht von mindestens zwei Jahren ab Lieferung der Ware. Ist die Ware nicht vertragsgemäß, haben Sie zunächst Anspruch auf kostenlose Nachbesserung (Reparatur) oder Ersatzlieferung; unter den gesetzlichen Voraussetzungen können Sie stattdessen den Preis mindern oder vom Vertrag zurücktreten. Diese Rechte bestehen unabhängig von einer Garantie.
+312
View File
@@ -0,0 +1,312 @@
import { createHash } from 'node:crypto'
import { euro } from './geld.js'
import { STANDARD_EINSTELLUNGEN } from './einstellungen.js'
/**
* Muster-Rechtstexte des Haendlers (Plan 2026-10-05-webshop.md, Task C5; Spec 4.6).
* Alles rein: Einstellungen rein, Klartext raus (Absaetze mit '\n\n' getrennt), Sie-Form fuer Verbraucher.
* Das sind MUSTER, keine Rechtsberatung. Der Haendler prueft, passt an und bestaetigt selbst (BESTAETIGUNG_TEXT).
* Quellen: Anlage 1 und 2 zu Art. 246a § 1 Abs. 2 S. 2 EGBGB (Stand 2026, mit Baustein zur Widerrufsfunktion, § 356a BGB),
* § 312g Abs. 2 BGB, § 36 VSBG, VO (EU) 2025/1960 Anhang I; siehe /root/reports/webshop-2026-10-05/recherche-recht-stripe.md
* (B.2, B.3, B.3a, B.8). Dieselben Texte mit Platzhaltern: docs/funktionen/shop-muster-rechtstexte.md.
*/
export const BESTAETIGUNG_TEXT =
'Ich habe die Texte geprüft. Ich bin als Verkäufer für Rechtstexte, Produktangaben und Preise verantwortlich. WEBklar stellt Muster bereit und berät nicht rechtlich.'
export const MAX_AGB = 30000
export const MAX_WIDERRUFSBELEHRUNG = 20000
export const MAX_WIDERRUFSFORMULAR = 5000
const MAX_URL = 500
const LANDNAMEN = { DE: 'Deutschland', AT: 'Österreich' }
const ZAHLUNGSARTEN_REIHENFOLGE = ['card', 'klarna', 'sepa_debit']
const ZAHLUNGSARTEN_TEXT = { card: 'Kreditkarte, Apple Pay, Google Pay', klarna: 'Klarna', sepa_debit: 'SEPA-Lastschrift' }
/** Einstellungen mit Standardwerten auffuellen (Unterobjekte eine Ebene tief), damit Teilobjekte nicht werfen. */
function mitStandard(e) {
const std = STANDARD_EINSTELLUNGEN
const roh = e && typeof e === 'object' ? e : {}
const sub = (k) => ({ ...std[k], ...(roh[k] && typeof roh[k] === 'object' && !Array.isArray(roh[k]) ? roh[k] : {}) })
return { ...std, ...roh, anbieter: sub('anbieter'), versand: sub('versand'), abholung: sub('abholung') }
}
const text = (s) => (typeof s === 'string' ? s.trim() : '')
function anschrift(a) {
const ort = [text(a.plz), text(a.ort)].filter(Boolean).join(' ')
const land = text(a.land) && a.land !== 'DE' ? (LANDNAMEN[a.land] ?? a.land) : ''
return [text(a.strasse), ort, land].filter(Boolean).join(', ') || '[Anschrift]'
}
function anbieterZeile(a) {
return [text(a.name) || '[Name]', anschrift(a), text(a.telefon) ? `Telefon: ${text(a.telefon)}` : '', `E-Mail: ${text(a.email) || '[E-Mail-Adresse]'}`]
.filter(Boolean)
.join(', ')
}
function laenderListe(e) {
const laender = Array.isArray(e.versand.laender) ? e.versand.laender : []
return laender.map((l) => LANDNAMEN[l] ?? l).join(', ')
}
function steuerSatz(e) {
return e.kleinunternehmer
? 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet und ausgewiesen.'
: 'Alle Preise enthalten die gesetzliche Umsatzsteuer.'
}
/** 'Kreditkarte, Apple Pay, Google Pay, Klarna': aktive Zahlungsarten in fester Reihenfolge. Auch fuer die Widget-Konfiguration. */
export function zahlungsartenText(e) {
const arten = Array.isArray(e?.zahlungsarten) ? e.zahlungsarten : []
return ZAHLUNGSARTEN_REIHENFOLGE.filter((a) => arten.includes(a))
.map((a) => ZAHLUNGSARTEN_TEXT[a])
.join(', ')
}
/**
* Widerrufsbelehrung fuer Waren: gesetzliches Muster (Anlage 1 zu Art. 246a § 1 Abs. 2 S. 2 EGBGB, Stand 2026) mit Baustein
* zur Widerrufsfunktion (§ 356a BGB).
* Gestaltungshinweise, so gewaehlt: Fristbeginn = Alternative "Kaufvertrag" (Waren in Besitz genommen) UND Alternative "mehrere
* Waren einer einheitlichen Bestellung, getrennt geliefert" (letzte Ware), beide als eigene Saetze, weil der Warenkorb beides
* erzeugen kann. Teilsendungen/regelmaessige Lieferung entfallen (kommt im Shop nicht vor). Nicht abgeholt: "Wir holen die Waren ab"
* entfaellt, dafuer die Rueckgabe-Alternative (zurueckzusenden oder zu uebergeben) samt Rueckzahlungsverweigerungs-Satz.
* Rueckkosten nach e.ruecksendekosten ('kaeufer' = Standard, 'haendler').
* @param {object} e Einstellungen
* @param {{ widerrufUrl?: string }} [opts] Adresse der Widerrufsfunktion im Shop
*/
export function musterWiderrufsbelehrung(e, { widerrufUrl = '' } = {}) {
e = mitStandard(e)
const a = e.anbieter
const kontakt = anbieterZeile(a)
const url = text(widerrufUrl) || '[Internetadresse der Widerrufsfunktion]'
const rueckadresse = `${text(a.name) || '[Name]'}, ${anschrift(a)}`
const kosten = e.ruecksendekosten === 'haendler' ? 'Wir tragen die Kosten der Rücksendung der Waren.' : 'Sie tragen die unmittelbaren Kosten der Rücksendung der Waren.'
const abholung = e.abholung.aktiv
? `Haben Sie die Waren bei uns abgeholt, übergeben Sie diese zur Rückgabe an unserer Anschrift: ${text(e.abholung.adresse) || rueckadresse}.`
: ''
return [
'Widerrufsbelehrung',
'Widerrufsrecht',
'Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.',
'Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die Waren in Besitz genommen haben bzw. hat.',
'Haben Sie mehrere Waren im Rahmen einer einheitlichen Bestellung bestellt und werden diese getrennt geliefert, beträgt die Widerrufsfrist vierzehn Tage ab dem Tag, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die letzte Ware in Besitz genommen haben bzw. hat.',
`Um Ihr Widerrufsrecht auszuüben, müssen Sie uns (${kontakt}) mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail) über Ihren Entschluss, diesen Vertrag zu widerrufen, informieren. Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.`,
`Sie können Ihr Widerrufsrecht auch online unter ${url} ausüben. Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.`,
'Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.',
'Folgen des Widerrufs',
'Wenn Sie diesen Vertrag widerrufen, haben wir Ihnen alle Zahlungen, die wir von Ihnen erhalten haben, einschließlich der Lieferkosten (mit Ausnahme der zusätzlichen Kosten, die sich daraus ergeben, dass Sie eine andere Art der Lieferung als die von uns angebotene, günstigste Standardlieferung gewählt haben), unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist. Für diese Rückzahlung verwenden wir dasselbe Zahlungsmittel, das Sie bei der ursprünglichen Transaktion eingesetzt haben, es sei denn, mit Ihnen wurde ausdrücklich etwas anderes vereinbart; in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.',
'Wir können die Rückzahlung verweigern, bis wir die Waren wieder zurückerhalten haben oder bis Sie den Nachweis erbracht haben, dass Sie die Waren zurückgesandt haben, je nachdem, welches der frühere Zeitpunkt ist.',
`Sie haben die Waren unverzüglich und in jedem Fall spätestens binnen vierzehn Tagen ab dem Tag, an dem Sie uns über den Widerruf dieses Vertrags unterrichten, an uns (${rueckadresse}) zurückzusenden oder zu übergeben. Die Frist ist gewahrt, wenn Sie die Waren vor Ablauf der Frist von vierzehn Tagen absenden.`,
...(abholung ? [abholung] : []),
kosten,
'Sie müssen für einen etwaigen Wertverlust der Waren nur aufkommen, wenn dieser Wertverlust auf einen zur Prüfung der Beschaffenheit, Eigenschaften und Funktionsweise der Waren nicht notwendigen Umgang mit ihnen zurückzuführen ist.',
'Ausschluss bzw. vorzeitiges Erlöschen des Widerrufsrechts',
[
'Das Widerrufsrecht besteht nicht bei Verträgen zur Lieferung von Waren, die nicht vorgefertigt sind und für deren Herstellung eine individuelle Auswahl oder Bestimmung durch den Verbraucher maßgeblich ist oder die eindeutig auf die persönlichen Bedürfnisse des Verbrauchers zugeschnitten sind (§ 312g Abs. 2 Nr. 1 BGB), z. B. Waren mit Wunschaufdruck oder Maßanfertigungen.',
'Das Widerrufsrecht besteht nicht bei Verträgen zur Lieferung von Waren, die schnell verderben können oder deren Verfallsdatum schnell überschritten würde (§ 312g Abs. 2 Nr. 2 BGB), z. B. frische Lebensmittel.',
'Das Widerrufsrecht erlischt vorzeitig bei Verträgen zur Lieferung versiegelter Waren, die aus Gründen des Gesundheitsschutzes oder der Hygiene nicht zur Rückgabe geeignet sind, wenn ihre Versiegelung nach der Lieferung entfernt wurde (§ 312g Abs. 2 Nr. 3 BGB).',
].join('\n'),
'Ende der Widerrufsbelehrung',
].join('\n\n')
}
/** Muster-Widerrufsformular (Anlage 2 zu Art. 246a § 1 Abs. 2 S. 1 Nr. 1 EGBGB), Unternehmer eingesetzt. */
export function musterWiderrufsformular(e) {
e = mitStandard(e)
const a = e.anbieter
return [
'Muster-Widerrufsformular',
'(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)',
`An ${text(a.name) || '[Name]'}, ${anschrift(a)}, E-Mail: ${text(a.email) || '[E-Mail-Adresse]'}:`,
'Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)',
['– Bestellt am (*)/erhalten am (*)', '– Name des/der Verbraucher(s)', '– Anschrift des/der Verbraucher(s)', '– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)', '– Datum'].join('\n'),
'(*) Unzutreffendes streichen.',
].join('\n\n')
}
/** Kurze Muster-AGB fuer kleine Shops. Kopfzeile kennzeichnet sie als Muster; der Haendler loescht sie nach der Pruefung. */
export function musterAgb(e) {
e = mitStandard(e)
const a = e.anbieter
const name = text(a.name) || '[Name]'
const arten = Array.isArray(e.zahlungsarten) ? e.zahlungsarten : []
const zahlung = zahlungsartenText(e)
const abs = []
abs.push('Muster – bitte prüfen und anpassen')
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)}` : ''}.`
)
abs.push(
'§ 2 Vertragsschluss\n\nDie Darstellung der Waren im Shop ist kein verbindliches Angebot. Mit dem Klick auf die Schaltfläche „Zahlungspflichtig bestellen“ geben Sie eine verbindliche Bestellung ab. Wir bestätigen den Eingang der Bestellung unverzüglich per E-Mail. Der Vertrag kommt zustande, sobald wir Ihnen nach erfolgreicher Zahlung die Bestellbestätigung per E-Mail senden. Bestellungen, die nicht bezahlt werden, werden nach Ablauf der Zahlungsfrist verworfen.'
)
abs.push(
`§ 3 Preise und Versandkosten\n\nEs gelten die zum Zeitpunkt der Bestellung im Shop angegebenen Preise in Euro. ${steuerSatz(e)} Versandkosten und etwaige weitere Kosten werden vor Abgabe der Bestellung gesondert ausgewiesen und richten sich nach dem Zielland. Einzelheiten finden Sie unter „Versand & Zahlung“.`
)
let z = `§ 4 Zahlung\n\nDie Zahlung erfolgt über den Zahlungsdienstleister Stripe. Es stehen folgende Zahlungsarten zur Verfügung: ${zahlung || '[Zahlungsarten]'}.`
if (arten.includes('sepa_debit')) z += ' Bei SEPA-Lastschrift versenden wir die Ware erst nach Eingang der Zahlung.'
abs.push(z)
let l = '§ 5 Lieferung und Abholung\n\n'
const teile = []
if (e.versand.aktiv) teile.push(`Wir liefern in folgende Länder: ${laenderListe(e) || '[Länder]'}.`)
if (e.abholung.aktiv) teile.push(`Sie können die Waren auch bei uns abholen: ${text(e.abholung.adresse) || '[Abholadresse]'}.${text(e.abholung.hinweis) ? ` ${text(e.abholung.hinweis)}` : ''}`)
if (!teile.length) teile.push('Angaben zur Lieferung und Abholung: [bitte ergänzen].')
l += teile.join(' ')
l += ` Die Lieferzeit beträgt ${text(e.lieferzeit) || '[Lieferzeit]'}, gerechnet ab Zahlungseingang. Ist eine Ware nicht verfügbar, informieren wir Sie unverzüglich.`
abs.push(l)
abs.push(`§ 6 Eigentumsvorbehalt\n\nDie Waren bleiben bis zur vollständigen Bezahlung unser Eigentum.`)
abs.push('§ 7 Widerrufsrecht\n\nVerbrauchern steht ein gesetzliches Widerrufsrecht zu. Einzelheiten ergeben sich aus der Widerrufsbelehrung, die Sie unter „Widerrufsbelehrung“ im Shop finden.')
abs.push(
'§ 8 Gewährleistung\n\nEs gelten die gesetzlichen Mängelhaftungsrechte. Der Hinweis auf das gesetzliche Gewährleistungsrecht steht vor Abgabe der Bestellung im Shop.'
)
abs.push(
'§ 9 Vertragstext und Sprache\n\nWir speichern den Vertragstext nach Vertragsschluss nicht für Sie abrufbar. Ihre Bestelldaten, diese Allgemeinen Geschäftsbedingungen und die Widerrufsbelehrung erhalten Sie mit der Bestellbestätigung per E-Mail; bitte bewahren Sie diese E-Mail auf. Die Vertragssprache ist Deutsch.'
)
abs.push(
`§ 10 Streitbeilegung\n\n${name} ist nicht bereit und nicht verpflichtet, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen (§ 36 VSBG).`
)
return abs.join('\n\n')
}
/** Aus den Einstellungen erzeugter Text zu Versand und Zahlung (nicht frei editierbar, damit er stimmt). */
export function versandUndZahlung(e) {
e = mitStandard(e)
const abs = ['Versand und Zahlung']
if (e.versand.aktiv) {
abs.push(`Liefergebiet: ${laenderListe(e) || '–'}`)
const laender = Array.isArray(e.versand.laender) ? e.versand.laender : []
const zeilen = laender
.filter((l) => Number.isInteger(e.versand.kostenCent?.[l]))
.map((l) => `${LANDNAMEN[l] ?? l}: ${euro(e.versand.kostenCent[l])}`)
if (zeilen.length) abs.push(`Versandkosten:\n${zeilen.join('\n')}`)
if (Number.isInteger(e.versand.freiAbCent)) abs.push(`Versandkostenfrei ab einem Bestellwert von ${euro(e.versand.freiAbCent)}.`)
} else {
abs.push('Versand: Wir versenden derzeit nicht.')
}
if (e.abholung.aktiv) {
abs.push(`Abholung: ${text(e.abholung.adresse) || '–'}${text(e.abholung.hinweis) ? `\n${text(e.abholung.hinweis)}` : ''}`)
}
abs.push(`Lieferzeit: ${text(e.lieferzeit) || '–'}`)
abs.push(`Zahlungsarten: ${zahlungsartenText(e) || '–'}`)
if (Number.isInteger(e.mindestbestellCent)) abs.push(`Mindestbestellwert: ${euro(e.mindestbestellCent)}`)
abs.push(e.kleinunternehmer ? 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet und ausgewiesen.' : 'Alle Preise inkl. gesetzlicher USt.')
return abs.join('\n\n')
}
/**
* Harmonisierter Hinweis auf das gesetzliche Gewährleistungsrecht (Art. 246a § 1 Abs. 1 S. 1 Nr. 11 EGBGB, VO (EU) 2025/1960 Anhang I,
* seit 27.09.2026), in Kasse hervorgehoben.
* Wortlaut prüfen (Kanzlei): Der amtliche deutsche Wortlaut von Anhang I stand in der Recherche nicht vor (EUR-Lex nicht geladen);
* dies ist eine sinngemaesse Fassung (Mindestdauer zwei Jahre, wichtigste Elemente). Grafik/Symbol und Farbe kommen spaeter.
*/
export function gewaehrleistungsHinweis() {
return [
'Gesetzliches Gewährleistungsrecht',
'Für Waren besteht ein gesetzliches Gewährleistungsrecht von mindestens zwei Jahren ab Lieferung der Ware. Ist die Ware nicht vertragsgemäß, haben Sie zunächst Anspruch auf kostenlose Nachbesserung (Reparatur) oder Ersatzlieferung; unter den gesetzlichen Voraussetzungen können Sie stattdessen den Preis mindern oder vom Vertrag zurücktreten. Diese Rechte bestehen unabhängig von einer Garantie.',
].join('\n\n')
}
// --- Normalisierung --------------------------------------------------------------------------------------------------------
// Steuerzeichen (ausser Zeilenumbruch) und unsichtbare Formatzeichen raus
const STEUER = /[\p{Cc}\p{Cf}\u2028\u2029]/gu
function bereinigeText(s) {
return s
.replace(/\r\n?/g, '\n')
.replace(/\t/g, ' ')
.replace(STEUER, (z) => (z === '\n' ? z : ''))
.replace(/[ ]+\n/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim()
}
function pruefeUrl(wert) {
if (wert === undefined || wert === null) return { wert: '' }
if (typeof wert !== 'string') return { fehler: 'Bitte gib die Adresse als Text an.' }
const s = wert.trim()
if (s === '') return { wert: '' }
if (s.length > MAX_URL) return { fehler: `Die Adresse darf höchstens ${MAX_URL} Zeichen lang sein.` }
if (/[\s\p{Cc}\p{Cf}]/u.test(s)) return { fehler: 'Die Adresse darf keine Leerzeichen oder Sonderzeichen enthalten.' }
let u
try {
u = new URL(s)
} catch {
return { fehler: 'Bitte gib eine vollständige Adresse an, die mit https:// beginnt.' }
}
if (u.protocol !== 'https:' || !u.hostname || !/^https:\/\//i.test(s)) return { fehler: 'Die Adresse muss mit https:// beginnen.' }
if (u.username || u.password) return { fehler: 'Die Adresse darf keine Zugangsdaten enthalten.' }
return { wert: s }
}
/**
* Pruefen und Normalisieren der Haendler-Rechtstexte. Texte duerfen im Entwurf leer sein; URLs muessen https sein oder leer.
* @returns {{rechtstexte: {agb, widerrufsbelehrung, widerrufsformular, impressumUrl, datenschutzUrl}} | {fehler: Object<string,string>}}
*/
export function normalisiereRechtstexte(eingabe) {
if (!eingabe || typeof eingabe !== 'object' || Array.isArray(eingabe)) return { fehler: { rechtstexte: 'Die Rechtstexte fehlen oder sind unlesbar.' } }
const fehler = {}
const out = {}
const texte = [
['agb', 'AGB', MAX_AGB],
['widerrufsbelehrung', 'Widerrufsbelehrung', MAX_WIDERRUFSBELEHRUNG],
['widerrufsformular', 'Widerrufsformular', MAX_WIDERRUFSFORMULAR],
]
for (const [key, name, max] of texte) {
const w = Object.hasOwn(eingabe, key) ? eingabe[key] : undefined
if (w === undefined || w === null) {
out[key] = ''
continue
}
if (typeof w !== 'string') {
fehler[key] = `Bitte gib die ${name} als Text an.`
out[key] = ''
continue
}
const s = bereinigeText(w)
if ([...s].length > max) {
fehler[key] = `Die ${name} darf höchstens ${max} Zeichen lang sein.`
out[key] = ''
} else out[key] = s
}
for (const [key, name] of [['impressumUrl', 'Impressum'], ['datenschutzUrl', 'Datenschutzerklärung']]) {
const r = pruefeUrl(Object.hasOwn(eingabe, key) ? eingabe[key] : undefined)
if (r.fehler) {
fehler[key] = `${name}: ${r.fehler}`
out[key] = ''
} else out[key] = r.wert
}
if (Object.keys(fehler).length) return { fehler }
return {
rechtstexte: {
agb: out.agb,
widerrufsbelehrung: out.widerrufsbelehrung,
widerrufsformular: out.widerrufsformular,
impressumUrl: out.impressumUrl,
datenschutzUrl: out.datenschutzUrl,
},
}
}
/** Alle drei Texte gefuellt und beide Adressen gesetzt: Voraussetzung fuer die Bestaetigung. */
export function vollstaendig(rechtstexte) {
const r = rechtstexte && typeof rechtstexte === 'object' ? rechtstexte : {}
return ['agb', 'widerrufsbelehrung', 'widerrufsformular', 'impressumUrl', 'datenschutzUrl'].every((k) => typeof r[k] === 'string' && r[k].trim() !== '')
}
/** 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 : {}
const s = (k) => (typeof r[k] === 'string' ? r[k] : '')
const kanonisch = JSON.stringify([
['agb', s('agb')],
['widerrufsbelehrung', s('widerrufsbelehrung')],
['widerrufsformular', s('widerrufsformular')],
['impressumUrl', s('impressumUrl')],
['datenschutzUrl', s('datenschutzUrl')],
['versandUndZahlung', versandUndZahlung(einstellungen)],
['gewaehrleistungsHinweis', gewaehrleistungsHinweis()],
])
return createHash('sha256').update(kanonisch).digest('hex')
}
+311
View File
@@ -0,0 +1,311 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import {
musterWiderrufsbelehrung,
musterWiderrufsformular,
musterAgb,
versandUndZahlung,
gewaehrleistungsHinweis,
zahlungsartenText,
normalisiereRechtstexte,
vollstaendig,
rechtstexteHash,
BESTAETIGUNG_TEXT,
} from '../server/services/shop/rechtstexte.js'
import { STANDARD_EINSTELLUNGEN } from '../server/services/shop/einstellungen.js'
const NB = '\u00a0'
const basis = () => {
const e = JSON.parse(JSON.stringify(STANDARD_EINSTELLUNGEN))
e.anbieter = { name: 'Brot & Mehr GmbH', strasse: 'Hauptstraße 5', plz: '67655', ort: 'Kaiserslautern', land: 'DE', email: 'shop@brot.example', telefon: '0631 123456', ustId: 'DE123456789' }
return e
}
const WIDERRUF_URL = 'https://brot.example/shop#widerruf'
test('Widerrufsbelehrung: gesetzliche Kernsaetze und eingesetzte Anbieterdaten', () => {
const t = musterWiderrufsbelehrung(basis(), { widerrufUrl: WIDERRUF_URL })
for (const s of [
'Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.',
'Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag, an dem Sie oder ein von Ihnen benannter Dritter, der nicht der Beförderer ist, die Waren in Besitz genommen haben bzw. hat.',
'die letzte Ware in Besitz genommen haben bzw. hat.',
'mittels einer eindeutigen Erklärung (z. B. ein mit der Post versandter Brief oder eine E-Mail)',
'Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.',
'Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.',
'unverzüglich und spätestens binnen vierzehn Tagen ab dem Tag zurückzuzahlen, an dem die Mitteilung über Ihren Widerruf dieses Vertrags bei uns eingegangen ist.',
'in keinem Fall werden Ihnen wegen dieser Rückzahlung Entgelte berechnet.',
'Wir können die Rückzahlung verweigern, bis wir die Waren wieder zurückerhalten haben oder bis Sie den Nachweis erbracht haben, dass Sie die Waren zurückgesandt haben, je nachdem, welches der frühere Zeitpunkt ist.',
'Sie müssen für einen etwaigen Wertverlust der Waren nur aufkommen, wenn dieser Wertverlust auf einen zur Prüfung der Beschaffenheit, Eigenschaften und Funktionsweise der Waren nicht notwendigen Umgang mit ihnen zurückzuführen ist.',
]) {
assert.ok(t.includes(s), `fehlt: ${s}`)
}
assert.ok(t.includes('(Brot & Mehr GmbH, Hauptstraße 5, 67655 Kaiserslautern, Telefon: 0631 123456, E-Mail: shop@brot.example)'))
assert.ok(t.includes('an uns (Brot & Mehr GmbH, Hauptstraße 5, 67655 Kaiserslautern) zurückzusenden oder zu übergeben'))
assert.ok(!t.includes('['), 'keine offenen Platzhalter bei vollen Angaben')
assert.ok(t.split('\n\n').length > 10)
})
test('Widerrufsbelehrung: Hinweis auf die Widerrufsfunktion mit Adresse', () => {
const t = musterWiderrufsbelehrung(basis(), { widerrufUrl: WIDERRUF_URL })
assert.ok(t.includes(`Sie können Ihr Widerrufsrecht auch online unter ${WIDERRUF_URL} ausüben.`))
assert.ok(
t.includes(
'Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung mit Informationen zum Inhalt der Widerrufserklärung sowie dem Datum und der Uhrzeit ihres Eingangs.'
)
)
// ohne Adresse: sichtbarer Platzhalter statt leerer Stelle
assert.ok(musterWiderrufsbelehrung(basis()).includes('[Internetadresse der Widerrufsfunktion]'))
})
test('Widerrufsbelehrung: Variante Ruecksendekosten', () => {
const e = basis()
assert.ok(musterWiderrufsbelehrung(e, { widerrufUrl: WIDERRUF_URL }).includes('Sie tragen die unmittelbaren Kosten der Rücksendung der Waren.'))
e.ruecksendekosten = 'haendler'
const t = musterWiderrufsbelehrung(e, { widerrufUrl: WIDERRUF_URL })
assert.ok(t.includes('Wir tragen die Kosten der Rücksendung der Waren.'))
assert.ok(!t.includes('Sie tragen die unmittelbaren Kosten'))
})
test('Widerrufsbelehrung: Abholung nur wenn angeboten', () => {
const e = basis()
assert.ok(!musterWiderrufsbelehrung(e, { widerrufUrl: WIDERRUF_URL }).includes('abgeholt'))
e.abholung = { aktiv: true, adresse: 'Lager, Bahnhofstraße 1, 67655 Kaiserslautern', hinweis: '' }
assert.ok(musterWiderrufsbelehrung(e, { widerrufUrl: WIDERRUF_URL }).includes('Haben Sie die Waren bei uns abgeholt, übergeben Sie diese zur Rückgabe an unserer Anschrift: Lager, Bahnhofstraße 1, 67655 Kaiserslautern.'))
})
test('Widerrufsbelehrung: Ausschluss und vorzeitiges Erlöschen nach § 312g Abs. 2 Nr. 1, 2, 3', () => {
const t = musterWiderrufsbelehrung(basis(), { widerrufUrl: WIDERRUF_URL })
assert.ok(t.includes('Ausschluss bzw. vorzeitiges Erlöschen des Widerrufsrechts'))
assert.ok(t.includes('die nicht vorgefertigt sind und für deren Herstellung eine individuelle Auswahl oder Bestimmung durch den Verbraucher maßgeblich ist oder die eindeutig auf die persönlichen Bedürfnisse des Verbrauchers zugeschnitten sind (§ 312g Abs. 2 Nr. 1 BGB)'))
assert.ok(t.includes('Waren, die schnell verderben können oder deren Verfallsdatum schnell überschritten würde (§ 312g Abs. 2 Nr. 2 BGB)'))
assert.ok(t.includes('Das Widerrufsrecht erlischt vorzeitig bei Verträgen zur Lieferung versiegelter Waren, die aus Gründen des Gesundheitsschutzes oder der Hygiene nicht zur Rückgabe geeignet sind, wenn ihre Versiegelung nach der Lieferung entfernt wurde (§ 312g Abs. 2 Nr. 3 BGB)'))
})
test('Widerrufsbelehrung: leere Anbieterdaten ergeben Platzhalter, kein Absturz', () => {
const t = musterWiderrufsbelehrung(STANDARD_EINSTELLUNGEN, { widerrufUrl: WIDERRUF_URL })
assert.ok(t.includes('[Name]'))
assert.ok(t.includes('[Anschrift]'))
assert.ok(t.includes('[E-Mail-Adresse]'))
assert.doesNotThrow(() => musterWiderrufsbelehrung(undefined))
assert.doesNotThrow(() => musterAgb({}))
assert.doesNotThrow(() => versandUndZahlung(null))
})
test('Widerrufsformular nach Anlage 2 mit Anschrift', () => {
const t = musterWiderrufsformular(basis())
assert.ok(t.includes('An Brot & Mehr GmbH, Hauptstraße 5, 67655 Kaiserslautern, E-Mail: shop@brot.example:'))
assert.ok(t.includes('Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)'))
assert.ok(t.includes('– Bestellt am (*)/erhalten am (*)'))
assert.ok(t.includes('– Name des/der Verbraucher(s)'))
assert.ok(t.includes('– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)'))
assert.ok(t.includes('(*) Unzutreffendes streichen.'))
assert.ok(t.startsWith('Muster-Widerrufsformular'))
})
test('AGB: Kopf, Paragraphen, Vertragsschluss, Zahlung, Streitbeilegung ohne OS-Plattform', () => {
const e = basis()
e.zahlungsarten = ['card', 'klarna', 'sepa_debit']
e.versand.laender = ['DE', 'AT']
e.kleinunternehmer = false
const t = musterAgb(e)
assert.ok(t.startsWith('Muster – bitte prüfen und anpassen'))
for (let i = 1; i <= 10; i += 1) assert.ok(new RegExp(`^§ ${i} `, 'm').test(t), `§ ${i} fehlt`)
assert.ok(t.includes('Mit dem Klick auf die Schaltfläche „Zahlungspflichtig bestellen“ geben Sie eine verbindliche Bestellung ab.'))
assert.ok(t.includes('Der Vertrag kommt zustande, sobald wir Ihnen nach erfolgreicher Zahlung die Bestellbestätigung per E-Mail senden.'))
assert.ok(t.includes('Die Zahlung erfolgt über den Zahlungsdienstleister Stripe.'))
assert.ok(t.includes('Kreditkarte, Apple Pay, Google Pay, Klarna, SEPA-Lastschrift'))
assert.ok(t.includes('Bei SEPA-Lastschrift versenden wir die Ware erst nach Eingang der Zahlung.'))
assert.ok(t.includes('Wir liefern in folgende Länder: Deutschland, Österreich.'))
assert.ok(t.includes('Die Lieferzeit beträgt 2–4 Werktage'))
assert.ok(t.includes('Die Waren bleiben bis zur vollständigen Bezahlung unser Eigentum.'))
assert.ok(t.includes('Es gelten die gesetzlichen Mängelhaftungsrechte.'))
assert.ok(t.includes('Wir speichern den Vertragstext nach Vertragsschluss nicht für Sie abrufbar.'))
assert.ok(t.includes('Die Vertragssprache ist Deutsch.'))
assert.ok(t.includes('ist nicht bereit und nicht verpflichtet, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen (§ 36 VSBG)'))
assert.ok(t.includes('Alle Preise enthalten die gesetzliche Umsatzsteuer.'))
assert.ok(t.includes('USt-IdNr.: DE123456789'))
assert.doesNotMatch(t, /ec\.europa\.eu|OS-Plattform|ODR/i)
})
test('AGB: Varianten Kleinunternehmer, nur Karte, Abholung, kein Versand', () => {
const e = basis()
e.kleinunternehmer = true
e.abholung = { aktiv: true, adresse: 'Laden, Hauptstraße 5', hinweis: 'Di–Fr 9–17 Uhr' }
e.versand.aktiv = false
const t = musterAgb(e)
assert.ok(t.includes('Gemäß § 19 UStG wird keine Umsatzsteuer berechnet und ausgewiesen.'))
assert.ok(!t.includes('SEPA'), 'SEPA-Satz nur bei SEPA')
assert.ok(!t.includes('Wir liefern in folgende Länder'))
assert.ok(t.includes('Sie können die Waren auch bei uns abholen: Laden, Hauptstraße 5. Di–Fr 9–17 Uhr'))
})
test('versandUndZahlung: Liefergebiet, Kosten je Land, frei ab, Abholung, Zahlungsarten, Mindestbestellwert', () => {
const e = basis()
e.versand = { aktiv: true, laender: ['DE', 'AT'], kostenCent: { DE: 490, AT: 990 }, freiAbCent: 5000 }
e.abholung = { aktiv: true, adresse: 'Laden, Hauptstraße 5', hinweis: 'Di–Fr 9–17 Uhr' }
e.zahlungsarten = ['card', 'klarna']
e.mindestbestellCent = 1000
e.kleinunternehmer = false
const t = versandUndZahlung(e)
assert.ok(t.includes('Liefergebiet: Deutschland, Österreich'))
assert.ok(t.includes(`Deutschland: 4,90${NB}€`))
assert.ok(t.includes(`Österreich: 9,90${NB}€`))
assert.ok(t.includes(`Versandkostenfrei ab einem Bestellwert von 50,00${NB}€.`))
assert.ok(t.includes('Abholung: Laden, Hauptstraße 5\nDi–Fr 9–17 Uhr'))
assert.ok(t.includes('Lieferzeit: 2–4 Werktage'))
assert.ok(t.includes('Zahlungsarten: Kreditkarte, Apple Pay, Google Pay, Klarna'))
assert.ok(t.includes(`Mindestbestellwert: 10,00${NB}€`))
assert.ok(t.includes('Alle Preise inkl. gesetzlicher USt.'))
e.kleinunternehmer = true
e.mindestbestellCent = null
e.versand.freiAbCent = null
e.abholung.aktiv = false
const k = versandUndZahlung(e)
assert.ok(k.includes('§ 19 UStG'))
assert.ok(!k.includes('Mindestbestellwert'))
assert.ok(!k.includes('Versandkostenfrei'))
assert.ok(!k.includes('Abholung'))
e.versand.aktiv = false
assert.ok(versandUndZahlung(e).includes('Wir versenden derzeit nicht.'))
})
test('zahlungsartenText: feste Reihenfolge, nur aktive Arten', () => {
assert.equal(zahlungsartenText({ zahlungsarten: ['card'] }), 'Kreditkarte, Apple Pay, Google Pay')
assert.equal(zahlungsartenText({ zahlungsarten: ['klarna', 'card'] }), 'Kreditkarte, Apple Pay, Google Pay, Klarna')
assert.equal(zahlungsartenText({ zahlungsarten: ['sepa_debit', 'klarna'] }), 'Klarna, SEPA-Lastschrift')
assert.equal(zahlungsartenText({ zahlungsarten: ['paypal', 'card'] }), 'Kreditkarte, Apple Pay, Google Pay')
assert.equal(zahlungsartenText({}), '')
assert.equal(zahlungsartenText(undefined), '')
})
test('Gewaehrleistungshinweis: zwei Jahre, stabil, ohne Platzhalter', () => {
const t = gewaehrleistungsHinweis()
assert.ok(t.startsWith('Gesetzliches Gewährleistungsrecht'))
assert.ok(t.includes('mindestens zwei Jahren'))
assert.equal(gewaehrleistungsHinweis(), t)
})
test('BESTAETIGUNG_TEXT genau nach Spec 4.6', () => {
assert.equal(BESTAETIGUNG_TEXT, 'Ich habe die Texte geprüft. Ich bin als Verkäufer für Rechtstexte, Produktangaben und Preise verantwortlich. WEBklar stellt Muster bereit und berät nicht rechtlich.')
})
test('normalisiereRechtstexte: gueltig, Entwurf mit leeren Texten, Bereinigung', () => {
const r = normalisiereRechtstexte({
agb: ' Zeile 1\r\nZeile 2​ \n\n\n\nZeile 3 ',
widerrufsbelehrung: 'B',
widerrufsformular: 'F',
impressumUrl: ' https://brot.example/impressum ',
datenschutzUrl: 'https://brot.example/datenschutz?x=1#a',
unbekannt: 'wird ignoriert',
})
assert.deepEqual(r, {
rechtstexte: {
agb: 'Zeile 1\nZeile 2\n\nZeile 3',
widerrufsbelehrung: 'B',
widerrufsformular: 'F',
impressumUrl: 'https://brot.example/impressum',
datenschutzUrl: 'https://brot.example/datenschutz?x=1#a',
},
})
const leer = normalisiereRechtstexte({})
assert.deepEqual(leer, { rechtstexte: { agb: '', widerrufsbelehrung: '', widerrufsformular: '', impressumUrl: '', datenschutzUrl: '' } })
assert.equal(vollstaendig(leer.rechtstexte), false)
})
test('normalisiereRechtstexte: Laengen', () => {
assert.ok(normalisiereRechtstexte({ agb: 'a'.repeat(30000) }).rechtstexte)
assert.ok(normalisiereRechtstexte({ agb: 'a'.repeat(30001) }).fehler.agb)
assert.ok(normalisiereRechtstexte({ widerrufsbelehrung: 'a'.repeat(20000) }).rechtstexte)
assert.ok(normalisiereRechtstexte({ widerrufsbelehrung: 'a'.repeat(20001) }).fehler.widerrufsbelehrung)
assert.ok(normalisiereRechtstexte({ widerrufsformular: 'a'.repeat(5000) }).rechtstexte)
assert.ok(normalisiereRechtstexte({ widerrufsformular: 'a'.repeat(5001) }).fehler.widerrufsformular)
})
test('normalisiereRechtstexte: URLs nur https ohne Zugangsdaten und Leerraum', () => {
for (const u of ['http://x.example/a', 'javascript:alert(1)', 'ftp://x.example', '//x.example/a', 'x.example/impressum', 'https://user:pw@x.example/a', 'https://user@x.example/a', 'https://x.example/a b', 'https://', 'data:text/html,hi']) {
const r = normalisiereRechtstexte({ impressumUrl: u })
assert.ok(r.fehler?.impressumUrl, `muss abgelehnt werden: ${u}`)
}
assert.ok(normalisiereRechtstexte({ datenschutzUrl: 'https://' + 'a'.repeat(500) + '.example' }).fehler.datenschutzUrl)
assert.ok(normalisiereRechtstexte({ datenschutzUrl: 'https://x.example/datenschutz' }).rechtstexte)
assert.equal(normalisiereRechtstexte({ impressumUrl: '' }).rechtstexte.impressumUrl, '')
})
test('normalisiereRechtstexte: falsche Typen und Nicht-Objekte', () => {
for (const v of [null, undefined, 'x', 5, [], true]) assert.ok(normalisiereRechtstexte(v).fehler.rechtstexte)
const r = normalisiereRechtstexte({ agb: 5, widerrufsbelehrung: {}, impressumUrl: 7 })
assert.ok(r.fehler.agb && r.fehler.widerrufsbelehrung && r.fehler.impressumUrl)
assert.ok(!('rechtstexte' in r))
// Prototyp-Schluessel werden nie gelesen
const p = normalisiereRechtstexte(JSON.parse('{"__proto__":{"agb":"x"}}'))
assert.equal(p.rechtstexte.agb, '')
})
test('vollstaendig: alle drei Texte und beide URLs', () => {
const voll = { agb: 'a', widerrufsbelehrung: 'b', widerrufsformular: 'c', impressumUrl: 'https://x.example/i', datenschutzUrl: 'https://x.example/d' }
assert.equal(vollstaendig(voll), true)
for (const k of Object.keys(voll)) {
assert.equal(vollstaendig({ ...voll, [k]: '' }), false, k)
assert.equal(vollstaendig({ ...voll, [k]: ' ' }), false, `${k} nur Leerraum`)
}
assert.equal(vollstaendig(null), false)
assert.equal(vollstaendig(undefined), false)
})
test('rechtstexteHash: stabil, sha256 hex, reagiert auf jedes Feld und die Einstellungen', () => {
const e = basis()
const r = { agb: 'a', widerrufsbelehrung: 'b', widerrufsformular: 'c', impressumUrl: 'https://x.example/i', datenschutzUrl: 'https://x.example/d' }
const h = rechtstexteHash(r, e)
assert.match(h, /^[0-9a-f]{64}$/)
assert.equal(rechtstexteHash({ ...r }, JSON.parse(JSON.stringify(e))), h)
// Reihenfolge der Schluessel spielt keine Rolle
assert.equal(rechtstexteHash({ datenschutzUrl: r.datenschutzUrl, impressumUrl: r.impressumUrl, widerrufsformular: 'c', widerrufsbelehrung: 'b', agb: 'a' }, e), h)
// zusaetzliche Felder zaehlen nicht
assert.equal(rechtstexteHash({ ...r, extra: 'x' }, e), h)
for (const k of Object.keys(r)) assert.notEqual(rechtstexteHash({ ...r, [k]: `${r[k]}x` }, e), h, k)
// Versand-/Zahlungstext fliesst ein
const e2 = JSON.parse(JSON.stringify(e))
e2.versand.kostenCent.DE = 590
assert.notEqual(rechtstexteHash(r, e2), h)
const e3 = JSON.parse(JSON.stringify(e))
e3.zahlungsarten = ['card', 'klarna']
assert.notEqual(rechtstexteHash(r, e3), h)
// Felder ohne Einfluss auf den erzeugten Text aendern den Hash nicht
const e4 = JSON.parse(JSON.stringify(e))
e4.akzent = '#ff0000'
assert.equal(rechtstexteHash(r, e4), h)
// fehlende Felder wie leere Texte
assert.equal(rechtstexteHash({}, e), rechtstexteHash({ agb: '', widerrufsbelehrung: '', widerrufsformular: '', impressumUrl: '', datenschutzUrl: '' }, e))
})
test('Markdown-Muster und Code teilen die Kernsaetze', () => {
const md = readFileSync(new URL('../docs/funktionen/shop-muster-rechtstexte.md', import.meta.url), 'utf8')
assert.ok(md.includes('Muster – keine Rechtsberatung'))
const e = basis()
e.zahlungsarten = ['card', 'klarna', 'sepa_debit']
e.kleinunternehmer = false
// Absaetze ohne Haendlerdaten muessen wortgleich im Markdown stehen
const absaetze = [
...musterWiderrufsbelehrung(e, { widerrufUrl: WIDERRUF_URL }).split('\n\n'),
...musterAgb(e).split('\n\n'),
...musterWiderrufsformular(e).split('\n\n'),
...gewaehrleistungsHinweis().split('\n\n'),
]
const ohneDaten = absaetze.filter((a) => !/Brot & Mehr|brot\.example|Hauptstra|Kaiserslautern|2–4 Werktage|0631|DE123456789/.test(a))
assert.ok(ohneDaten.length >= 25, `zu wenige vergleichbare Absaetze: ${ohneDaten.length}`)
const mdFlach = md.replace(/ {2}\n/g, '\n')
for (const a of ohneDaten) assert.ok(mdFlach.includes(a), `im Markdown fehlt: ${a.slice(0, 80)}`)
// Absaetze mit Haendlerdaten: Kernsaetze davon
for (const s of [
'Mit dem Klick auf die Schaltfläche „Zahlungspflichtig bestellen“ geben Sie eine verbindliche Bestellung ab.',
'Der Vertrag kommt zustande, sobald wir Ihnen nach erfolgreicher Zahlung die Bestellbestätigung per E-Mail senden.',
'Sie können dafür das beigefügte Muster-Widerrufsformular verwenden, das jedoch nicht vorgeschrieben ist.',
'Wenn Sie diese Online-Funktion nutzen, übermitteln wir Ihnen auf einem dauerhaften Datenträger (z. B. durch eine E-Mail) unverzüglich eine Eingangsbestätigung',
'ist nicht bereit und nicht verpflichtet, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen (§ 36 VSBG)',
'Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag',
]) {
assert.ok(md.includes(s), `Markdown: ${s}`)
}
assert.ok(md.includes('Wir tragen die Kosten der Rücksendung der Waren.'))
assert.doesNotMatch(md, /ec\.europa\.eu\/consumers\/odr/)
})