Merge auftraege/l7-rechtstexte (ohne Datenschutz-Commit): Rechtstexte (Strang L7)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
8f53b560d5
12 files changed
+1376
-48
No files matched your search
@@ -0,0 +1,149 @@
|
||||
# Allgemeine Geschäftsbedingungen
|
||||
|
||||
**Fassung 2026-09-30** · Wortgleiche Kopie der Allgemeinen Geschäftsbedingungen auf webklar.com/agb
|
||||
|
||||
## Kaufvertrag – WEBklar (Modularer Projektvertrag)
|
||||
|
||||
**zwischen**
|
||||
|
||||
WEBklar (im Folgenden „Anbieter“)
|
||||
|
||||
**und**
|
||||
|
||||
Kunde laut Angebot (im Folgenden „Kunde“)
|
||||
|
||||
## 1. Vertragsgegenstand
|
||||
|
||||
(1) Gegenstand dieses Vertrages ist die Erbringung der im Angebot definierten Leistungen.
|
||||
|
||||
(2) Der Vertrag besteht aus diesem Grundvertrag sowie den ausgewählten Leistungsmodulen.
|
||||
|
||||
(3) Maßgeblich ist das jeweils angenommene Angebot von WEBklar.
|
||||
|
||||
## 2. Leistungsart
|
||||
|
||||
(1) Sämtliche Leistungen von WEBklar stellen Dienst- und Entwicklungsleistungen dar.
|
||||
|
||||
(2) Ein bestimmter wirtschaftlicher, technischer oder rechtlicher Erfolg wird nicht geschuldet.
|
||||
|
||||
(3) WEBklar erbringt keinen laufenden Betrieb, sofern dieser nicht explizit vereinbart wurde.
|
||||
|
||||
## 3. Leistungsmodul A – Webseite (einmalig)
|
||||
|
||||
(aktiv, wenn im Angebot enthalten)
|
||||
|
||||
(1) WEBklar erstellt eine individuelle Webseite gemäß Angebot.
|
||||
|
||||
(2) Die Umsetzung erfolgt nach den vom Kunden gelieferten Inhalten und Vorgaben.
|
||||
|
||||
(3) Zusätzliche Leistungen wie Domain, Hosting, Wartung oder SEO sind nicht Bestandteil, sofern sie nicht gesondert beauftragt wurden.
|
||||
|
||||
(4) Der Kunde ist nicht berechtigt, Änderungen am Quellcode selbst vorzunehmen.
|
||||
|
||||
(5) Änderungen erfolgen ausschließlich durch WEBklar gegen gesonderte Vergütung.
|
||||
|
||||
## 4. Leistungsmodul B – Automatisierung / Virtualisierung (einmalig)
|
||||
|
||||
(aktiv, wenn im Angebot enthalten)
|
||||
|
||||
(1) WEBklar entwickelt individuelle Automatisierungen, Apps oder Virtualisierungssysteme.
|
||||
|
||||
(2) Die Leistung stellt eine reine Entwicklungsleistung dar.
|
||||
|
||||
(3) Optional kann eine Beratungsleistung Bestandteil des Projektes sein.
|
||||
|
||||
(4) Ein laufender Betrieb, Monitoring oder Wartung ist nicht geschuldet, außer dies wurde explizit vereinbart.
|
||||
|
||||
(5) Der Kunde entscheidet über Inhalte, Daten und Prozesse und trägt dafür die rechtliche Verantwortung.
|
||||
|
||||
## 5. Leistungsmodul C – Hosting
|
||||
|
||||
(aktiv, wenn im Angebot enthalten)
|
||||
|
||||
(1) Hosting bietet WEBklar als Hosting-Abo an. Für das Hosting-Abo und das Wartungs-Abo gelten die Vertragsbedingungen für die WEBklar-Abos mit Leistungsbeschreibungen; sie gehen diesen AGB vor.
|
||||
|
||||
(2) Bestehende Hosting-Verträge mit jährlicher Laufzeit behalten die bei ihrem Abschluss vereinbarten Bedingungen. Abweichend davon können Verbraucher solche Verträge nach einer automatischen Verlängerung jederzeit mit einer Frist von einem Monat kündigen.
|
||||
|
||||
(3) WEBklar ist berechtigt, externe Anbieter (z. B. Rechenzentren) einzusetzen.
|
||||
|
||||
## 6. Quellcode und Eigentum
|
||||
|
||||
### 6.1 Webseite
|
||||
|
||||
(1) Der Kunde erhält ein einfaches, zeitlich unbegrenztes Nutzungsrecht an der fertigen Webseite.
|
||||
|
||||
(2) Der Quellcode der Webseite wird nur auf ausdrückliche Anfrage und nach Vereinbarung herausgegeben.
|
||||
|
||||
(3) Ohne Vereinbarung verbleibt der Quellcode bei WEBklar und wird archiviert.
|
||||
|
||||
### 6.2 Apps, Automatisierungen und Backend
|
||||
|
||||
(1) Der Quellcode von Apps, Automatisierungen und Backend-Systemen verbleibt vollständig bei WEBklar.
|
||||
|
||||
(2) Eine Herausgabe erfolgt ausschließlich nach gesonderter schriftlicher Vereinbarung.
|
||||
|
||||
(3) Der Kunde erhält lediglich Zugriff auf die Bedienoberfläche bzw. das Frontend.
|
||||
|
||||
## 7. Mitwirkungspflichten des Kunden
|
||||
|
||||
(1) Der Kunde stellt alle benötigten Inhalte, Daten und Freigaben rechtzeitig bereit.
|
||||
|
||||
(2) Verzögerungen durch fehlende Mitwirkung gehen nicht zu Lasten von WEBklar.
|
||||
|
||||
(3) WEBklar ist nicht verpflichtet, rechtliche Prüfungen der Inhalte vorzunehmen.
|
||||
|
||||
## 8. Abnahme
|
||||
|
||||
(1) Nach Fertigstellung wird dem Kunden die Leistung zur Abnahme bereitgestellt.
|
||||
|
||||
(2) Erfolgt innerhalb von 14 Tagen keine Rückmeldung, gilt die Leistung als abgenommen.
|
||||
|
||||
(3) Nach Abnahme sind nur noch kostenpflichtige Änderungen möglich.
|
||||
|
||||
## 9. Vergütung und Zahlung
|
||||
|
||||
(1) Die Vergütung ergibt sich aus dem Angebot.
|
||||
|
||||
(2) Wird nach Aufwand abgerechnet, erfasst WEBklar die Arbeitszeit je Arbeitsschritt in Schritten von 15 Minuten; angefangene 15 Minuten werden aufgerundet. Wirkt eine weitere Person unterstützend mit, rechnet WEBklar für sie die Hälfte der Arbeitszeit des Arbeitsschritts an, aufgerundet auf volle 15 Minuten; bei mehreren unterstützenden Personen gilt das für jede. Jeder Arbeitsschritt und jede Unterstützung ist im Kundenportal einsehbar.
|
||||
|
||||
(3) Projektleistungen sind nach Vereinbarung fällig.
|
||||
|
||||
(4) Für die Abos gelten die Vertragsbedingungen für die WEBklar-Abos.
|
||||
|
||||
## 10. Zahlungsverzug
|
||||
|
||||
(1) Bei Zahlungsverzug erfolgen bis zu zwei Mahnungen.
|
||||
|
||||
(2) Danach ist WEBklar berechtigt:
|
||||
|
||||
- Leistungen zu sperren
|
||||
- Verzugszinsen zu berechnen
|
||||
- den Vertrag außerordentlich zu kündigen
|
||||
|
||||
## 11. Haftung
|
||||
|
||||
(1) WEBklar haftet unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit WEBklar eine Garantie übernommen hat.
|
||||
|
||||
(2) Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haftet WEBklar nur für den vertragstypischen, vorhersehbaren Schaden. Wesentlich sind Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf. Im Übrigen ist die Haftung für leichte Fahrlässigkeit ausgeschlossen.
|
||||
|
||||
(3) Der Kunde bewahrt Kopien der Inhalte und Daten auf, die er WEBklar übergibt oder selbst einpflegt. Für den Verlust solcher Inhalte und Daten haftet WEBklar außer in den Fällen von Absatz 1 nur in Höhe des Aufwands, der für ihre Wiederherstellung nötig gewesen wäre, wenn der Kunde diese Kopien aufbewahrt hätte.
|
||||
|
||||
(4) Diese Regeln gelten auch zugunsten der Mitarbeiter und Beauftragten von WEBklar.
|
||||
|
||||
## 12. Kündigung
|
||||
|
||||
(1) Laufzeitverträge können zum Ende der jeweiligen Laufzeit gekündigt werden. Laufzeit und Kündigung der Abos richten sich nach den Vertragsbedingungen für die WEBklar-Abos.
|
||||
|
||||
(2) Das Recht zur außerordentlichen Kündigung bleibt unberührt.
|
||||
|
||||
## 13. Referenzen
|
||||
|
||||
WEBklar darf das Projekt nur nach ausdrücklicher Zustimmung des Kunden als Referenz verwenden.
|
||||
|
||||
## 14. Schlussbestimmungen
|
||||
|
||||
(1) Es gilt deutsches Recht.
|
||||
|
||||
(2) Gerichtsstand ist der Sitz von WEBklar, soweit zulässig.
|
||||
|
||||
(3) Sollten einzelne Bestimmungen unwirksam sein, bleibt der Vertrag im Übrigen wirksam.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,200 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="webklar-fassung" content="2026-09-30">
|
||||
<title>Allgemeine Geschäftsbedingungen – WEBklar</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #f7f6f3; color: #1d1d1f; font: 16px/1.6 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
|
||||
.brand { font-weight: 800; letter-spacing: 0.12em; font-size: 13px; color: #6d1fd1; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; }
|
||||
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #e3e0da; }
|
||||
h3 { font-size: 16.5px; margin: 22px 0 6px; }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 24px; }
|
||||
.meta { color: #6b6b70; font-size: 14px; }
|
||||
.box { background: #fff; border: 1px solid #e3e0da; border-radius: 14px; padding: 16px 20px; margin: 18px 0; }
|
||||
.box.widerspruch { border: 2px solid #6d1fd1; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 15px; }
|
||||
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #eee; vertical-align: top; text-align: left; }
|
||||
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #6b6b70; }
|
||||
a { color: #6d1fd1; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
.offen { background: #fff3cd; padding: 0 4px; border-radius: 4px; }
|
||||
@media print {
|
||||
@page { size: A4; margin: 18mm 17mm 18mm;
|
||||
@top-left { content: "WEBklar · AGB"; font: 8pt sans-serif; color: #888; }
|
||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #888; } }
|
||||
body { background: #fff; font-size: 10pt; line-height: 1.45; }
|
||||
main { max-width: none; padding: 0; }
|
||||
nav.top { display: none; }
|
||||
h1 { font-size: 20pt; }
|
||||
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.box, tr, ol.abs > li { break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/dashboard.html">← Kundenportal</a> · <a href="/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.html">Widerrufsbelehrung Projektaufträge</a> · <a href="/rechtliches/agb-2026-09-30.html">AGB</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Allgemeine Geschäftsbedingungen</h1>
|
||||
|
||||
<p class="meta"><strong>Fassung 2026-09-30</strong> · Wortgleiche Kopie der Allgemeinen Geschäftsbedingungen auf webklar.com/agb</p>
|
||||
|
||||
<h2>Kaufvertrag – WEBklar (Modularer Projektvertrag)</h2>
|
||||
|
||||
<p><strong>zwischen</strong></p>
|
||||
|
||||
<p>WEBklar (im Folgenden „Anbieter“)</p>
|
||||
|
||||
<p><strong>und</strong></p>
|
||||
|
||||
<p>Kunde laut Angebot (im Folgenden „Kunde“)</p>
|
||||
|
||||
<h2>1. Vertragsgegenstand</h2>
|
||||
|
||||
<p>(1) Gegenstand dieses Vertrages ist die Erbringung der im Angebot definierten Leistungen.</p>
|
||||
|
||||
<p>(2) Der Vertrag besteht aus diesem Grundvertrag sowie den ausgewählten Leistungsmodulen.</p>
|
||||
|
||||
<p>(3) Maßgeblich ist das jeweils angenommene Angebot von WEBklar.</p>
|
||||
|
||||
<h2>2. Leistungsart</h2>
|
||||
|
||||
<p>(1) Sämtliche Leistungen von WEBklar stellen Dienst- und Entwicklungsleistungen dar.</p>
|
||||
|
||||
<p>(2) Ein bestimmter wirtschaftlicher, technischer oder rechtlicher Erfolg wird nicht geschuldet.</p>
|
||||
|
||||
<p>(3) WEBklar erbringt keinen laufenden Betrieb, sofern dieser nicht explizit vereinbart wurde.</p>
|
||||
|
||||
<h2>3. Leistungsmodul A – Webseite (einmalig)</h2>
|
||||
|
||||
<p>(aktiv, wenn im Angebot enthalten)</p>
|
||||
|
||||
<p>(1) WEBklar erstellt eine individuelle Webseite gemäß Angebot.</p>
|
||||
|
||||
<p>(2) Die Umsetzung erfolgt nach den vom Kunden gelieferten Inhalten und Vorgaben.</p>
|
||||
|
||||
<p>(3) Zusätzliche Leistungen wie Domain, Hosting, Wartung oder SEO sind nicht Bestandteil, sofern sie nicht gesondert beauftragt wurden.</p>
|
||||
|
||||
<p>(4) Der Kunde ist nicht berechtigt, Änderungen am Quellcode selbst vorzunehmen.</p>
|
||||
|
||||
<p>(5) Änderungen erfolgen ausschließlich durch WEBklar gegen gesonderte Vergütung.</p>
|
||||
|
||||
<h2>4. Leistungsmodul B – Automatisierung / Virtualisierung (einmalig)</h2>
|
||||
|
||||
<p>(aktiv, wenn im Angebot enthalten)</p>
|
||||
|
||||
<p>(1) WEBklar entwickelt individuelle Automatisierungen, Apps oder Virtualisierungssysteme.</p>
|
||||
|
||||
<p>(2) Die Leistung stellt eine reine Entwicklungsleistung dar.</p>
|
||||
|
||||
<p>(3) Optional kann eine Beratungsleistung Bestandteil des Projektes sein.</p>
|
||||
|
||||
<p>(4) Ein laufender Betrieb, Monitoring oder Wartung ist nicht geschuldet, außer dies wurde explizit vereinbart.</p>
|
||||
|
||||
<p>(5) Der Kunde entscheidet über Inhalte, Daten und Prozesse und trägt dafür die rechtliche Verantwortung.</p>
|
||||
|
||||
<h2>5. Leistungsmodul C – Hosting</h2>
|
||||
|
||||
<p>(aktiv, wenn im Angebot enthalten)</p>
|
||||
|
||||
<p>(1) Hosting bietet WEBklar als Hosting-Abo an. Für das Hosting-Abo und das Wartungs-Abo gelten die Vertragsbedingungen für die WEBklar-Abos mit Leistungsbeschreibungen; sie gehen diesen AGB vor.</p>
|
||||
|
||||
<p>(2) Bestehende Hosting-Verträge mit jährlicher Laufzeit behalten die bei ihrem Abschluss vereinbarten Bedingungen. Abweichend davon können Verbraucher solche Verträge nach einer automatischen Verlängerung jederzeit mit einer Frist von einem Monat kündigen.</p>
|
||||
|
||||
<p>(3) WEBklar ist berechtigt, externe Anbieter (z. B. Rechenzentren) einzusetzen.</p>
|
||||
|
||||
<h2>6. Quellcode und Eigentum</h2>
|
||||
|
||||
<h3>6.1 Webseite</h3>
|
||||
|
||||
<p>(1) Der Kunde erhält ein einfaches, zeitlich unbegrenztes Nutzungsrecht an der fertigen Webseite.</p>
|
||||
|
||||
<p>(2) Der Quellcode der Webseite wird nur auf ausdrückliche Anfrage und nach Vereinbarung herausgegeben.</p>
|
||||
|
||||
<p>(3) Ohne Vereinbarung verbleibt der Quellcode bei WEBklar und wird archiviert.</p>
|
||||
|
||||
<h3>6.2 Apps, Automatisierungen und Backend</h3>
|
||||
|
||||
<p>(1) Der Quellcode von Apps, Automatisierungen und Backend-Systemen verbleibt vollständig bei WEBklar.</p>
|
||||
|
||||
<p>(2) Eine Herausgabe erfolgt ausschließlich nach gesonderter schriftlicher Vereinbarung.</p>
|
||||
|
||||
<p>(3) Der Kunde erhält lediglich Zugriff auf die Bedienoberfläche bzw. das Frontend.</p>
|
||||
|
||||
<h2>7. Mitwirkungspflichten des Kunden</h2>
|
||||
|
||||
<p>(1) Der Kunde stellt alle benötigten Inhalte, Daten und Freigaben rechtzeitig bereit.</p>
|
||||
|
||||
<p>(2) Verzögerungen durch fehlende Mitwirkung gehen nicht zu Lasten von WEBklar.</p>
|
||||
|
||||
<p>(3) WEBklar ist nicht verpflichtet, rechtliche Prüfungen der Inhalte vorzunehmen.</p>
|
||||
|
||||
<h2>8. Abnahme</h2>
|
||||
|
||||
<p>(1) Nach Fertigstellung wird dem Kunden die Leistung zur Abnahme bereitgestellt.</p>
|
||||
|
||||
<p>(2) Erfolgt innerhalb von 14 Tagen keine Rückmeldung, gilt die Leistung als abgenommen.</p>
|
||||
|
||||
<p>(3) Nach Abnahme sind nur noch kostenpflichtige Änderungen möglich.</p>
|
||||
|
||||
<h2>9. Vergütung und Zahlung</h2>
|
||||
|
||||
<p>(1) Die Vergütung ergibt sich aus dem Angebot.</p>
|
||||
|
||||
<p>(2) Wird nach Aufwand abgerechnet, erfasst WEBklar die Arbeitszeit je Arbeitsschritt in Schritten von 15 Minuten; angefangene 15 Minuten werden aufgerundet. Wirkt eine weitere Person unterstützend mit, rechnet WEBklar für sie die Hälfte der Arbeitszeit des Arbeitsschritts an, aufgerundet auf volle 15 Minuten; bei mehreren unterstützenden Personen gilt das für jede. Jeder Arbeitsschritt und jede Unterstützung ist im Kundenportal einsehbar.</p>
|
||||
|
||||
<p>(3) Projektleistungen sind nach Vereinbarung fällig.</p>
|
||||
|
||||
<p>(4) Für die Abos gelten die Vertragsbedingungen für die WEBklar-Abos.</p>
|
||||
|
||||
<h2>10. Zahlungsverzug</h2>
|
||||
|
||||
<p>(1) Bei Zahlungsverzug erfolgen bis zu zwei Mahnungen.</p>
|
||||
|
||||
<p>(2) Danach ist WEBklar berechtigt:</p>
|
||||
|
||||
<ul>
|
||||
<li>Leistungen zu sperren</li>
|
||||
<li>Verzugszinsen zu berechnen</li>
|
||||
<li>den Vertrag außerordentlich zu kündigen</li>
|
||||
</ul>
|
||||
|
||||
<h2>11. Haftung</h2>
|
||||
|
||||
<p>(1) WEBklar haftet unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit WEBklar eine Garantie übernommen hat.</p>
|
||||
|
||||
<p>(2) Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haftet WEBklar nur für den vertragstypischen, vorhersehbaren Schaden. Wesentlich sind Pflichten, deren Erfüllung die ordnungsgemäße Durchführung des Vertrags überhaupt erst ermöglicht und auf deren Einhaltung der Kunde regelmäßig vertrauen darf. Im Übrigen ist die Haftung für leichte Fahrlässigkeit ausgeschlossen.</p>
|
||||
|
||||
<p>(3) Der Kunde bewahrt Kopien der Inhalte und Daten auf, die er WEBklar übergibt oder selbst einpflegt. Für den Verlust solcher Inhalte und Daten haftet WEBklar außer in den Fällen von Absatz 1 nur in Höhe des Aufwands, der für ihre Wiederherstellung nötig gewesen wäre, wenn der Kunde diese Kopien aufbewahrt hätte.</p>
|
||||
|
||||
<p>(4) Diese Regeln gelten auch zugunsten der Mitarbeiter und Beauftragten von WEBklar.</p>
|
||||
|
||||
<h2>12. Kündigung</h2>
|
||||
|
||||
<p>(1) Laufzeitverträge können zum Ende der jeweiligen Laufzeit gekündigt werden. Laufzeit und Kündigung der Abos richten sich nach den Vertragsbedingungen für die WEBklar-Abos.</p>
|
||||
|
||||
<p>(2) Das Recht zur außerordentlichen Kündigung bleibt unberührt.</p>
|
||||
|
||||
<h2>13. Referenzen</h2>
|
||||
|
||||
<p>WEBklar darf das Projekt nur nach ausdrücklicher Zustimmung des Kunden als Referenz verwenden.</p>
|
||||
|
||||
<h2>14. Schlussbestimmungen</h2>
|
||||
|
||||
<p>(1) Es gilt deutsches Recht.</p>
|
||||
|
||||
<p>(2) Gerichtsstand ist der Sitz von WEBklar, soweit zulässig.</p>
|
||||
|
||||
<p>(3) Sollten einzelne Bestimmungen unwirksam sein, bleibt der Vertrag im Übrigen wirksam.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
d271a7ed066ee758dd5dd85b865134a4f9f65e1bc2ee375af7ab959b6455d2de
|
||||
@@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="webklar-fassung" content="2026-10-06">
|
||||
<title>Widerrufsbelehrung und Muster-Widerrufsformular (Projektaufträge) – WEBklar</title>
|
||||
<style>
|
||||
:root { color-scheme: light; }
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; background: #f7f6f3; color: #1d1d1f; font: 16px/1.6 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; }
|
||||
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
|
||||
.brand { font-weight: 800; letter-spacing: 0.12em; font-size: 13px; color: #6d1fd1; }
|
||||
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; }
|
||||
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #e3e0da; }
|
||||
h3 { font-size: 16.5px; margin: 22px 0 6px; }
|
||||
p, li { margin: 6px 0; }
|
||||
ul { padding-left: 24px; }
|
||||
.meta { color: #6b6b70; font-size: 14px; }
|
||||
.box { background: #fff; border: 1px solid #e3e0da; border-radius: 14px; padding: 16px 20px; margin: 18px 0; }
|
||||
.box.widerspruch { border: 2px solid #6d1fd1; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 15px; }
|
||||
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #eee; vertical-align: top; text-align: left; }
|
||||
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #6b6b70; }
|
||||
a { color: #6d1fd1; }
|
||||
nav.top { font-size: 14px; margin-bottom: 22px; }
|
||||
.offen { background: #fff3cd; padding: 0 4px; border-radius: 4px; }
|
||||
@media print {
|
||||
@page { size: A4; margin: 18mm 17mm 18mm;
|
||||
@top-left { content: "WEBklar · Widerrufsbelehrung Projektaufträge"; font: 8pt sans-serif; color: #888; }
|
||||
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #888; } }
|
||||
body { background: #fff; font-size: 10pt; line-height: 1.45; }
|
||||
main { max-width: none; padding: 0; }
|
||||
nav.top { display: none; }
|
||||
h1 { font-size: 20pt; }
|
||||
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
|
||||
a { color: inherit; text-decoration: none; }
|
||||
.box, tr, ol.abs > li { break-inside: avoid; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<nav class="top"><a href="/dashboard.html">← Kundenportal</a> · <a href="/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/rechtliches/widerrufsbelehrung-projektauftrag-2026-10-06.html">Widerrufsbelehrung Projektaufträge</a> · <a href="/rechtliches/agb-2026-09-30.html">AGB</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
|
||||
|
||||
<div class="brand">WEBKLAR</div>
|
||||
<h1>Widerrufsbelehrung und Muster-Widerrufsformular (Projektaufträge)</h1>
|
||||
|
||||
<p class="meta"><strong>Fassung 2026-10-06</strong></p>
|
||||
|
||||
<p>Das Widerrufsrecht gilt nur für Verbraucher. Wir fügen die Belehrung und das Muster-Widerrufsformular jeder Auftragsbestätigung an eine Privatperson bei und zeigen sie hier zusätzlich allen Kunden. Die Belehrung gilt für den Auftrag, den die jeweilige Auftragsbestätigung bestätigt.</p>
|
||||
|
||||
<h2>Widerrufsbelehrung</h2>
|
||||
|
||||
<h3>Widerrufsrecht</h3>
|
||||
|
||||
<p>Sie haben das Recht, binnen vierzehn Tagen ohne Angabe von Gründen diesen Vertrag zu widerrufen.</p>
|
||||
|
||||
<p>Die Widerrufsfrist beträgt vierzehn Tage ab dem Tag des Vertragsabschlusses.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>Zur Wahrung der Widerrufsfrist reicht es aus, dass Sie die Mitteilung über die Ausübung des Widerrufsrechts vor Ablauf der Widerrufsfrist absenden.</p>
|
||||
|
||||
<h3>Folgen des Widerrufs</h3>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<p>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.</p>
|
||||
|
||||
<h2>Muster-Widerrufsformular</h2>
|
||||
|
||||
<p>(Wenn Sie den Vertrag widerrufen wollen, dann füllen Sie bitte dieses Formular aus und senden Sie es zurück.)</p>
|
||||
|
||||
<p>– An WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, E-Mail: support@webklar.com:</p>
|
||||
|
||||
<p>– Hiermit widerrufe(n) ich/wir (*) den von mir/uns (*) abgeschlossenen Vertrag über den Kauf der folgenden Waren (*)/die Erbringung der folgenden Dienstleistung (*)</p>
|
||||
|
||||
<p>– Bestellt am (*)/erhalten am (*)</p>
|
||||
|
||||
<p>– Name des/der Verbraucher(s)</p>
|
||||
|
||||
<p>– Anschrift des/der Verbraucher(s)</p>
|
||||
|
||||
<p>– Unterschrift des/der Verbraucher(s) (nur bei Mitteilung auf Papier)</p>
|
||||
|
||||
<p>– Datum</p>
|
||||
|
||||
<p>(*) Unzutreffendes streichen.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
f9948c0a656e5eaf62c964a933d4fda4b162cf1cdc4069632e1068c6488b8408
|
||||
@@ -7,8 +7,11 @@
|
||||
// node scripts/build-legal-html.mjs termine --check vergleicht mit den eingecheckten Seiten (Exit 1 bei Abweichung, schreibt nichts)
|
||||
// node scripts/build-legal-html.mjs shop [--check] dasselbe für Leistungsbeschreibung und AVV des Webshops (die Widerrufsbelehrung der
|
||||
// Funktionen gehört zum Satz termine und wird von shop nicht angefasst)
|
||||
// node scripts/build-legal-html.mjs auftraege [--check]
|
||||
// dasselbe für die Widerrufsbelehrung der Projektaufträge (Wortlaut aus server/services/auftraege/texte.js)
|
||||
// und die AGB-Kopie (Quelle docs/rechtliches/agb.md)
|
||||
//
|
||||
// Fassung = config.termine.avvVersion (Satz termine) bzw. config.shop.fassung (Satz shop). Die Ausgabe enthält keine Datumsangabe außer der Fassung und hängt nur vom Markdown und
|
||||
// Fassung = config.termine.avvVersion (Satz termine), config.shop.fassung (Satz shop) bzw. TEXTE_FASSUNG und AGB_FASSUNG (Satz auftraege). Die Ausgabe enthält keine Datumsangabe außer der Fassung und hängt nur vom Markdown und
|
||||
// von der Fassung ab (zweimal erzeugen ergibt dieselben Bytes). Unterstützte Teilmenge von Markdown: Überschriften # bis ###
|
||||
// (# nur als erste Zeile), Absätze, Listen mit "- ", **fett**, `code` und Tabellen (Kopfzeile, Trennzeile |---|---|, Zeilen).
|
||||
// Jede andere Syntax (Codeblöcke, Zitate, Bilder, Links, rohes HTML, nummerierte Listen, Einrückungen, Trennlinien) bricht laut ab,
|
||||
@@ -18,6 +21,8 @@ import { basename, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { config } from '../server/config.js'
|
||||
import { legalPaths } from '../server/services/legalDocs.js'
|
||||
import { TEXTE_FASSUNG, WIDERRUFSBELEHRUNG_PROJEKT, WIDERRUFSFORMULAR } from '../server/services/auftraege/texte.js'
|
||||
import { AGB_FASSUNG } from '../server/services/auftraege/konstanten.js'
|
||||
|
||||
const ROOT = fileURLToPath(new URL('..', import.meta.url))
|
||||
const FASSUNG_RE = /^\d{4}-\d{2}-\d{2}$/
|
||||
@@ -309,25 +314,83 @@ export function shopDokumente(fassung = config.shop.fassung) {
|
||||
return dokumenteDesSatzes('shop', fassung, SHOP_NAMEN)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------- Projektaufträge
|
||||
|
||||
/** Einleitung der Seite zur Widerrufsbelehrung der Projektaufträge (Seitentext, nicht Teil der Belehrung nach dem Muster). */
|
||||
export const PROJEKT_WIDERRUF_EINLEITUNG =
|
||||
'Das Widerrufsrecht gilt nur für Verbraucher. Wir fügen die Belehrung und das Muster-Widerrufsformular jeder Auftragsbestätigung an eine Privatperson bei '
|
||||
+ 'und zeigen sie hier zusätzlich allen Kunden. Die Belehrung gilt für den Auftrag, den die jeweilige Auftragsbestätigung bestätigt.'
|
||||
|
||||
/**
|
||||
* Markdown der Seite „Widerrufsbelehrung und Muster-Widerrufsformular (Projektaufträge)“ aus server/services/auftraege/texte.js: Überschrift,
|
||||
* Fassung, Einleitung, dann die Abschnitte der Belehrung und das Formular, jede Formularzeile als eigener Absatz.
|
||||
*/
|
||||
export function projektWiderrufMarkdown(fassung = TEXTE_FASSUNG, belehrung = WIDERRUFSBELEHRUNG_PROJEKT, formular = WIDERRUFSFORMULAR) {
|
||||
const teile = [
|
||||
'# Widerrufsbelehrung und Muster-Widerrufsformular (Projektaufträge)',
|
||||
`**Fassung ${fassung}**`,
|
||||
PROJEKT_WIDERRUF_EINLEITUNG,
|
||||
`## ${belehrung.titel}`,
|
||||
...belehrung.abschnitte.flatMap((a) => [`### ${a.ueberschrift}`, ...a.absaetze]),
|
||||
`## ${formular.titel}`,
|
||||
formular.einleitung,
|
||||
...formular.zeilen,
|
||||
]
|
||||
return `${teile.join('\n\n')}\n`
|
||||
}
|
||||
|
||||
/**
|
||||
* Die zwei Seiten des Satzes auftraege: die Widerrufsbelehrung der Projektaufträge (Fassung TEXTE_FASSUNG, Wortlaut aus texte.js, ohne Markdown-Datei)
|
||||
* und die AGB-Kopie (Fassung AGB_FASSUNG, Quelle docs/rechtliches/agb.md). Beide Seiten verlinken einander. Die Adressen stehen nicht in legalDocs.js:
|
||||
* die Mails und das PDF der Auftragsbestätigung verlinken die versionierten Dateien (public/rechtliches/<slug>-<Fassung>.html|pdf) unmittelbar.
|
||||
*/
|
||||
export function auftraegeDokumente(texteFassung = TEXTE_FASSUNG, agbFassung = AGB_FASSUNG) {
|
||||
const widerrufAdresse = `/rechtliches/widerrufsbelehrung-projektauftrag-${texteFassung}.html`
|
||||
const agbAdresse = `/rechtliches/agb-${agbFassung}.html`
|
||||
const nav = [
|
||||
{ href: '/dashboard.html', text: '← Kundenportal' },
|
||||
{ href: '/abo-bedingungen.html', text: 'Abo-Bedingungen' },
|
||||
{ href: widerrufAdresse, text: 'Widerrufsbelehrung Projektaufträge' },
|
||||
{ href: agbAdresse, text: 'AGB' },
|
||||
{ href: '/datenschutz.html', text: 'Datenschutzerklärung' },
|
||||
{ href: 'https://webklar.com/impressum', text: 'Impressum' },
|
||||
]
|
||||
return [
|
||||
{
|
||||
doc: 'widerruf',
|
||||
md: null,
|
||||
markdown: () => projektWiderrufMarkdown(texteFassung),
|
||||
html: `public${widerrufAdresse}`,
|
||||
meta: { fassung: texteFassung, titel: 'Widerrufsbelehrung und Muster-Widerrufsformular (Projektaufträge)', seitenkopf: 'Widerrufsbelehrung Projektaufträge', nav },
|
||||
},
|
||||
{
|
||||
doc: 'agb',
|
||||
md: 'docs/rechtliches/agb.md',
|
||||
html: `public${agbAdresse}`,
|
||||
meta: { fassung: agbFassung, titel: 'Allgemeine Geschäftsbedingungen', seitenkopf: 'AGB', nav },
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/** Sätze der Kommandozeile: Name => Dokumente. */
|
||||
const SAETZE = { termine: termineDokumente, shop: shopDokumente }
|
||||
const SAETZE = { termine: termineDokumente, shop: shopDokumente, auftraege: auftraegeDokumente }
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------- Aufruf
|
||||
|
||||
/**
|
||||
* Kommandozeile: `termine|shop [--check]`. root = Wurzel des Projekts (Tests übergeben ein Verzeichnis mit Kopien).
|
||||
* Kommandozeile: `termine|shop|auftraege [--check]`. root = Wurzel des Projekts (Tests übergeben ein Verzeichnis mit Kopien).
|
||||
* => Exit-Code: 0 ok, 1 mindestens eine Seite weicht ab (nur --check), 2 falscher Aufruf
|
||||
*/
|
||||
export function lauf(argv, { root = ROOT, log = console.log, fehler = console.error } = {}) {
|
||||
const [satz, ...optionen] = argv
|
||||
const pruefen = optionen.includes('--check')
|
||||
if (!Object.hasOwn(SAETZE, satz ?? '') || optionen.some((o) => o !== '--check')) {
|
||||
fehler('Aufruf: node scripts/build-legal-html.mjs termine|shop [--check]')
|
||||
fehler('Aufruf: node scripts/build-legal-html.mjs termine|shop [--check]\n node scripts/build-legal-html.mjs auftraege [--check]')
|
||||
return 2
|
||||
}
|
||||
let abweichungen = 0
|
||||
for (const d of SAETZE[satz]()) {
|
||||
const html = render(readFileSync(join(root, d.md), 'utf8'), d.meta)
|
||||
const html = render(d.markdown ? d.markdown() : readFileSync(join(root, d.md), 'utf8'), d.meta)
|
||||
const ziel = join(root, d.html)
|
||||
if (pruefen) {
|
||||
const gleich = existsSync(ziel) && readFileSync(ziel, 'utf8') === html
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Erzeugt die Druck-/PDF-Fassungen der Vertragsunterlagen aus den HTML-Quellen.
|
||||
# Laeuft auf dem Host (braucht Chrome/Chromium), die PDFs werden eingecheckt.
|
||||
# Usage: CHROME=/pfad/zu/chrome scripts/build-legal-pdfs.sh [all|analyse|termine|shop|datenschutz]
|
||||
# Usage: CHROME=/pfad/zu/chrome scripts/build-legal-pdfs.sh [all|analyse|termine|shop|auftraege|datenschutz]
|
||||
# analyse = nur AVV und Leistungsbeschreibung der Website-Analyse (Fassung ANALYSE_VERSION), andere PDFs bleiben unveraendert
|
||||
# termine = Leistungsbeschreibung, AVV und Widerrufsbelehrung des Terminplaners (Fassung TERMINE_VERSION, Standard: config.termine.avvVersion);
|
||||
# die HTML-Seiten kommen vorher aus `node scripts/build-legal-html.mjs termine`. Neben jedem PDF liegt <PDF>.sha256 mit der
|
||||
@@ -10,6 +10,9 @@
|
||||
# shop = Leistungsbeschreibung und AVV des Webshops (Fassung SHOP_VERSION, Standard: config.shop.fassung); die HTML-Seiten kommen vorher
|
||||
# aus `node scripts/build-legal-html.mjs shop`, Pruefsumme und Neudruck wie bei termine. Die Widerrufsbelehrung der Funktionen
|
||||
# gehoert zu termine (dieselbe Datei) und wird hier nicht gedruckt. `all` druckt auch diese beiden PDFs.
|
||||
# auftraege = Widerrufsbelehrung der Projektauftraege (Fassung TEXTE_VERSION, Standard: TEXTE_FASSUNG aus server/services/auftraege/texte.js) und AGB
|
||||
# (Fassung AGB_VERSION, Standard: AGB_FASSUNG aus server/services/auftraege/konstanten.js); die HTML-Seiten kommen vorher aus
|
||||
# `node scripts/build-legal-html.mjs auftraege`, Pruefsumme und Neudruck wie bei termine und shop. `all` druckt auch diese beiden PDFs.
|
||||
# datenschutz = nur die Datenschutzerklaerung (docs/WEBklar-Datenschutzerklaerung.pdf); daneben liegt <PDF>.sha256 mit der SHA-256 von
|
||||
# public/datenschutz.html (test/legalTexts.test.js prueft sie); ein PDF mit passender Pruefsumme wird nicht neu gedruckt (FORCE=1 druckt neu)
|
||||
# Vorher `free -m` pruefen und mit `nice` aufrufen; jeder Chrome-Aufruf laeuft hoechstens 120 Sekunden (timeout), die PDFs nacheinander.
|
||||
@@ -47,6 +50,47 @@ shop_version() {
|
||||
echo "$v"
|
||||
}
|
||||
|
||||
# Fassung der Projektauftrags-Texte (Widerrufsbelehrung): TEXTE_VERSION, sonst TEXTE_FASSUNG aus server/services/auftraege/texte.js
|
||||
texte_version() {
|
||||
local v="${TEXTE_VERSION:-}"
|
||||
if [ -z "$v" ]; then
|
||||
v="$(timeout 60 node --max-old-space-size=256 --input-type=module -e "const { TEXTE_FASSUNG } = await import('./server/services/auftraege/texte.js'); console.log(TEXTE_FASSUNG)")"
|
||||
fi
|
||||
[[ "$v" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { echo "Ungueltige Fassung: $v" >&2; exit 1; }
|
||||
echo "$v"
|
||||
}
|
||||
|
||||
# Fassung der AGB-Kopie: AGB_VERSION, sonst AGB_FASSUNG aus server/services/auftraege/konstanten.js
|
||||
agb_version() {
|
||||
local v="${AGB_VERSION:-}"
|
||||
if [ -z "$v" ]; then
|
||||
v="$(timeout 60 node --max-old-space-size=256 --input-type=module -e "const { AGB_FASSUNG } = await import('./server/services/auftraege/konstanten.js'); console.log(AGB_FASSUNG)")"
|
||||
fi
|
||||
[[ "$v" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { echo "Ungueltige Fassung: $v" >&2; exit 1; }
|
||||
echo "$v"
|
||||
}
|
||||
|
||||
# Widerrufsbelehrung der Projektauftraege und AGB drucken (je PDF nur, wenn die Pruefsumme der HTML-Seite nicht mehr passt)
|
||||
auftraege_pdfs() {
|
||||
local texte agb eintrag slug fassung quelle ziel summe
|
||||
texte="$(texte_version)"
|
||||
agb="$(agb_version)"
|
||||
for eintrag in "widerrufsbelehrung-projektauftrag:$texte" "agb:$agb"; do
|
||||
slug="${eintrag%%:*}"
|
||||
fassung="${eintrag##*:}"
|
||||
quelle="public/rechtliches/$slug-$fassung.html"
|
||||
ziel="public/rechtliches/$slug-$fassung.pdf"
|
||||
[ -f "$quelle" ] || { echo "HTML fehlt: $quelle (zuerst node scripts/build-legal-html.mjs auftraege)"; exit 1; }
|
||||
summe="$(sha256sum "$quelle" | cut -d' ' -f1)"
|
||||
if [ -z "${FORCE:-}" ] && [ -f "$ziel" ] && [ "$(cat "$ziel.sha256" 2>/dev/null)" = "$summe" ]; then
|
||||
echo "= $ziel (unveraendert)"
|
||||
else
|
||||
pdf "$quelle" "$ziel"
|
||||
echo "$summe" > "$ziel.sha256"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Leistungsbeschreibung und AVV des Webshops drucken (je PDF nur, wenn die Pruefsumme der HTML-Seite nicht mehr passt)
|
||||
shop_pdfs() {
|
||||
local fassung slug quelle ziel summe
|
||||
@@ -88,6 +132,9 @@ termine)
|
||||
shop)
|
||||
shop_pdfs
|
||||
;;
|
||||
auftraege)
|
||||
auftraege_pdfs
|
||||
;;
|
||||
datenschutz)
|
||||
quelle="public/datenschutz.html"
|
||||
ziel="docs/WEBklar-Datenschutzerklaerung.pdf"
|
||||
@@ -105,8 +152,9 @@ all)
|
||||
pdf public/abo-bedingungen.html public/WEBklar-Abo-Vertragsunterlagen.pdf
|
||||
pdf public/datenschutz.html docs/WEBklar-Datenschutzerklaerung.pdf
|
||||
shop_pdfs
|
||||
auftraege_pdfs
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [all|analyse|termine|shop|datenschutz]"; exit 1
|
||||
echo "Usage: $0 [all|analyse|termine|shop|datenschutz]"; echo " $0 auftraege (Widerrufsbelehrung der Projektauftraege und AGB)"; exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -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)',
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
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'])
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------- AGB-Kopie
|
||||
|
||||
const AGB_MD = 'docs/rechtliches/agb.md'
|
||||
// Quelle der Website-AGB auf dem Server (Repository webklar.com). Fehlt die Datei (anderer Rechner, Container), entfällt nur der Vergleich mit der Website.
|
||||
const AGB_WEBSITE = process.env.WEBKLAR_AGB_QUELLE || '/home/webklar/apps/webklar.com/build/src/pages/AGB.tsx'
|
||||
// SHA-256 des normierten Klartexts der Website-AGB vom 30.09.2026 (Commit 72f278b der Website; Titelblock und Nr. 1 bis 14). Ändert sich der Text der Kopie,
|
||||
// braucht sie eine neue Fassung: AGB_FASSUNG hochziehen, agb.md ändern, Seite und PDF neu erzeugen, diesen Hash eintragen.
|
||||
const AGB_TEXT_SHA256 = 'f3374352ef97e6050bb0908c1b4abd806f759851b5b85fa9d030512d46b36659'
|
||||
|
||||
/** Textknoten der Kopie: alles nach der Fassungszeile, ohne Überschriftszeichen, Aufzählungszeichen, Absatznummern „(1)“ und Fettdruck. */
|
||||
function agbKnotenAusMarkdown(md) {
|
||||
const zeilen = md.replace(/\r/g, '').split('\n')
|
||||
const start = zeilen.findIndex((z) => z.startsWith('**Fassung '))
|
||||
assert.ok(start > 0, 'agb.md: Fassungszeile fehlt')
|
||||
return zeilen
|
||||
.slice(start + 1)
|
||||
.filter((z) => z.trim() !== '')
|
||||
.map((z) => z.replace(/^#{1,3} /, '').replace(/^- /, '').replace(/^\(\d+\) /, '').replace(/\*\*/g, '').replace(/\s+/g, ' ').trim())
|
||||
}
|
||||
|
||||
/** Textknoten der Website-Seite AGB.tsx: vom Seitentitel bis zum Ende des Vertragstexts, ohne JSX-Kommentare und Markup. */
|
||||
function agbKnotenAusWebsite(tsx) {
|
||||
const von = tsx.indexOf('<h1 className="wk-seite__titel">')
|
||||
const bis = tsx.indexOf('</article>')
|
||||
assert.ok(von > 0 && bis > von, 'AGB.tsx: Aufbau hat sich geändert (Seitentitel oder article nicht gefunden)')
|
||||
return tsx
|
||||
.slice(von, bis)
|
||||
.replace(/\{\/\*[\s\S]*?\*\/\}/g, ' ')
|
||||
.replace(/\{" "\}/g, ' ')
|
||||
.replace(/<[^>]*>/g, '\n')
|
||||
.split('\n')
|
||||
.map((z) => z.replace(/\s+/g, ' ').trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
const agbText = (knoten) => norm(knoten.join(' '))
|
||||
/** Die erste Stelle, an der zwei Texte auseinanderlaufen, mit etwas Umgebung (für die Fehlermeldung). */
|
||||
const ersterUnterschied = (a, b) => {
|
||||
let i = 0
|
||||
while (i < a.length && i < b.length && a[i] === b[i]) i += 1
|
||||
return `Kopie: „…${a.slice(Math.max(0, i - 40), i + 60)}…“, Website: „…${b.slice(Math.max(0, i - 40), i + 60)}…“`
|
||||
}
|
||||
|
||||
test('agb_fassung: AGB_FASSUNG ist der 30.09.2026, der Tag der letzten Textänderung der Website-AGB (Commit 72f278b); die Kopie trägt dieselbe Fassung', () => {
|
||||
assert.equal(AGB_FASSUNG, '2026-09-30')
|
||||
assert.ok(lies(AGB_MD).startsWith(`# Allgemeine Geschäftsbedingungen\n\n**Fassung ${AGB_FASSUNG}**`))
|
||||
})
|
||||
|
||||
test('agb_md_aufbau: vierzehn Abschnitte in Reihenfolge; die Verweise der Auftragsbestätigung auf Nr. 7, 8 und 9 treffen', () => {
|
||||
const md = lies(AGB_MD)
|
||||
const abschnitte = md.split('\n').filter((z) => /^## \d+\. /.test(z)).map((z) => z.replace(/^## /, ''))
|
||||
assert.deepEqual(abschnitte.map((z) => Number.parseInt(z, 10)), Array.from({ length: 14 }, (_, i) => i + 1))
|
||||
const titel = Object.fromEntries(abschnitte.map((z) => [Number.parseInt(z, 10), z.replace(/^\d+\. /, '')]))
|
||||
assert.equal(titel[7], 'Mitwirkungspflichten des Kunden')
|
||||
assert.equal(titel[8], 'Abnahme')
|
||||
assert.equal(titel[9], 'Vergütung und Zahlung')
|
||||
assert.ok(T.mitwirkung.includes('Nr. 7 und Nr. 8'))
|
||||
assert.ok(T.aufwand({ satz: '100,00 €' }).includes('(AGB Nr. 9)'))
|
||||
const nr9 = md.slice(md.indexOf('## 9. '), md.indexOf('## 10. '))
|
||||
assert.ok(nr9.includes('in Schritten von 15 Minuten') && nr9.includes('die Hälfte der Arbeitszeit'), 'Nr. 9 enthält die Regel, auf die die Auftragsbestätigung verweist')
|
||||
// der Titelblock der Website steht als Überschrift und Parteien, die Verweise auf die Abos bleiben Text
|
||||
assert.ok(md.includes('## Kaufvertrag – WEBklar (Modularer Projektvertrag)'))
|
||||
assert.ok(md.includes('**zwischen**') && md.includes('**und**'))
|
||||
})
|
||||
|
||||
test('agb_text_pin: der normierte Text der Kopie hat den festgehaltenen SHA-256 (keine versehentliche Änderung eines Rechtstextes)', () => {
|
||||
const hash = sha256(agbText(agbKnotenAusMarkdown(lies(AGB_MD))))
|
||||
assert.equal(hash, AGB_TEXT_SHA256, `Der Text der AGB-Kopie hat sich geändert (Hash jetzt ${hash}). Änderungen nur mit neuer Fassung: AGB_FASSUNG hochziehen, Seite und PDF neu erzeugen.`)
|
||||
})
|
||||
|
||||
test('agb_text_gleich_website: die Kopie ist wortgleich mit der Seite AGB.tsx der Website (Titelblock und Nr. 1 bis 14)', (t) => {
|
||||
if (!existsSync(AGB_WEBSITE)) return t.skip(`Website-AGB nicht erreichbar: ${AGB_WEBSITE}`)
|
||||
const website = agbText(agbKnotenAusWebsite(readFileSync(AGB_WEBSITE, 'utf8')))
|
||||
const kopie = agbText(agbKnotenAusMarkdown(lies(AGB_MD)))
|
||||
assert.ok(kopie === website, `Die AGB-Kopie weicht von den AGB der Website ab (${ersterUnterschied(kopie, website)}). Ist die Website neu, braucht die Kopie eine neue Fassung (AGB_FASSUNG, agb.md, Seite, PDF, Hash).`)
|
||||
assert.equal(sha256(website), AGB_TEXT_SHA256)
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------- Seiten der Widerrufsbelehrung und der AGB
|
||||
|
||||
// jedes Dokument wird in den Tests neu geholt, damit ein fehlender Generator nur die betroffenen Tests scheitern lässt (nicht das Laden der Datei)
|
||||
const dokumente = () => gen.auftraegeDokumente()
|
||||
const klartext = (html) =>
|
||||
html
|
||||
.replace(/<style>[\s\S]*?<\/style>/g, '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/"/g, '"').replace(/'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
const quelleVon = (d) => (d.markdown ? d.markdown() : lies(d.md))
|
||||
|
||||
test('auftraegeDokumente: widerruf (aus texte.js) und agb (aus docs/rechtliches/agb.md) mit Quelle, Ziel und Navigation auf die Fassungen', () => {
|
||||
const DOKUMENTE = dokumente()
|
||||
assert.deepEqual(DOKUMENTE.map((d) => d.doc), ['widerruf', 'agb'])
|
||||
assert.deepEqual(
|
||||
DOKUMENTE.map((d) => [d.md, d.html]),
|
||||
[
|
||||
[null, `public/rechtliches/widerrufsbelehrung-projektauftrag-${texte.TEXTE_FASSUNG}.html`],
|
||||
['docs/rechtliches/agb.md', `public/rechtliches/agb-${AGB_FASSUNG}.html`],
|
||||
],
|
||||
)
|
||||
assert.deepEqual(DOKUMENTE.map((d) => d.meta.fassung), [texte.TEXTE_FASSUNG, AGB_FASSUNG])
|
||||
const adressen = DOKUMENTE.map((d) => d.html.replace(/^public/, ''))
|
||||
for (const d of DOKUMENTE) {
|
||||
const hrefs = d.meta.nav.map((l) => l.href)
|
||||
for (const a of adressen) assert.ok(hrefs.includes(a), `Navigation von ${d.doc} verlinkt ${a}`)
|
||||
assert.ok(hrefs.includes('/abo-bedingungen.html') && hrefs.includes('/datenschutz.html'))
|
||||
}
|
||||
// andere Fassungen wirken auf Dateinamen und Meta; die Quelle der Belehrung folgt der Fassung
|
||||
const andere = gen.auftraegeDokumente('2027-01-02', '2027-01-03')
|
||||
assert.equal(andere[0].html, 'public/rechtliches/widerrufsbelehrung-projektauftrag-2027-01-02.html')
|
||||
assert.equal(andere[1].html, 'public/rechtliches/agb-2027-01-03.html')
|
||||
assert.ok(andere[0].markdown().includes('**Fassung 2027-01-02**'))
|
||||
})
|
||||
|
||||
test('auftraege_html_gleich_generator: die eingecheckten Seiten sind genau render(Quelle), deterministisch, ohne Datum außer den Fassungen; --check Exit 0', () => {
|
||||
const DOKUMENTE = dokumente()
|
||||
for (const d of DOKUMENTE) {
|
||||
const md = quelleVon(d)
|
||||
const html = gen.render(md, d.meta)
|
||||
assert.equal(lies(d.html), html, `${d.html} weicht vom Generator ab (node scripts/build-legal-html.mjs auftraege)`)
|
||||
assert.equal(gen.render(md, d.meta), html, 'deterministisch: zweiter Lauf gleiche Bytes')
|
||||
assert.doesNotMatch(html.replaceAll(texte.TEXTE_FASSUNG, '').replaceAll(AGB_FASSUNG, ''), /\b\d{4}-\d{2}-\d{2}\b/, `${d.html}: Datum außer den Fassungen`)
|
||||
}
|
||||
const lauf = spawnSync(process.execPath, ['--max-old-space-size=512', 'scripts/build-legal-html.mjs', 'auftraege', '--check'], { cwd: ROOT, encoding: 'utf8', timeout: 60000 })
|
||||
assert.equal(lauf.status, 0, lauf.stdout + lauf.stderr)
|
||||
assert.equal([...lauf.stdout.matchAll(/^ok /gm)].length, 2)
|
||||
})
|
||||
|
||||
test('auftraege_html_text_gleich_quelle: der sichtbare Text jeder Seite ist genau der Text der Quelle (plus Navigation und Marke)', () => {
|
||||
const DOKUMENTE = dokumente()
|
||||
for (const d of DOKUMENTE) {
|
||||
const aus = quelleVon(d).replace(/\r/g, '').split('\n').filter((z) => z.trim() !== '').map((z) => z.replace(/^#{1,3} /, '').replace(/^- /, ''))
|
||||
const erwartet = norm(aus.join(' ').replace(/\*\*|`/g, ''))
|
||||
const html = lies(d.html)
|
||||
const main = html.slice(html.indexOf('<main>'), html.indexOf('</main>')).replace(/<nav class="top">[\s\S]*?<\/nav>/, '').replace('<div class="brand">WEBKLAR</div>', '')
|
||||
assert.equal(klartext(main), erwartet, `${d.html}: sichtbarer Text weicht von der Quelle ab`)
|
||||
}
|
||||
})
|
||||
|
||||
test('auftraege_html_sauber: keine Kommentare, Entwurfsmarken, Skripte oder Ereignis-Attribute; Fassung im Kopf; Links nur relativ oder https', () => {
|
||||
const DOKUMENTE = dokumente()
|
||||
for (const d of DOKUMENTE) {
|
||||
const html = lies(d.html)
|
||||
assert.doesNotMatch(html, /<!--|Entwurf|ENTWURF|Prüfpunkt|<script|<iframe|<img|<form|<link|\sstyle=|\ssrc=/, `${d.html}: verbotener Inhalt`)
|
||||
assert.doesNotMatch(html, /\son[a-z]+\s*=/i, `${d.html}: Ereignis-Attribut`)
|
||||
assert.doesNotMatch(html, /⚖|\bQ\d+\b|\{\{|\}\}|undefined|NaN/, `${d.html}: Marker oder Platzhalter`)
|
||||
assert.ok(html.includes(`<strong>Fassung ${d.meta.fassung}</strong>`), `${d.html}: Fassung fehlt im Kopf`)
|
||||
assert.ok(html.includes(`<meta name="webklar-fassung" content="${d.meta.fassung}">`))
|
||||
assert.match(html, /<html lang="de">/)
|
||||
assert.match(html, /<title>[^<]+ – WEBklar<\/title>/)
|
||||
const hrefs = [...html.matchAll(/\shref="([^"]*)"/g)].map((m) => m[1])
|
||||
assert.ok(hrefs.length >= 6)
|
||||
for (const h of hrefs) assert.ok(/^\/(?!\/)/.test(h) || /^https:\/\//.test(h), `${d.html}: Link ${h}`)
|
||||
assert.ok(html.includes(`content: "WEBklar · ${d.meta.seitenkopf}"`))
|
||||
assert.equal([...html.matchAll(/<h1>/g)].length, 1)
|
||||
assert.ok(html.includes('<div class="brand">WEBKLAR</div>'))
|
||||
}
|
||||
})
|
||||
|
||||
test('auftraege_lauf: --check meldet fehlende oder abweichende Seiten mit Exit 1 und schreibt nichts; ohne --check werden beide Seiten geschrieben; Aufruf nennt alle Sätze', () => {
|
||||
const DOKUMENTE = dokumente()
|
||||
const dir = mkdtempSync(join(tmpdir(), 'legalhtml-auftraege-'))
|
||||
try {
|
||||
mkdirSync(join(dir, 'docs/rechtliches'), { recursive: true })
|
||||
mkdirSync(join(dir, 'public/rechtliches'), { recursive: true })
|
||||
cpSync(join(ROOT, AGB_MD), join(dir, AGB_MD))
|
||||
const zeilen = []
|
||||
const ruf = (...args) => gen.lauf(args, { root: dir, log: (z) => zeilen.push(z), fehler: (z) => zeilen.push(z) })
|
||||
// noch keine Seiten: --check meldet beide und schreibt nichts
|
||||
assert.equal(ruf('auftraege', '--check'), 1)
|
||||
assert.equal(zeilen.filter((z) => z.startsWith('ABWEICHT')).length, 2)
|
||||
assert.equal(existsSync(join(dir, DOKUMENTE[0].html)), false, '--check schreibt nichts')
|
||||
// schreiben, dann stimmt --check, und die Bytes sind die der eingecheckten Seiten
|
||||
assert.equal(ruf('auftraege'), 0)
|
||||
assert.equal(ruf('auftraege', '--check'), 0)
|
||||
for (const d of DOKUMENTE) assert.equal(readFileSync(join(dir, d.html), 'utf8'), lies(d.html), 'dieselben Bytes wie die eingecheckte Seite')
|
||||
// eine Seite von Hand geändert, die andere Quelle geändert: Abweichung, die Dateien bleiben wie sie sind
|
||||
const ziel = join(dir, DOKUMENTE[1].html)
|
||||
const handgemacht = readFileSync(ziel, 'utf8').replace('<h2>1. Vertragsgegenstand</h2>', '<h2>1. Vertragsgegenstand (von Hand)</h2>')
|
||||
assert.notEqual(handgemacht, readFileSync(ziel, 'utf8'))
|
||||
writeFileSync(ziel, handgemacht)
|
||||
zeilen.length = 0
|
||||
assert.equal(ruf('auftraege', '--check'), 1)
|
||||
assert.deepEqual(zeilen.filter((z) => z.startsWith('ABWEICHT')), [`ABWEICHT ${DOKUMENTE[1].html}`])
|
||||
assert.equal(readFileSync(ziel, 'utf8'), handgemacht, '--check ändert die Datei nicht')
|
||||
writeFileSync(ziel, lies(DOKUMENTE[1].html))
|
||||
writeFileSync(join(dir, AGB_MD), `${lies(AGB_MD)}\n## 15. Neu\n\n(1) Neuer Absatz.\n`)
|
||||
assert.equal(ruf('auftraege', '--check'), 1)
|
||||
// falscher Aufruf: Exit 2, die Meldung nennt termine, shop und auftraege
|
||||
zeilen.length = 0
|
||||
assert.equal(ruf('analyse'), 2)
|
||||
assert.equal(ruf('auftraege', '--schreiben'), 2)
|
||||
assert.equal(ruf(), 2)
|
||||
assert.match(zeilen[0], /Aufruf: node scripts\/build-legal-html\.mjs termine\|shop \[--check\]/)
|
||||
assert.match(zeilen[0], /node scripts\/build-legal-html\.mjs auftraege \[--check\]/)
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------- PDFs
|
||||
|
||||
test('auftraege_pdf_dateien: je Seite ein PDF unter demselben Namen, Prüfsumme der HTML-Seite daneben (HTML geändert ohne neues PDF fällt auf)', () => {
|
||||
const DOKUMENTE = dokumente()
|
||||
const mindest = { widerruf: { seiten: 1, bytes: 5 * 1024 }, agb: { seiten: 2, bytes: 20 * 1024 } }
|
||||
for (const d of DOKUMENTE) {
|
||||
const pdf = d.html.replace(/\.html$/, '.pdf')
|
||||
const bytes = readFileSync(join(ROOT, pdf))
|
||||
assert.equal(bytes.subarray(0, 5).toString('latin1'), '%PDF-', `${pdf}: kein PDF`)
|
||||
assert.ok(bytes.length > mindest[d.doc].bytes, `${pdf}: ${bytes.length} Bytes sind zu wenig`)
|
||||
assert.ok(bytes.subarray(-1024).toString('latin1').includes('%%EOF'), `${pdf}: PDF ist abgeschnitten`)
|
||||
const seiten = bytes.toString('latin1').match(/\/Type\s*\/Page[^s]/g) ?? []
|
||||
assert.ok(seiten.length >= mindest[d.doc].seiten, `${pdf}: ${seiten.length} Seiten`)
|
||||
const summe = lies(`${pdf}.sha256`)
|
||||
assert.match(summe, /^[0-9a-f]{64}\n$/, `${pdf}.sha256: nur die Prüfsumme und ein Zeilenende`)
|
||||
assert.equal(summe.trim(), sha256(readFileSync(join(ROOT, d.html))), `${d.html} wurde geändert, ohne das PDF neu zu drucken (scripts/build-legal-pdfs.sh auftraege)`)
|
||||
}
|
||||
// die AGB liegen unter dem Namen, den die Auftragsbestätigung (Mail K31 und Bestätigungsseite) verlinkt und anhängt
|
||||
assert.ok(existsSync(join(ROOT, `public/rechtliches/agb-${AGB_FASSUNG}.pdf`)))
|
||||
})
|
||||
|
||||
test('auftraege_script_modus: build-legal-pdfs.sh kennt auftraege (Fassungen aus texte.js und konstanten.js), all druckt es mit, bestehende Modi und Grenzen unverändert', () => {
|
||||
const sh = lies('scripts/build-legal-pdfs.sh')
|
||||
assert.match(sh, /^auftraege\)\n {2}auftraege_pdfs\n {2};;$/m)
|
||||
for (const name of ['TEXTE_VERSION', 'TEXTE_FASSUNG', 'AGB_VERSION', 'AGB_FASSUNG', 'widerrufsbelehrung-projektauftrag:$texte', 'agb:$agb']) assert.ok(sh.includes(name), name)
|
||||
// die Funktionen stehen vor shop_pdfs (test/buildLegalHtml.test.js liest ab dort den Modus shop) und zeigen auf die HTML-Quelle
|
||||
const auftraege = sh.slice(sh.indexOf('\nauftraege_pdfs() {'), sh.indexOf('\nshop_pdfs() {'))
|
||||
assert.ok(auftraege.length > 100 && sh.indexOf('\nauftraege_pdfs() {') > 0, 'auftraege_pdfs steht vor shop_pdfs')
|
||||
assert.ok(auftraege.includes('sha256sum "$quelle"') && auftraege.includes('echo "$summe" > "$ziel.sha256"') && auftraege.includes('(unveraendert)') && auftraege.includes('${FORCE:-}'))
|
||||
assert.ok(auftraege.includes('node scripts/build-legal-html.mjs auftraege'), 'fehlt die HTML-Seite, nennt die Meldung den Generator')
|
||||
assert.ok(sh.slice(sh.indexOf('\nall)'), sh.indexOf('\n*)')).includes(' auftraege_pdfs\n'), 'all druckt auch diese PDFs')
|
||||
assert.ok(sh.includes('Usage: $0 [all|analyse|termine|shop|datenschutz]') && sh.includes('$0 auftraege'), 'Aufruf nennt alle Modi')
|
||||
// je Chrome-Aufruf höchstens 120 Sekunden, kein paralleler Lauf, Node-Aufrufe mit Heap-Grenze und Zeitgrenze
|
||||
assert.match(sh, /^ {2}timeout 120 "\$CHROME" /m)
|
||||
assert.equal([...sh.matchAll(/timeout 60 node --max-old-space-size=256 /g)].length, 4, 'je Fassungsfunktion ein Aufruf: termine, shop, texte und agb')
|
||||
assert.ok(!/ &$|^\s*wait\b|xargs -P/m.test(sh), 'keine parallelen Läufe')
|
||||
assert.equal(spawnSync('bash', ['-n', 'scripts/build-legal-pdfs.sh'], { cwd: ROOT, encoding: 'utf8' }).status, 0)
|
||||
})
|
||||
Reference in new issue
Block a user