Rechtstexte der Aufträge: Endfassung der Textbausteine, Bestätigung, Frühbeginn-Erklärung und Widerrufsbelehrung (Fassung 2026-10-06)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 14:11:47 +00:00
1 parent 8627ed1124
commit 445d234695
3 files changed
+574 -41

No files matched your search

@@ -0,0 +1,103 @@
Normen, auf die sich die Texte der Auftragsbestätigung, der Kundenseiten und der Mails stützen (Auszüge, unverändert, abgerufen 2026-10-06 von gesetze-im-internet.de).
Die Datei dient als Nachweis der Zählung; test/auftraegeRechtstexte.test.js gleicht die Zitate in den Texten mit ihr ab.
Prüfung vom 06.10.2026 (zweiter Abruf von § 649 und § 650 BGB): Der Kostenanschlag steht in § 649 BGB (amtliche Überschrift „Kostenanschlag“). Absatz 2 lautet: „Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.“ § 650 BGB regelt dagegen den Werklieferungsvertrag und Verbraucherverträge über die Herstellung digitaler Produkte; ein Verweis auf „§ 650“ für den Kostenanschlag ist falsch. Der Satz „Zeichnet sich eine deutliche Überschreitung ab, melden wir uns vorher.“ (AB_TEXTE.aufwand) stützt sich auf § 649 Absatz 2.
Quelle: https://www.gesetze-im-internet.de/bgb/__312f.html (Auszug: § 312f Absatz 1 und 2 BGB)
(1) Bei außerhalb von Geschäftsräumen geschlossenen Verträgen ist der Unternehmer verpflichtet, dem Verbraucher alsbald auf Papier zur Verfügung zu stellen
1.
eine Abschrift eines Vertragsdokuments, das von den Vertragsschließenden so unterzeichnet wurde, dass ihre Identität erkennbar ist, oder
2.
eine Bestätigung des Vertrags, in der der Vertragsinhalt wiedergegeben ist.
Wenn der Verbraucher zustimmt, kann für die Abschrift oder die Bestätigung des Vertrags auch ein anderer dauerhafter Datenträger verwendet werden. Die Bestätigung nach Satz 1 muss die in Artikel 246a des Einführungsgesetzes zum Bürgerlichen Gesetzbuche genannten Angaben nur enthalten, wenn der Unternehmer dem Verbraucher diese Informationen nicht bereits vor Vertragsschluss in Erfüllung seiner Informationspflichten nach § 312d Absatz 1 auf einem dauerhaften Datenträger zur Verfügung gestellt hat.
(2) Bei Fernabsatzverträgen ist der Unternehmer verpflichtet, dem Verbraucher eine Bestätigung des Vertrags, in der der Vertragsinhalt wiedergegeben ist, innerhalb einer angemessenen Frist nach Vertragsschluss, spätestens jedoch bei der Lieferung der Ware oder bevor mit der Ausführung der Dienstleistung begonnen wird, auf einem dauerhaften Datenträger zur Verfügung zu stellen. Die Bestätigung nach Satz 1 muss die in Artikel 246a des Einführungsgesetzes zum Bürgerlichen Gesetzbuche genannten Angaben enthalten, es sei denn, der Unternehmer hat dem Verbraucher diese Informationen bereits vor Vertragsschluss in Erfüllung seiner Informationspflichten nach § 312d Absatz 1 auf einem dauerhaften Datenträger zur Verfügung gestellt.
Quelle: https://www.gesetze-im-internet.de/bgb/__355.html (Auszug: § 355 Absatz 1 BGB)
(1) Wird einem Verbraucher durch Gesetz ein Widerrufsrecht nach dieser Vorschrift eingeräumt, so sind der Verbraucher und der Unternehmer an ihre auf den Abschluss des Vertrags gerichteten Willenserklärungen nicht mehr gebunden, wenn der Verbraucher seine Willenserklärung fristgerecht widerrufen hat. Der Widerruf erfolgt durch Erklärung gegenüber dem Unternehmer. Aus der Erklärung muss der Entschluss des Verbrauchers zum Widerruf des Vertrags eindeutig hervorgehen. Der Widerruf muss keine Begründung enthalten. Zur Fristwahrung genügt die rechtzeitige Absendung des Widerrufs.
Quelle: https://www.gesetze-im-internet.de/bgb/__356.html (Auszug: § 356 Absatz 4 bis 6 BGB)
(4) Das Widerrufsrecht erlischt spätestens zwölf Monate und 14 Tage nach dem in Absatz 2 oder § 355 Absatz 2 Satz 2 genannten Zeitpunkt. Bei Verträgen über Finanzdienstleistungen gilt Satz 1 nicht, wenn der Verbraucher nicht nach Artikel 246b § 1 Absatz 1 Nummer 16 des Einführungsgesetzes zum Bürgerlichen Gesetzbuche über sein Widerrufsrecht belehrt wurde.
(5) Das Widerrufsrecht erlischt bei Verträgen über die Erbringung von Dienstleistungen auch unter folgenden Voraussetzungen:
1.
bei einem Vertrag, der den Verbraucher nicht zur Zahlung eines Preises verpflichtet, wenn der Unternehmer die Dienstleistung vollständig erbracht hat,
2.
bei einem Vertrag, der den Verbraucher zur Zahlung eines Preises verpflichtet, mit der vollständigen Erbringung der Dienstleistung, wenn der Verbraucher vor Beginn der Erbringung
a)
ausdrücklich zugestimmt hat, dass der Unternehmer mit der Erbringung der Dienstleistung vor Ablauf der Widerrufsfrist beginnt,
b)
bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag die Zustimmung nach Buchstabe a auf einem dauerhaften Datenträger übermittelt hat und
c)
seine Kenntnis davon bestätigt hat, dass sein Widerrufsrecht mit vollständiger Vertragserfüllung durch den Unternehmer erlischt,
3.
bei einem Vertrag, bei dem der Verbraucher den Unternehmer ausdrücklich aufgefordert hat, ihn aufzusuchen, um Reparaturarbeiten auszuführen, mit der vollständigen Erbringung der Dienstleistung, wenn der Verbraucher die in Nummer 2 Buchstabe a und b genannten Voraussetzungen erfüllt hat,
4.
bei einem Vertrag über die Erbringung von Finanzdienstleistungen, wenn der Vertrag von beiden Seiten auf ausdrücklichen Wunsch des Verbrauchers vollständig erfüllt ist, bevor der Verbraucher sein Widerrufsrecht ausübt.
(6) Das Widerrufsrecht erlischt bei Verträgen über die Bereitstellung von nicht auf einem körperlichen Datenträger befindlichen digitalen Inhalten auch unter folgenden Voraussetzungen:
1.
bei einem Vertrag, der den Verbraucher nicht zur Zahlung eines Preises verpflichtet, wenn der Unternehmer mit der Vertragserfüllung begonnen hat,
2.
bei einem Vertrag, der den Verbraucher zur Zahlung eines Preises verpflichtet, wenn
a)
der Unternehmer mit der Vertragserfüllung begonnen hat,
b)
der Verbraucher ausdrücklich zugestimmt hat, dass der Unternehmer mit der Vertragserfüllung vor Ablauf der Widerrufsfrist beginnt,
c)
der Verbraucher seine Kenntnis davon bestätigt hat, dass durch seine Zustimmung nach Buchstabe b mit Beginn der Vertragserfüllung sein Widerrufsrecht erlischt, und
d)
der Unternehmer dem Verbraucher eine Bestätigung gemäß § 312f zur Verfügung gestellt hat.
Quelle: https://www.gesetze-im-internet.de/bgb/__356a.html (Auszug: § 356a Absatz 2 und 4 BGB)
(2) Die Widerrufsfunktion muss dem Verbraucher ermöglichen, eine Widerrufserklärung an den Unternehmer zu übermitteln und dem Unternehmer in oder mit der Widerrufserklärung ohne Weiteres folgende Informationen bereitzustellen oder zu bestätigen:
1.
den Namen des Verbrauchers,
2.
Angaben zur Identifizierung des Vertrags oder des Teils des Vertrags, den der Verbraucher widerrufen möchte,
3.
Angaben zum elektronischen Kommunikationsmittel, mit welchem dem Verbraucher eine Eingangsbestätigung für den Widerruf zu übermitteln ist.
(4) Der Unternehmer hat dem Verbraucher, wenn dieser die Bestätigungsfunktion aktiviert hat, auf einem dauerhaften Datenträger unverzüglich eine Eingangsbestätigung zu übermitteln, die zumindest den Inhalt der Widerrufserklärung nach Absatz 2 sowie das Datum und die Uhrzeit ihres Eingangs enthält.
Quelle: https://www.gesetze-im-internet.de/bgb/__357a.html (Auszug: § 357a Absatz 2 BGB)
(2) Der Verbraucher hat Wertersatz für die bis zum Widerruf erbrachten Dienstleistungen, für die der Vertrag die Zahlung eines Preises vorsieht, oder die bis zum Widerruf erfolgte Lieferung von Wasser, Gas oder Strom in nicht bestimmten Mengen oder nicht begrenztem Volumen oder von Fernwärme zu leisten, wenn
1.
der Verbraucher von dem Unternehmer ausdrücklich verlangt hat, dass mit der Leistung vor Ablauf der Widerrufsfrist begonnen werden soll,
2.
bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag der Verbraucher das Verlangen nach Nummer 1 auf einem dauerhaften Datenträger übermittelt hat und
3.
der Unternehmer den Verbraucher nach Artikel 246a § 1 Absatz 2 Satz 1 Nummer 1 und 3 des Einführungsgesetzes zum Bürgerlichen Gesetzbuche ordnungsgemäß informiert hat.
Bei der Berechnung des Wertersatzes ist der vereinbarte Gesamtpreis zu Grunde zu legen. Ist der vereinbarte Gesamtpreis unverhältnismäßig hoch, so ist der Wertersatz auf der Grundlage des Marktwerts der erbrachten Leistung zu berechnen.
Quelle: https://www.gesetze-im-internet.de/bgbeg/art_246a__1.html (Auszug: Artikel 246a § 1 Absatz 3 EGBGB)
(3) Der Unternehmer hat den Verbraucher auch zu informieren, wenn
1.
dem Verbraucher nach § 312g Absatz 2 Nummer 1, 2, 5 und 7 bis 13 des Bürgerlichen Gesetzbuchs ein Widerrufsrecht nicht zusteht, dass der Verbraucher seine Willenserklärung nicht widerrufen kann, oder
2.
das Widerrufsrecht des Verbrauchers nach § 312g Absatz 2 Nummer 3, 4 und 6 sowie § 356 Absatz 5 und 6 des Bürgerlichen Gesetzbuchs vorzeitig erlöschen kann, über die Umstände, unter denen der Verbraucher ein zunächst bestehendes Widerrufsrecht verliert.
Quelle: https://www.gesetze-im-internet.de/bgb/__640.html (Auszug: § 640 Absatz 2 BGB)
(2) Als abgenommen gilt ein Werk auch, wenn der Unternehmer dem Besteller nach Fertigstellung des Werks eine angemessene Frist zur Abnahme gesetzt hat und der Besteller die Abnahme nicht innerhalb dieser Frist unter Angabe mindestens eines Mangels verweigert hat. Ist der Besteller ein Verbraucher, so treten die Rechtsfolgen des Satzes 1 nur dann ein, wenn der Unternehmer den Besteller zusammen mit der Aufforderung zur Abnahme auf die Folgen einer nicht erklärten oder ohne Angabe von Mängeln verweigerten Abnahme hingewiesen hat; der Hinweis muss in Textform erfolgen.
Quelle: https://www.gesetze-im-internet.de/bgb/__649.html (§ 649 BGB, erneut abgerufen 2026-10-06; die amtliche Überschrift lautet „Kostenanschlag“)
§ 649 Kostenanschlag
(1) Ist dem Vertrag ein Kostenanschlag zugrunde gelegt worden, ohne dass der Unternehmer die Gewähr für die Richtigkeit des Anschlags übernommen hat, und ergibt sich, dass das Werk nicht ohne eine wesentliche Überschreitung des Anschlags ausführbar ist, so steht dem Unternehmer, wenn der Besteller den Vertrag aus diesem Grund kündigt, nur der im § 645 Abs. 1 bestimmte Anspruch zu.
(2) Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.
Quelle: https://www.gesetze-im-internet.de/bgb/__650.html (Auszug: § 650 Absatz 2 BGB, zur Abgrenzung: § 650 ist nicht der Kostenanschlag; er regelt den Werklieferungsvertrag und Verbraucherverträge über digitale Produkte)
§ 650 Werklieferungsvertrag; Verbrauchervertrag über die Herstellung digitaler Produkte
(2) Auf einen Verbrauchervertrag, bei dem der Unternehmer sich verpflichtet,
1.
digitale Inhalte herzustellen,
2.
einen Erfolg durch eine digitale Dienstleistung herbeizuführen oder
3.
einen körperlichen Datenträger herzustellen, der ausschließlich als Träger digitaler Inhalte dient,
sind die §§ 633 bis 639 über die Rechte bei Mängeln sowie § 640 über die Abnahme nicht anzuwenden. An die Stelle der nach Satz 1 nicht anzuwendenden Vorschriften treten die Vorschriften des Abschnitts 3 Titel 2a. Die §§ 641, 644 und 645 sind mit der Maßgabe anzuwenden, dass an die Stelle der Abnahme die Bereitstellung des digitalen Produkts (§ 327b Absatz 3 bis 5) tritt.
Quelle: https://www.gesetze-im-internet.de/ao_1977/__147.html (Auszug: § 147 Absatz 1 Nummer 2 bis 4, Absatz 3 Satz 1 und Absatz 4 AO)
2.
die empfangenen Handels- oder Geschäftsbriefe,
3.
Wiedergaben der abgesandten Handels- oder Geschäftsbriefe,
4.
Buchungsbelege,
(3) Die in Absatz 1 Nummer 1 und 4a aufgeführten Unterlagen sind zehn Jahre, die in Absatz 1 Nummer 4 aufgeführten Unterlagen acht Jahre und die sonstigen in Absatz 1 aufgeführten Unterlagen sechs Jahre aufzubewahren, sofern nicht in anderen Steuergesetzen kürzere Aufbewahrungsfristen zugelassen sind.
(4) Die Aufbewahrungsfrist beginnt mit dem Schluss des Kalenderjahrs, in dem die letzte Eintragung in das Buch gemacht, das Inventar, die Eröffnungsbilanz, der Jahresabschluss oder der Lagebericht aufgestellt, der Handels- oder Geschäftsbrief empfangen oder abgesandt worden oder der Buchungsbeleg entstanden ist, ferner die Aufzeichnung vorgenommen worden ist oder die sonstigen Unterlagen entstanden sind.
+120 -41
View File
@@ -1,91 +1,170 @@
/**
* Textbausteine der Auftragsbestätigung (Spec 2026-10-06, Abschnitt 2 „Inhalt des PDFs“, Übersicht V7).
* Reine Daten und Textfunktionen ohne Importe. VORLÄUFIGER Stand (angelegt von Strang L0): die endgültigen Wortlaute
* macht Strang L7 nach der Rechtsrunde, dabei ändern sich nur Wortlaute, nie Exportnamen oder Formen.
* Kundentexte stehen in der Sie-Form. Werte kommen fertig formatiert herein: Daten als 'TT.MM.JJJJ', Beträge wie von
* euroDe(...), Tage und Prozent als Zahl, Stunden als Text ('10', '2,25'), `weg` als deutsches Wort ('telefonisch'),
* `privat` (nur bei termine) als Boolean.
* Wortlaute der Auftragsbestätigung (AB), der Bestätigung durch den Kunden und der Texte für Privatpersonen (Frühbeginn,
* Widerrufsbelehrung, Muster-Widerrufsformular). Reine Konstanten und Funktionen ohne Zugriff auf Appwrite, Stripe oder die
* Konfiguration. Endfassung der Rechtsrunde vom 06.10.2026 (Strang L7); die Zusammenstellung zur Freigabe liegt in
* /root/reports/auftraege-rechnungen-2026-10-06/rechtsrunde.md.
*
* Alle Kundentexte stehen in der Sie-Form und ohne Gedankenstriche im Fließtext. Widerrufsbelehrung und Muster-Widerrufsformular sind
* die Muster der Anlagen 1 und 2 zu Art. 246a § 1 Abs. 2 Satz 2 EGBGB für einen Dienstleistungsvertrag (Quellen:
* docs/rechtliches/quellen/anlage-1.txt und anlage-2.txt) und gleich der Belehrung für die Bestellung einer Funktion
* (widerrufsbelehrungFunktion in services/termine/rechtstexte.js); test/auftraegeRechtstexte.test.js prüft beides.
* Die Frühbeginn-Erklärung folgt den Erklärungen der Funktionen: § 356 Abs. 5 Nr. 2 BGB (Erlöschen), § 357a Abs. 2 BGB (Wertersatz).
*
* Ändert sich ein Wortlaut nach der Auslieferung, bekommt TEXTE_FASSUNG ein neues Datum. Die Fassung steht im Nachweis jeder
* Auftragsbestätigung (texteFassung); die Dateien früherer Fassungen unter public/rechtliches/ bleiben unverändert.
*/
import { AUFTRAG_WEGE, LIMITS } from './konstanten.js'
export const TEXTE_FASSUNG = '2026-10-06'
export const ABSENDER = {
name: 'WEBklar GbR',
strasse: 'Am Schwimmbad 10',
plzOrt: '67722 Winnweiler',
telefon: '0176 23726355',
email: 'support@webklar.com',
web: 'webklar.com',
vertretenDurch: 'Kenso Grimm und Justin Klein',
export const ABSENDER = { name: 'WEBklar GbR', strasse: 'Am Schwimmbad 10', plzOrt: '67722 Winnweiler',
telefon: '0176 23726355', email: 'support@webklar.com', web: 'webklar.com', vertretenDurch: 'Kenso Grimm und Justin Klein' }
// ---------------------------------------------------------------------------------------------------------- Hilfen
/** Pflichtwert: fehlt er, entsteht ein Fehler statt eines Textes mit „undefined“ (eine Auftragsbestätigung darf nie unvollständig entstehen). */
function pflicht(wert, name) {
if (wert === undefined || wert === null || String(wert).trim() === '') throw new TypeError(`Text der Auftragsbestätigung: ${name} fehlt.`)
return String(wert).trim()
}
/** Zahlungsziel als Satzteil: 0 Tage heißt sofort, sonst „innerhalb von N Tagen nach Rechnung“. */
const zielText = (ziel) => (ziel === 0 ? 'sofort nach Rechnungserhalt' : `innerhalb von ${ziel} ${ziel === 1 ? 'Tag' : 'Tagen'} nach Rechnung`)
/** Klartext der Absprache: der Schlüssel aus AUFTRAG_WEGE („telefon“) wird zu „telefonisch“, ein fertiger Klartext bleibt, ohne Angabe entfällt er. */
function wegText(weg) {
const w = typeof weg === 'string' ? weg.trim() : ''
if (w === '') return ''
return Object.hasOwn(AUFTRAG_WEGE, w) ? AUFTRAG_WEGE[w] : w
}
/** Zahlungsziel als Satzteil: 0 = sofort, sonst „innerhalb von n Tagen nach Rechnungsdatum“; fehlend oder ungültig = Vorgabe (14 Tage). */
function zielText(ziel) {
const n = typeof ziel === 'number' ? ziel : Number.parseInt(String(ziel), 10)
const tage = Number.isInteger(n) && n >= 0 ? n : LIMITS.standardZahlungszielTage
if (tage === 0) return 'sofort nach Rechnungsstellung'
return `innerhalb von ${tage} ${tage === 1 ? 'Tag' : 'Tagen'} nach Rechnungsdatum`
}
/** Zahl mit Dezimalkomma (33.5 wird „33,5“). */
const zahl = (wert) => String(wert).replace('.', ',')
const PRIVAT_BEGINN = 'Ohne Ihr ausdrückliches Verlangen beginnen wir erst nach Ablauf der Widerrufsfrist.'
// ---------------------------------------------------------------------------------------------------------- Auftragsbestätigung
export const AB_TEXTE = {
titel: 'Auftragsbestätigung',
einleitung: ({ datum, weg }) => `Vielen Dank für Ihren Auftrag vom ${datum}${weg ? ` (${weg})` : ''}. Wir bestätigen ihn wie folgt:`,
aufwand: ({ satz, schaetzung }) =>
`Die Abrechnung erfolgt nach Aufwand: ${satz} pro Stunde in Schritten von 15 Minuten. Unterstützt eine weitere Person, berechnen wir für sie die Hälfte der Arbeitszeit (AGB Nr. 9).` +
(schaetzung ? ` Geschätzter Umfang: etwa ${schaetzung} ${schaetzung === '1' ? 'Stunde' : 'Stunden'} (unverbindlich).` : '') +
' Zeichnet sich eine deutliche Überschreitung ab, melden wir uns vorher.',
kleinunternehmer: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.',
zahlungsplan: {
nach_abnahme: ({ ziel }) => `Die Rechnung stellen wir nach Abnahme der Leistung; zahlbar ${zielText(ziel)}.`,
anzahlung: ({ prozent, anzahlung, rest, ziel }) =>
`Anzahlung ${prozent} % (${anzahlung}) nach Auftragsbestätigung, Rest (${rest}) nach Abnahme; zahlbar jeweils ${zielText(ziel)}.`,
monatlich: ({ ziel }) => `Wir rechnen monatlich nach dem entstandenen Aufwand ab; zahlbar ${zielText(ziel)}.`,
frei: ({ text, ziel }) => `${text.trim().replace(/[\s.;:,]+$/, '')}; zahlbar ${zielText(ziel)}.`,
einleitung: ({ datum, weg } = {}) => {
const art = wegText(weg)
return `Vielen Dank für Ihren Auftrag vom ${pflicht(datum, 'datum')}${art ? ` (${art})` : ''}. Wir bestätigen ihn wie folgt:`
},
aufwand: ({ satz, schaetzung } = {}) => {
const saetze = [
`Wir rechnen nach Aufwand ab: ${pflicht(satz, 'satz')} pro Stunde, erfasst je Arbeitsschritt in Schritten von 15 Minuten; angefangene 15 Minuten werden aufgerundet.`,
'Wirkt eine weitere Person unterstützend mit, berechnen wir für sie die Hälfte der Arbeitszeit (AGB Nr. 9).',
]
if (schaetzung !== undefined && schaetzung !== null && String(schaetzung).trim() !== '') {
const stunden = String(schaetzung).trim()
saetze.push(`Der geschätzte Umfang beträgt etwa ${stunden} ${stunden === '1' ? 'Stunde' : 'Stunden'}; die Schätzung ist unverbindlich.`)
saetze.push('Zeichnet sich eine deutliche Überschreitung ab, melden wir uns vorher.')
}
return saetze.join(' ')
},
kleinunternehmer: 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.',
zahlungsplan: {
nach_abnahme: ({ ziel } = {}) => `Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar ${zielText(ziel)}.`,
anzahlung: ({ prozent, anzahlung, rest, ziel } = {}) =>
`Anzahlung ${zahl(pflicht(prozent, 'prozent'))} % (${pflicht(anzahlung, 'anzahlung')}) nach Auftragsbestätigung, `
+ `Rest (${pflicht(rest, 'rest')}) nach Abnahme; zahlbar jeweils ${zielText(ziel)}.`,
monatlich: ({ ziel } = {}) => `Wir stellen den angefallenen Aufwand monatlich in Rechnung; zahlbar ${zielText(ziel)}.`,
frei: ({ text, ziel } = {}) => {
const t = pflicht(text, 'text')
return `${t}${/[.!?]$/.test(t) ? '' : '.'} Zahlbar jeweils ${zielText(ziel)}.`
},
},
termine: ({ start, fertig, privat = false } = {}) => {
const teile = []
if (start && fertig) teile.push(`Beginn: ${start}; voraussichtliche Fertigstellung: ${fertig}.`)
else if (start) teile.push(`Beginn: ${start}.`)
else if (fertig) teile.push(`Voraussichtliche Fertigstellung: ${fertig}.`)
if (privat) teile.push('Ohne Ihr ausdrückliches Verlangen beginnen wir erst nach Ablauf der Widerrufsfrist.')
return teile.length ? teile.join(' ') : null
if (privat) teile.push(PRIVAT_BEGINN)
return teile.length > 0 ? teile.join(' ') : null
},
mitwirkung: 'Mitwirkungspflichten und Abnahme richten sich nach Nr. 7 und Nr. 8 unserer Allgemeinen Geschäftsbedingungen.',
agb: ({ fassung }) => `Es gelten unsere Allgemeinen Geschäftsbedingungen in der Fassung vom ${fassung}, die dieser Bestätigung beiliegen.`,
ersetzt: ({ nummer, datum }) => `Diese Auftragsbestätigung ersetzt ${nummer} vom ${datum}.`,
mitwirkung:
'Mitwirkung und Abnahme richten sich nach Nr. 7 und Nr. 8 unserer Allgemeinen Geschäftsbedingungen. '
+ 'Bitte stellen Sie uns die benötigten Inhalte, Daten und Freigaben rechtzeitig bereit.',
agb: ({ fassung } = {}) =>
`Es gelten unsere Allgemeinen Geschäftsbedingungen in der Fassung vom ${pflicht(fassung, 'fassung')}, die dieser Bestätigung beiliegen.`,
ersetzt: ({ nummer, datum } = {}) => `Diese Auftragsbestätigung ersetzt ${pflicht(nummer, 'nummer')} vom ${pflicht(datum, 'datum')}.`,
gruss: 'Mit freundlichen Grüßen',
}
// ---------------------------------------------------------------------------------------------------------- Bestätigung und Frühbeginn
/** Häkchen auf der Bestätigungsseite und im Kundenportal (Wortlaut der Spezifikation; Alternative mit Bezug auf die AGB: Entscheidung E2 im Freigabe-Dokument). */
export const BESTAETIGUNG_TEXT = 'Ich bestätige den Auftrag zu diesen Bedingungen.'
/** Fassung der Frühbeginn-Erklärung; steht im Nachweis der Bestätigung (fruehbeginnFassung). */
export const FRUEHBEGINN_FASSUNG = '2026-10-06'
export const FRUEHBEGINN_TEXT =
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Arbeit an diesem Auftrag beginnt. Mir ist bekannt, dass ich bei einem Widerruf einen angemessenen Betrag für die bis dahin erbrachten Leistungen zahle (§ 357a Abs. 2 BGB).'
/** Zweites, freiwilliges Häkchen für Privatpersonen: Verlangen (§ 357a Abs. 2 Nr. 1 BGB), Zustimmung und Kenntnis (§ 356 Abs. 5 Nr. 2 BGB). */
export const FRUEHBEGINN_TEXT =
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Ausführung dieses Projektauftrags beginnt. '
+ 'Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). '
+ 'Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).'
// ---------------------------------------------------------------------------------------------------------- Widerrufsbelehrung und Formular
const KONTAKT = `${ABSENDER.name}, ${ABSENDER.strasse}, ${ABSENDER.plzOrt}, Telefon: ${ABSENDER.telefon}, E-Mail: ${ABSENDER.email}`
/** Muster der Anlage 1 für einen Dienstleistungsvertrag; Lücken: Frist „des Vertragsabschlusses.“, Kontaktangaben, Widerrufsfunktion, Wertersatz. */
export const WIDERRUFSBELEHRUNG_PROJEKT = {
titel: 'Widerrufsbelehrung',
abschnitte: [
{
ueberschrift: 'Widerrufsrecht',
absaetze: [
'Diese Belehrung gilt für den Projektauftrag, den wir Ihnen mit dieser Auftragsbestätigung bestätigen.',
'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 des Vertragsabschlusses.',
'Um Ihr Widerrufsrecht auszuüben, müssen Sie uns (WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, Telefon: 0176 23726355, E-Mail: support@webklar.com) 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 https://project.webklar.com/widerrufen.html ausüben; wählen Sie dort die Vertragsart „Projektauftrag (Auftragsbestätigung)“ und tragen Sie die Nummer Ihrer Auftragsbestätigung ein. 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.',
`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 https://project.webklar.com/widerrufen.html 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.',
],
},
{
ueberschrift: 'Folgen des Widerrufs',
absaetze: [
'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.',
'Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.',
'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.',
'Haben Sie verlangt, dass die Dienstleistungen während der Widerrufsfrist beginnen soll, so haben Sie uns einen angemessenen Betrag zu zahlen, '
+ 'der dem Anteil der bis zu dem Zeitpunkt, zu dem Sie uns von der Ausübung des Widerrufsrechts hinsichtlich dieses Vertrags unterrichten, '
+ 'bereits erbrachten Dienstleistungen im Vergleich zum Gesamtumfang der im Vertrag vorgesehenen Dienstleistungen entspricht.',
],
},
],
}
/** Muster der Anlage 2; „–“ ist das Aufzählungszeichen des Musters, die letzte Zeile gehört zum Muster. */
export const WIDERRUFSFORMULAR = {
titel: 'Muster-Widerrufsformular',
einleitung: '(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)',
zeilen: [
'– An WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, E-Mail: support@webklar.com:',
`– An ${ABSENDER.name}, ${ABSENDER.strasse}, ${ABSENDER.plzOrt}, E-Mail: ${ABSENDER.email}:`,
'– 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)',
+351
View File
@@ -0,0 +1,351 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawnSync } from 'node:child_process'
// Rechtstexte der Aufträge und Rechnungen (Strang L7; Spec Abschnitt 8 „Rechtsrunde“, Übersicht V7).
// Geprüft werden die Wortlaute in server/services/auftraege/texte.js gegen die gesetzlichen Muster (docs/rechtliches/quellen/), die Seiten
// und PDFs unter public/rechtliches/ (Satz „auftraege“ von scripts/build-legal-html.mjs und build-legal-pdfs.sh), die AGB-Kopie gegen die
// Website und die Abschnitte der Datenschutzerklärung. Es wird nur gelesen, geschrieben wird nur in temporären Verzeichnissen.
const ROOT = fileURLToPath(new URL('..', import.meta.url))
const lies = (rel) => readFileSync(join(ROOT, rel), 'utf8')
const sha256 = (daten) => createHash('sha256').update(daten).digest('hex')
const norm = (text) => String(text).replace(/\s+/g, ' ').trim()
const { config } = await import('../server/config.js')
const texte = await import('../server/services/auftraege/texte.js')
const { AGB_FASSUNG, AUFTRAG_WEGE } = await import('../server/services/auftraege/konstanten.js')
const { widerrufsbelehrungFunktion } = await import('../server/services/termine/rechtstexte.js')
const gen = await import('../scripts/build-legal-html.mjs')
const T = texte.AB_TEXTE
// ---------------------------------------------------------------------------------------------------------- Exporte und Formen (V7)
const V7_EXPORTE = ['ABSENDER', 'AB_TEXTE', 'BESTAETIGUNG_TEXT', 'FRUEHBEGINN_FASSUNG', 'FRUEHBEGINN_TEXT', 'TEXTE_FASSUNG', 'WIDERRUFSBELEHRUNG_PROJEKT', 'WIDERRUFSFORMULAR']
test('texte_exporte: genau die Exporte und Formen aus V7 der Übersicht, Fassungen im Format JJJJ-MM-TT', () => {
assert.deepEqual(Object.keys(texte).sort(), [...V7_EXPORTE].sort())
for (const fassung of [texte.TEXTE_FASSUNG, texte.FRUEHBEGINN_FASSUNG]) {
assert.match(fassung, /^\d{4}-\d{2}-\d{2}$/)
assert.ok(fassung >= '2026-10-06', `Fassung ${fassung} liegt vor der Rechtsrunde`)
}
assert.deepEqual(texte.ABSENDER, {
name: 'WEBklar GbR', strasse: 'Am Schwimmbad 10', plzOrt: '67722 Winnweiler', telefon: '0176 23726355',
email: 'support@webklar.com', web: 'webklar.com', vertretenDurch: 'Kenso Grimm und Justin Klein',
})
assert.deepEqual(Object.keys(T).sort(), ['agb', 'aufwand', 'einleitung', 'ersetzt', 'gruss', 'kleinunternehmer', 'mitwirkung', 'termine', 'titel', 'zahlungsplan'])
assert.deepEqual(Object.keys(T.zahlungsplan).sort(), ['anzahlung', 'frei', 'monatlich', 'nach_abnahme'])
for (const name of ['einleitung', 'aufwand', 'termine', 'agb', 'ersetzt']) assert.equal(typeof T[name], 'function', name)
for (const name of ['anzahlung', 'frei', 'monatlich', 'nach_abnahme']) assert.equal(typeof T.zahlungsplan[name], 'function', name)
for (const name of ['titel', 'kleinunternehmer', 'mitwirkung', 'gruss']) assert.equal(typeof T[name], 'string', name)
assert.equal(typeof texte.BESTAETIGUNG_TEXT, 'string')
assert.equal(typeof texte.FRUEHBEGINN_TEXT, 'string')
assert.deepEqual(Object.keys(texte.WIDERRUFSBELEHRUNG_PROJEKT), ['titel', 'abschnitte'])
for (const abschnitt of texte.WIDERRUFSBELEHRUNG_PROJEKT.abschnitte) {
assert.deepEqual(Object.keys(abschnitt), ['ueberschrift', 'absaetze'])
assert.ok(abschnitt.absaetze.length > 0 && abschnitt.absaetze.every((s) => typeof s === 'string' && s !== ''))
}
assert.deepEqual(Object.keys(texte.WIDERRUFSFORMULAR), ['titel', 'einleitung', 'zeilen'])
})
// ---------------------------------------------------------------------------------------------------------- Textbausteine der AB
test('ab_texte_einleitung: Datum und Weg der Absprache; Schlüssel aus AUFTRAG_WEGE wird zum Klartext; Pflichtwert fehlt = Fehler', () => {
assert.equal(T.titel, 'Auftragsbestätigung')
assert.equal(
T.einleitung({ datum: '05.10.2026', weg: 'telefonisch' }),
'Vielen Dank für Ihren Auftrag vom 05.10.2026 (telefonisch). Wir bestätigen ihn wie folgt:',
)
for (const [schluessel, klartext] of Object.entries(AUFTRAG_WEGE)) {
const erwartet = `Vielen Dank für Ihren Auftrag vom 05.10.2026 (${klartext}). Wir bestätigen ihn wie folgt:`
assert.equal(T.einleitung({ datum: '05.10.2026', weg: schluessel }), erwartet, schluessel)
assert.equal(T.einleitung({ datum: '05.10.2026', weg: klartext }), erwartet, klartext)
}
assert.equal(T.einleitung({ datum: '05.10.2026', weg: '' }), 'Vielen Dank für Ihren Auftrag vom 05.10.2026. Wir bestätigen ihn wie folgt:')
assert.equal(T.einleitung({ datum: '05.10.2026', weg: 'constructor' }), 'Vielen Dank für Ihren Auftrag vom 05.10.2026 (constructor). Wir bestätigen ihn wie folgt:', 'kein Treffer in der Prototypkette')
assert.throws(() => T.einleitung({ weg: 'telefon' }), TypeError)
assert.throws(() => T.einleitung(), TypeError)
})
test('ab_texte_aufwand: Stundensatz, 15-Minuten-Schritte, Unterstützung (AGB Nr. 9), unverbindliche Schätzung und Zusage nach § 649 BGB (Kostenanschlag) nur mit Schätzung', () => {
const regel = 'Wir rechnen nach Aufwand ab: 100,00 € pro Stunde, erfasst je Arbeitsschritt in Schritten von 15 Minuten; angefangene 15 Minuten werden aufgerundet. '
+ 'Wirkt eine weitere Person unterstützend mit, berechnen wir für sie die Hälfte der Arbeitszeit (AGB Nr. 9).'
const schaetzung = (stunden, wort) => `${regel} Der geschätzte Umfang beträgt etwa ${stunden} ${wort}; die Schätzung ist unverbindlich. Zeichnet sich eine deutliche Überschreitung ab, melden wir uns vorher.`
assert.equal(T.aufwand({ satz: '100,00 €', schaetzung: '10' }), schaetzung('10', 'Stunden'))
assert.equal(T.aufwand({ satz: '100,00 €', schaetzung: '1' }), schaetzung('1', 'Stunde'))
assert.equal(T.aufwand({ satz: '100,00 €', schaetzung: '7,5' }), schaetzung('7,5', 'Stunden'))
// ohne Schätzung gibt es nichts zu überschreiten: weder Schätzung noch Zusage
for (const leer of [null, undefined, '']) assert.equal(T.aufwand({ satz: '100,00 €', schaetzung: leer }), regel)
assert.throws(() => T.aufwand({ schaetzung: '10' }), TypeError)
})
test('ab_texte_kleinunternehmer_mitwirkung_agb_ersetzt_gruss: Wortlaut', () => {
assert.equal(T.kleinunternehmer, 'Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.')
assert.equal(
T.mitwirkung,
'Mitwirkung und Abnahme richten sich nach Nr. 7 und Nr. 8 unserer Allgemeinen Geschäftsbedingungen. '
+ 'Bitte stellen Sie uns die benötigten Inhalte, Daten und Freigaben rechtzeitig bereit.',
)
assert.equal(
T.agb({ fassung: '30.09.2026' }),
'Es gelten unsere Allgemeinen Geschäftsbedingungen in der Fassung vom 30.09.2026, die dieser Bestätigung beiliegen.',
)
assert.equal(T.ersetzt({ nummer: 'AB-2026-0001', datum: '05.10.2026' }), 'Diese Auftragsbestätigung ersetzt AB-2026-0001 vom 05.10.2026.')
assert.equal(T.gruss, 'Mit freundlichen Grüßen')
assert.throws(() => T.agb({}), TypeError)
assert.throws(() => T.ersetzt({ nummer: 'AB-2026-0001' }), TypeError)
})
test('ab_texte_zahlungsplan: vier Pläne, Zahlungsziel als „innerhalb von n Tagen nach Rechnungsdatum“', () => {
const Z = T.zahlungsplan
const ziel14 = 'innerhalb von 14 Tagen nach Rechnungsdatum'
assert.equal(Z.nach_abnahme({ ziel: 14 }), `Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar ${ziel14}.`)
assert.equal(
Z.anzahlung({ prozent: 50, anzahlung: '400,00 €', rest: '400,00 €', ziel: 14 }),
`Anzahlung 50 % (400,00 €) nach Auftragsbestätigung, Rest (400,00 €) nach Abnahme; zahlbar jeweils ${ziel14}.`,
)
assert.equal(Z.anzahlung({ prozent: 33.5, anzahlung: '335,00 €', rest: '665,00 €', ziel: 7 }), 'Anzahlung 33,5 % (335,00 €) nach Auftragsbestätigung, Rest (665,00 €) nach Abnahme; zahlbar jeweils innerhalb von 7 Tagen nach Rechnungsdatum.')
assert.equal(Z.monatlich({ ziel: 14 }), `Wir stellen den angefallenen Aufwand monatlich in Rechnung; zahlbar ${ziel14}.`)
assert.equal(
Z.frei({ text: 'Je ein Drittel bei Auftrag, zur Halbzeit und nach Abnahme', ziel: 7 }),
'Je ein Drittel bei Auftrag, zur Halbzeit und nach Abnahme. Zahlbar jeweils innerhalb von 7 Tagen nach Rechnungsdatum.',
)
assert.equal(Z.frei({ text: 'Zahlung in zwei Raten.', ziel: 14 }), `Zahlung in zwei Raten. Zahlbar jeweils ${ziel14}.`, 'kein doppelter Punkt')
// Zahlungsziel: 0 = sofort, 1 = Einzahl, Strings und Zahlen, ungültig oder fehlend = 14 Tage
assert.equal(Z.nach_abnahme({ ziel: 0 }), 'Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar sofort nach Rechnungsstellung.')
assert.equal(Z.nach_abnahme({ ziel: 1 }), 'Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar innerhalb von 1 Tag nach Rechnungsdatum.')
assert.equal(Z.nach_abnahme({ ziel: '30' }), 'Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar innerhalb von 30 Tagen nach Rechnungsdatum.')
for (const ungueltig of [undefined, null, '', 'bald', 7.5, -3, NaN]) {
assert.equal(Z.nach_abnahme({ ziel: ungueltig }), `Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar ${ziel14}.`, String(ungueltig))
}
assert.equal(Z.nach_abnahme(), `Wir stellen die Vergütung nach Abnahme in Rechnung; zahlbar ${ziel14}.`)
// Pflichtwerte
assert.throws(() => Z.anzahlung({ prozent: 50, anzahlung: '400,00 €', ziel: 14 }), TypeError)
assert.throws(() => Z.frei({ text: ' ', ziel: 14 }), TypeError)
})
test('ab_texte_termine: Beginn und voraussichtliche Fertigstellung; für Privatpersonen der Satz zum Beginn nach Ablauf der Widerrufsfrist', () => {
const privat = 'Ohne Ihr ausdrückliches Verlangen beginnen wir erst nach Ablauf der Widerrufsfrist.'
assert.equal(T.termine({ start: '08.10.2026', fertig: '31.10.2026' }), 'Beginn: 08.10.2026; voraussichtliche Fertigstellung: 31.10.2026.')
assert.equal(T.termine({ start: '08.10.2026', fertig: null }), 'Beginn: 08.10.2026.')
assert.equal(T.termine({ start: null, fertig: '31.10.2026' }), 'Voraussichtliche Fertigstellung: 31.10.2026.')
assert.equal(T.termine({ start: null, fertig: null }), null)
assert.equal(T.termine({}), null)
assert.equal(T.termine({ start: '08.10.2026', fertig: '31.10.2026', privat: false }), 'Beginn: 08.10.2026; voraussichtliche Fertigstellung: 31.10.2026.')
assert.equal(T.termine({ start: '08.10.2026', fertig: '31.10.2026', privat: true }), `Beginn: 08.10.2026; voraussichtliche Fertigstellung: 31.10.2026. ${privat}`)
assert.equal(T.termine({ start: null, fertig: null, privat: true }), privat, 'der Satz steht auch ohne Termine')
})
test('ab_texte_stil: Sie-Form, keine Gedankenstriche im Fließtext, keine veraltete Zählung, kein „undefined“', () => {
const alle = [
T.titel,
T.einleitung({ datum: '05.10.2026', weg: 'telefon' }),
T.aufwand({ satz: '100,00 €', schaetzung: '10' }),
T.aufwand({ satz: '100,00 €' }),
T.kleinunternehmer,
T.zahlungsplan.nach_abnahme({ ziel: 14 }),
T.zahlungsplan.anzahlung({ prozent: 50, anzahlung: '400,00 €', rest: '400,00 €', ziel: 14 }),
T.zahlungsplan.monatlich({ ziel: 14 }),
T.zahlungsplan.frei({ text: 'Je ein Drittel', ziel: 7 }),
T.termine({ start: '08.10.2026', fertig: '31.10.2026', privat: true }),
T.mitwirkung,
T.agb({ fassung: '30.09.2026' }),
T.ersetzt({ nummer: 'AB-2026-0001', datum: '05.10.2026' }),
T.gruss,
texte.BESTAETIGUNG_TEXT,
texte.FRUEHBEGINN_TEXT,
]
for (const text of alle) {
assert.doesNotMatch(text, /[–—]/, `Gedankenstrich: ${text}`)
assert.doesNotMatch(text, /\bdein|\bdu\b|\bdir\b|\bdich\b/i, `Du-Form: ${text}`)
assert.doesNotMatch(text, /undefined|null|NaN|\{\{|\}\}/, `Platzhalter: ${text}`)
assert.doesNotMatch(text, /Abs\. 4/, `veraltete Zählung: ${text}`)
}
})
// ---------------------------------------------------------------------------------------------------------- Bestätigung und Frühbeginn
test('bestaetigung_text: Häkchen im Wortlaut der Spezifikation (Alternative mit Bezug auf die AGB ist Entscheidung E2 der Rechtsrunde)', () => {
assert.equal(texte.BESTAETIGUNG_TEXT, 'Ich bestätige den Auftrag zu diesen Bedingungen.')
})
const NORMEN = lies('docs/rechtliches/quellen/normen-auftraege-2026-10-06.txt')
test('normen_zaehlung: § 356 Abs. 5 Nr. 2 BGB regelt das Erlöschen bei entgeltlichen Dienstleistungen (Abs. 4 ist die Höchstfrist), § 357a Abs. 2 BGB den Wertersatz', () => {
const n = norm(NORMEN)
// Abs. 4 = Höchstfrist, kein Erlöschen durch Leistung
assert.ok(n.includes('(4) Das Widerrufsrecht erlischt spätestens zwölf Monate und 14 Tage nach dem in Absatz 2 oder § 355 Absatz 2 Satz 2 genannten Zeitpunkt.'))
// Abs. 5 Nr. 2 = Vertrag mit Preis: Zustimmung (a), bei Verträgen außerhalb von Geschäftsräumen Datenträger (b), Kenntnis (c)
const abs5 = n.slice(n.indexOf('(5) Das Widerrufsrecht erlischt bei Verträgen über die Erbringung von Dienstleistungen'), n.indexOf('(6) Das Widerrufsrecht erlischt bei Verträgen über die Bereitstellung'))
assert.ok(abs5.length > 500, 'Absatz 5 fehlt in der Quelle')
const nr2 = abs5.slice(abs5.indexOf('2. bei einem Vertrag, der den Verbraucher zur Zahlung eines Preises verpflichtet'), abs5.indexOf('3. bei einem Vertrag, bei dem der Verbraucher den Unternehmer ausdrücklich aufgefordert hat'))
for (const teil of [
'mit der vollständigen Erbringung der Dienstleistung, wenn der Verbraucher vor Beginn der Erbringung',
'a) ausdrücklich zugestimmt hat, dass der Unternehmer mit der Erbringung der Dienstleistung vor Ablauf der Widerrufsfrist beginnt,',
'b) bei einem außerhalb von Geschäftsräumen geschlossenen Vertrag die Zustimmung nach Buchstabe a auf einem dauerhaften Datenträger übermittelt hat und',
'c) seine Kenntnis davon bestätigt hat, dass sein Widerrufsrecht mit vollständiger Vertragserfüllung durch den Unternehmer erlischt,',
]) assert.ok(nr2.includes(teil), `§ 356 Abs. 5 Nr. 2: ${teil}`)
// Nr. 1 betrifft unentgeltliche Verträge, Abs. 6 die Bereitstellung digitaler Inhalte: beides passt nicht zum Projektauftrag
assert.ok(abs5.includes('1. bei einem Vertrag, der den Verbraucher nicht zur Zahlung eines Preises verpflichtet'))
// § 357a Abs. 2: Wertersatz für Dienstleistungen mit Preis, ausdrückliches Verlangen als Voraussetzung
assert.ok(n.includes('(2) Der Verbraucher hat Wertersatz für die bis zum Widerruf erbrachten Dienstleistungen, für die der Vertrag die Zahlung eines Preises vorsieht'))
assert.ok(n.includes('1. der Verbraucher von dem Unternehmer ausdrücklich verlangt hat, dass mit der Leistung vor Ablauf der Widerrufsfrist begonnen werden soll,'))
assert.ok(n.includes('Bei der Berechnung des Wertersatzes ist der vereinbarte Gesamtpreis zu Grunde zu legen.'))
})
test('normen_kostenanschlag: § 649 BGB ist der Kostenanschlag (Abs. 2: unverzügliche Anzeige), § 650 BGB regelt etwas anderes', () => {
const n = norm(NORMEN)
const block = (nr) => {
const a = n.indexOf(`Quelle: https://www.gesetze-im-internet.de/bgb/__${nr}.html`)
assert.ok(a >= 0, `§ ${nr} fehlt im Nachweis`)
const b = n.indexOf('Quelle: ', a + 10)
return n.slice(a, b < 0 ? undefined : b)
}
const kostenanschlag = block(649)
assert.ok(kostenanschlag.includes('§ 649 Kostenanschlag'), 'amtliche Überschrift von § 649')
assert.ok(kostenanschlag.includes('(1) Ist dem Vertrag ein Kostenanschlag zugrunde gelegt worden'))
assert.ok(kostenanschlag.includes('(2) Ist eine solche Überschreitung des Anschlags zu erwarten, so hat der Unternehmer dem Besteller unverzüglich Anzeige zu machen.'))
// § 650 trägt eine andere Überschrift; der Kostenanschlag kommt in seinem Gesetzestext nicht vor
const werklieferung = block(650)
assert.ok(werklieferung.includes('§ 650 Werklieferungsvertrag; Verbrauchervertrag über die Herstellung digitaler Produkte'))
assert.ok(!werklieferung.slice(werklieferung.indexOf('§ 650 Werklieferungsvertrag')).includes('Kostenanschlag'))
// das Ergebnis der Prüfung vom 06.10.2026 steht mit dem Zitat im Kopf der Datei
assert.ok(n.includes('Der Kostenanschlag steht in § 649 BGB (amtliche Überschrift „Kostenanschlag“).'))
assert.ok(n.includes('ein Verweis auf „§ 650“ für den Kostenanschlag ist falsch'))
assert.doesNotMatch(lies('server/services/auftraege/texte.js'), /§ 650/, 'texte.js nennt § 650 nicht')
})
test('normen_kundenseiten: Widerruf formfrei (§ 355 Abs. 1), Widerrufsfunktion und Eingangsbestätigung (§ 356a Abs. 2 und 4), digitale Produkte (§ 650 Abs. 2)', () => {
const n = norm(NORMEN)
assert.ok(n.includes('Der Widerruf erfolgt durch Erklärung gegenüber dem Unternehmer. Aus der Erklärung muss der Entschluss des Verbrauchers zum Widerruf des Vertrags eindeutig hervorgehen.'))
assert.ok(n.includes('2. Angaben zur Identifizierung des Vertrags oder des Teils des Vertrags, den der Verbraucher widerrufen möchte,'))
assert.ok(n.includes('(4) Der Unternehmer hat dem Verbraucher, wenn dieser die Bestätigungsfunktion aktiviert hat, auf einem dauerhaften Datenträger unverzüglich eine Eingangsbestätigung zu übermitteln, die zumindest den Inhalt der Widerrufserklärung nach Absatz 2 sowie das Datum und die Uhrzeit ihres Eingangs enthält.'))
assert.ok(n.includes('(2) Auf einen Verbrauchervertrag, bei dem der Unternehmer sich verpflichtet, 1. digitale Inhalte herzustellen,'))
assert.ok(n.includes('sind die §§ 633 bis 639 über die Rechte bei Mängeln sowie § 640 über die Abnahme nicht anzuwenden.'))
})
// Zeichen, die Figtree Regular und SemiBold haben (die Schrift des PDFs, server/assets/fonts/ von Strang L4): Basic Latin, Umlaute, ß, § und die
// typografischen Zeichen der Texte. Geprüft am 06.10.2026 mit fontkit (hasGlyphForCodePoint, @expo-google-fonts/figtree 0.4.1, beide Schnitte).
const FIGTREE_ZEICHEN = new Set([
...Array.from({ length: 95 }, (_, i) => String.fromCodePoint(0x20 + i)),
...'äöüÄÖÜß§€–—„“‚‘’”…−•→',
String.fromCodePoint(0xa0),
'\n',
])
test('zeichenvorrat_figtree: texte.js enthält nur Zeichen der PDF-Schrift Figtree (sonst ersetzt das PDF still ein Zeichen und weicht vom freigegebenen Wortlaut ab)', () => {
const fremd = [...new Set(lies('server/services/auftraege/texte.js'))].filter((zeichen) => !FIGTREE_ZEICHEN.has(zeichen))
const bezeichnet = fremd.map((zeichen) => `U+${zeichen.codePointAt(0).toString(16).toUpperCase().padStart(4, '0')} ${zeichen}`)
assert.deepEqual(bezeichnet, [], 'Zeichen außerhalb des geprüften Zeichenvorrats (auch in Kommentaren): vor der Aufnahme gegen Figtree prüfen und FIGTREE_ZEICHEN ergänzen; der Wächter-Test von L4 (auftraegePdfDaten) prüft nach dem Merge gegen die Schrift selbst')
})
test('fruehbeginn_text: Verlangen, Kenntnis vom Erlöschen und Wertersatz in einem Satzgefüge; zitiert § 356 Abs. 5 Nr. 2 und § 357a Abs. 2 BGB', () => {
assert.equal(texte.FRUEHBEGINN_TEXT,
'Ich verlange ausdrücklich, dass WEBklar vor Ablauf der Widerrufsfrist mit der Ausführung dieses Projektauftrags beginnt. '
+ 'Mir ist bekannt, dass mein Widerrufsrecht mit der vollständigen Erbringung der Leistung erlischt (§ 356 Abs. 5 Nr. 2 BGB). '
+ 'Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).')
const saetze = texte.FRUEHBEGINN_TEXT.split(/(?<=\.) (?=Mir ist bekannt|Widerrufe ich)/)
assert.equal(saetze.length, 3, 'drei Sätze: Verlangen, Kenntnis, Wertersatz')
// Verlangen (§ 357a Abs. 2 Nr. 1) und Zustimmung (§ 356 Abs. 5 Nr. 2 Buchstabe a): ausdrücklich, vor Ablauf der Widerrufsfrist, für diesen Auftrag
assert.ok(saetze[0].startsWith('Ich verlange ausdrücklich') && saetze[0].includes('vor Ablauf der Widerrufsfrist') && saetze[0].includes('dieses Projektauftrags'))
// Kenntnis (§ 356 Abs. 5 Nr. 2 Buchstabe c): Erlöschen mit vollständiger Erbringung
assert.ok(saetze[1].startsWith('Mir ist bekannt') && saetze[1].includes('erlischt') && saetze[1].includes('vollständigen Erbringung'))
// Wertersatz (§ 357a Abs. 2): angemessener Betrag für die bis dahin erbrachte Leistung
assert.ok(saetze[2].includes('angemessenen Betrag') && saetze[2].includes('bis dahin erbrachte Leistung'))
assert.doesNotMatch(texte.FRUEHBEGINN_TEXT, /§ 356 Abs\. 4|§ 356 Abs\. 6|§ 357 Abs\. 8/)
// dasselbe Satzende wie bei den Funktionen (News, Terminplaner, Webshop, Öffnungszeiten): gleiche Normen, gleiche Wertersatz-Formel
const ende = '(§ 356 Abs. 5 Nr. 2 BGB). Widerrufe ich vor der vollständigen Erbringung, schulde ich einen angemessenen Betrag für die bis dahin erbrachte Leistung (§ 357a Abs. 2 BGB).'
assert.ok(texte.FRUEHBEGINN_TEXT.endsWith(ende))
assert.ok(config.funktionen.consentText.endsWith(ende), 'die Erklärung der Funktionen hat dasselbe Satzende')
// beide Zitate stehen in der Quelle der Normen
assert.ok(norm(NORMEN).includes('Quelle: https://www.gesetze-im-internet.de/bgb/__356.html'))
assert.ok(norm(NORMEN).includes('Quelle: https://www.gesetze-im-internet.de/bgb/__357a.html'))
})
// ---------------------------------------------------------------------------------------------------------- Widerrufsbelehrung und Formular
const ANLAGE_1 = lies('docs/rechtliches/quellen/anlage-1.txt')
const ANLAGE_2 = lies('docs/rechtliches/quellen/anlage-2.txt')
const zeilenVon = (text) => text.split('\n').map((z) => z.trim())
const KONTAKT = 'WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, Telefon: 0176 23726355, E-Mail: support@webklar.com'
const B = texte.WIDERRUFSBELEHRUNG_PROJEKT
const F = texte.WIDERRUFSFORMULAR
/** Die Belehrung als ein Text: Titel, Überschriften und Absätze in Dokumentreihenfolge. */
const belehrungText = () => [B.titel, ...B.abschnitte.flatMap((a) => [a.ueberschrift, ...a.absaetze])].join('\n')
test('belehrung_muster: jeder Absatz der Anlage 1 steht wörtlich darin, nur die Lücken sind gefüllt (Frist „des Vertragsabschlusses.“, Kontaktangaben)', () => {
const z = zeilenVon(ANLAGE_1)
const von = z.indexOf('Widerrufsbelehrung')
const bis = z.findIndex((s) => s.startsWith('Gestaltungshinweise:'))
assert.ok(von >= 0 && bis > von, 'Aufbau von anlage-1.txt hat sich geändert')
const muster = z.slice(von, bis).filter(Boolean)
assert.equal(muster.length, 8, 'Anlage 1: Titel, Widerrufsrecht, 3 Absätze, Folgen des Widerrufs und 1 Absatz')
const frist = /Dienstleistungsvertrags[^„]*„([^“]+)“/.exec(ANLAGE_1)?.[1]
assert.equal(frist, 'des Vertragsabschlusses.', 'Textbaustein a) für Dienstleistungsverträge')
const text = belehrungText()
for (const zeile of muster) {
const erwartet = zeile.replace('ab dem Tag .', `ab dem Tag ${frist}`).replace('uns ( )', `uns (${KONTAKT})`)
assert.ok(text.includes(erwartet), `Absatz fehlt oder ist verändert: ${erwartet}`)
}
})
test('belehrung_widerrufsfunktion: Satzrahmen der Anlage wörtlich, die Lücke „…“ ist die Adresse der Widerrufsseite', () => {
const m = /„(Sie können Ihr Widerrufsrecht auch online unter) … \[[^\]]+\] (ausüben\.) (Wenn Sie diese Online-Funktion nutzen, [^“]+?)\s*“/.exec(ANLAGE_1)
assert.ok(m, 'Gestaltungshinweis zur Widerrufsfunktion fehlt oder hat sich geändert in anlage-1.txt')
assert.ok(m[3].includes('Eingangsbestätigung') && m[3].includes('Uhrzeit ihres Eingangs'))
const absatz = `${m[1]} https://project.webklar.com/widerrufen.html ${m[2]} ${m[3]}`
assert.ok(belehrungText().includes(absatz), 'Absatz zur Widerrufsfunktion fehlt oder weicht vom Satzrahmen der Anlage ab')
// die Widerrufsseite kennt die Vertragsart „Projektauftrag“ (L5/L6); die Adresse ist dieselbe wie bei den Funktionen
assert.equal(belehrungText().split('https://project.webklar.com/widerrufen.html').length - 1, 1)
})
test('belehrung_wertersatz: Wertersatz für Dienstleistungen, Wasser/Gas/Strom/Fernwärme gestrichen, keine Bausteine zu Waren', () => {
const m = /Haben Sie verlangt, dass die Dienstleistungen oder Lieferung von Wasser\/Gas\/Strom\/Fernwärme \[Unzutreffendes streichen\] (während[^“]+?)\s*“/.exec(ANLAGE_1)
assert.ok(m, 'Wertersatz-Absatz fehlt in anlage-1.txt')
const text = belehrungText()
assert.ok(text.includes(`Haben Sie verlangt, dass die Dienstleistungen ${m[1]}`), 'Wertersatz-Absatz fehlt oder weicht ab')
assert.doesNotMatch(text, /Wasser|Gas\b|Strom|Fernwärme|\[Unzutreffendes|Rücksendung|in Besitz genommen|Wertverlust/)
})
test('formular_muster: Muster-Widerrufsformular der Anlage 2 wörtlich, Unternehmer in der „An“-Zeile, (*)-Alternativen unverändert', () => {
const z = zeilenVon(ANLAGE_2)
const start = z.indexOf('Muster-Widerrufsformular')
assert.ok(start >= 0, 'Aufbau von anlage-2.txt hat sich geändert')
const inhalt = z.slice(start).filter((s) => s && s !== '–')
assert.equal(inhalt.length, 10, 'Anlage 2: Titel, Einleitung, 7 Zeilen und der Hinweis (*)')
const zeilen = [F.titel, F.einleitung, ...F.zeilen.map((s) => s.replace(/^– /, ''))]
let anZeilen = 0
for (const zeile of inhalt) {
if (zeile.startsWith('An [hier ist')) {
anZeilen += 1
assert.ok(zeilen.includes('An WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, E-Mail: support@webklar.com:'), '„An“-Zeile mit dem Unternehmer fehlt')
} else {
assert.ok(zeilen.includes(zeile), `Formularzeile fehlt oder ist verändert: ${zeile}`)
}
}
assert.equal(anZeilen, 1)
assert.equal(F.titel, 'Muster-Widerrufsformular')
// Aufzählungszeichen des Musters vor jeder Formularzeile, der Hinweis (*) steht zuletzt ohne Zeichen
assert.ok(F.zeilen.slice(0, -1).every((s) => s.startsWith('– ')))
assert.equal(F.zeilen.at(-1), '(*) Unzutreffendes streichen.')
})
test('belehrung_gleich_funktionen: Belehrung und Formular sind wörtlich die Belehrung für die Bestellung einer Funktion', () => {
const absaetze = [
B.titel,
...B.abschnitte.flatMap((a) => [a.ueberschrift, ...a.absaetze]),
F.titel,
F.einleitung,
F.zeilen.slice(0, -1).join('\n'),
F.zeilen.at(-1),
]
assert.equal(absaetze.join('\n\n'), widerrufsbelehrungFunktion())
assert.deepEqual(B.abschnitte.map((a) => a.ueberschrift), ['Widerrufsrecht', 'Folgen des Widerrufs'])
})