I-6: Neue Collection shopRechtstexteArchiv (ID r + 35 Hex aus sha256(shopId + hash)); das Bestaetigen legt die Fassung samt Versand-/Zahlungstext und Gewaehrleistungstext ab, G09 zeigt die Fassung zum rechtstexteHash der Bestellung (Rueckfall mit Warnung auf die aktuell bestaetigte), loescheShopDaten loescht das Archiv mit. I-7: Grenzen AGB 20000, Belehrung 12000, Formular 3000 plus Pruefung der maskierten Laenge beim Speichern; G09 ersetzt die AGB nie mehr durch einen Link, sondern scheitert (Marker bleibt offen, wird wiederholt). Test mit Texten in Hoechstlaenge gegen die Grenzen der Warteschlange. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
392 lines
26 KiB
JavaScript
392 lines
26 KiB
JavaScript
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.'
|
||
|
||
// Grenzen so gewaehlt, dass die Bestaetigungs-E-Mail G09 mit allen drei Texten in die Warteschlange passt (html 100000, text 50000;
|
||
// die Vorlage braucht rund 12000 Zeichen) und der Volltext nie durch einen Link ersetzt werden muss (§ 312f Abs. 2 BGB).
|
||
export const MAX_AGB = 20000
|
||
export const MAX_WIDERRUFSBELEHRUNG = 12000
|
||
export const MAX_WIDERRUFSFORMULAR = 3000
|
||
/** So viele Zeichen duerfen die drei Texte zusammen im HTML der Mail belegen (maskierte Zeichen und Zeilenumbrueche zaehlen mehrfach). */
|
||
export const MAX_MAIL_HTML_TEXTE = 60000
|
||
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']
|
||
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_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)}` : ''}.`
|
||
)
|
||
const sofortArten = arten.filter((a) => a !== 'sepa_debit')
|
||
const sofort = sofortArten.length || !arten.includes('sepa_debit')
|
||
const bestaetigung = [
|
||
'Den Eingang Ihrer Bestellung bestätigen wir Ihnen sofort auf einer Bestätigungsseite im Shop.',
|
||
...(sofort
|
||
? [
|
||
`Bei ${sofortArten.length ? `den Zahlungsarten ${zahlungsartenText({ zahlungsarten: sofortArten })}` : 'der Zahlung'} 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.`,
|
||
]
|
||
: []),
|
||
...(arten.includes('sepa_debit')
|
||
? ['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.']
|
||
: []),
|
||
].join(' ')
|
||
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. ${bestaetigung} 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 Technische Schritte, Vertragstext und Sprache\n\nZum Vertragsschluss führen folgende Schritte: Sie legen die gewünschten Waren in den Warenkorb, prüfen die Bestellübersicht mit allen Angaben und klicken auf „Zahlungspflichtig bestellen“. Danach zahlen Sie bei unserem Zahlungsdienstleister Stripe. Der Vertrag kommt nach § 2 mit unserer Bestellbestätigung per E-Mail zustande. Vor dem Klick auf „Zahlungspflichtig bestellen“ können Sie Eingabefehler erkennen und berichtigen: Im Warenkorb ändern Sie Mengen oder entfernen Waren, aus der Bestellübersicht gelangen Sie über „Zurück zum Warenkorb“ dorthin zurück, und Ihre Angaben zu Adresse und Zahlung prüfen und korrigieren Sie bei Stripe, bevor Sie die Zahlung abschließen. 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. Wir unterwerfen uns keinen besonderen Verhaltenskodizes.'
|
||
)
|
||
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')
|
||
}
|
||
|
||
/**
|
||
* Harmonisierte Mitteilung ueber das gesetzliche Gewaehrleistungsrecht (Art. 246a § 1 Abs. 1 S. 1 Nr. 11 EGBGB,
|
||
* Durchfuehrungsverordnung (EU) 2025/1960 Anhang I, ABl. L vom 2.10.2025, seit 27.09.2026). Die Mitteilung ist eine feste Grafik
|
||
* (keines der Elemente editierbar, online farbig RGB): public/img/gewaehrleistung-eu.webp, aus dem Amtsblatt ausgeschnitten.
|
||
* Dieser Text ist der woertliche Text der Grafik (Reihenfolge der Grafik), fuer Alternativtext und Bestellbestaetigung.
|
||
*/
|
||
export const GEWAEHRLEISTUNG_BILD = '/img/gewaehrleistung-eu.webp'
|
||
|
||
export function gewaehrleistungsHinweis() {
|
||
return [
|
||
'GESETZLICHE GEWÄHRLEISTUNG',
|
||
'Mindestens zwei Jahre gesetzliche Gewährleistung der Vertragsmäßigkeit für Waren, die in der Europäischen Union verkauft werden.',
|
||
'Verbraucherinnen und Verbraucher können ihre Rechte im Rahmen des gesetzlichen Gewährleistungsrechts geltend machen, z. B. wenn die Waren nicht der Beschreibung entsprechen, nicht bestimmungsgemäß funktionieren.',
|
||
'Verkäufer haften für jede Vertragswidrigkeit, die zum Zeitpunkt der Lieferung der Waren bestand und innerhalb des Zeitraums der gesetzlichen Gewährleistung erkennbar wird. Verkäufer müssen in solchen Fällen Folgendes anbieten: kostenlose Nachbesserung oder kostenlose Ersatzlieferung, in bestimmten Fällen eine Preisminderung oder eine vollständige Erstattung des Kaufpreises.',
|
||
'In einigen Ländern gilt ein längerer Zeitraum für die gesetzliche Gewährleistung. Für gebrauchte Waren kann ein kürzerer Zeitraum gelten, jedoch nicht weniger als ein Jahr.',
|
||
'Für weitere Informationen zu Ihren Rechten in einem bestimmten Land scannen Sie den nachstehenden QR-Code oder fragen Sie den Verkäufer. europa.eu/youreurope/garantien',
|
||
'Was ist zu tun, wenn Sie vertragswidrige Waren erhalten? 1. Melden Sie dem Verkäufer das Problem so bald wie möglich. 2. Legen Sie einen Kaufnachweis vor, z. B. die Quittung, Rechnung oder einen Kontoauszug.',
|
||
'Verkäufer und Hersteller können auch gewerbliche Garantien gewähren, die unabhängig von der gesetzlichen Gewährleistung gelten. Diese GARAN-Kennzeichnung zeigt beispielsweise, dass der Hersteller eine gewerbliche Haltbarkeitsgarantie ohne zusätzliche Kosten gewährt, die die gesamte Ware abdeckt.',
|
||
].join('\n\n')
|
||
}
|
||
|
||
/** Laenge eines Textes im HTML der Mail (wie mailTemplates.renderMail: & < > \" ' maskiert, jeder Umbruch wird <br>). */
|
||
export function maskierteLaenge(wert) {
|
||
let n = 0
|
||
for (const z of String(wert)) {
|
||
if (z === '&') n += 5
|
||
else if (z === '<' || z === '>') n += 4
|
||
else if (z === '"') n += 6
|
||
else if (z === "'") n += 5
|
||
else if (z === '\n') n += 4
|
||
else if (z !== '\r') n += 1
|
||
}
|
||
return 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
|
||
}
|
||
// Passt die Bestaetigungs-E-Mail mit allen Texten noch in die Warteschlange? (& < > " ' werden mehrfach lang, jeder Umbruch wird <br>)
|
||
if (Object.keys(fehler).length === 0 && ['agb', 'widerrufsbelehrung', 'widerrufsformular'].reduce((summe, k) => summe + maskierteLaenge(out[k]), 0) > MAX_MAIL_HTML_TEXTE) {
|
||
fehler.rechtstexte = 'AGB, Widerrufsbelehrung und Widerrufsformular sind zusammen zu lang für die Bestätigungs-E-Mail an deine Kunden (viele Sonderzeichen und Zeilenumbrüche zählen mehrfach). Bitte kürze die Texte.'
|
||
}
|
||
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() !== '')
|
||
}
|
||
|
||
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 : {}
|
||
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')
|
||
}
|