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, pruefeBestaetigbar, MUSTER_KOPFZEILE, } 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' } e.lieferzeit = '2–4 Werktage' e.kleinunternehmer = false 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.')) // I-1 c: Ablauf wie im Shop - Bestätigungsseite, Karte/Klarna sofort, SEPA erst Eingangsmail und Vertrag nach Zahlungseingang assert.ok(t.includes('Den Eingang Ihrer Bestellung bestätigen wir Ihnen sofort auf einer Bestätigungsseite im Shop.')) assert.ok(t.includes('Bei den Zahlungsarten Kreditkarte, Apple Pay, Google Pay, Klarna erhalten Sie die Bestellbestätigung per E-Mail unmittelbar nach erfolgreicher Zahlung; mit dieser E-Mail nehmen wir Ihre Bestellung an und der Vertrag kommt zustande.')) assert.ok(t.includes('Bei SEPA-Lastschrift senden wir Ihnen zunächst eine E-Mail über den Eingang Ihrer Bestellung; der Vertrag kommt in diesem Fall erst zustande, wenn wir Ihnen nach Eingang der Zahlung die Bestellbestätigung per E-Mail senden.')) assert.doesNotMatch(t, /Wir bestätigen den Eingang der Bestellung unverzüglich per E-Mail/) // Art. 246c EGBGB: technische Schritte, Eingabefehler, Speicherung, Sprache, Verhaltenskodizes assert.ok(t.includes('Zum Vertragsschluss führen folgende Schritte:')) assert.ok(t.includes('Eingabefehler erkennen und berichtigen')) assert.ok(t.includes('Zurück zum Warenkorb')) assert.ok(t.includes('Wir unterwerfen uns keinen besonderen Verhaltenskodizes.')) 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')) assert.ok(t.includes('Bei den Zahlungsarten Kreditkarte, Apple Pay, Google Pay erhalten Sie die Bestellbestätigung per E-Mail unmittelbar nach erfolgreicher Zahlung')) }) test('AGB § 2 I-1: nur SEPA - keine Sofort-Bestätigung, nur Eingangsmail und Vertrag nach Zahlungseingang', () => { const e = basis() e.zahlungsarten = ['sepa_debit'] const t = musterAgb(e) assert.doesNotMatch(t, /unmittelbar nach erfolgreicher Zahlung/) assert.ok(t.includes('Bei SEPA-Lastschrift senden wir Ihnen zunächst eine E-Mail über den Eingang Ihrer Bestellung;')) }) test('I-4: Steuerart noch nicht gewählt (null) rechnet in den Mustern wie Regelbesteuerung, Lieferzeit leer ergibt Platzhalter', () => { const e = basis() e.kleinunternehmer = null e.lieferzeit = '' assert.ok(musterAgb(e).includes('Alle Preise enthalten die gesetzliche Umsatzsteuer.')) assert.ok(musterAgb(e).includes('Die Lieferzeit beträgt [Lieferzeit]')) assert.ok(versandUndZahlung(e).includes('Alle Preise inkl. gesetzlicher USt.')) }) 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('GESETZLICHE GEWÄHRLEISTUNG')) assert.ok(t.includes('Mindestens zwei Jahre gesetzliche Gewährleistung der Vertragsmäßigkeit')) assert.ok(t.includes('europa.eu/youreurope/garantien')) 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.', 'mit dieser E-Mail nehmen wir Ihre Bestellung an und der Vertrag kommt zustande.', 'Zum Vertragsschluss führen folgende Schritte:', '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/) }) 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 }), []) })