Merge feature/kundenansicht-neu (P5, P6, P8, P6b) in kn/ui vor P3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-16 17:55:13 +00:00
commit 1cd16ff293
123 files changed
+11184 -205

No files matched your search

+155
View File
@@ -0,0 +1,155 @@
# Abo-Bedingungen Fassung 6 (Entwurf)
Fassung 2026-10-16-v6 · Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung · Basis: Fassung 5 (Stand 30.09.2026, `public/vertragsunterlagen/v5/abo-bedingungen.html`)
Dieser Entwurf listet alle Änderungen gegenüber Fassung 5 im Wortlaut. Die Paragrafenfolge § 1 bis § 16 und alle Anker bleiben, damit Querverweise in Mails und der Paragraf zum Vertragsende (§ 11) stimmen. Die Sätze unter jeder Überschrift sind die Pflichtsätze; sie stehen unverändert in der Seite `public/neu/rechtliches/abo-bedingungen.html`. Zu jeder Stelle nennt die Zeile „Begründung“ die Entscheidung des Inhabers (E…), die Festlegung nach dem Builder-Check (C…) oder den Spec-Abschnitt und verweist bei offenen Punkten auf die Entscheidungsliste (`docs/rechtliches/kundenansicht-entscheidungen.md`, R… und KQ…). Voraussetzung für den Livegang ist die Freigabe durch den Inhaber (E20).
## Das Wichtigste auf einen Blick
Das Hosting-Abo enthält die Website-Analyse, das Wartungs-Abo zusätzlich News-Pinnwand, Terminplaner, Webshop, Buchungskalender und Öffnungszeiten live. Das Einschalten kostet nichts extra; die Einrichtung geht vom Stundenkontingent ab (§ 4 Abs. 8).
**Einordnung.** Neue Zeile „Funktionen“ in der Übersichtsbox, hinter der Zeile „Wartungs-Abo“. Die Box behält ihre Preiszeilen und die Anrede „Ihr“.
**Begründung.** E6 (die Funktionen stecken im Abo: Hosting-Abo mit Analyse, Wartungs-Abo mit allen Funktionen), E7 und E17 (Einrichtung vom Stundenkonto, keine Extra-Rechnung), C10 (Analyse im Hosting-Abo), Spec 5.9. Die Zeile nennt für Funktionen keinen Preis. Offen: Geltung für laufende Abos (R2, KQ1), Anrede der Rechtsdokumente (R12).
## § 1 Abs. 2
Die Bestellseiten richten sich nach der Kundenart, die wir im Kundenkonto hinterlegt haben („Betrieb“ oder „privat“). Wir hinterlegen sie nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat. Ob der Kunde Verbraucher oder Unternehmer ist, richtet sich nach dem Zweck, zu dem er den Vertrag abschließt, nicht nach dieser Angabe.
**Einordnung.** Hängt an den bestehenden Absatz 2 an („Sie gelten für Verbraucher und Unternehmer“).
**Begründung.** E2 bis E4 und C13: Die Kundenart legt das Team beim Anlegen fest, leer gilt als privat, und die Bestellseiten richten sich danach. Rechtlich entscheidet der Zweck des Vertrags (§§ 13, 14 BGB), nicht die hinterlegte Angabe; der Satz sagt das ausdrücklich, damit die Angabe nicht als Selbstauskunft oder als Rechtsfolge gelesen wird. Offen: Belastbarkeit der Kundenart (R5, KQ4).
## § 2 Abs. 2 c
Direkt über der Schaltfläche steht der Hinweis, dass diese Bedingungen mit den Leistungsbeschreibungen, dem Auftragsverarbeitungsvertrag (Anlage 1) und der Widerrufsbelehrung gelten, jeweils mit Fassung und Link; die Unterlagen erhält der Kunde nach der Bestellung zusätzlich als PDF per E-Mail. Ist die Kundenart „privat“ hinterlegt, kann der Kunde dort durch ein Kästchen verlangen, dass wir sofort mit der Leistung beginnen; ohne das Kästchen beginnen wir erst nach Ablauf der 14-tägigen Widerrufsfrist. Ist die Kundenart „Betrieb“ hinterlegt, beginnen wir mit Vertragsschluss, ohne dass der Kunde etwas markieren muss.
**Einordnung.** Ersetzt den bisherigen Buchstaben c (Zustimmung zu den Bedingungen und Wahl des Beginns). Die Buchstaben a, b, d und e bleiben.
**Begründung.** Spec 5.6, E1, E3, E4, C4 und recht.md Kurzfassung 3 und 4: Eine Linkzeile über dem Knopf ersetzt die Häkchen für Bedingungen, Leistungsbeschreibung, Auftragsverarbeitung und Belehrung; übrig bleibt das eine Häkchen „Bitte beginnt sofort …“ nur für Privatkunden in der Widerrufsfrist. Bei Betrieben (kein Widerrufsrecht) beginnt die Leistung mit Vertragsschluss. Die Kundenart kommt vom Server (§ 1 Abs. 2). Offen: Wortlaut des Häkchens ohne Normzitat (R6, KQ5), Belehrung nur per Link und PDF (R11, KQ6), AVV per Klick ohne eigenes Häkchen (R10, KQ7).
## § 2 Abs. 6
Wir speichern außerdem den Wortlaut der angezeigten Hinweise und Kästchen, die hinterlegte Kundenart sowie Zeit, IP-Adresse und Browserkennung des Klicks.
**Einordnung.** Hängt an den bestehenden Absatz 6 (Vertragssprache und Speicherung des Vertragstextes) an.
**Begründung.** Nachweise (Global Constraints der Übersicht, N34): Wortlaut, Fassung, Zeit, IP-Adresse, Browserkennung und Konto werden weiter gespeichert, ab jetzt mit der Kundenart zum Klickzeitpunkt. Der Satz macht das für den Kunden sichtbar; die Datenschutzerklärung nennt die Speicherung heute schon (Prüfpunkte D1 bis D3). Offen: Datenschutz-Prüfpunkte (R14).
## § 3 Abs. 8
Zum Abo gehören Funktionen, die der Kunde im Kundenportal unter „Funktionen“ einschaltet. Das Hosting-Abo enthält die Website-Analyse. Das Wartungs-Abo enthält News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse. Umfang, Voraussetzungen und Pflichten jeder Funktion beschreiben ihre Leistungsbeschreibung und, soweit die Funktion personenbezogene Daten im Auftrag des Kunden verarbeitet, ihr Auftragsverarbeitungsvertrag, jeweils in der Fassung, die das Kundenportal beim Einschalten anzeigt. Eine Funktion kostet keine zusätzliche Gebühr; die Einrichtung richtet sich nach § 4 Abs. 8 und 9.
Vorbehaltlich § 11 Abs. 4 läuft eine Funktion nur, solange das Abo läuft, das sie enthält. Das gilt auch für eine Funktion, die wir auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut haben. Funktionen, die der Kunde vor Veröffentlichung dieser Fassung einzeln gekauft hat, laufen nach den Bedingungen weiter, unter denen er sie gekauft hat.
**Einordnung.** Neuer Absatz „(8) Funktionen“ nach Absatz (7) mit zwei Absätzen.
**Begründung.** E6, E9, E10, E21, C10, C11, C12 und Spec 5.5, 5.9: Funktionen sind Teil des Abos und kosten keine zusätzliche Gebühr; die Einrichtung läuft nach § 4 Abs. 8 und 9. Eine Funktion läuft nur mit dem Abo, das sie enthält, auch wenn sie schon bei der Website-Erstellung eingebaut wurde (E21). Einzeln gekaufte Funktionen behalten ihre Bedingungen (E10). Offen: Geltung für laufende Abos (R2, KQ1), Einrichtung beim Projektstart (R4, KQ8), Ende und Fristen (R8, KQ8), Analyse im Hosting-Abo (R9).
## § 4 Abs. 2
Einen Vorschuss nach Absatz 8 gleichen die folgenden Kontingente zuerst aus.
**Einordnung.** Hängt als letzter Satz an den bestehenden Absatz 2 (Verfügbarkeit und Verfall der Kontingente) an.
**Begründung.** E17: Ein offener Vorschuss wird vor allem anderen aus den folgenden Monatskontingenten ausgeglichen (FIFO, Spec 5.5). Der Satz verknüpft die bestehende Verbrauchsregel mit Absatz 8. Offen: Vorschuss-Regel insgesamt (R3, KQ2).
## § 4 Abs. 8
Die Einrichtung einer Funktion rechnen wir als Arbeitszeit auf das Stundenkontingent an. Angerechnet wird die tatsächlich aufgewendete Zeit nach Absatz 4. Reicht das verfügbare Kontingent nicht aus, führen wir die Einrichtung trotzdem ohne weitere Rückfrage aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen, sobald sie bereitstehen. Dafür berechnen wir nichts, weder als Mehraufwand nach Absatz 6 noch in einer gesonderten Rechnung. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest; der Kunde schuldet ihn nicht.
**Einordnung.** Neuer Absatz 8 nach Absatz 7.
**Begründung.** E7, E8 und E17 (E17 ersetzt die Absprache aus E8), Spec 5.5: Die Einrichtung startet ohne Rückfrage und geht mit der echten Arbeitszeit (Absatz 4) vom Stundenkonto ab. Reicht das Kontingent nicht, ist die fehlende Zeit ein Vorschuss, nie Mehraufwand nach Absatz 6; ein Rest bei Vertragsende verfällt zugunsten des Kunden. Normale Arbeit außerhalb von Einrichtungen bleibt beim Mehraufwand nach Absatz 6. Offen: Vorschuss ohne Obergrenze (R3, KQ2).
## § 4 Abs. 9
Im Hosting-Abo ist die Einrichtung der Website-Analyse für den Kunden kostenlos; ein Stundenkontingent gibt es dort nicht (Absatz 7). Lässt der Kunde eine Funktion auf seinen Wunsch schon bei der Erstellung seiner Website einbauen, rechnen wir die Einrichtung nicht über das Stundenkontingent ab, sondern über die Kosten des Projekts, wie wir es mit ihm vereinbart haben.
**Einordnung.** Neuer Absatz 9 nach Absatz 8.
**Begründung.** E9, E21, C6 und C7, Spec 5.5: Im Hosting-Abo gibt es kein Kontingent (Absatz 7), die Einrichtung der Analyse ist dort kostenlos (C6). Bei einer Funktion, die schon bei der Erstellung der Website eingebaut wird, zählen die Einrichtungs-Arbeitsblätter nicht aufs Stundenkonto, sondern laufen über die Projektkosten, wie mit dem Kunden vereinbart (C7). Offen: Einrichtung beim Projektstart (R4, KQ8), Analyse im Hosting-Abo (R9).
## § 6 Abs. 6
Schaltet der Kunde die Website-Analyse ein, ist er für die Messung auf seiner Website Verantwortlicher. Er entfernt oder umgeht das Cookie-Banner nicht, bindet ohne Abstimmung mit uns keine weiteren Mess- oder Tracking-Werkzeuge ein und ergänzt seine Datenschutzerklärung um einen Hinweis auf die von uns erzeugte Zusatzseite „/datenschutz-analyse.html“.
**Einordnung.** Neuer Absatz 6 nach Absatz 5.
**Begründung.** C10 und Spec 5.5 (Analyse): Wer die Website-Analyse einschaltet, ist für die Messung auf seiner Website Verantwortlicher. Der Absatz stellt die Pflichten aus der Abo-Ergänzung Analyse (Cookie-Banner, keine weiteren Mess- oder Tracking-Werkzeuge ohne Abstimmung, Hinweis auf die Zusatzseite) in die Abo-Bedingungen, weil die Analyse jetzt auch im Hosting-Abo liegt. Die Tatsachenangabe zur Zielgruppe bleibt als Zeile mit Häkchen in der Oberfläche (Spec 5.5, N22). Offen: Analyse im Hosting-Abo (R9, KQ8), Ausschlüsse als Vertragsklausel (R13, KQ9).
## § 9 Abs. 6
Will der Kunde im Hosting-Abo eine Funktion einschalten, die nur das Wartungs-Abo enthält, führt ihn das Kundenportal zum Wechsel. Nach Wirksamkeit des Wechsels schaltet er die Funktion in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung nach Absatz 2.
**Einordnung.** Neuer Absatz 6 nach Absatz 5.
**Begründung.** E14 und C5: Wer im Hosting-Abo eine Funktion einschalten will, die nur das Wartungs-Abo enthält, wird zum Wechsel geführt und schaltet danach in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung mit eigener Widerrufsfrist (§ 2 Abs. 5); das Einschalten ist keine Bestellung. Offen: Sofortbeginn des Wechsels nur mit Häkchen bei Privatkunden (R6, KQ5).
## § 11 Abs. 4
Mit dem Ende des Abos, das eine Funktion enthält, endet die Funktion; abweichend davon läuft Öffnungszeiten live noch 30 Tage unverändert weiter. Wir schalten sie ab und löschen ihre Daten nach den Fristen in ihrer Leistungsbeschreibung; die Messdaten der Website-Analyse löschen wir 60 Tage nach der Abschaltung. Beginnt innerhalb dieser Fristen wieder ein Abo, das die Funktion enthält, schalten wir sie mit den vorhandenen Daten wieder ein. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; die übrigen Funktionen enden. Das Gleiche gilt, wenn der Kunde den Wechsel ins Wartungs-Abo widerruft.
**Einordnung.** Neuer Absatz 4 nach Absatz 3.
**Begründung.** E21, C12 und Nachtrag N31 (mit P4 und P6 abgestimmt): Funktionen enden mit dem Abo, das sie enthält. Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob); Öffnungszeiten live läuft nach dem Ende noch 30 Tage unverändert weiter, ein „Behalten“ gegen Zahlung gibt es nicht mehr. Gelöscht wird nach den Fristen der Leistungsbeschreibungen: 30 Tage (Terminplaner, News, Öffnungszeiten live), 90 Tage (Webshop, Buchungskalender), 60 Tage für die Messdaten der Analyse. Ein neues passendes Abo in der Frist holt die Funktion zurück. Beim Wechsel ins Hosting-Abo und beim Widerruf des Wechsels bleibt die Analyse (E6, C10). Offen: R8 (KQ8).
## § 14 Abs. 2
Für Funktionen, die personenbezogene Daten im Auftrag des Kunden verarbeiten, gilt zusätzlich der Auftragsverarbeitungsvertrag der Funktion, dem der Kunde beim Einschalten mit dem Klick auf „Einschalten“ zustimmt.
**Einordnung.** Hängt an den bestehenden Absatz 2 (Auftragsverarbeitungsvertrag in Anlage 1) an.
**Begründung.** Spec 5.9 (AVV je Funktion): Option A der AVV-Struktur, die Auftragsverarbeitungsverträge der Funktionen bleiben eigene Verträge mit neuer Vorbemerkung, Anlage 1 bleibt der Rahmen; der Klick auf „Einschalten“ unter der Linkzeile ist die Zustimmung. Offen: Struktur (R10, KQ7), Klick statt eigenes Häkchen (KQ7).
## § 15
Fassung 6 gilt für Abos, die ab ihrer Veröffentlichung abgeschlossen werden. Für Abos, die nach einer früheren Fassung abgeschlossen wurden, gelten diese Fassungen weiter. Schaltet der Kunde mit einem solchen Abo eine Funktion ein, gelten für die Funktion ihre Leistungsbeschreibung und ihr Auftragsverarbeitungsvertrag in der angezeigten Fassung, einschließlich der Regeln zur Einrichtung und zum Stundenkontingent.
**Einordnung.** Neuer zweiter Absatz hinter dem bisherigen Text. Der bisherige Text bleibt wörtlich.
**Begründung.** C11 und E10: Fassung 6 gilt für neue Abschlüsse; laufende Abos der Fassungen 3 bis 5 behalten ihren Vertrag. Beim Einschalten gelten die Leistungsbeschreibung und der Auftragsverarbeitungsvertrag der Funktion über die Linkzeile und den Klick, auch mit den Regeln zur Einrichtung und zum Stundenkonto. Die Änderung ist für laufende Abos vorteilhaft (Spec 5.9), deshalb keine Zustimmungsabfrage nach dem bisherigen Text. Offen: Info-Mail an laufende Abos (R2, KQ1).
## Leistungsbeschreibung A Nr. 1
Website-Analyse im Kundenportal (§ 3 Abs. 8)
**Einordnung.** Neuer Listenpunkt am Ende von Nr. 1 „Umfang“.
**Begründung.** E6 und C10: Das Hosting-Abo enthält die Website-Analyse. Offen: Analyse im Hosting-Abo (R9, KQ8).
## Leistungsbeschreibung B Nr. 1
Funktionen im Kundenportal: News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse (§ 3 Abs. 8)
**Einordnung.** Neuer Listenpunkt am Ende von Nr. 1 „Umfang“.
**Begründung.** E6: Das Wartungs-Abo enthält alle Funktionen und als Obermenge die Analyse.
## Widerruf: Hinweise
Das Einschalten einer Funktion ist kein eigener Vertrag, sondern Teil des Abos. Es verlängert die Widerrufsfrist des Abos nicht. Widerruft ein Verbraucher das Abo, endet auch die Funktion (§ 11 Abs. 4).
**Einordnung.** Neue Nummer 5 am Ende der „Hinweise zum Widerruf“. Widerrufsbelehrung und Muster-Widerrufsformular bleiben wortgleich mit Fassung 5.
**Begründung.** Spec 5.5 und 5.9, recht.md § 2: Das Einschalten löst keine neue Zahlungspflicht aus und ist kein eigener Vertrag; deshalb gibt es keine eigene Widerrufsfrist. Bei Widerruf des Abos endet die Funktion (§ 11 Abs. 4). Wie sich die Einrichtungszeit innerhalb der Frist auf den Wertersatz auswirkt, regelt das Häkchen für Privatkunden (§ 2 Abs. 2 c); die Entscheidung dazu steht in R7. Offen: R7 (KQ3), Belehrung per Link (R11, KQ6).
## Anlage 3
Server für die Website-Analyse (Messdienst, nur bei eingeschalteter Funktion)
Deutschland (Rechenzentrum Falkenstein)
**Einordnung.** Neue Tabellenzeile hinter den beiden Hetzner-Zeilen (Unternehmen wie dort, Leistung und Ort wie im Pflichtsatz).
**Begründung.** Spec 5.9 (AVV je Funktion), Art. 28 Abs. 2 DSGVO: Der Server des Messdienstes der Website-Analyse (Hetzner, Rechenzentrum Falkenstein, wie in der Auftragsverarbeitung der Analyse) wird als Unterauftragsverarbeiter genannt, solange die Analyse eingeschaltet ist. Offen: AVV-Struktur (R10, KQ7).
## Was gleich bleibt
- Preise: Hosting-Abo 20,00 € pro Monat, Wartungs-Abo 50,00 € pro Monat, E-Mail-Zusatzspeicher 1,00 € pro Monat je Paket von 5 GB, Mehraufwand 100 € pro Stunde (alle Endpreise, `§ 19 UStG`). Funktionen bekommen keinen Preis.
- Laufzeiten: Mindestlaufzeit zwölf Monate ab Leistungsbeginn, Kündigung zum Ende der Mindestlaufzeit mit einem Monat Frist, danach monatlich (§ 8), Sonderkündigung bei Geschäftsaufgabe.
- Paragrafenfolge § 1 bis § 16 und alle Anker (`p1` bis `p16`, `wechsel`, `zusatzspeicher`, `hosting`, `wartung`, `widerruf`, `formular`, `widerruf-hinweise`, `avv`, `tom`, `sub`).
- Widerrufsbelehrung und Muster-Widerrufsformular: wortgleich mit Fassung 5; die Hinweise zum Widerruf Nr. 1 bis 4 bleiben.
- § 10 E-Mail-Zusatzspeicher mit Absatz 3 (Bereitstellung sofort, kein Wertersatz) und den Hinweisen zum Widerruf Nr. 4.
- Anlage 1 (Auftragsverarbeitungsvertrag) und Anlage 2 (technische und organisatorische Maßnahmen); Anlage 3 behält ihre beiden Hetzner-Zeilen.
- Die Rechtsdokumente bleiben in der bisherigen Anrede („der Kunde“, in der Übersichtsbox „Ihr“); nur Oberflächentexte stehen in „du“ (R12).
+145
View File
@@ -0,0 +1,145 @@
# Auftragsverarbeitungsvertrag Terminplaner (Art. 28 DSGVO)
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Terminplaner“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Terminplaner“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Terminplaner. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Terminplaner geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Verwaltung im Kundenportal („Terminplaner“) und verarbeitet dabei personenbezogene Daten der Gäste und der Beschäftigten des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Terminplaner schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 8 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Erheben über das Widget auf der Website des Kunden, Übermitteln an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen eines Kalender-Links und einer CSV-Datei und Löschen.
(2) Zwecke: Der Kunde soll Termine seiner Gäste verwalten. Dazu nimmt der Terminplaner Buchungen und Terminanfragen entgegen, verhindert Doppelbuchungen, bestätigt, ändert und sagt Termine ab, erinnert Gäste auf deren Wunsch und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Terminplaner für den Kunden vor Missbrauch (Mengenbegrenzung, Erkennen automatisierter Buchungen).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der gelöschten Buchungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.
(2) Daten der Beschäftigten des Kunden: Namen oder Bezeichnungen der Personen oder Plätze, deren Arbeitszeiten und Abwesenheiten sowie ein geheimer Kalender-Link je Person oder Platz.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt; der Kunde fordert von seinen Gästen keine Gesundheitsangaben (Nummer 11 Abs. 4).
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse nur im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Buchung wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. In den Server-Protokollen stehen sie 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Terminplaner setzt keine Cookies und speichert nichts im Browser des Gastes.
(6) Betroffene: Gäste, die über die Website des Kunden Termine buchen oder anfragen, und Beschäftigte des Kunden, die im Terminplaner geführt werden.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) Die Zahlung der Einrichtung über Stripe betrifft nur den Vertrag zwischen dem Kunden und WEBklar. Daten von Gästen oder Beschäftigten des Kunden gehen nicht an Stripe. Stripe ist deshalb kein Unterauftragsverarbeiter dieses Vertrags.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Gästen und Beschäftigten, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal die Aktion „Gastdaten löschen“ und den Export der Termine als CSV-Datei. Mit „Gastdaten löschen“ leert der Kunde Name, E-Mail-Adresse, Telefonnummer, Nachricht und Zusatzfelder einer Buchung; Status und Zeiten bleiben. Dabei löscht WEBklar die E-Mails zu dieser Buchung, die noch auf den Versand warten, und die Links des Gastes werden ungültig. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Eine noch nicht bestätigte Buchung verfällt von selbst (Nummer 9 Abs. 1).
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.
(6) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten.
(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.
(3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage.
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen (zum Beispiel Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze und die Wünsche des Kunden zum Einbau) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Bis zur Löschung kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Entwürfe aus dem Fragebogen, aus denen die Funktion nicht eingeschaltet wurde, löscht WEBklar 90 Tage nach der letzten Änderung.
(8) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(9) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste und seiner Beschäftigten im Terminplaner.
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für die Online-Terminbuchung in seine Datenschutzerklärung auf (Nr. 4 Abs. 9 der Leistungsbeschreibung) und lässt den Datenschutz-Hinweis am Buchungsformular unverändert.
(3) Der Kunde informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt (Nr. 4 Abs. 7 der Leistungsbeschreibung).
(4) Der Kunde setzt den Terminplaner nicht im Gesundheitsbereich ein und fordert von seinen Gästen keine Angaben zu ihrer Gesundheit, auch nicht über Zusatzfelder oder das Nachrichtenfeld (Nr. 4 Abs. 3 der Leistungsbeschreibung).
(5) Der Kunde verändert den Haken für die Erinnerung und die Texte der übrigen Haken nicht (Nr. 4 Abs. 6 der Leistungsbeschreibung). Erinnerungen versendet der Terminplaner nur, wenn der Gast den Haken gesetzt hat.
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Terminplaner für seine Website nach Nr. 9 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 9 Abs. 3 der Leistungsbeschreibung aussetzen. Das Wartungs-Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Terminplaners (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Seiten der Gäste und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. |
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Terminplaner speichert nichts im Browser des Gastes (kein lokaler Speicher, kein Sitzungsspeicher). Das Widget sendet seine Anfragen ohne Cookies, liest weder den Referrer noch die Zeitzone des Browsers und ruft nur die Schnittstelle des Terminplaners auf. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen eingeschalteten Terminplaner Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Buchungen nimmt sie nur von Seiten des Kunden und des Portals an. Der Kalender-Link enthält ein geheimes Token, das mit einem Vergleich in konstanter Zeit geprüft wird. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Buchungen. Grenzen je IP-Adresse, je Website und je E-Mail-Adresse sowie das Mailbudget je Stunde (insgesamt und je Funktion) gelten nur im Arbeitsspeicher; ein Neustart leert sie. |
| Datenminimierung | Pflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte, nicht aus diesen Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Trennung | Alle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion. Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht. Die Einstellungen liegen je Funktion in einem eigenen Dokument. |
| Löschung | Täglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht. |
| Protokolle | Die Programmprotokolle des Terminplaners und der Tagesbericht enthalten nur Zählungen und Buchungsnummern, keine Namen, E-Mail-Adressen oder Nachrichten von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Archiv der Fassungen der Gästetexte (Nummer 8 Abs. 5), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+157
View File
@@ -0,0 +1,157 @@
# Auftragsverarbeitungsvertrag Buchungskalender (Art. 28 DSGVO)
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Vermieter“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Buchungskalender“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Buchungskalender“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion auch ohne Bestellung im Portal einschalten (zum Beispiel für einen Kunden, dem WEBklar sie als Teil seines Website-Projekts bereitstellt), aber nur, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Buchungskalender. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Buchungskalender geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Gäste im Buchungskalender. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4). Die Plattformen, deren Kalender der Kunde einbindet (zum Beispiel Booking.com oder Airbnb), sind weder Auftragsverarbeiter noch Unterauftragsverarbeiter von WEBklar.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Buchungsseite und einer Verwaltung im Kundenportal („Buchungskalender“) und verarbeitet dabei personenbezogene Daten der Gäste des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Buchungskalender schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Übermitteln der Anfrage und der Buchung vom Widget und von der Buchungsseite an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Berechnen des Preises, Erzeugen von Rechnungen und Stornorechnungen im Namen des Kunden (Nr. 3 Abs. 6 der Leistungsbeschreibung), Abgleichen von Kalendern, Versenden von E-Mails, Übernehmen von Zahlungsstatus aus Stripe, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
(2) Zwecke: Der Kunde soll Anfragen und Buchungen seiner Gäste abwickeln. Dazu nimmt der Buchungskalender Anfragen entgegen, reserviert Zeiträume nach der Freigabe durch den Kunden, nimmt die verbindliche Buchung des Gastes entgegen, stellt die Rechnung aus, gleicht Online-Zahlungen mit Stripe ab, bildet Zahlungen vor Ort ab, bildet Stornierungen ab und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Buchungskalender für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der Anfragen je IP-Adresse, Erkennen automatisierter Anfragen).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Anfragen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Gäste, die WEBklar aus dem Widget, von der Buchungsseite und aus Stripe erhält und speichert:
- bei der Anfrage: Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und eine Nachricht, die gewünschte Unterkunft, der Zeitraum, die Zahl der Personen, die Anfragenummer, ein Hashwert der IP-Adresse (Absatz 4) und der Zeitpunkt;
- bei der Freigabe: die Posten und Summen des Preises (Text, Menge, Einzelbetrag, Steuersatz), die Reservierungsfrist, eine Nachricht des Kunden an den Gast, die Stornobedingungen, die galten, und die Fassung der Rechtstexte, der der Gast gesehen hat, jeweils als Schnappschuss;
- bei der Buchung: die Rechnungsadresse (Name, Firma, Straße, Postleitzahl, Ort, Land), die Zahlart, der Zeitpunkt der Buchung und die Rechnungsnummer;
- bei der Zahlung und der Erstattung: Status, Zeitpunkt, Art (online über Stripe oder vor Ort), die Kennungen der Zahlung bei Stripe, erstattete Beträge und der Grund einer Stornierung;
- die Rechnung und die Stornorechnung als unveränderlicher Schnappschuss (Angaben des Ausstellers, Empfänger, Unterkunft, Zeitraum, Posten, Summen, Steuerart, Bezug), aus dem WEBklar das PDF bei jedem Abruf erzeugt;
- ein Hashwert des geheimen Zugangs zur Buchungsseite (der Link in der E-Mail wird nur als Prüfsumme gespeichert);
- der Zustellstatus der E-Mails an den Gast.
**Kartendaten des Gastes erhält und speichert WEBklar nicht;** die gibt der Gast nur bei Stripe ein. Online-Zahlungen sind derzeit nur per Karte (einschließlich Apple Pay und Google Pay) möglich, nicht per SEPA-Lastschrift. Die Bankverbindung des Kunden selbst (Angabe auf der Rechnung) und seine Firmen- und Steuerangaben gehören nicht zu den Gästedaten.
(2) Von Kalendern anderer Plattformen übernimmt WEBklar nur die belegten Zeiträume (Datum von und bis), nicht Namen, Beschreibungen oder andere Angaben der dortigen Buchungen, auch wenn der Kalender sie enthält. Die iCal-Adresse, die WEBklar dem Kunden bereitstellt, enthält nur belegte Nächte, keine Angaben zu Gästen. Wer die geheime Adresse kennt, sieht die belegten Nächte; der Kunde kann sie jederzeit erneuern.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Kunde fordert von seinen Gästen keine entsprechenden Angaben und bittet sie, solche Angaben nicht in das Nachrichtenfeld zu schreiben (Nummer 11 Abs. 4). Meldedaten nach dem Bundesmeldegesetz (Meldeschein) und Daten zur Kurtaxe verarbeitet der Buchungskalender nicht.
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Anfrage werden weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: ein SHA-256-Wert, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz. Er dient nicht dazu, Gäste zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Anfrage. WEBklar entfernt ihn aus dem Datensatz, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 9 Abs. 1). In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Buchungskalender setzt keine Cookies. Das Widget speichert nichts im Browser. Auch die Buchungsseite legt weder Cookies noch Daten im lokalen oder im Sitzungsspeicher des Browsers ab.
(6) Betroffene: Gäste, die über die Website des Kunden Anfragen stellen oder buchen, und Personen, deren Angaben der Gast in der Anfrage oder Buchung macht (zum Beispiel Mitreisende oder abweichende Rechnungsempfänger). Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Buchungskalender nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben, in seinem Namen Rechnungen auszustellen und Zahlungen, die nach einer Stornierung oder nach dem Ablauf einer Reservierung eingehen, sowie zweite Zahlungen auf eine bereits bezahlte Buchung ohne weitere Anweisung in voller Höhe über sein Stripe-Konto zu erstatten (dauerhafte Weisung; der Stand der Buchung ändert sich dabei nicht). Bei einer Stornierung schließt WEBklar eine noch offene Zahlungsseite bei Stripe. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Unterkünfte, Preise, Sperren, Kalenderverbindungen, Freigabe und Ablehnung von Anfragen, Zahlungseingang vor Ort, Stornierungen und Erstattungen, Online-Zahlung, Öffnen und Schließen des Buchungskalenders). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Buchungskalender abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um, soweit Aufbewahrungspflichten dem nicht entgegenstehen (Nummer 9 Abs. 8).
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Buchungskalender läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Buchungskalender abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 3 der Leistungsbeschreibung); die Zahlung des Gastes fließt direkt auf dieses Konto. Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs- und Kartendaten (derzeit nur Karte, einschließlich Apple Pay und Google Pay), die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden legt WEBklar bei Stripe die Zahlung an, ruft die Angaben zu Zahlungen ab, die der Buchungskalender braucht (Nummer 2 Abs. 1), schließt offene Zahlungsseiten und veranlasst Erstattungen (auch die automatischen nach Nummer 4 Abs. 2). An Stripe gehen dabei von WEBklar nur: die Bezeichnung des Aufenthalts mit Rechnungsnummer als Zahlungsposition, der Betrag, die Kennungen der Buchung und der Zahlung (Metadaten), die E-Mail-Adresse des Gastes zur Vorbelegung der Zahlungsseite, soweit WEBklar sie übergibt, die Erstattung und der **geheime Link zur Buchungsseite mit dem Token als Rücksprungadresse** (bei Erfolg und bei Abbruch). Dieser Link ist damit für Stripe und im Stripe-Dashboard des Kunden sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
(5) Plattformen, deren iCal-Kalender der Kunde einbindet oder denen er die iCal-Adresse des Buchungskalenders mitteilt, sind Empfänger nach seiner Entscheidung. WEBklar übermittelt ihnen keine Daten von Gästen.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Gästen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal die Anzeige der Anfragen und Buchungen, mit denen der Kunde Auskunft erteilen kann, und den Abruf der Rechnungen als PDF. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Gästedaten einer Anfrage oder Buchung unverzüglich: Name, Firma, E-Mail-Adresse, Telefonnummer und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt in der Regel längst entfernt); Anfragenummer, Zeitraum, Beträge und Status bleiben. Rechnungen und Rechnungsadresse einer gebuchten Anfrage bleiben, solange eine Aufbewahrungspflicht besteht (Nummer 9 Abs. 3). Dabei löscht WEBklar die E-Mails zu dieser Anfrage, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 5 löscht (48 Stunden nach dem Fehlschlag).
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Den IP-Hash entfernt WEBklar aus dem Datensatz einer Anfrage, sobald der Kunde sie freigibt oder ablehnt, spätestens 30 Tage nach dem Anlegen.
(2) Anfragen, aus denen keine Buchung wurde (abgelehnt, abgelaufen, oder neu und nicht innerhalb von 60 Tagen entschieden), anonymisiert WEBklar nach 6 Monaten: Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht werden gelöscht. Anfragenummer, Zeitraum, Status und, soweit vorhanden, der Preisvorschlag bleiben ohne Personenbezug erhalten.
(3) Bei gebuchten Aufenthalten anonymisiert WEBklar die Gästedaten (Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht) 3 Jahre nach dem Ende des Kalenderjahres, in dem der Gast abgereist ist (Abreisejahr; maßgeblich ist die Zeit in Berlin). **Rechnungen, Stornorechnungen und die Rechnungsadresse bleiben 8 Jahre erhalten**, gerechnet ab dem Ende des Kalenderjahres der Rechnung, solange der Buchungskalender läuft (Buchungsbelege, § 147 AO); sie sind für den Kunden als Aussteller bestimmt. Anfragenummer, Zeitraum, Beträge und Status bleiben ebenfalls.
(4) Die Anonymisierung nach den Absätzen 2 und 3 sowie die Entfernung des IP-Hash laufen im täglichen Löschlauf; anonymisiert oder entfernt wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(5) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(6) Ist der Buchungskalender abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Anfragen, Buchungen und Rechnungen 90 Tage lang im Kundenportal lesen und die Rechnungen einzeln als PDF herunterladen; WEBklar weist ihn nach dem Abschalten per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern. Danach löscht WEBklar alle Daten des Buchungskalenders: Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen. Damit endet auch die Aufbewahrung der Rechnungen nach Absatz 3; sie bewahrt der Kunde dann selbst auf. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 6 (bei Abschaltung 90 Tage später); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. alle Daten des Buchungskalenders löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Rechnungen und Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung. Die Aufbewahrung der Rechnungen nach Absatz 3 geschieht als Dienst für den Kunden und für die Dauer des Buchungskalenders. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Löschung nach Absatz 6 durch den Download. Soweit eine gesetzliche Aufbewahrungspflicht besteht, löscht oder anonymisiert WEBklar auch auf Weisung des Kunden nicht früher; stattdessen sperrt es die Daten für die weitere Verarbeitung.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste im Buchungskalender.
(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Buchungskalender in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst, wenn er die Online-Zahlung nutzt, und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
(4) Der Kunde fordert von seinen Gästen keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten und weist sie auf der Website darauf hin, solche Angaben nicht in das Nachrichtenfeld zu schreiben.
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht (Nr. 5 Abs. 9 der Leistungsbeschreibung).
(6) Der Kunde erfüllt seine Pflichten zu Meldeschein, Kurtaxe und sonstigen Meldungen an Behörden selbst und verarbeitet die dafür nötigen Daten außerhalb des Buchungskalenders (Nr. 5 Abs. 6 der Leistungsbeschreibung).
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Buchungskalender für seine Website nach Nr. 12 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 12 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Buchungskalenders (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Buchungsseite und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. Kalender anderer Plattformen ruft WEBklar nur über https ab. |
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Buchungskalenders auf, keine Dienste Dritter. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Buchungskalender Daten und ist über den Website-Schlüssel (Site-Key) adressiert; die belegten Nächte nennt sie ohne Quelle und ohne Daten von Gästen. Die Buchungsseite ruft der Gast nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert und zeitkonstant verglichen wird. Der Token wird nur als Prüfsumme gespeichert; er wird nur als Teil der Rücksprungadresse an Stripe übergeben (Nummer 7 Abs. 4) und öffnet nur diese eine Buchungsseite; er gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage), danach antwortet die Buchungsseite wie bei einem unbekannten Link. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. Die iCal-Adresse des Buchungskalenders ist ein geheimer, erneuerbarer Zufallswert. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und Grenzen je IP-Adresse und je Buchungskalender schützen vor automatisierten Anfragen. Die Grenzen gelten im Arbeitsspeicher; ein Neustart leert sie. Mit der Anfrage wird nur ein gesalzener IP-Hash gespeichert (Nummer 3 Abs. 4). Beim Abruf von Kalendern anderer Plattformen sind nur Adressen mit https erlaubt, private und lokale Ziele sind gesperrt, und Größe und Dauer des Abrufs sind begrenzt. |
| Datenminimierung | Kartendaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Preisen und Posten, nie aus Angaben des Browsers. Von anderen Kalendern werden nur Zeiträume übernommen. Mit der Anfrage werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert; der IP-Hash wird mit der Entscheidung des Kunden entfernt. Die Gästedaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Integrität der Rechnungen | Rechnungen und Stornorechnungen werden als unveränderlicher Schnappschuss gespeichert; das PDF entsteht bei jedem Abruf aus diesem Schnappschuss. Rechnungsnummern werden unter Sperre lückenlos je Kunde und Jahr vergeben. Freigabe und Buchung prüfen unter Sperre erneut, dass der Zeitraum frei ist. |
| Trennung | Alle Unterkünfte, Anfragen und Rechnungen tragen die Kennung des Kunden, des Projekts und der Funktion; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Buchungskalenders. Die Hintergrundläufe (Ablauf von Reservierungen, Kalenderabgleich, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
| Löschung | Täglicher Löschlauf für Anfragen, Buchungen und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Buchungskalender 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). |
| Protokolle | Die Programmprotokolle des Buchungskalenders und der Tagesbericht enthalten nur Zählungen und Anfragenummern, keine Namen, E-Mail-Adressen oder Adressen von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+145
View File
@@ -0,0 +1,145 @@
# Auftragsverarbeitungsvertrag Webshop (Art. 28 DSGVO)
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Händler“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Webshop“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Webshop“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.
(3) Dieser Vertrag ist ein eigener Vertrag für den Webshop. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Webshop geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.
(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Käufer im Webshop. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4).
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden ein Shop-Widget mit einer Bestellverwaltung im Kundenportal („Webshop“) und verarbeitet dabei personenbezogene Daten der Käufer des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Webshop schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Übermitteln der Bestellung vom Widget an den Server von WEBklar, Übernehmen der Zahlungs- und Adressangaben aus der Checkout-Sitzung bei Stripe, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen einer CSV-Datei, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.
(2) Zwecke: Der Kunde soll Bestellungen seiner Käufer abwickeln. Dazu nimmt der Webshop Bestellungen entgegen, reserviert den Bestand, gleicht Zahlungen mit Stripe ab, bestätigt Bestellungen (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, zunächst nur mit einer Eingangsmail ohne Vertragsschluss), bildet Versand und Abholung ab, nimmt Widerrufe entgegen, erstattet Zahlungen im Auftrag des Kunden und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Webshop für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der offenen Bestellungen je IP-Adresse, Erkennen automatisierter Bestellungen und Widerrufe).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Käufer. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Bestellungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.
## 3. Datenarten und Betroffene
(1) Daten der Käufer, die WEBklar aus der Checkout-Sitzung bei Stripe und aus dem Widget erhält und speichert: Name, E-Mail-Adresse, Lieferadresse und Rechnungsadresse (die Rechnungsadresse nur, soweit die Zahlungsseite von Stripe sie für die gewählte Zahlungsart abfragt); die Bestellung (Bestellnummer, Positionen mit Namen, Mengen und Preisen zum Zeitpunkt der Bestellung, Versandkosten, Lieferart, Steuerausweis, Gesamtbetrag); der Status der Bestellung mit den Zeitpunkten (bestellt, bezahlt, versendet, abholbereit, abgeschlossen, storniert, erstattet); Zahlungsstatus, Zahlungsart (zum Beispiel Karte oder SEPA-Lastschrift) und Kennungen der Zahlung bei Stripe; Versanddienst und Sendungsnummer, wenn der Kunde sie einträgt; Erstattungen mit Betrag, Zeitpunkt und Grund; die Prüfsumme (Hash) der Rechtstexte des Kunden, die der Käufer bei der Bestellung gesehen hat (den Wortlaut der Fassung verwahrt das Archiv nach Nummer 8 Abs. 5); ein Hashwert der IP-Adresse (Absatz 4); der Zustellstatus der E-Mails an den Käufer, auch der Eingangsmail bei ausstehender Zahlung. **Kartendaten und Kontodaten (IBAN) erhält und speichert WEBklar nicht;** die gibt der Käufer nur bei Stripe ein. **Eine Telefonnummer des Käufers erhebt und speichert WEBklar nicht:** Das Widget hat kein Telefonfeld, die Zahlungsseite von Stripe ist so eingestellt, dass sie keine Telefonnummer verlangt, und eine Telefonnummer, die Stripe in den Kundendaten der Sitzung dennoch übermitteln sollte, übernimmt WEBklar nicht in die Bestellung. Die Telefonnummer des Kunden selbst (Anbieterangaben in seinen Einstellungen) gehört nicht zu den Käuferdaten.
(2) Daten von Personen, die über die Widerrufsfunktion einen Vertrag widerrufen (Käufer und Interessenten, auch ohne passende Bestellung): Name, E-Mail-Adresse, Bestellnummer, die Erklärung als Freitext und der Zeitpunkt des Eingangs, außerdem der Zeitpunkt der Eingangsbestätigung und der Bearbeitung durch den Kunden.
(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Waren, aus deren Kauf sich besondere Kategorien ergeben können, etwa Arzneimittel, sind nach Nr. 3 der Leistungsbeschreibung ausgeschlossen; der Kunde fordert von seinen Käufern keine entsprechenden Angaben (Nummer 11 Abs. 4).
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit dem Widerruf wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Mit der Bestellung werden ebenfalls weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein **Hashwert der IP-Adresse („IP-Hash“)**: die ersten 16 Hexadezimalzeichen eines SHA-256-Werts, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz: Je IP-Hash und Webshop sind höchstens drei offene (noch nicht bezahlte) Bestellungen innerhalb von 30 Minuten zulässig. Er dient nicht dazu, Käufer zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Bestellung. WEBklar löscht ihn aus dem Datensatz der Bestellung, sobald die Bestellung bezahlt oder storniert wird, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen (Nummer 9 Abs. 1); bei bezahlten Bestellungen bleibt er nicht bis zur Anonymisierung (Nummer 9 Abs. 2) erhalten. In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).
(5) Der Webshop setzt keine Cookies. Das Widget speichert im Browser des Besuchers nur den Warenkorb (Kennungen der Artikel und Mengen, ohne Personenbezug) im Sitzungsspeicher (sessionStorage) des Tabs, solange der Tab geöffnet ist; das ist für den Warenkorb technisch erforderlich. Andere Daten legt das Widget im Browser nicht ab.
(6) Betroffene: Käufer, die über die Website des Kunden Waren bestellen, und Personen, die über die Widerrufsfunktion einen Vertrag widerrufen. Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, den Webshop nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Artikel, Versand, Abholung, Zahlungsarten, Rechtstexte, Bestellstatus, Erstattungen, Öffnen und Schließen des Webshops, Export). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Webshop abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um.
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Webshop läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Bestell-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Webshop abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) **Stripe ist kein Unterauftragsverarbeiter von WEBklar.** Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 4 der Leistungsbeschreibung). Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs-, Karten- und Kontodaten, die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden ruft WEBklar bei Stripe die Angaben zu Zahlungen und Bestellungen ab, die der Webshop braucht (Nummer 2 Abs. 1), und veranlasst Erstattungen. Daten von Käufern gehen dabei nicht von WEBklar an Stripe, außer der Erstattung selbst und der Kennung der Zahlung. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen von Käufern, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Dafür bietet das Kundenportal den Export der Bestellungen als CSV-Datei und die Anzeige der Bestellungen und Widerrufe, mit denen der Kunde Auskunft erteilen kann. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Käuferdaten einer Bestellung oder eines Widerrufs unverzüglich: Name, E-Mail-Adresse, Liefer- und Rechnungsadresse und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt längst gelöscht); Bestellnummer, Positionen, Beträge und Status bleiben. Dabei löscht WEBklar die E-Mails zu dieser Bestellung, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Gesetzliche Aufbewahrungspflichten des Kunden (Nummer 9 Abs. 8) prüft der Kunde vor seiner Weisung selbst.
(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.
(5) Bestätigt der Kunde seine Rechtstexte im Kundenportal (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular, Versand und Zahlung, Gewährleistungshinweis sowie die Adressen von Impressum und Datenschutzerklärung), legt WEBklar jede bestätigte Fassung einmal im Archiv der Rechtstexte ab: mit ihrer Prüfsumme (Hash), dem Zeitpunkt und der Angabe, welche angemeldete Person des Kunden (Name oder E-Mail-Adresse) sie bestätigt hat. Die Bestellung speichert die Prüfsumme der Fassung, die der Käufer bei der Bestellung gesehen hat. Die Bestellbestätigung (G09) zeigt die archivierte Fassung zu dieser Prüfsumme, nicht die Fassung, die beim Versand der E-Mail gerade gilt; fehlt die archivierte Fassung ausnahmsweise, verwendet WEBklar die aktuell bestätigte Fassung. Das Archiv enthält keine Daten von Käufern. WEBklar löscht es mit den übrigen Daten des Webshops (Nummer 9 Abs. 6), nicht früher. Das Archiv hilft dem Kunden, die Fassung nachzuweisen, die ein Käufer erhalten hat; es ersetzt nicht seine eigene Aufbewahrung (Nummer 9 Abs. 8). Die Bestellbestätigung enthält diese Texte; WEBklar löscht ihren Inhalt nach dem Versand aus der Versandwarteschlange (Nummer 9 Abs. 4).
(6) Die Käufer informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).
## 9. Löschung und Rückgabe
(1) Eine Bestellung, die nicht bezahlt wird, verfällt 30 Minuten nach dem Anlegen und gilt als storniert. WEBklar löscht nicht bezahlte Bestellungen (offen oder storniert) 30 Tage nach dem Anlegen. Den IP-Hash löscht WEBklar aus dem Datensatz, sobald die Bestellung bezahlt oder storniert wird, spätestens mit dieser Löschung.
(2) Bei bezahlten Bestellungen anonymisiert WEBklar die Käuferdaten 3 Jahre nach dem Ende des Kalenderjahres, in dem die Bestellung angelegt wurde (Bestelljahr; maßgeblich ist die Zeit in Berlin): Name, E-Mail-Adresse sowie Liefer- und Rechnungsadresse werden gelöscht (der IP-Hash ist mit der Zahlung bereits gelöscht; eine Telefonnummer des Käufers wird nicht gespeichert). Bestellnummer, Positionen, Beträge, Steuerausweis, Status, Versanddienst und Sendungsnummer bleiben für die Buchhaltung des Kunden erhalten.
(3) Widerrufe nach Nummer 3 Abs. 2 behandelt WEBklar wie bezahlte Bestellungen: Name, E-Mail-Adresse und Erklärung werden 3 Jahre nach dem Ende des Kalenderjahres des Eingangs gelöscht; die Bestellnummer, die die Person angegeben hat, und der Zeitpunkt des Eingangs bleiben.
(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.
(5) Der Löschlauf läuft täglich; gelöscht oder anonymisiert wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.
(6) Ist der Webshop abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Bestellungen und Widerrufe 90 Tage lang im Kundenportal lesen und als CSV-Datei exportieren. Danach löscht WEBklar alle Daten des Webshops: Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen der Rechtstexte (Nummer 8 Abs. 5). Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Käufern; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); alle Daten des Webshops löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.
(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung, und das Archiv der Rechtstexte dient nur dem Nachweis der Texte. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Anonymisierung oder Löschung durch den Export und die Belege von Stripe.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Käufer im Webshop.
(2) Der Kunde informiert seine Käufer nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Webshop in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.
(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).
(4) Der Kunde verkauft keine ausgeschlossenen Waren (Nr. 3 der Leistungsbeschreibung) und fordert von seinen Käufern keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten, auch nicht im Freitext der Widerrufserklärung.
(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht und entfernt den Link „Vertrag widerrufen“ nicht (Nr. 5 Abs. 6 und 9 der Leistungsbeschreibung).
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Webshop für seine Website nach Nr. 11 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 11 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen des Webshops (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Das Widget, die Seiten der Käufer und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. |
| Speicher im Browser | Es werden keine Cookies gesetzt. Das Widget speichert nur den Warenkorb (Kennungen der Artikel und Mengen) im Sitzungsspeicher des Tabs. Es sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Webshops auf, keine Dienste Dritter. |
| Zugriffskontrolle | Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Webshop Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Bestellungen und Widerrufe nimmt sie nur von Seiten des Kunden und des Portals an. Den Stand einer Bestellung ruft der Käufer nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert wird. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. |
| Missbrauchsschutz | Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Bestellungen und Widerrufen. Grenzen je IP-Adresse und je Webshop gelten im Arbeitsspeicher; ein Neustart leert sie. Die Obergrenze offener Bestellungen je IP-Adresse (drei in 30 Minuten) arbeitet mit einem gesalzenen Hashwert der IP-Adresse in der Bestellung (Nummer 3 Abs. 4). |
| Datenminimierung | Kartendaten und Kontodaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Artikeln, nie aus Angaben des Browsers. Eine Telefonnummer wird weder abgefragt noch gespeichert, die Rechnungsadresse nur, soweit Stripe sie für die Zahlungsart braucht. Mit der Bestellung und mit dem Widerruf werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert, bei der Bestellung nur der gesalzene IP-Hash, und der nur, solange die Bestellung offen ist (gelöscht mit Zahlung oder Stornierung). Die Käuferdaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert. |
| Trennung | Alle Artikel, Bestellungen und Widerrufe tragen die Kennung des Webshops und des Kunden; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Webshops. Artikelbilder liegen in einem eigenen Ordner je Webshop. Die Hintergrundläufe (Abgleich offener Bestellungen, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht. |
| Löschung | Täglicher Löschlauf für Bestellungen, Widerrufe und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Webshops einschließlich des Archivs der bestätigten Rechtstexte 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6). |
| Protokolle | Die Programmprotokolle des Webshops und der Tagesbericht enthalten nur Zählungen und Bestellnummern, keine Namen, E-Mail-Adressen oder Adressen von Käufern. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen). |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
+138
View File
@@ -0,0 +1,138 @@
# Auftragsverarbeitungsvertrag Website-Analyse (Art. 28 DSGVO)
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen
zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).
## Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen
(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Website-Analyse“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Website-Analyse“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.
(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält die E-Mail nach Absatz 1.
(3) Dieser Vertrag ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für die Website-Analyse geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung „Website-Analyse“ vor.
## 1. Gegenstand und Dauer
(1) WEBklar betreibt für die öffentliche Website des Kunden eine Reichweitenmessung mit Einwilligungsbanner („Website-Analyse“) und verarbeitet dabei personenbezogene Daten der Besucher der Website im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung „Website-Analyse“.
(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Ausschalten, spätestens mit dem Ende des Abos, das die Website-Analyse enthält (Hosting-Abo oder Wartungs-Abo); endet nur das Wartungs-Abo und läuft ein Hosting-Abo weiter, läuft dieser Vertrag weiter. Den Messdienst schaltet WEBklar bei Vertragsende spätestens am folgenden Tag ab. Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.
## 2. Art und Zweck der Verarbeitung
(1) Art: Erheben über ein Skript, das WEBklar beim Veröffentlichen in die Website des Kunden einfügt, Übermitteln an den Messdienst von WEBklar, Speichern, Verdichten zu Tageswerten, Auswerten im Kundenportal (Kennzahlen, Heatmap) und Löschen.
(2) Zwecke: Der Kunde soll erkennen, wie seine Website genutzt wird, um sie zu verbessern. Außerdem schützt WEBklar den Messdienst für den Kunden vor Missbrauch und verfälschten Zahlen (Mengenbegrenzung, Erkennen von Bots).
(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Besucher. Die Besucher-Kennung dient nur dazu, die Besucher eines Tages zu zählen.
## 3. Datenarten und Betroffene
(1) Solange der Besucher im Banner keine Wahl getroffen hat und sein Browser kein Signal nach Absatz 2 Satz 2 sendet: Pfad der aufgerufenen Seite (ohne Suchparameter und Anker), auch bei einem Seitenwechsel ohne Neuladen; Land (aus der IP-Adresse abgeleitet); Geräteklasse (Handy, Tablet oder Computer; aus dem Browser-Kennzeichen abgeleitet); Zeitpunkt (auf die volle Stunde gerundet); ein Zähler dafür, dass das Banner angezeigt wurde.
(2) Nach „Ablehnen“ sendet das Skript einmalig einen Zähler „abgelehnt“ und danach nichts mehr. Sendet der Browser die Signale „Global Privacy Control“ oder „Do Not Track“, zeigt das Skript kein Banner und sendet nichts.
(3) Nach „Annehmen“ zusätzlich: Domain der Website, von der der Besucher kam; Größenklasse des Browserfensters; eine täglich wechselnde pseudonyme Besucher-Kennung; Verweildauer (ab dem Laden der Seite) und Scrolltiefe je Seite; Klickpositionen mit einem Elementpfad aus Tag-Namen und Position (ohne Texte, Kennungen und Klassennamen); die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp oder Link auf eine andere Website, dann mit deren Domain); ein Zähler „angenommen“.
(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Skripts und bei jeder Messanfrage, auch nach „Ablehnen“. Der Messdienst verarbeitet sie nur kurz im Arbeitsspeicher, um Land, Geräteklasse, Bots, die Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung zu bestimmen. Er speichert und protokolliert sie nicht. Nicht erhoben werden Cookies, Formularinhalte, Seitentexte, Mausbewegungen, Tastatureingaben und Sitzungsaufzeichnungen.
(5) Tageswerte sind Summen je Tag und Merkmal (etwa Aufrufe je Seite, je Land oder je Geräteklasse), ohne Besucher-Kennung. Enthalten die Pfade der Website personenbezogene Angaben, können auch Tageswerte personenbezogen sein (Nummer 11 Abs. 2).
(6) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht gezielt verarbeitet. Je nach Inhalt der Website können sich aus Pfaden oder Domains aber Rückschlüsse ergeben (Nummer 11 Abs. 3).
(7) Betroffene: Besucher der Website des Kunden.
## 4. Weisungen
(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.
(2) Mit dem Einschalten weist der Kunde WEBklar an, die Website-Analyse in der Standardkonfiguration nach der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Einschalten, Ausschalten, Akzentfarbe des Banners). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die von der Standardkonfiguration abweichen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde die Funktion ausschalten.
(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.
## 5. Vertraulichkeit
WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.
## 6. Technische und organisatorische Maßnahmen
(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.
(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.
## 7. Unterauftragsverarbeiter
(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Messdienst läuft, und für dessen Sicherung. Ort der Verarbeitung ist das Rechenzentrum Falkenstein (Deutschland). Eine Übermittlung in Drittländer findet nicht statt.
(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde die Website-Analyse ausschalten und WEBklar sie für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Verlangen des Kunden sofort.
(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).
(4) Die Länderzuordnung nutzt eine Datenbank, die WEBklar auf dem eigenen Server speichert („IP to Country Lite“ von DB-IP.com, Lizenz CC BY 4.0). Daten der Besucher werden dabei nicht an DB-IP übermittelt. DB-IP ist kein Unterauftragsverarbeiter.
## 8. Unterstützung des Kunden, Betroffenenrechte
(1) WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.
(2) Die gespeicherten Messdaten enthalten keine IP-Adresse, keinen Namen und keine Kennung, die länger als einen Tag gilt; Zeitpunkte sind auf die volle Stunde gerundet. Einzelne Besucher kann WEBklar daher in der Regel nicht identifizieren (Art. 11 DSGVO). Ein Abgleich kann aber möglich sein, solange die Zugriffsprotokolle der Website (Anlage 1 und 2 der Abo-Bedingungen, 14 Tage) vorhanden sind. Nennt eine betroffene Person zusätzliche Angaben, etwa Datum, Uhrzeit, besuchte Seiten oder ihre IP-Adresse, prüft WEBklar auf Weisung des Kunden, auch anhand dieser Protokolle, ob eine Zuordnung möglich ist, und teilt ihm das Ergebnis mit.
(3) Der Messung können Besucher technisch widersprechen (Art. 21 DSGVO): Nach „Ablehnen“ und bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“ sendet das Skript nichts mehr.
(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht, und eine kurze Interessenabwägung für die Zählung nach Art. 6 Abs. 1 lit. f DSGVO, die WEBklar mit jeder Fassung dieses Vertrags bereitstellt.
(5) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.
(6) Zum Nachweis der Einwilligungen bewahrt WEBklar jede Fassung des Banner-Textes und der Zusatzseite mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende der Website-Analyse. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung. Ein Protokoll je Besucher führt WEBklar nicht.
(7) Die Besucher informiert der Kunde (Art. 13 DSGVO). WEBklar stellt ihm dafür die Zusatzseite „/datenschutz-analyse.html“ als Muster bereit. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar dort einen Hinweis auf die Zusatzseite ein; sonst erhält der Kunde den Hinweis als Textbaustein (Leistungsbeschreibung Nr. 4 Abs. 4).
## 9. Löschung und Rückgabe
(1) Einzelne Ereignisse und Klickpositionen löscht WEBklar automatisch nach 30 Tagen. Der Löschlauf läuft täglich, die Löschung erfolgt also in der Regel spätestens am 31. Tag; fällt ein Lauf aus, holt der nächste ihn nach.
(2) Tageswerte löscht WEBklar 25 Monate nach dem jeweiligen Tag. Nach dem Ende der Website-Analyse (Ausschalten oder Ende des Abos, das sie enthält) löscht WEBklar alle Daten der Website am 61. Tag. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse verlangen; WEBklar stellt sie innerhalb von zwei Wochen als CSV-Datei bereit. Auf Weisung des Kunden löscht WEBklar die Daten früher.
(3) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.
(4) Gesetzliche Aufbewahrungspflichten bleiben unberührt.
## 10. Nachweise und Kontrollen
WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.
## 11. Pflichten des Kunden
(1) Der Kunde ist Verantwortlicher für die Messung. Er entfernt, verdeckt oder umgeht das Banner nicht. Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess- oder Tracking-Werkzeuge ein.
(2) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
(3) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren oder lassen ihre Inhalte auf besondere Kategorien personenbezogener Daten schließen (etwa Gesundheit, Religion oder politische Meinung), gilt Nr. 3 Abs. 4 der Leistungsbeschreibung.
## 12. Haftung, Änderungen, Schlussbestimmungen
(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.
(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.
(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar die Website-Analyse für seine Website nach Nr. 8 Abs. 2 der Leistungsbeschreibung beenden oder aussetzen. Das Abo bleibt davon unberührt.
(4) Im Übrigen gilt § 16 der Abo-Bedingungen.
## Anlage: Technische und organisatorische Maßnahmen der Website-Analyse (Art. 32 DSGVO)
| Bereich | Maßnahmen |
|---|---|
| Übertragung | Skript und Messdaten werden nur über TLS (HTTPS) übertragen. |
| Speicher im Browser | Es werden keine Cookies gesetzt, und der Messdienst wertet keine Cookies aus. Messanfragen sendet das Skript ohne Cookies. Im lokalen Speicher des Browsers liegt nur die Banner-Wahl (Eintrag „wk-consent“ mit Wahl, Zeitpunkt und Fassung des Banner-Textes). Sie gilt 183 Tage; danach und bei einer neuen Fassung des Banner-Textes fragt das Banner erneut. |
| Einwilligung | Solange keine Wahl getroffen ist und kein Browser-Signal vorliegt, sendet das Skript nur den Pfad der Seite, auch beim Seitenwechsel ohne Neuladen, und den Zähler „Banner angezeigt“. Nach „Ablehnen“ sendet es einmalig den Zähler „abgelehnt“ und danach nichts mehr, auch nicht aus anderen geöffneten Tabs. Bei „Global Privacy Control“ oder „Do Not Track“ zeigt es kein Banner und sendet nichts. Klick-, Scroll- und Verweildauer-Ereignisse registriert es erst nach „Annehmen“. Die Schaltflächen „Ablehnen“ und „Annehmen“ sind gleich gestaltet. |
| Datenminimierung | IP-Adresse und Browser-Kennzeichen werden weder gespeichert noch protokolliert. Der Messdienst verwendet sie nur im Arbeitsspeicher für Land, Geräteklasse, Bot-Erkennung, Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung. Zeitpunkte werden auf die volle Stunde gerundet gespeichert. Pfade werden ohne Suchparameter und Anker gespeichert; Abschnitte, die wie Kennungen aussehen (lange Ziffernfolgen, UUIDs, E-Mail-Adressen), werden vorher ersetzt. Elementpfade bestehen nur aus Tag-Namen und Position. Formulareingaben und Seitentexte werden nicht erfasst. |
| Pseudonymisierung | Die Besucher-Kennung (nur nach Einwilligung) ist ein auf 64 Bit gekürzter HMAC-SHA256 aus Website-Schlüssel, IP-Adresse und Browser-Kennzeichen mit einem Tagesschlüssel. Auch der Schlüssel der Mengenbegrenzung wird mit dem Tagesschlüssel gebildet. Der Tagesschlüssel liegt nur im Arbeitsspeicher; die Auslagerung auf die Festplatte ist für den Messdienst gesperrt. Er wird beim ersten Ereignis eines neuen Kalendertags (Europe/Berlin) und bei jedem Neustart neu erzeugt und nie gesichert. Ältere Kennungen lassen sich danach nicht mehr zuordnen. |
| Zugriffskontrolle | Die Verwaltungsschnittstelle des Messdienstes (Auswertung, Website-Verwaltung) läuft auf einem internen Port, der nicht öffentlich erreichbar ist, und ist durch ein Zugriffstoken geschützt. Zugriff haben nur das Kundenportal und die Verwaltung von WEBklar. Das Portal zeigt jedem Kunden nur die Daten seiner eigenen Websites. |
| Heatmap | Die Heatmap öffnet ein signierter Link, den nur der angemeldete Kunde im Kundenportal erzeugen kann. Der Link gilt 30 Minuten und liefert nur Klickpunkte der gewählten Seite und Ansicht, ohne Zeitpunkte und Kennungen. |
| Eingangsprüfung | Messdaten werden nur angenommen, wenn die Website registriert und eingeschaltet ist und Origin bzw. Referer zu ihren Domains passen. Bots werden verworfen. Größe und Menge der Anfragen sind je Website und Absender begrenzt (Zähler nur im Arbeitsspeicher). Für ausgeschaltete Websites liefert der Messdienst ein leeres Skript aus. |
| Löschung | Täglicher Löschlauf um 02:30 Uhr (Europe/Berlin): Ereignisse und Klicks nach 30 Tagen, Tageswerte nach 25 Monaten, alle Daten einer Website 60 Tage nach dem Ausschalten. Gelöschte Datensätze werden in der Datenbankdatei überschrieben und aus dem Schreibprotokoll (WAL) entfernt. |
| Trennung | Der Messdienst läuft in einem eigenen Container mit eigenem Datenverzeichnis und hat keinen Zugriff auf andere Kundendaten. Alle Datensätze tragen den Website-Schlüssel; Abfragen erfolgen nur je Website. |
| Protokolle | Der Reverse-Proxy führt kein Zugriffsprotokoll, und der Messdienst protokolliert keine Anfragen. Wird ein Zugriffsprotokoll eingeschaltet, dann nicht für die Adresse des Messdienstes. Meldungen der Container werden nach 14 Tagen gelöscht. |
| Datensicherung | Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). Einen gesonderten Datenbank-Export des Messdienstes gibt es nicht. |
| Rechenzentrum | Hetzner Online GmbH, Rechenzentrum Falkenstein (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren. |
| Organisation | Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 5), Archiv der Fassungen von Banner-Text und Zusatzseite (Nummer 8 Abs. 6), Überprüfung der Maßnahmen mindestens einmal im Jahr. |
@@ -0,0 +1,23 @@
# Datenschutzerklärung: Prüfpunkte für die Kundenansicht neu
Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung
Diese Liste ändert die Datenschutzerklärung nicht. `public/datenschutz.html` bleibt unverändert (eingefroren); die Stellen unten sind die, die mit der neuen Kundenansicht und den Texten der Fassung 6 geprüft werden müssen. Die Änderung der Datenschutzerklärung erfolgt erst mit dem Umschalten (Entscheidung R14 in `docs/rechtliches/kundenansicht-entscheidungen.md`), als eigene Fassung, sonst würde sie sofort öffentlich. Zeilenangaben beziehen sich auf den Stand von `public/datenschutz.html` am 16.10.2026.
Je Prüfpunkt: **Fundstelle**, was dort steht, was sich ändert, und die Frage an den Inhaber oder die Kanzlei.
**D1** · Fundstelle `public/datenschutz.html:120`. Nachweis bei der Bestellung einer Funktion. Dort steht der „Klick auf ‚Zahlungspflichtig bestellen‘“, die Stripe-Sitzung und eine Funktionsliste nur mit News-Pinnwand und Terminplaner. Künftig löst das Einschalten einer Funktion im Abo keine Zahlung aus und hat keine Stripe-Sitzung; gespeichert werden der Wortlaut der angezeigten Zeilen, die Fassungen, Zeitpunkt des Klicks auf „Einschalten“, IP-Adresse, Browserkennung, Konto, angemeldete Person und die Kundenart (Fassungen: Abo-Bedingungen 2026-10-16-v6, Funktionsunterlagen 2026-10-16). Die Funktionsliste muss alle Funktionen nennen (News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live, Website-Analyse). Frage: Neuer Absatz „Funktion einschalten“ neben dem Absatz für Bestellungen, damit der Bestand (Funktionen, die einzeln gekauft wurden) unverändert beschrieben bleibt?
**D2** · Fundstelle `public/datenschutz.html:119`. Nachweis bei Abo, Wechsel und Zusatzspeicher. Dort stehen „gegebenenfalls Ihre Angaben zum Leistungsbeginn“ sowie Zeitpunkt, Version, IP-Adresse und Browserkennung. Künftig kommen der Wortlaut der angezeigten Zeilen (Linkzeile, Häkchen für Privatkunden) und die Kundenart zum Klickzeitpunkt hinzu (§ 2 Abs. 6 der Abo-Bedingungen Fassung 6). Frage: Genügt es, die Kundenart in den Aufzählungssatz aufzunehmen, oder braucht sie wegen D3 einen eigenen Hinweis?
**D3** · Fundstelle `public/datenschutz.html:81`. Abschnitt 6 „Kundenportal“, Kundenkonto-Absatz in der Zeile darunter. Die Kundenart („Betrieb“ oder „privat“) ist ein neues Datum im Kundenkonto. Das Team legt sie nach den Angaben des Kunden fest (Entscheidung E3); ist sie leer, gilt der Kunde als privat (E4). Zu ergänzen sind Zweck (Bestellseiten richten sich danach, Nachweis), Rechtsgrundlage (Art. 6 Abs. 1 lit. b und f DSGVO), Quelle (Angaben des Kunden an das Team) und Dauer (wie das Kundenkonto; im Nachweis wie in D1). Frage: Eigener Satz im Kundenkonto-Absatz oder in Abschnitt 7?
**D4** · Fundstelle `public/datenschutz.html:121`, ergänzend `public/datenschutz.html:108` und `public/datenschutz.html:114`. Der Stripe-Absatz beschreibt Zahlungen für Abos und Zusatzspeicher; das Einschalten einer Funktion im Abo löst keine Zahlung und keine Übermittlung an Stripe aus. Die Abschnitte zu Stripe Connect beim Webshop und beim Buchungskalender (Zahlungen der Käufer und Gäste auf das eigene Stripe-Konto des Kunden) bleiben inhaltlich wie sie sind. Frage: Genügt ein Satz, dass Funktionen im Abo ohne Zahlung eingeschaltet werden, oder sollen die Abschnitte nur an der Einleitung („bestellen“ statt „einschalten“) angepasst werden?
**D5** · Fundstelle `public/datenschutz.html:95`. Abschnitt zur Website-Analyse: „Kunden mit Wartungs-Abo“ können die Reichweitenmessung einschalten. Künftig steckt die Website-Analyse im Hosting-Abo und im Wartungs-Abo (Leistungsbeschreibung Website-Analyse Nr. 1 Abs. 1). Der Absatz muss beide Abos nennen und beschreiben, dass die Funktion mit dem Abo endet (Löschfrist 60 Tage, AVV Website-Analyse Nr. 9). Frage: Reicht eine Anpassung der Einleitung?
**D6** · Fundstellen `public/datenschutz.html:98` (Terminplaner), `public/datenschutz.html:106` (Webshop), `public/datenschutz.html:112` (Buchungskalender), außerdem der Abschnitt zur News-Pinnwand und zu Öffnungszeiten live. Die Funktionsabschnitte sprechen von „Kunden mit Wartungs-Abo“, vom „Freischalten“ und vom Ende mit dem Vertrag. Künftig gilt: Funktionen sind Teil des Wartungs-Abos, enden mit dem Abo (Terminplaner, News-Pinnwand, Webshop und Buchungskalender spätestens am Folgetag, Öffnungszeiten live nach 30 Tagen Weiterlauf) und werden nach den Fristen der Leistungsbeschreibungen gelöscht (30 Tage Terminplaner, News-Pinnwand und Öffnungszeiten live; 90 Tage Webshop und Buchungskalender). Frage: Die Fristen in den Abschnitten und in Abschnitt 7 (Speicherdauer) müssen übereinstimmen; soll Abschnitt 7 die Fristen künftig als Tabelle führen?
**D7** · Fundstelle `public/datenschutz.html:131`, ergänzend die Beschreibung des Stundenkontingents im Kundenkonto-Absatz. Das Stundenkonto und der Vorschuss für die Einrichtung (§ 4 Abs. 8 der Abo-Bedingungen Fassung 6) sind Abrechnungsdaten: Arbeitszeit, Verbrauch, Vorschuss, Ablaufdaten. Sie stehen heute unter „gegebenenfalls Ihr Stundenkontingent“. Frage: Ist der Vorschuss als Abrechnungsdatum mit demselben Zweck, derselben Rechtsgrundlage (Art. 6 Abs. 1 lit. b DSGVO) und derselben Dauer ausreichend beschrieben, oder braucht er einen eigenen Satz?
**D8** · Fundstelle `public/datenschutz.html:123`. Abschnitt zu den Seiten Kündigen und Widerrufen. Die Seiten erscheinen in der neuen Kundenansicht in der du-Form (Entscheidung R16) und zeigen angemeldeten Kunden die laufenden Widerrufsfristen aus bereits vorhandenen Daten (Sammelabfrage der Fristen im Kundenportal, `GET /api/fristen`). Das ist ein Lesen vorhandener Daten ohne neues Datum und ohne neuen Zweck; der Absatz bleibt inhaltlich unverändert. Frage: Genügt die Feststellung, dass kein neuer Absatz nötig ist, oder soll die Anzeige der Fristen im Konto erwähnt werden?
@@ -0,0 +1,143 @@
# Rechtsrunde Fassung 6: Entscheidungen, Kanzlei-Fragen und Ablauf
Stand 16.10.2026 · Entwurf – Prüfung durch Kanzlei offen · Entwurf zur Freigabe, keine Rechtsberatung
Dieses Blatt ist die Grundlage für die Freigabe der Rechtstexte der Kundenansicht neu durch den Inhaber (Entscheidung E20). Die Kanzlei-Fragen halten den Livegang nicht auf; die Fragen aus den früheren Rechtsrunden stehen in `/root/reports/kundenansicht-neu-2026-10-15/bestand/recht.md` § 4. Die Entwürfe liegen unter `docs/rechtliches/*-v6.md`, die Seiten unter `public/neu/rechtliches/` (Abo-Bedingungen: `abo-bedingungen.html`, Funktionsunterlagen: `<slug>-2026-10-16.html`). Datenschutz-Prüfpunkte D1 bis D8 stehen in `docs/rechtliches/datenschutz-kundenansicht-pruefpunkte.md`.
## Zu entscheiden
| Nr. | Punkt | Vorschlag | Alternative | Kanzlei-Frage |
|---|---|---|---|---|
| R1 | Fassungsnummer und Datum | Abo `2026-10-16-v6`, Funktionen `2026-10-16`; bei Freigabe auf den Freigabetag setzen | Entwurfstag bleibt (keine Umbenennung der Dateien und Fassungen) | – |
| R2 | Geltung für laufende Abos v3 bis v5 (C11) | nur neue Abschlüsse (§ 15 Abs. 2); Funktionen gelten über Linkzeile und Klick, auch mit der Stundenkonto- und Vorschuss-Regel; keine Pflicht-Info-Mail, eine freiwillige Mail später | Info-Mail an laufende Abos mit Bitte um Zustimmung nach § 15 | KQ1 |
| R3 | Vorschuss-Regel (E17) | § 4 Abs. 8 und jede Leistungsbeschreibung außer der Analyse: echte Arbeitszeit, nie Mehraufwand, ein Rest bei Vertragsende verfällt zugunsten des Kunden | Obergrenze (zum Beispiel 3 Stunden) mit Rückfrage darüber | KQ2 |
| R4 | Einrichtung beim Projektstart (E9, E21) | § 4 Abs. 9 und Baustein „projekt“ der Leistungsbeschreibungen: Abrechnung über die Projektkosten, die Funktion läuft nur mit dem passenden Abo | nur in den Leistungsbeschreibungen, nicht in den Abo-Bedingungen | KQ8 |
| R5 | Kundenart als Grundlage (E3, E4) | § 1 Abs. 2: hinterlegt nach Angabe des Kunden, leer gilt als privat, maßgeblich ist der Zweck des Vertrags | Selbstauskunft des Kunden in den Bedingungen | KQ4 |
| R6 | Häkchen-Wortlaut für Abo, Wechsel und Einrichtung | Texte aus `WORTLAUT_V6.beginn` ohne Paragrafenzitate; Betriebe setzen nichts | Zitate §§ 356 Abs. 5 Nr. 2, 357a Abs. 2 BGB im Häkchen | KQ5 |
| R7 | Einrichtung in der Widerrufsfrist | Teil des Abos, kein eigener Vertrag; Widerruf beendet die Funktion; Wertersatz nach aufgewendeter Einrichtungszeit zum Stundensatz des Abos | WEBklar verzichtet auf Wertersatz für die Einrichtung (wie beim Zusatzspeicher), das Häkchen für die Einrichtung entfällt | KQ3 |
| R8 | Ende mit dem Abo, Fristen, kein „Behalten“ (C12); mit P4/P6 abgestimmt | Terminplaner, News, Webshop und Buchungskalender schalten spätestens am Folgetag ab (Tagesjob), die Analyse wie bisher; Öffnungszeiten live läuft nach dem Ende noch 30 Tage unverändert weiter und wird dann abgeschaltet (kein Behalten gegen 30 €); Eingaben danach: Terminplaner, News und Öffnungszeiten 30 Tage, Webshop und Buchungskalender 90, Analyse 60; ein neues Abo in der Frist holt die Funktion zurück | Öffnungszeiten live schalten wie die übrigen spätestens am Folgetag ab (P4 Text Z2, P6 Mail K40 und der Zeiten-Abgleich müssten dafür geändert werden) | KQ8 |
| R9 | Analyse im Hosting-Abo (C10) | in beiden Abos, Einrichtung kostenlos (C6): § 3 Abs. 8, § 6 Abs. 6, Leistungsbeschreibung A | nur im Wartungs-Abo, wie heute | KQ8 |
| R10 | AVV-Struktur | Option A: die AVV je Funktion bleiben eigene Verträge mit neuer Vorbemerkung, Anlage 1 bleibt der Rahmen (§ 14 Abs. 2) | B: Anlagen 4 bis 7 zu den Abo-Bedingungen; C: Rahmen-AVV mit einer Anlage je Funktion | KQ7 |
| R11 | Widerrufsbelehrung der Funktionen | bleibt als Datei für den Bestand, die neuen Texte verlinken sie nicht | bleibt Anlage der Leistungsbeschreibungen | KQ6 |
| R12 | Anrede der Rechtsdokumente (E15, Spec 5.7) | bleiben im bisherigen Stil („der Kunde“, in der Übersichtsbox „Ihr“); nur Oberflächentexte stehen in „du“ | Übersichtsbox und Leistungsbeschreibungen auf „du“ | – |
| R13 | Webshop nur für Betriebe, Terminplaner nicht im Gesundheitsbereich (C15) | steht in den Leistungsbeschreibungen, das Team prüft bei der Freigabe | Webshop auch für Privatkunden mit Hinweis | KQ4, KQ9 |
| R14 | Datenschutzerklärung | Prüfpunkte D1 bis D8; `public/datenschutz.html` wird erst mit dem Umschalten als eigene Fassung geändert (sonst sofort öffentlich) | sofort ändern | – |
| R15 | Zusatzspeicher und Übernahmegutschrift | Zusatzspeicher: Linkzeile ohne Häkchen (§ 10 Abs. 3 und Hinweise zum Widerruf Nr. 4: kein Wertersatz); Gutschrift: Satz über dem Knopf (P7) | das Häkchen bleibt | KQ5 |
| R16 | Du-Anrede der Seiten Kündigen und Widerrufen (N38) | beide Seiten sprechen ab Fassung 6 „du“; sie speichern keinen Wortlaut und haben keine eigene Version (C14); Belehrung und Muster-Formular folgen R12 | Sie-Form bleibt bis zur Freigabe von Belehrung und Formular | KQ6 |
| R17 | Zeilen „Anbieter“ und „Abrechnung“ im Block der Bestellseiten (N37) | beide Zeilen entfallen im Block; der Anbieter steht im Impressum im Konto und in § 1 der Bedingungen, die Abrechnung (monatlich im Voraus per Stripe) in den Zeilen zu Preis und Fälligkeit und in § 7 | beide Zeilen bleiben im Block über dem Knopf | KQ6 |
## Fragen an die Kanzlei
Jede Frage hat vier Teile wie in `docs/rechtliches/zeiten-kanzlei-fragen.md`: **Frage** (was wir wissen möchten), **Fundstelle im Text** (wo der Wortlaut steht), **Unsere Annahme** (der Stand, mit dem wir arbeiten; bitte bestätigen oder korrigieren), **Alternative** (was wir stattdessen täten). Annahmen sind keine Rechtsurteile.
### KQ1: Geltung der Fassung 6 und Information an laufende Abos
**Frage.** Fassung 6 gilt nur für neue Abschlüsse. Wer ein laufendes Abo nach Fassung 3 bis 5 hat und eine Funktion einschaltet, stimmt Leistungsbeschreibung und Auftragsverarbeitung der Funktion in der angezeigten Fassung durch den Klick auf „Einschalten“ zu; dazu gehören die Regeln zur Einrichtung (Stundenkontingent) und zum Vorschuss. Reicht der Klick, um diese Regeln in das laufende Abo einzubeziehen, oder braucht es eine Zustimmung nach dem bisherigen § 15 (Mitteilung mit sechs Wochen Frist, Zustimmung)? Muss WEBklar laufende Kunden über Fassung 6 informieren, obwohl sie für sie nur vorteilhaft ist?
**Fundstelle im Text.** § 15 Abs. 2 der Abo-Bedingungen Fassung 6; § 3 Abs. 8; § 4 Abs. 8; Nr. 1 Abs. 1 der Leistungsbeschreibungen (Baustein „geltung“).
**Unsere Annahme.** Der Klick genügt, weil jede Funktion ein zusätzliches Angebot ohne Gebühr ist und die Änderung des Abos nur für neue Abschlüsse gilt. Eine Info-Mail ist keine Pflicht; wir würden später freiwillig informieren.
**Alternative.** Info-Mail an alle laufenden Abos mit Bitte um Zustimmung nach § 15; Funktionen für Altkunden erst nach dieser Zustimmung.
### KQ2: Vorschuss-Regel für die Einrichtung
**Frage.** Die Einrichtung einer Funktion geht mit der tatsächlich aufgewendeten Zeit vom Stundenkontingent ab. Reicht es nicht, entsteht ein Vorschuss, den die folgenden Monatskontingente ausgleichen; ein Rest bei Vertragsende verfällt zugunsten des Kunden, und es wird nie Mehraufwand berechnet. Ist das transparent und wirksam, auch gegenüber Verbrauchern (Überraschungsklausel, Kontingent als Vorleistung)? Braucht es eine Obergrenze oder eine Rückfrage?
**Fundstelle im Text.** § 4 Abs. 2 und 8 der Abo-Bedingungen Fassung 6; Nr. 5 Abs. 2 (Terminplaner), Nr. 3 Abs. 1 (News-Pinnwand), Nr. 4 Abs. 1 (Öffnungszeiten live), Nr. 9 Abs. 1 (Webshop, Buchungskalender).
**Unsere Annahme.** Die Regel ist für den Kunden günstig (keine zusätzliche Rechnung, Verfall zu seinen Gunsten) und wird im Kundenportal mit „Vorschuss für Einrichtung“ angezeigt.
**Alternative.** Obergrenze (zum Beispiel 3 Stunden) mit Rückfrage darüber; oder Einrichtung nur, soweit das Kontingent reicht.
### KQ3: Einrichtung in der Widerrufsfrist
**Frage.** Das Einschalten einer Funktion ist nach unserem Text Teil des Abos und kein eigener Vertrag; es verlängert die Widerrufsfrist nicht. Ist das richtig, oder ist die Einrichtung eine eigene Leistung mit eigenem Widerrufsrecht? Erlischt das Widerrufsrecht nach § 356 Abs. 5 Nr. 2 BGB bei einem Dauerschuldverhältnis überhaupt durch „vollständige Erbringung“, und wie bemisst sich der Wertersatz nach § 357a Abs. 2 BGB für Einrichtungszeit innerhalb der Frist?
**Fundstelle im Text.** Hinweise zum Widerruf Nr. 5 und § 2 Abs. 2 c der Abo-Bedingungen Fassung 6; Baustein „widerruf“ der Leistungsbeschreibungen; `WORTLAUT_V6.beginn.einrichtung` in `server/services/products.js`.
**Unsere Annahme.** Teil des Abos; Wertersatz nach aufgewendeter Einrichtungszeit zum Stundensatz des Abos; das Häkchen gibt es nur für Privatkunden in der Frist.
**Alternative.** WEBklar verzichtet auf Wertersatz für die Einrichtung (wie beim Zusatzspeicher nach § 10 Abs. 3) und das Häkchen für die Einrichtung entfällt.
### KQ4: Belastbarkeit der Kundenart
**Frage.** Das Team hinterlegt die Kundenart („Betrieb“ oder „privat“) nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat; maßgeblich bleibt der Zweck des Vertrags (§ 1 Abs. 2). Wie belastbar ist das, wenn sich später ein Verbraucher als Betrieb geführt herausstellt (Existenzgründer, Vereine, Freiberufler)? Wer trägt das Risiko einer fehlenden Belehrung? Das ist die Frage 5 aus `recht.md` § 4.
**Fundstelle im Text.** § 1 Abs. 2 und § 2 Abs. 2 c der Abo-Bedingungen Fassung 6; `WORTLAUT_V6.beginn`; Nr. 1 Abs. 1 und Nr. 5 Abs. 1 der Leistungsbeschreibung Webshop.
**Unsere Annahme.** Unsere Kunden sind fast nur Betriebe (E2). Widerrufsbelehrung, Muster-Formular und die Funktion „Vertrag widerrufen“ bleiben für alle erreichbar; nur das Häkchen und der Beginn nach der Frist hängen an der Kundenart.
**Alternative.** Selbstauskunft des Kunden bei der Bestellung („Ich bestelle als Unternehmer“) oder das Häkchen für alle Kunden.
### KQ5: Häkchentexte ohne Normzitat, Wechsel nur mit Häkchen
**Frage.** Dürfen die Häkchen für den Sofortbeginn auf zwei bis drei Sätze ohne Paragrafenzitate gekürzt werden, ohne die Information nach Art. 246a § 1 Abs. 3 EGBGB über das Erlöschen und den Wertersatz zu verlieren (Frage 2 aus `recht.md` § 4)? Ist es zulässig, dass der Wechsel ins Wartungs-Abo für Privatkunden nur mit Häkchen möglich ist, weil er sofort wirkt? Genügt beim Zusatzspeicher und bei der Übernahmegutschrift eine Linkzeile ohne Häkchen, weil WEBklar auf Wertersatz verzichtet?
**Fundstelle im Text.** `WORTLAUT_V6.beginn` (Abo, Wechsel, Einrichtung); § 2 Abs. 2 c und § 9 Abs. 6 der Abo-Bedingungen Fassung 6; § 10 Abs. 3 und Hinweise zum Widerruf Nr. 4.
**Unsere Annahme.** Die Wortlaute der Konstante genügen; für Privatkunden ist der Wechsel ohne Häkchen gesperrt; Zusatzspeicher und Gutschrift brauchen kein Häkchen.
**Alternative.** Zitate §§ 356 Abs. 5 Nr. 2, 357a Abs. 2 BGB im Häkchen; Wechsel für Privatkunden mit Beginn nach Ablauf der Frist.
### KQ6: Belehrung und Pflichtinformationen per Link und PDF, Anrede der Seiten Kündigen und Widerrufen
**Frage.** Genügt der Link auf Widerrufsbelehrung und Muster-Formular vor der Bestellung, wenn der Volltext erst im Bestätigungs-PDF steht (Frage 4 aus `recht.md` § 4)? Passen Belehrung und Formular zum Gestaltungshinweis in Anlage 1 EGBGB zu § 356a? Dürfen die Zeilen „Anbieter“ und „Abrechnung“ im Block über dem Knopf entfallen, solange der Anbieter im Impressum und in § 1 steht und die Abrechnung in den Zeilen zu Preis und Fälligkeit? Ändert die Du-Anrede der Seiten Kündigen und Widerrufen etwas an der Wirksamkeit von Kündigung (§ 312k BGB) und Widerruf (§ 356a BGB)?
**Fundstelle im Text.** § 2 Abs. 2 c, Widerrufsbelehrung, Muster-Widerrufsformular und Hinweise zum Widerruf der Abo-Bedingungen Fassung 6; Seiten `kuendigen.html` und `widerrufen.html` in der neuen Ansicht; Block „Was du bestellst“ der Bestellseiten.
**Unsere Annahme.** Der Link genügt, das PDF kommt vor Leistungsbeginn per E-Mail (§ 312f BGB); Anbieter und Abrechnung sind an anderer Stelle erreichbar; die Du-Anrede ändert die Wirksamkeit nicht.
**Alternative.** Volltext der Belehrung auf der Bestellseite; Zeilen „Anbieter“ und „Abrechnung“ bleiben im Block; Sie-Form der beiden Seiten.
### KQ7: AVV per Klick auf die Linkzeile, Struktur A, B oder C
**Frage.** Genügt der Klick auf „Einschalten“ unter der Linkzeile als Abschluss des Auftragsverarbeitungsvertrags nach Art. 28 Abs. 3 und 9 DSGVO („schriftlich, auch elektronisch“), ohne eigenes Häkchen (Frage 3 aus `recht.md` § 4)? Welche Struktur ist am sichersten: A (eigene Verträge je Funktion), B (Anlagen zu den Abo-Bedingungen) oder C (Rahmen-AVV mit Anlage je Funktion)?
**Fundstelle im Text.** § 14 Abs. 2 der Abo-Bedingungen Fassung 6; Vorbemerkung Abs. 1 jedes AVV in `docs/rechtliches/avv-*-v6.md`; Abschnitt „AVV-Struktur“ unten.
**Unsere Annahme.** Der Klick genügt, weil Wortlaut, Fassung, Zeit, Konto und angemeldete Person gespeichert werden; wir bleiben bei Option A.
**Alternative.** Ein eigenes Häkchen für den AVV; Option B oder C.
### KQ8: Funktionen als Teil des Abos, Kopplung an das Abo-Ende, Rückkehr in der Frist
**Frage.** Sind die Funktionen als Teil der Abo-Leistung wirksam geregelt (§ 3 Abs. 8), einschließlich der Kopplung an das Abo (§ 11 Abs. 4: Ende mit dem Abo, Weiterlauf von Öffnungszeiten live für 30 Tage, Löschfristen von 30, 60 und 90 Tagen)? Ist es zulässig, dass ein neues Abo in der Frist die Funktion mit den vorhandenen Daten ohne neue Zustimmung zurückholt? Tragen die Analyse im Hosting-Abo (C10) und die Abrechnung der Einrichtung beim Projektstart über die Projektkosten (E9, E21) ohne weitere Hinweise?
**Fundstelle im Text.** § 3 Abs. 8, § 4 Abs. 9 und § 11 Abs. 4 der Abo-Bedingungen Fassung 6; Abschnitt „Ende der Leistung“ jeder Leistungsbeschreibung; Nr. 10 Abs. 5 der Leistungsbeschreibungen Webshop und Buchungskalender („auf Wunsch in Textform“).
**Unsere Annahme.** Die Regeln sind als Leistungsbeschreibung wirksam; die Rückkehr in der Frist holt die Funktion zurück. Offen ist für uns, ob die Rückkehr bei Webshop und Buchungskalender wie heute „auf Wunsch in Textform“ oder automatisch geschehen soll.
**Alternative.** Öffnungszeiten live wie die übrigen Funktionen am Folgetag abschalten (R8); Rückkehr nur auf Wunsch in Textform; einheitliche Löschfrist von 30 Tagen.
### KQ9: Ausschlüsse als Vertragsklausel
**Frage.** Sind die Ausschlüsse in den Leistungsbeschreibungen als Leistungsgrenzen wirksam: Terminplaner nicht im Gesundheitsbereich, Webshop nur für Betriebe und ohne die ausgeschlossenen Waren, Buchungskalender nur für Unternehmer? Gilt das auch, wenn die Prüfung beim Einschalten nur durch das Team oder durch eine Selbstangabe im Fragebogen erfolgt? Darf WEBklar bei Verstößen abschalten?
**Fundstelle im Text.** Nr. 4 Abs. 3 der Leistungsbeschreibung Terminplaner; Nr. 1 Abs. 1, Nr. 3 und Nr. 5 Abs. 1 der Leistungsbeschreibung Webshop; Nr. 1 Abs. 1 der Leistungsbeschreibung Buchungskalender.
**Unsere Annahme.** Die Ausschlüsse sind als Leistungsbeschreibung wirksam; das Team prüft bei der Freigabe (R13).
**Alternative.** Webshop auch für Privatkunden mit Hinweis (C15 verschoben); keine automatische Abschaltung, nur ein Hinweis in Textform.
## AVV-Struktur
Die Auftragsverarbeitung der Funktionen ist in Fassung 6 so geregelt, dass der Kunde sie mit dem Klick auf „Einschalten“ unter der Linkzeile abschließt. Drei Aufbauten sind möglich; empfohlen wird Option A.
**Option A, empfohlen: eigene Verträge je Funktion, Anlage 1 bleibt der Rahmen.** Die AVV Terminplaner, Webshop, Buchungskalender und Website-Analyse bleiben eigene Dokumente mit einer neuen Vorbemerkung (Abschluss per Klick) und gelten neben Anlage 1 der Abo-Bedingungen (§ 14 Abs. 2). Vorteil: jede Funktion hat ihre eigenen Datenarten, Fristen und Unterauftragsverarbeiter, und die bestehenden Verträge ändern sich kaum. Nachteil: vier Einzelverträge, deren Gleichlauf mit Anlage 1 bei jeder Änderung geprüft werden muss.
**Option B: Anlagen 4 bis 7 zu den Abo-Bedingungen.** Die AVV der Funktionen werden Anlagen der Abo-Bedingungen. Vorteil: ein Dokument mit einer Fassung, der Kunde stimmt den Bedingungen einmal zu. Nachteil: die Abo-Bedingungen werden lang und müssen bei jeder Änderung einer Funktion neu versioniert werden; Funktionen, die ein Kunde nie nutzt, stehen trotzdem in seinem Vertrag.
**Option C: Rahmen-AVV mit einer Anlage je Funktion.** Ein neuer Rahmenvertrag regelt das Gemeinsame (Weisungen, Sicherheit, Löschung), die Anlagen je Funktion nennen nur Daten und Unterauftragsverarbeiter. Vorteil: einheitliche Regeln, bei Änderungen ändert sich nur die Anlage. Nachteil: ein neuer Vertrag neben Anlage 1, abzustimmen mit der Kanzlei, und mehr Arbeit als jetzt nötig.
## Ablauf nach der Freigabe
Die Reihenfolge ist fest; jeder Schritt erst, wenn der vorige grün ist. Die Skripte laufen nur auf ausdrückliche Anweisung des Controllers nach der Freigabe.
1. Freigabe des Inhabers je Punkt (R1 bis R17). Die Antworten auf die Kanzlei-Fragen KQ1 bis KQ9 halten den Livegang nicht auf (E20). Entschiedene Änderungen werden zuerst in den Texten und Tests nachgezogen.
2. Datum der Fassungen auf den Freigabetag setzen: `config.abo.termsVersionNeu` (Abo), `FUNKTIONEN_FASSUNG_V6` in `server/services/products.js`, die Datei- und Testnamen mit dem Datum (`<slug>-2026-10-16`), die Fassungszeilen der Markdown-Dateien, `Version 6 · Stand …` und den Druckkopf in der Abo-Seite.
3. Entwurfsmarke aus allen Dateien entfernen (Fassungszeilen der Markdown-Dateien, der Absatz unter der Kopfzeile und das meta robots in der Abo-Seite), `ENTWURF_V6 = false` in `scripts/build-legal-html.mjs`, und `ENTWURF = false` in `test/aboBedingungenV6.test.js`, `test/funktionenRechtV6.test.js` und `test/kundenansichtEntscheidungen.test.js`. Auch der Vermerk im Kopf dieser Datei und der Prüfpunkte-Datei fällt weg.
4. `node --max-old-space-size=256 scripts/build-legal-html.mjs funktionen6` erzeugt die zehn Seiten neu; `scripts/build-legal-html.mjs funktionen6 --check` meldet danach nichts.
5. `free -m` (mindestens 2000 MB verfügbar), dann `nice -n 10 scripts/build-legal-pdfs.sh abo6` und `nice -n 10 scripts/build-legal-pdfs.sh funktionen6`, einzeln und nie parallel (`FORCE=1` nur nach einer Textänderung). Die PDFs und die `.sha256`-Dateien werden mit committet.
6. `node scripts/rechtliches-hochladen.mjs --probe`. Meldet der Vorflug `STOPP: Karte fehlt: …` (laut `setup-legal.mjs` des WOMS fehlen drei der elf Karten: News, Website-Analyse und AVV Website-Analyse), zuerst `node scripts/rechtliches-hochladen.mjs --karten-anlegen`, danach den Vorflug wiederholen. Ist er grün, folgt ein Aufruf je Dokument (elf Aufrufe): `node scripts/rechtliches-hochladen.mjs <documentKey> --freigabe=JJJJ-MM-TT`. Zuletzt `node scripts/rechtliches-hochladen.mjs --pruefen`: Exit 0 heißt, alle elf Dokumente haben eine aktuelle Version mit passender Quelle; sonst weiter bei Schritt 6.
7. Erst danach `KUNDENANSICHT_NEU` auf `on` (P10); P10 prüft vorher, dass `WORTLAUT_STATUS` auf `'freigegeben'` steht und die Gleichheitstests grün sind (Nachtrag N48). Die Verdrahtung der Analyse (`config.analyse.avvVersion`, `legalPaths`, die Weiche der Mail K18) folgt der Freigabe (Nachtrag N33); P10 verschiebt Seite und PDF der Abo-Bedingungen nach `public/`.
@@ -0,0 +1,45 @@
# Leistungsbeschreibung News-Pinnwand
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Funktion im Sinne dieser Leistungsbeschreibung ist die News-Pinnwand. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die News-Pinnwand diese Leistungsbeschreibung vor.
## 2. Leistung
(1) WEBklar baut die News-Pinnwand in die Website ein, die der Kunde für das gewählte Projekt bei WEBklar im Kundenportal führt. Die Pinnwand zeigt die Beiträge des Kunden als Zettel mit Titel, Text und Foto; pro Seite erscheinen sechs Beiträge, die neuesten zuerst.
(2) Die Beiträge schreibt, ändert und löscht der Kunde selbst im Kundenportal im Bereich „Funktionen“. Er wählt je Beitrag Zettelfarbe und Anzeigedauer; danach verschwindet der Beitrag von seiner Website. Hochgeladene Fotos werden verkleinert, alle Metadaten einschließlich Aufnahmeort (GPS) werden entfernt.
(3) Änderungen an der Website, die über den Einbau der Pinnwand hinausgehen, sind nicht Teil dieser Leistung.
## 3. Einrichtung
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
(2) Der Einbau ist erfahrungsgemäß meist innerhalb von 1–2 Wochen abgeschlossen. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins. Nach Abschluss des Einbaus schaltet WEBklar die Funktion frei; ab dann erscheinen die Beiträge auf der Website.
(3) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.
## 4. Pflichten des Kunden
(1) Der Kunde ist für die Inhalte seiner Beiträge und Fotos verantwortlich. Er versichert, dass er die nötigen Rechte an Texten und Fotos hat und dass die Inhalte keine Rechte Dritter verletzen, etwa Urheberrechte, Persönlichkeitsrechte oder das Recht am eigenen Bild abgebildeter Personen.
## 5. Ende der Leistung
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.
(2) Auf Wunsch des Kunden schaltet WEBklar die Pinnwand jederzeit ab; für den Widerruf gilt Nr. 6.
(3) Beiträge und Fotos bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt in dieser Zeit wieder ein Wartungs-Abo, schaltet WEBklar die Pinnwand mit den vorhandenen Beiträgen ohne weitere Kosten wieder ein. Der Kunde kann seine Beiträge und Fotos jederzeit selbst löschen; andernfalls gelten die Fristen der Datenschutzerklärung (Abschnitt 6a).
## 6. Widerruf
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
@@ -0,0 +1,125 @@
# Leistungsbeschreibung Terminplaner
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Funktion im Sinne dieser Leistungsbeschreibung ist der Terminplaner. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Terminplaner.
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Terminplaner diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Terminplaner.
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für den Datenschutz-Absatz nach Nr. 4 Abs. 9 und die Gästetexte nach Nr. 2 Abs. 6. Beides ist Teil des Terminplaners.
## 2. Leistung
(1) Der Terminplaner ist ein Buchungs-Widget für die Website des Kunden mit einer Verwaltung im Kundenportal. WEBklar baut je Website-Projekt ein Widget an einer Stelle der Website ein, entweder eingebettet in die Seite oder hinter einem Knopf. Die Gestaltung richtet sich nach dem Fragebogen, den der Kunde vor dem Einschalten im Kundenportal beantwortet.
(2) Der Kunde legt seine Leistungen, seine Personen oder Plätze, seine Zeiten und seine Regeln für Buchungen fest. Gäste buchen im Widget einen Termin; je nach Einstellung ist die Buchung verbindlich oder eine Anfrage.
(3) Der Terminplaner versendet im Auftrag des Kunden E-Mails an die Gäste: den Bestätigungslink, die Bestätigung, Änderungen, Absagen, auf Wunsch des Gastes eine Erinnerung (nur mit seinem Haken) und die Eingangsbestätigung eines Widerrufs. Den Kunden benachrichtigt er über neue und geänderte Buchungen.
(4) Der Kunde sieht und verwaltet seine Termine im Kundenportal und kann sie als CSV-Datei exportieren. Dazu erhält er einen geheimen Kalender-Link (ICS), mit dem er seine Termine in einem Kalenderprogramm abonnieren kann.
(5) Der Terminplaner enthält einen Missbrauchsschutz gegen automatisierte Buchungen. Die Löschfristen für Buchungsdaten nennt Nummer 9 des AVV Terminplaner.
(6) Bezahlt wird nur vor Ort. Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz für die Website des Kunden und die Widerrufsbelehrung für Gäste sind Mustertexte der Standardkonfiguration (Nr. 11 Abs. 1).
## 3. Nicht enthalten
(1) Nicht enthalten sind SMS, Online-Zahlung, ein Kalender-Abgleich (es gibt nur das Abonnement des Kalender-Links nach Nr. 2 Abs. 4), Konten für Gäste, Marketing-E-Mails, Gruppentermine, Mehrsprachigkeit und der Einsatz im Gesundheitsbereich (Nr. 4 Abs. 3).
(2) Zusätzliche Wünsche, etwa ein zweites Widget, Knöpfe je Leistung oder ein Sonderdesign, gehören nicht zur Einrichtung nach Nr. 5. WEBklar führt sie nach Stunden aus (§ 4 der Abo-Bedingungen).
## 4. Voraussetzungen und Pflichten des Kunden
(1) Der Kunde hat ein laufendes Wartungs-Abo.
(2) Der Kunde trägt vor dem Einschalten seine Betriebsangaben ein, dazu die Adresse seines Impressums und die Adresse seiner Datenschutzerklärung.
(3) Der Terminplaner ist nicht für Angebote im Gesundheitsbereich bestimmt, weil dort besondere Kategorien personenbezogener Daten anfallen können (Art. 9 DSGVO). Der Kunde setzt ihn dort nicht ein und fordert von seinen Gästen keine Gesundheitsangaben. WEBklar darf die Einrichtung für eine solche Website ablehnen oder den Terminplaner dort abschalten. Der Fragebogen fragt die Branche ab; wählt der Kunde den Gesundheitsbereich, ist der Terminplaner für ihn nicht verfügbar.
(4) Richtet sich das Angebot des Kunden gezielt an Kinder unter 16 Jahren, nutzt der Kunde den Terminplaner nicht.
(5) Der Kunde nennt eine Telefonnummer, unter der Gäste Termine vereinbaren können, wenn das Widget nicht erreichbar ist (Nr. 6 Abs. 2).
(6) Er lässt die Texte der Haken und Hinweise im Widget unverändert.
(7) Er informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt.
(8) Die Pflichten gegenüber seinen Gästen trägt der Kunde: Preisangaben, Allgemeine Geschäftsbedingungen und Stornobedingungen sowie die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung.
(9) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für die Online-Terminbuchung ein; das gehört zur Einrichtung. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
## 5. Einrichtung
(1) Die Einrichtung umfasst die Einrichtung nach dem Fragebogen, den Einbau des Widgets in die Website, den Datenschutz-Absatz nach Nr. 4 Abs. 9, die Abnahme durch den Kunden und die Freischaltung durch WEBklar.
(2) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
(3) Die voraussichtliche Dauer der Einrichtung beträgt meist 1–2 Wochen. Sie ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(4) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(5) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen. Für den Betrieb des Terminplaners berechnet WEBklar neben dem Wartungs-Abo nichts weiter; er läuft, solange das Wartungs-Abo läuft (Nr. 8).
## 6. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Terminplaner. Störungen des Terminplaners beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Ist der Terminplaner nicht erreichbar, zeigt das Widget einen Ausweichtext mit der Telefonnummer des Kunden (Nr. 4 Abs. 5).
(3) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(4) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(5) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 7. Haftung für verpasste Termine
(1) Der Terminplaner ist ein Hilfsmittel. Die Einhaltung der Termine bleibt Sache des Kunden.
(2) WEBklar übernimmt keine Gewähr für die Zustellung der E-Mails (Nr. 6 Abs. 4).
(3) Für entgangenen Gewinn und Folgeschäden aus verpassten Terminen haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(4) Der Haftungsrahmen des Kunden gegenüber seinen Gästen bleibt unberührt.
## 8. Ende der Leistung
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.
(2) Mit dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an, und das Widget zeigt den Ausweichtext. Die Termine bleiben im Kundenportal sichtbar, und der Kunde kann sie als CSV-Datei exportieren. Gäste können über ihren Link absagen. Bereits angeforderte Erinnerungen laufen weiter. Der Kunde informiert seine Gäste.
(3) WEBklar löscht alle Termindaten und die Einstellungen 30 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Terminplaner).
(4) Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Einrichtung nötig (neuer Fragebogen, Einrichtung nach Nr. 5).
(5) Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 5 Abs. 2 verfällt.
(6) Auf Wunsch des Kunden in Textform schaltet WEBklar den Terminplaner jederzeit ab; für den Widerruf gilt Nr. 10.
## 9. Änderungen und Einstellung
(1) WEBklar darf die Technik des Terminplaners verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Terminplaner.
(2) WEBklar darf den Terminplaner für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Terminplaner Nummer 12), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Terminplaner ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 10. Widerruf
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
## 11. Hinweise
(1) **Keine Rechtsberatung.** Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz und die Widerrufsbelehrung für Gäste sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung trägt der Kunde. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde.
@@ -0,0 +1,192 @@
# Leistungsbeschreibung Buchungskalender
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Diese Leistungsbeschreibung regelt die Funktion „Buchungskalender“. Mit dem Buchungskalender nimmt der Kunde über seine Website Anfragen für die Vermietung von Unterkünften entgegen (Ferienwohnungen, Monteurzimmer, kleine Pensionen) und rechnet die Aufenthalte ab („Vermieter“); Gäste sind die Besucher seiner Website, die im Buchungskalender anfragen und buchen („Gäste“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung vermieten.
(2) Diese Leistungsbeschreibung gilt, wenn der Vermieter die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Vermieter die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Buchungskalender. Einschalten kann der Vermieter erst, nachdem WEBklar seine Anfrage für den Buchungskalender geprüft und angenommen hat (Nr. 2 Abs. 1). Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt, schaltet WEBklar sie nach seiner Zustimmung in Textform frei; die Bedingungen dieser Leistungsbeschreibung gelten dann ebenso, die Einrichtung rechnet WEBklar nach Nr. 9 Abs. 3 ab.
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Buchungskalender diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Buchungskalender.
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Buchungskalender nicht entgegen. Das Widget des Buchungskalenders läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und der Muster-Text zur Stornierung nach Nr. 6 ist Teil des Buchungskalenders.
## 2. Leistung
(1) Der Vermieter fragt den Buchungskalender im Kundenportal an und beschreibt seine Unterkünfte (Art der Unterkunft, Zahl der Einheiten, ob er Booking.com oder Airbnb nutzt). WEBklar prüft die Anfrage und nimmt sie an oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Annahme besteht nicht. Erst nach der Annahme kann der Vermieter den Buchungskalender einschalten.
(2) Die Einrichtung umfasst den Einbau eines Kalender-Widgets an einer Stelle der Website des Vermieters, die Bereitstellung der Verwaltung im Kundenportal, den Einbau des Datenschutz-Absatzes für den Buchungskalender (Nr. 5 Abs. 10) und die Freischaltung durch WEBklar. Für jede Website wird ein Buchungskalender eingerichtet; er kann mehrere Unterkünfte desselben Vermieters enthalten. Zusätzlich gibt es für jeden freigegebenen Aufenthalt eine Buchungsseite bei WEBklar, die der Gast über den Link in seiner E-Mail erreicht.
(3) So läuft eine Buchung ab:
- Das Widget zeigt dem Gast die belegten Nächte. Er wählt Anreise und Abreise, gibt Personenzahl, Namen, E-Mail-Adresse und auf Wunsch Firma, Telefonnummer und Nachricht an und sendet eine **unverbindliche Anfrage**. Er erhält eine Eingangsbestätigung, der Vermieter eine Benachrichtigung.
- Der Vermieter prüft die Anfrage im Kundenportal, sieht den vorgerechneten Preis, kann die Posten ändern und gibt die Anfrage mit einer Reservierungsfrist frei oder lehnt sie ab. Gibt er sie frei, reserviert der Buchungskalender die Nächte bis zum Ablauf der Frist, und der Gast erhält eine E-Mail mit Preis, Stornobedingungen und dem Link zur Buchungsseite.
- Auf der Buchungsseite sieht der Gast alle Angaben zum Aufenthalt, trägt seine Rechnungsadresse ein, wählt die Zahlart (online über Stripe, wenn der Vermieter sie eingeschaltet hat, oder vor Ort) und bucht mit der Schaltfläche zur zahlungspflichtigen Buchung. Erst damit kommt der Vertrag zwischen Vermieter und Gast zustande, nicht schon mit der Anfrage oder der Freigabe.
- Mit der Buchung erzeugt der Buchungskalender eine Rechnung mit lückenloser Nummer und sendet dem Gast eine Buchungsbestätigung mit der Rechnung als PDF; der Vermieter wird benachrichtigt.
- Verstreicht die Frist ohne Buchung, läuft die Reservierung ab und die Nächte werden frei; der Gast erhält darüber eine E-Mail.
- Der Vermieter kann einen Aufenthalt stornieren. Dann erhält der Gast eine Stornorechnung mit eigener Nummer und, soweit der Vermieter eine Online-Zahlung erstattet, eine Mitteilung über den Erstattungsbetrag. Eine Stornierung durch den Gast gibt es im Widget nicht; er wendet sich an den Vermieter.
(4) Im Kundenportal verwaltet der Vermieter:
- Unterkünfte mit Beschreibung, Höchstzahl der Personen, Mindestnächten und Check-in- und Check-out-Zeiten,
- Preise (Staffel nach Aufenthaltsdauer, Endreinigung, Zusatzpersonen, Direktbuchungsrabatt, Steuerart und Steuersätze) und seine Stornobedingungen,
- Anfragen und Buchungen mit Status, die Eintragung „bezahlt (vor Ort)“ und Stornierungen,
- Kalender mit manuellen Sperren,
- die Verbindungen zu anderen Kalendern (Nr. 4),
- seine Rechnungen als PDF,
- seine Angaben als Aussteller der Rechnungen (Name, Anschrift, Steuernummer oder Umsatzsteuer-Identifikationsnummer, Bankverbindung, Rechnungspräfix) und die Verbindung mit seinem Stripe-Konto (Nr. 3).
(5) Der Buchungskalender versendet im Namen des Vermieters E-Mails an die Gäste: Eingang der Anfrage, Freigabe mit Buchungslink, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf der Reservierung und Stornierung. Absender ist die Versandadresse von WEBklar mit dem Namen des Vermieters, als Antwortadresse dient seine E-Mail-Adresse. Den Vermieter benachrichtigt der Buchungskalender über neue Anfragen und neue Buchungen sowie, wenn der Abgleich mit einem anderen Kalender seit mindestens sechs Stunden fehlschlägt.
(6) Der Buchungskalender enthält einen Missbrauchsschutz gegen automatisierte Anfragen (verstecktes Feld, Mindest-Ausfüllzeit, Obergrenzen je IP-Adresse). Dafür speichert WEBklar mit der Anfrage nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird entfernt, sobald der Vermieter die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 3 Abs. 4 des AVV Buchungskalender).
(7) Nicht enthalten sind:
- der Meldeschein nach dem Bundesmeldegesetz und die Verwaltung der Meldedaten,
- Kurtaxe, Gästebeitrag und Beherbergungsabgaben,
- Anzahlungen und Teilzahlungen (der Gast zahlt den Gesamtbetrag in einer Zahlung, online oder vor Ort),
- Sofortbuchung ohne Freigabe,
- saisonale Preise (die Preise richten sich nur nach der Aufenthaltsdauer),
- Gutscheine, Channel-Manager-Anbindungen außer dem Kalenderabgleich nach Nr. 4 und Mehrsprachigkeit (der Buchungskalender ist deutschsprachig),
- die Buchführung des Vermieters.
Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Buchungskalender oder eine über das übliche Maß hinausgehende Zahl von Unterkünften, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).
## 3. Zahlungen über Stripe und Rechnungen im Namen des Vermieters
(1) Online-Zahlungen im Buchungskalender sind optional und laufen über Stripe Connect. Der Vermieter verbindet dazu sein eigenes Stripe-Konto mit dem Buchungskalender. Hat er für den Webshop von WEBklar bereits ein Stripe-Konto verbunden, kann er es auch für den Buchungskalender verwenden. Den Vertrag über dieses Konto schließt der Vermieter unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Vermieters durch Stripe und die Auszahlungen gelten die Bedingungen von Stripe. Ohne verbundenes und freigeschaltetes Stripe-Konto ist nur die Zahlart „vor Ort“ möglich.
(2) Das Geld der Gäste fließt direkt an den Vermieter (Zahlung unmittelbar auf dem Konto des Vermieters, sogenannte Direct Charge). WEBklar erhält und verwahrt kein Geld der Gäste, leitet keines weiter und beteiligt sich nicht am Umsatz des Vermieters. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Vermieter; sie fallen nur zwischen dem Vermieter und Stripe an.
(3) Der Vermieter prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt ihm im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Gästen und Zahlungsausfälle regeln der Vermieter und Stripe.
(4) Erstattungen bei einer Stornierung veranlasst der Vermieter im Kundenportal; WEBklar führt die Erstattung über das Stripe-Konto des Vermieters aus. Er bestimmt den Betrag bis zur Höhe der Zahlung des Gastes. Storniert der Vermieter einen Aufenthalt, schließt WEBklar eine noch offene Zahlungsseite bei Stripe. **Automatische Erstattung:** Geht eine Zahlung ein, nachdem die Buchung storniert oder die Reservierung abgelaufen ist, oder zahlt der Gast eine bereits bezahlte Buchung ein zweites Mal, erstattet WEBklar den Betrag ohne weitere Anweisung im Namen des Vermieters in voller Höhe über dessen Stripe-Konto. Dazu erteilt der Vermieter WEBklar mit der Zustimmung zum Buchungskalender eine dauerhafte Weisung; der Stand der Buchung ändert sich dadurch nicht. Erstattungen aus dem Stripe-Dashboard trägt WEBklar in die Buchung nach, soweit Stripe sie meldet.
(5) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Online-Zahlungen sind derzeit nur per Karte möglich, einschließlich Apple Pay und Google Pay; eine SEPA-Lastschrift bietet der Buchungskalender nicht an. Kartendaten gibt der Gast nur dort ein; WEBklar erhält sie nicht. Beim Start der Zahlung übergibt WEBklar Stripe als Rücksprungadresse (bei Erfolg und bei Abbruch) den geheimen Link zur Buchungsseite des Gastes samt Token. Dieser Link ist damit für Stripe und im Stripe-Dashboard des Vermieters sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite.
(6) **Rechnungen im Namen des Vermieters.** Der Vermieter beauftragt WEBklar mit dem Rechnungsauftrag in den Einstellungen des Buchungskalenders, in seinem Namen und für seine Rechnung Rechnungen und Stornorechnungen an seine Gäste auszustellen (Rechnung durch einen Dritten im Namen und im Auftrag des Leistenden, § 14 Abs. 2 Satz 4 UStG). Aussteller der Rechnung ist der Vermieter; sie trägt seinen Namen, seine Anschrift, seine Steuernummer oder Umsatzsteuer-Identifikationsnummer und keinen Hinweis auf WEBklar. WEBklar erzeugt die Rechnung aus den Angaben, die der Vermieter hinterlegt und bei der Freigabe bestätigt hat. Er verantwortet die Richtigkeit seiner Angaben, der Preise, der Steuerart und der Steuersätze; WEBklar prüft sie nicht steuerrechtlich. Die Rechnung wird nach ihrer Ausstellung nicht mehr verändert; Fehler korrigiert der Vermieter durch Stornierung und neue Buchung nach den Möglichkeiten des Kundenportals. Ohne diesen Auftrag stellt WEBklar keine Rechnungen aus. Widerruft er den Auftrag in Textform, stellt der Buchungskalender keine Rechnungen mehr aus und ist für ihn nicht mehr nutzbar.
(7) Die Rechnungsnummern sind je Vermieter und Jahr lückenlos fortlaufend; sie bestehen aus dem Präfix, das der Vermieter vor der ersten Rechnung festlegt, dem Jahr und einer laufenden Nummer.
## 4. Verfügbarkeit und Kalenderabgleich (iCal)
(1) Der Vermieter kann Kalender anderer Plattformen (zum Beispiel Booking.com oder Airbnb) über eine iCal-Adresse einlesen lassen; die eingelesenen Zeiträume blockiert der Buchungskalender. Umgekehrt stellt der Buchungskalender dem Vermieter eine geheime iCal-Adresse bereit, die er bei anderen Plattformen einträgt; sie enthält die belegten Nächte ohne Daten der Gäste. Der Vermieter kann diese Adresse jederzeit erneuern; die bisherige Adresse funktioniert dann nicht mehr.
(2) Der Abgleich läuft regelmäßig, aber zeitversetzt: WEBklar liest die Kalender anderer Plattformen etwa alle 15 Minuten ein, und die anderen Plattformen lesen die Adresse des Buchungskalenders nach ihrem eigenen Takt, oft nur alle paar Stunden. **Zwischen einer Buchung hier und ihrem Eingang dort können deshalb Minuten bis Stunden vergehen, und umgekehrt.** WEBklar sagt keine Echtzeit-Verfügbarkeit zu und übernimmt keine Gewähr dafür, dass eine Unterkunft nicht doppelt gebucht wird.
(3) Dieses Risiko mindert die Freigabe: Jede Anfrage im Buchungskalender braucht die Freigabe des Vermieters. Er prüft vor der Freigabe seine anderen Plattformen und seinen Kalender. Beim Freigeben und beim Buchen prüft der Buchungskalender zusätzlich, ob die Nächte nach seinem Stand frei sind. Eine Doppelbuchung entsteht trotzdem, wenn eine andere Plattform eine Buchung noch nicht gemeldet hat oder der Abgleich ausfällt. Der Vermieter löst eine Doppelbuchung selbst (etwa durch Stornierung) und trägt die Folgen gegenüber seinen Gästen und den Plattformen.
(4) Fehlerhafte oder nicht erreichbare iCal-Adressen kann WEBklar nicht beheben. Schlägt der Abruf fehl, bleibt der zuletzt gelesene Stand erhalten, und der Vermieter wird benachrichtigt, wenn der Abruf seit mindestens sechs Stunden nicht gelungen ist (höchstens einmal je 24 Stunden und Kalender). Eingelesen werden nur Zeiträume von gestern bis 18 Monate in die Zukunft, höchstens 1 Megabyte je Kalender; nur Adressen mit https werden abgerufen.
## 5. Pflichten des Vermieters
(1) Der Vermieter hat ein laufendes Wartungs-Abo und ist Unternehmer. Will er Online-Zahlungen nutzen, stellt er sein eigenes Stripe-Konto bereit und schließt die Anmeldung bei Stripe selbst ab. Der Vermieter vermietet in eigenem Namen; für Preise, Stornobedingungen, Rechnungen und Meldepflichten ist er verantwortlich. Online-Zahlungen seiner Gäste laufen, sofern er sie nutzt, über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Gäste und erhebt keine Umsatzbeteiligung. Rechnungen an Gäste stellt WEBklar nur aus, wenn der Vermieter in den Einstellungen des Buchungskalenders den Rechnungsauftrag erteilt hat (§ 14 Abs. 2 Satz 4 UStG); für die Richtigkeit von Preisen und Steuerangaben bleibt er verantwortlich.
(2) Er verantwortet seine Unterkünfte, Preise und Angaben zum Angebot. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer, für die Beherbergung und die Endreinigung nach den dafür geltenden Steuersätzen, oder Hinweis auf § 19 UStG), Check-in- und Check-out-Zeiten, Höchstbelegung, Mindestnächte und alle Angaben, die er seinen Gästen vor der Buchung geben muss. Die Steuerart und die Steuersätze trägt er selbst im Kundenportal ein; WEBklar berät dazu nicht.
(3) Er legt vor der Freischaltung seine Stornobedingungen fest und verantwortet sie. Der Muster-Text nach Nr. 6 ist nur ein Vorschlag.
(4) Er verantwortet die Verfügbarkeit seiner Unterkünfte, pflegt Sperren und Kalenderverbindungen im Kundenportal und prüft regelmäßig auf neue Anfragen und Buchungen. Die Folgen von Angaben, die er setzt (zum Beispiel einer fälschlich freien oder gesperrten Zeit), trägt er.
(5) Er gibt Anfragen nur frei, wenn er die Unterkunft für den Zeitraum zu den angegebenen Bedingungen bereitstellen kann, und erfüllt die gebuchten Aufenthalte. Er trägt alle Pflichten eines Vermieters gegenüber seinen Gästen, auch Gewährleistung, Kundenservice und Beschwerden.
(6) Er erfüllt seine gesetzlichen Pflichten rund um die Beherbergung selbst. Das betrifft insbesondere den Meldeschein nach dem Bundesmeldegesetz, die Kurtaxe und andere kommunale Abgaben, Anzeige- und Genehmigungspflichten für die Vermietung (zum Beispiel Zweckentfremdungs- oder Gewerberecht) sowie die Pflichten aus seinem Steuerrecht. Der Buchungskalender nimmt ihm davon nichts ab.
(7) Anzahlungen und Teilzahlungen bietet der Buchungskalender nicht an. Vereinbart der Vermieter außerhalb des Buchungskalenders etwas anderes mit seinem Gast, ist das nicht Gegenstand dieser Leistungsbeschreibung.
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem. WEBklar bewahrt die Rechnungen des Vermieters im Buchungskalender 8 Jahre auf, solange der Buchungskalender läuft und nach Nr. 10 bis zur Löschung, hält sie ihm zum Abruf als PDF bereit und anonymisiert personenbezogene Daten der Gäste nach Nummer 9 des AVV Buchungskalender. Der Vermieter lädt seine Rechnungen deshalb rechtzeitig vor der Löschung nach Nr. 10 Abs. 3 herunter und bewahrt sie selbst auf.
(9) Er hält die Vorgaben dieser Leistungsbeschreibung ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht.
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Buchungskalender ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Vermieter den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Vermieter darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(11) Der Vermieter informiert seine Gäste nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Buchungskalender: Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Vermieters.
## 6. Muster-Text zur Stornierung, keine Rechtsberatung
(1) WEBklar stellt dem Vermieter einen Muster-Text für Stornobedingungen bereit. Er ist eine **Vorlage ohne Gewähr** und keine Rechtsberatung (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes). Er passt zur Technik des Buchungskalenders, kann aber auf den Betrieb des Vermieters nicht zugeschnitten sein.
(2) WEBklar empfiehlt, die Stornobedingungen und die Angaben zum Angebot vor der Freischaltung von einer Rechtsanwältin oder einem Rechtsanwalt prüfen zu lassen.
(3) Der Vermieter bleibt für seine Stornobedingungen und alle seine Texte verantwortlich, auch wenn er den Muster-Text unverändert übernimmt (Nr. 5 Abs. 3). Die Stornobedingungen, die bei der Freigabe galten, speichert der Buchungskalender mit der Anfrage; die Buchungsseite und die Buchungsbestätigung zeigen dem Gast diese Fassung.
## 7. Kein Widerrufsrecht des Gastes, Hinweise auf der Buchungsseite
(1) Für die Buchung von Beherbergungsleistungen zu einem bestimmten Zeitpunkt oder in einem bestimmten Zeitraum besteht nach § 312g Abs. 2 Satz 1 Nr. 9 BGB kein gesetzliches Widerrufsrecht. Die Buchungsseite weist den Gast darauf hin; der Vermieter kann ihm vertraglich ein weitergehendes Recht einräumen, der Buchungskalender bietet dafür aber keine Funktion.
(2) Die Buchungsseite zeigt dem Gast vor der Schaltfläche zur zahlungspflichtigen Buchung die wesentlichen Angaben nach Art. 246a EGBGB, soweit der Buchungskalender sie kennt (Anbieter, Unterkunft, Zeitraum, Leistung, Preis insgesamt, Zahlart, Stornobedingungen, Hinweis auf das fehlende Widerrufsrecht). Der Vermieter ergänzt, was darüber hinausgeht, in seinen Texten selbst (Nr. 5 Abs. 2).
(3) Die Buchungsbestätigung erhält der Gast als E-Mail mit der Rechnung als PDF; sie enthält die Angaben zum Vertrag und die Stornobedingungen auf einem dauerhaften Datenträger.
## 8. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Buchungskalender. WEBklar sagt für den Buchungskalender keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Buchungskalenders beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Buchungskalender kurzzeitig unterbrechen. Während einer Unterbrechung sind Anfragen und Buchungen nicht möglich; das Widget zeigt dann einen Ausweichtext.
(3) Zahlungen übernimmt der Buchungskalender über Benachrichtigungen von Stripe und einen Abgleich beim Aufruf der Buchungsseite. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Zahlung verzögern. Eine Zahlung, die nach einer Stornierung oder nach dem Ablauf der Reservierung eingeht, und eine zweite Zahlung auf eine bereits bezahlte Buchung erstattet WEBklar automatisch und in voller Höhe (Nr. 3 Abs. 4); die Buchung bleibt dabei unverändert, und die Erstattung braucht einige Tage, bis sie beim Gast ankommt. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(6) Die gesetzlichen Rechte des Vermieters sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 9. Einrichtung, Abrechnung und Widerruf
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Vermieters. Für den Betrieb des Buchungskalenders berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz oder an den Buchungen.
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 1 bis 2 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Vermieter Unterkünfte, Preise und gegebenenfalls das Stripe-Konto bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(3) Hat WEBklar die Funktion auf Wunsch des Vermieters schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Vermieters in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.
(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Vermieter die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat. Das Widerrufsrecht gegenüber WEBklar ist vom Widerrufsrecht der Gäste gegenüber dem Vermieter (Nr. 7) zu unterscheiden.
## 10. Laufzeit und Ende
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Vermieter per E-Mail und weist ihn darauf hin, seine Rechnungen als PDF herunterzuladen.
(2) Mit dem Abschalten nimmt der Buchungskalender keine neuen Anfragen und Buchungen mehr an, und das Widget zeigt den Ausweichtext. Reservierungen laufen aus. Bereits gebuchte Aufenthalte bleiben Verträge zwischen dem Vermieter und seinen Gästen; der Vermieter erfüllt sie und informiert seine Gäste, wenn er den Buchungskalender nicht mehr anbietet. Anfragen, Buchungen und Rechnungen bleiben im Kundenportal 90 Tage lesbar; Rechnungen kann er einzeln als PDF herunterladen.
(3) WEBklar löscht alle Daten des Buchungskalenders, also Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Buchungskalender). Die Aufbewahrungsfrist der Rechnungen (Nr. 5 Abs. 8) endet damit mit der Löschung; der Vermieter bewahrt sie danach selbst auf. Nach dem Abschalten weist WEBklar den Vermieter per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern; bis dahin sind sie im Kundenportal abrufbar. Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 3 (90 Tage nach dem Abschalten); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird.
(4) Das Stripe-Konto bleibt beim Vermieter. Das Ende des Buchungskalenders berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Vermieter kann die Verbindung zwischen dem Konto und dem Buchungskalender im Stripe-Dashboard beenden.
(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Vermieters in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).
(6) Endet das Wartungs-Abo, bevor WEBklar den Buchungskalender freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.
(7) Auf Wunsch des Vermieters in Textform schaltet WEBklar den Buchungskalender jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.
## 11. Haftung
(1) Der Buchungskalender ist ein Hilfsmittel für die Vermietung des Vermieters. Der Vertrag über die Unterkunft besteht allein zwischen dem Vermieter und dem Gast; WEBklar ist daran nicht beteiligt.
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Buchungskalenders, aus Verzögerungen oder Fehlern beim Kalenderabgleich (Nr. 4), aus Doppelbuchungen und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(3) WEBklar haftet nicht für Inhalte des Vermieters, insbesondere nicht für seine Unterkünfte, Preise, Steuerangaben, Stornobedingungen und Rechtstexte, auch nicht für den Muster-Text, den er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Gästen und den Behörden (Nr. 5).
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 3 Abs. 3), und nicht für Ausfälle, Änderungen und Entscheidungen anderer Plattformen, deren Kalender der Vermieter einbindet.
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Vermieters gegenüber seinen Gästen bleibt unberührt.
## 12. Änderungen und Einstellung
(1) WEBklar darf die Technik des Buchungskalenders verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Buchungskalender.
(2) WEBklar darf den Buchungskalender für eine Website mit einer Frist von vier Wochen beenden oder für alle Vermieter mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Vermieter einer dafür nötigen neuen Fassung nicht zustimmt (AVV Buchungskalender Nummer 12),
- Stripe die Anbindung nach Nr. 3 beendet oder wesentlich ändert, so dass die Online-Zahlung in der bisherigen Form nicht weiterbetrieben werden kann (dann entfällt nur die Online-Zahlung, soweit der Buchungskalender ohne sie weiterläuft), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Buchungskalender ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Vermieter darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 13. Hinweise
(1) **Keine Rechtsberatung.** Der Buchungskalender und der Muster-Text sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche und steuerliche Einordnung seiner Vermietung, seiner Preisangaben und seines Angebots trägt der Vermieter.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen der Tage, Bedienung mit Pfeiltasten, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Vermieter als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Vermieter.
@@ -0,0 +1,179 @@
# Leistungsbeschreibung Webshop
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Diese Leistungsbeschreibung regelt die Funktion „Webshop“. Mit dem Webshop verkauft der Kunde Waren über seine Website („Händler“); Käufer sind die Besucher seiner Website, die im Webshop bestellen („Käufer“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung verkaufen.
(2) Diese Leistungsbeschreibung gilt, wenn der Händler die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Händler die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Webshop. Einschalten kann der Händler erst, nachdem WEBklar seine Anfrage für den Webshop geprüft und freigegeben hat (Nr. 2 Abs. 1).
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Webshop diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Webshop.
(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Webshop nicht entgegen. Das Widget des Webshops läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und die Muster-Rechtstexte nach Nr. 6 sind Teil des Webshops.
## 2. Leistung
(1) Der Händler fragt den Webshop im Kundenportal an und beschreibt, was er verkauft. WEBklar prüft die Anfrage, vor allem nach Nr. 3, und gibt sie frei oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Freigabe besteht nicht. Erst nach der Freigabe kann der Händler den Webshop einschalten.
(2) Die Einrichtung umfasst den Einbau eines Shop-Widgets an einer Stelle der Website des Händlers, die Seite des Webshops mit einem Verweis im Menü und den Link „Vertrag widerrufen“ in der Fußzeile der Seiten der Website, außerdem die Freischaltung durch WEBklar. Für jede Website wird ein Webshop eingerichtet. Zusätzlich steht dem Händler eine eigenständige Seite des Webshops bei WEBklar zur Verfügung (Standalone-Seite), etwa als Vorschau.
(3) Im Kundenportal verwaltet der Händler:
- Artikel mit Beschreibung, Bildern, Preis, Varianten, Bestand und den Pflichtangaben zum Artikel,
- Versand (nur nach Deutschland und Österreich) und Abholung mit Versandkosten und Lieferzeiten,
- Bestellungen mit Status (zum Beispiel bezahlt, versendet, abholbereit, abgeschlossen), die er als CSV-Datei exportieren kann,
- Erstattungen, ganz oder teilweise, über sein Stripe-Konto (Nr. 4),
- die Widerrufe, die Käufer über die Widerrufsfunktion erklärt haben,
- die Verbindung mit seinem Stripe-Konto, seine Einstellungen und seine Rechtstexte, deren bestätigte Fassungen WEBklar archiviert (Nr. 6 Abs. 5).
(4) Der Webshop enthält die Widerrufsfunktion nach § 356a BGB: Das Widget zeigt in seiner Fußzeile und die Standalone-Seite zeigt unter einer eigenen Adresse die Schaltfläche „Vertrag widerrufen“. Sie führt zu einem Formular und danach zur Bestätigungsschaltfläche „Widerruf bestätigen“. WEBklar speichert den Eingang, sendet dem Erklärenden sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit und benachrichtigt den Händler. Die Funktion ist ohne Anmeldung erreichbar, auch wenn zur Erklärung keine Bestellung gefunden wird. Der Händler bearbeitet jeden Widerruf selbst (Nr. 5 Abs. 6).
(5) Der Webshop versendet im Namen des Händlers E-Mails an die Käufer: Eingang der Bestellung (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, vor dem Vertragsschluss), Bestellbestätigung, Versand, Abholbereitschaft, Eingangsbestätigung eines Widerrufs, Erstattung und Hinweis auf eine fehlgeschlagene Zahlung. Absender ist die Versandadresse von WEBklar mit dem Namen des Shops, als Antwortadresse dient die E-Mail-Adresse des Händlers. Den Händler benachrichtigt der Webshop über neue Bestellungen und eingegangene Widerrufe.
(6) Der Webshop enthält einen Missbrauchsschutz gegen automatisierte Bestellungen und Widerrufe, darunter eine Obergrenze von drei offenen Bestellungen je IP-Adresse in 30 Minuten. Dafür speichert WEBklar mit der offenen Bestellung nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird gelöscht, sobald die Bestellung bezahlt oder storniert wird (Nummer 3 Abs. 4 des AVV Webshop). Eine Telefonnummer des Käufers wird nicht erhoben und nicht gespeichert. Die Löschfristen für Bestelldaten nennt Nummer 9 des AVV Webshop.
(7) Nicht enthalten sind der Verkauf an Verbraucher außerhalb von Deutschland und Österreich, Rechnungen (Nr. 5 Abs. 7), die Verwaltung mehrerer Lager, Rabattcodes, Marktplatzfunktionen, die Anbindung an Warenwirtschaft oder Versanddienstleister, Mehrsprachigkeit und die Zahlung per PayPal (Nr. 4 Abs. 5). Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Webshop oder eine größere Zahl von Artikeln als vorgesehen, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).
## 3. Ausgeschlossene Waren und Leistungen
(1) Über den Webshop dürfen nicht verkauft werden:
- alkoholische Getränke,
- Tabakwaren, E-Zigaretten und Zubehör,
- Arzneimittel und Medizinprodukte,
- Waffen, Munition, Feuerwerk und Sprengstoffe,
- Waren mit erotischem oder pornografischem Inhalt,
- lebende Tiere,
- digitale Inhalte und Downloads,
- Gutscheine, Geschenkkarten und Guthaben,
- Abonnements und andere wiederkehrende Zahlungen,
- Waren und Leistungen, die nach der Liste der verbotenen und eingeschränkten Geschäfte von Stripe nicht erlaubt sind, und
- Waren, deren Verkauf oder Versand nach deutschem, österreichischem oder europäischem Recht verboten ist.
(2) Der Händler liefert nur nach Deutschland und Österreich. Eine Lieferung in andere Länder bietet der Webshop nicht an.
(3) Der Händler bestätigt bei der Anfrage und beim Einschalten, dass er keine dieser Waren und Leistungen verkauft. WEBklar darf eine Anfrage ablehnen, wenn der Händler Waren nach Absatz 1 verkaufen will oder die Angaben dazu nicht ausreichen. Verkauft der Händler später Waren nach Absatz 1, darf WEBklar den Webshop nach einem Hinweis in Textform abschalten; bei einem Verstoß, der Rechte Dritter oder Käufer gefährdet, darf WEBklar ihn auch ohne vorherigen Hinweis abschalten und informiert den Händler unverzüglich. Für das Abschalten gilt Nr. 10 Abs. 2 und 3. Die bereits auf das Stundenkontingent angerechnete Einrichtungszeit wird daraus nicht gutgeschrieben, wenn der Händler den Verstoß zu vertreten hat.
## 4. Zahlungen über Stripe
(1) Zahlungen im Webshop laufen über Stripe Connect. Der Händler verbindet dazu sein eigenes Stripe-Konto mit dem Webshop. Den Vertrag über dieses Konto schließt der Händler unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Händlers durch Stripe (zum Beispiel zur Identität) und die Auszahlungen gelten die Bedingungen von Stripe.
(2) Das Geld der Käufer fließt direkt an den Händler. WEBklar erhält und verwahrt kein Geld der Käufer, leitet keines weiter und beteiligt sich nicht am Umsatz des Händlers. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Händler; sie fallen nur zwischen dem Händler und Stripe an.
(3) Der Händler prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt dem Händler im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Käufern und Zahlungsausfälle regeln der Händler und Stripe.
(4) Der Händler erstattet Zahlungen im Kundenportal oder im Stripe-Dashboard. WEBklar trägt Erstattungen aus dem Stripe-Dashboard in die Bestellung nach. Eine Erstattung führt der Händler rechtzeitig aus (Nr. 5 Abs. 6).
(5) Der Webshop bietet die Zahlungsarten an, die Stripe im Checkout für das Konto des Händlers bereitstellt und der Händler in seinen Einstellungen wählt, zum Beispiel Karte und SEPA-Lastschrift. PayPal ist über Stripe für diese Art der Anbindung nicht verfügbar und deshalb nicht Teil des Webshops.
(6) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Kartendaten und Kontodaten gibt der Käufer nur dort ein; WEBklar erhält sie nicht.
## 5. Pflichten des Händlers
(1) Der Händler hat ein laufendes Wartungs-Abo, ist Unternehmer und stellt sein eigenes Stripe-Konto bereit; die Anmeldung bei Stripe schließt er selbst ab. Der Webshop steht nur Kunden zur Verfügung, deren Kundenart „Betrieb“ ist. Der Händler verkauft in eigenem Namen. Für seine Produkte, Preise, Produktangaben und Rechtstexte (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Impressum, Datenschutzerklärung) ist er selbst verantwortlich; WEBklar stellt Muster bereit und berät nicht rechtlich. Er verkauft keine der in Nr. 3 ausgeschlossenen Waren. Die Zahlungen seiner Käufer laufen über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Käufer und erhebt keine Umsatzbeteiligung.
(2) Er verantwortet seine Rechtstexte: Impressum, Datenschutzerklärung, Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular sowie die Angaben zu Versand und Zahlung. Die Mustertexte nach Nr. 6 prüft er, passt sie an seinen Betrieb an und bestätigt sie im Kundenportal, bevor er den Webshop öffnet. Er trägt vor dem Einschalten die Adressen von Impressum und Datenschutzerklärung seiner Website ein.
(3) Er verantwortet seine Artikel, Preise und Produktangaben. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer oder Hinweis auf § 19 UStG), Grundpreise, Versandkosten und Lieferzeiten, die Pflichtangaben für Lebensmittel (zum Beispiel Zutaten, Allergene, Füllmenge, verantwortlicher Lebensmittelunternehmer), die Angaben zur Produktsicherheit (zum Beispiel Hersteller, Warn- und Sicherheitshinweise), Widerrufsausschlüsse bei einzelnen Artikeln und die Rechte an seinen Texten und Bildern.
(4) Er liefert die Waren, wie bestellt und beschrieben, und trägt alle Pflichten eines Verkäufers gegenüber seinen Käufern, auch Gewährleistung, Kundenservice und Beschwerden.
(5) Er pflegt Bestand, Bestellstatus und Versandstatus im Kundenportal. Die Folgen falscher Angaben oder Statusmeldungen, die der Händler setzt, trägt er.
(6) Er bearbeitet Widerrufe und Erstattungen selbst, fristgerecht und nach dem Gesetz. Er entfernt den Link „Vertrag widerrufen“ nicht und verändert die Widerrufsfunktion nicht. Er prüft das Kundenportal regelmäßig auf neue Bestellungen und Widerrufe und lässt sich über die Benachrichtigungen informieren.
(7) Rechnungen und Belege an die Käufer erstellt der Händler selbst; WEBklar stellt sie nicht aus. Die Quittung von Stripe und die Bestellbestätigung des Webshops ersetzen keine Rechnung nach dem Umsatzsteuergesetz, soweit der Händler eine ausstellen muss.
(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem (das Archiv der Rechtstexte nach Nr. 6 Abs. 5 dient nur dem Nachweis der Texte): Personenbezogene Käuferdaten werden nach Nummer 9 des AVV Webshop anonymisiert und nach dem Ende des Webshops gelöscht. Der Händler exportiert seine Bestellungen deshalb rechtzeitig als CSV-Datei und bewahrt sie mit den Belegen von Stripe auf.
(9) Er hält die Vorgaben nach Nr. 3 ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht.
(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Webshop ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Händler den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Händler darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(11) Der Händler informiert seine Käufer nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Webshop: Der Händler ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Händlers.
## 6. Muster-Rechtstexte, keine Rechtsberatung
(1) WEBklar stellt dem Händler Mustertexte bereit: eine Muster-Widerrufsbelehrung, ein Muster-Widerrufsformular, Allgemeine Geschäftsbedingungen und den Text zu Versand und Zahlung. Das Widget zeigt außerdem die Hinweise, die für die Kasse und die Bestellbestätigung vorgesehen sind, darunter den Hinweis auf die gesetzliche Gewährleistung.
(2) Die Mustertexte sind **Muster ohne Gewähr**. Sie passen zur Standardkonfiguration des Webshops und können auf den Betrieb des Händlers nicht zugeschnitten sein. WEBklar sorgt dafür, dass sie die Technik des Webshops richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes).
(3) WEBklar empfiehlt, die Rechtstexte vor der Öffnung des Webshops durch eine Rechtsanwältin oder einen Rechtsanwalt prüfen zu lassen, besonders bei besonderen Waren (zum Beispiel Lebensmitteln, Kosmetik, Textilien mit Pflegekennzeichnung, Elektrogeräten).
(4) Der Händler bleibt für seine Rechtstexte verantwortlich, auch wenn er Mustertexte unverändert übernimmt (Nr. 5 Abs. 2).
(5) Bestätigt der Händler seine Rechtstexte im Kundenportal, legt WEBklar jede bestätigte Fassung mit Prüfsumme, Zeitpunkt und der bestätigenden Person im Archiv ab. Die Bestellbestätigung zeigt dem Käufer die archivierte Fassung, die zum Zeitpunkt seiner Bestellung galt. WEBklar löscht das Archiv mit den übrigen Daten des Webshops (Nr. 10 Abs. 3; Nummer 8 Abs. 5 des AVV Webshop). Der Händler bewahrt die Fassungen, die er für eigene Aufbewahrungspflichten braucht, selbst auf (Nr. 5 Abs. 8).
## 7. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Webshop. WEBklar sagt für den Webshop keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Webshops beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).
(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Webshop kurzzeitig unterbrechen. Während einer Unterbrechung sind Bestellungen im Widget nicht möglich; das Widget zeigt dann einen Ausweichtext.
(3) Zahlungen übernimmt der Webshop über Benachrichtigungen von Stripe und einen regelmäßigen Abgleich in die Bestellung. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Bestellung verzögern. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.
(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.
(6) Die gesetzlichen Rechte des Händlers sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 8. Haftung
(1) Der Webshop ist ein Hilfsmittel für den Verkauf des Händlers. Der Vertrag über die Ware besteht allein zwischen dem Händler und dem Käufer; WEBklar ist daran nicht beteiligt.
(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Webshops, aus Fehlern bei Bestand oder Bestellstatus und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.
(3) WEBklar haftet nicht für Inhalte des Händlers, insbesondere nicht für seine Artikel, Preise, Produktangaben und Rechtstexte, auch nicht für Mustertexte, die er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Käufern (Nr. 5).
(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 4 Abs. 3).
(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Händlers gegenüber seinen Käufern bleibt unberührt.
## 9. Einrichtung, Abrechnung und Widerruf
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Händlers. Für den Betrieb des Webshops berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz.
(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 2 bis 3 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Händler Stripe-Konto, Artikel und Rechtstexte bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.
(3) Hat WEBklar die Funktion auf Wunsch des Händlers schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Händlers in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.
(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Händler die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
## 10. Laufzeit und Ende
(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Händler per E-Mail.
(2) Mit dem Abschalten nimmt der Webshop keine neuen Bestellungen mehr an, und das Widget zeigt den Ausweichtext. Bestellungen, die noch nicht bezahlt sind, laufen aus. Die Bestellungen und Widerrufe bleiben im Kundenportal 90 Tage lesbar, und der Händler kann sie als CSV-Datei exportieren. Die Widerrufsfunktion bleibt bis zur Löschung nach Absatz 3 erreichbar, soweit das Widget noch eingebunden ist. Der Händler erfüllt seine Pflichten gegenüber den Käufern weiter (Nr. 5) und informiert sie, wenn er den Webshop nicht mehr anbietet.
(3) WEBklar löscht alle Daten des Webshops, also Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6 des AVV Webshop).
(4) Das Stripe-Konto bleibt beim Händler. Das Ende des Webshops berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Händler kann die Verbindung zwischen dem Konto und dem Webshop im Stripe-Dashboard beenden.
(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Händlers in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).
(6) Endet das Wartungs-Abo, bevor WEBklar den Webshop freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.
(7) Auf Wunsch des Händlers in Textform schaltet WEBklar den Webshop jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.
## 11. Änderungen und Einstellung
(1) WEBklar darf die Technik des Webshops verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Webshop.
(2) WEBklar darf den Webshop für eine Website mit einer Frist von vier Wochen beenden oder für alle Händler mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Händler einer dafür nötigen neuen Fassung nicht zustimmt (AVV Webshop Nummer 12),
- Stripe die Anbindung nach Nr. 4 beendet oder wesentlich ändert, so dass der Webshop in der bisherigen Form nicht weiterbetrieben werden kann, oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Webshop ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Händler darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 12. Hinweise
(1) **Keine Rechtsberatung.** Der Webshop und die Mustertexte sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche Einordnung seiner Waren, seiner Preisangaben und seines Angebots trägt der Händler.
(2) **Barrierefreiheit.** Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Händler als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Händler.
@@ -0,0 +1,109 @@
# Leistungsbeschreibung Website-Analyse
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Hosting-Abo und zum Wartungs-Abo
## 1. Geltung
(1) Funktion im Sinne dieser Leistungsbeschreibung ist die Website-Analyse. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist in beiden Abos enthalten. Sie setzt ein laufendes Hosting-Abo oder Wartungs-Abo voraus. Sie wird zusammen mit dem Auftragsverarbeitungsvertrag Website-Analyse („AVV Website-Analyse“) vereinbart. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Website-Analyse.
(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die Website-Analyse diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Website-Analyse.
(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für die Zusatzseite nach Nr. 2 Abs. 2 und den Hinweis in der Datenschutzerklärung nach Nr. 4 Abs. 4. Beides ist Teil der Website-Analyse.
(4) Das Einschalten der Website-Analyse ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen.
## 2. Leistung
(1) Die Website-Analyse misst, wie die öffentliche Website des Kunden genutzt wird. Sie ist im Hosting-Abo und im Wartungs-Abo ohne Aufpreis enthalten. Die Einrichtung der Website-Analyse ist für den Kunden kostenlos. WEBklar rechnet sie weder auf ein Stundenkontingent an noch berechnet WEBklar sie gesondert.
(2) Beim Veröffentlichen der Website fügt WEBklar Folgendes ein:
- ein Messskript in jede HTML-Datei der Website,
- ein Einwilligungsbanner am unteren Rand mit den gleich gestalteten Schaltflächen „Ablehnen“ und „Annehmen“ in der Akzentfarbe, die der Kunde beim Einschalten wählt (ist sie für weiße Schrift zu hell, nimmt das Banner Schwarz),
- eine Schaltfläche „Datenschutz-Einstellungen“ unten rechts, über die Besucher ihre Wahl jederzeit ändern können (hat die Website ein eigenes Element mit dem Attribut `data-webklar-consent`, übernimmt dieses Element die Aufgabe),
- eine Zusatzseite „/datenschutz-analyse.html“ mit den Datenschutzhinweisen zur Messung (Verantwortlicher: der Kunde).
(3) Gemessen wird:
- **solange der Besucher keine Wahl getroffen hat und sein Browser kein Signal nach dem nächsten Spiegelstrich sendet:** Seitenaufrufe mit Pfad (ohne Suchparameter), Land (aus der IP-Adresse, die nicht gespeichert wird), Geräteklasse und ein Zähler dafür, dass das Banner angezeigt wurde;
- **bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“:** nichts, und es erscheint kein Banner;
- **nach „Ablehnen“:** nichts mehr, außer einmal dem Zähler „abgelehnt“;
- **nach „Annehmen“ zusätzlich:** die Domain der Website, von der der Besucher kam, die Größenklasse des Browserfensters, eine täglich wechselnde pseudonyme Besucher-Kennung, Verweildauer, Scrolltiefe, Klickpositionen (Heatmap), die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp, Link auf eine andere Website mit deren Domain) und ein Zähler „angenommen“.
Die einzelnen Datenarten nennt der AVV Website-Analyse in Nummer 3.
(4) Es werden keine Cookies gesetzt. Gespeichert wird auf dem Gerät des Besuchers nur seine Banner-Wahl im lokalen Speicher des Browsers, für 183 Tage.
(5) Die Auswertung steht im Kundenportal zur Verfügung: Kennzahlen und Diagramme für die letzten 7, 30 oder 90 Tage und eine Heatmap je Seite und Ansicht (Handy, Tablet, Computer) aus den bis zu 5.000 jüngsten Klicks der letzten 30 Tage. Die Daten liegen auf dem Server von WEBklar bei der Hetzner Online GmbH in Deutschland; sie werden nicht in Drittländer übermittelt. Einzelne Ereignisse und Klicks werden nach 30 Tagen gelöscht, Tageswerte nach 25 Monaten, alle Daten rund 60 Tage nach dem Ende der Website-Analyse (AVV Website-Analyse Nummer 9).
(6) Die Zahlen sind Näherungswerte. Besucher mit Blockern, abgeschaltetem JavaScript, Browser-Signal oder Ablehnung werden nur eingeschränkt oder gar nicht erfasst. Besucherzahlen, Herkunftsseiten, Verweildauer, Scrolltiefe, Klicks und Kontakt-Klicks stammen nur von Besuchern, die „Annehmen“ gewählt haben.
(7) Nicht enthalten sind Sitzungsaufzeichnungen, die Auswertung von Formularinhalten, die Messung von Ladezeiten und Fehlern, Kampagnen- und Trichteranalysen sowie ein Datenexport außer nach Nr. 7 Abs. 3.
(8) Das Banner fragt nur nach der Einwilligung in die Website-Analyse von WEBklar. Andere einwilligungspflichtige Inhalte der Website, etwa eingebettete Karten, Videos oder Schriften von Drittanbietern, deckt es nicht ab.
## 3. Voraussetzungen
(1) Der Kunde hat ein laufendes Hosting-Abo oder Wartungs-Abo.
(2) Die Website ist öffentlich erreichbar und wird bei WEBklar gehostet (Adresse unter project.webklar.com oder eigene Domain, die auf den Server von WEBklar zeigt). Nicht öffentliche Websites und Vorschauen, die nur nach einer Anmeldung erreichbar sind, werden nicht gemessen, ebenso wenig Websites, die im Dev-Modus auf einem Entwicklungsserver laufen. Enthält eine öffentliche Website Bereiche, die erst nach einer Anmeldung zugänglich sind (etwa eine Web-Anwendung mit Kundenkonto), werden auch dort Seitenaufrufe gezählt; dafür gilt Nr. 4 Abs. 5.
(3) Auf der Zusatzseite erscheinen als Verantwortlicher die Firma (sonst der Name), die Anschrift, soweit sie im Kundenkonto hinterlegt ist, und die E-Mail-Adresse aus dem Kundenkonto. Das Portal zeigt diese Angaben vor dem Einschalten an. Der Kunde prüft sie und teilt Änderungen mit. Fehlen Firma und Name oder fehlt eine gültige E-Mail-Adresse, lässt sich die Funktion nicht einschalten.
(4) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren, schaltet der Kunde die Website-Analyse nicht ein, weil eine Einwilligung über das Banner dann nicht genügt (Art. 8 DSGVO). Lassen Inhalte oder Adressen der Website auf besondere Kategorien personenbezogener Daten schließen, etwa Gesundheit, Religion oder politische Meinung, teilt der Kunde das WEBklar vor dem Einschalten mit. WEBklar darf das Einschalten für eine solche Website ablehnen oder die Website-Analyse dort ausschalten.
## 4. Pflichten des Kunden
(1) Der Kunde ist für die Messung auf seiner Website Verantwortlicher im Sinne der DSGVO.
(2) Er entfernt, verdeckt oder umgeht das Banner nicht und verändert das Skript nicht. Text und Gestaltung des Banners legt WEBklar fest.
(3) Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess-, Tracking- oder Marketing-Werkzeuge ein (zum Beispiel Google Analytics oder Meta Pixel), weil sie eine eigene Einwilligung erfordern können.
(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einschalten dort einen kurzen Hinweis mit Link auf die Zusatzseite ein und entfernt ihn beim Ausschalten wieder; das wird nicht auf das Stundenkontingent angerechnet. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Hinweis als Textbaustein und fügt ihn selbst ein und entfernt ihn nach dem Ausschalten selbst. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf (etwa „Wir setzen keine Analyse-Werkzeuge ein“), weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(5) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.
(6) Er nutzt die Auswertung nur für eigene Zwecke, versucht nicht, einzelne Besucher zu identifizieren, und gibt Heatmap-Links nicht weiter.
## 5. Verfügbarkeit und Störungen
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Messung. Störungen der Messung beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen). Daten, die während einer Störung nicht erfasst wurden, lassen sich nicht nachträglich erheben.
(2) Fällt der Messdienst aus, wird die Website weiter ausgeliefert, nur ohne Banner und Messung.
(3) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.
## 6. Hinweis zur Rechtslage
Für die Zählung, solange der Besucher keine Wahl getroffen hat (Nr. 2 Abs. 3), übermittelt das Skript nur die Adresse der aufgerufenen Seite und die Angabe, dass das Banner angezeigt wurde. Land und Geräteklasse bestimmt der Messdienst aus den Daten, die jeder Browser bei einer Anfrage ohnehin mitsendet. Ob auch dafür eine Einwilligung nach § 25 TDDDG nötig ist, haben Gerichte und Aufsichtsbehörden nicht entschieden; die Aufsichtsbehörden legen den Begriff des Zugriffs auf das Endgerät eher weit aus. Ändert sich die Rechtslage, passt WEBklar die Messung an (Nr. 8).
## 7. Ende der Leistung
(1) Der Kunde kann die Website-Analyse jederzeit im Kundenportal ausschalten; das Portal bestätigt die Abschaltung. Die Website-Analyse läuft, solange das Abo läuft, das sie enthält. Endet es, schaltet WEBklar sie spätestens am folgenden Tag ab. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; das Gleiche gilt, wenn er den Wechsel ins Wartungs-Abo widerruft. WEBklar informiert den Kunden per E-Mail.
(2) Mit dem Ausschalten speichert der Messdienst keine Messdaten mehr, und die Heatmap ist nicht mehr abrufbar. Das Messskript liefert dann kein Banner mehr aus, sobald die Website einmal mit der aktuellen Fassung des Skripts veröffentlicht wurde. Skript, Zusatzseite und, soweit WEBklar ihn eingefügt hat, den Hinweis in der Datenschutzerklärung entfernt WEBklar mit der nächsten Veröffentlichung der Website. WEBklar löst sie in der Regel sofort aus, spätestens innerhalb von fünf Werktagen.
(3) Die gespeicherten Daten löscht WEBklar nach Nummer 9 des AVV Website-Analyse. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse als CSV-Datei verlangen.
## 8. Änderungen und Einstellung
(1) WEBklar darf die Technik der Website-Analyse verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Website-Analyse.
(2) WEBklar darf die Website-Analyse für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Website-Analyse Nummer 12 Abs. 3), oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Messung ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt die Messung wieder auf, sobald das zulässig und technisch möglich ist.
(4) Gegenüber Unternehmern darf WEBklar die Website-Analyse außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.
(5) Weil für die Website-Analyse kein Aufpreis berechnet wird, ändert ihre Einstellung den Preis des Abos nicht. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 9. Hinweise
(1) **Keine Rechtsberatung.** Die Zusatzseite und der Hinweis in der Datenschutzerklärung sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine Prüfung, ob die Website des Kunden insgesamt datenschutzkonform ist, und eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht. Abstimmungen nach dieser Leistungsbeschreibung betreffen nur die Technik.
(2) **Nachweis der Einwilligung.** Ein Protokoll je Besucher wird nicht geführt. Als Nachweis dienen die Dokumentation der Technik und das Archiv der Fassungen von Banner-Text und Zusatzseite (AVV Website-Analyse Nummer 8 Abs. 6).
(3) Die Länderzuordnung nutzt die Datenbank „IP to Country Lite“ von DB-IP.com (Lizenz CC BY 4.0). Im Kundenportal steht dazu unter der Auswertung der Hinweis „Länderdaten: IP-Geolocation by DB-IP“ mit Link auf db-ip.com.
@@ -0,0 +1,135 @@
# Leistungsbeschreibung Öffnungszeiten live
**Fassung 2026-10-16** · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo
## 1. Geltung
(1) Funktion im Sinne dieser Leistungsbeschreibung ist Öffnungszeiten live. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.
(2) Die Funktion ist im Wartungs-Abo ohne Aufpreis enthalten (§ 3 Abs. 8 der Abo-Bedingungen). Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Nicht enthalten“: neue Funktionen und Rechtstexte) gilt dafür nicht: Die Funktion und der Datenschutz-Absatz nach Nr. 6 Abs. 4 sind Teil dieser Leistung.
(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für Öffnungszeiten live diese Leistungsbeschreibung vor. Die Haftungsregeln stehen in Nr. 8 ausgeschrieben da, damit sie auch für Abos früherer Fassungen der Abo-Bedingungen gelten.
## 2. Leistung
(1) Der Kunde trägt seine Öffnungszeiten einmal im Kundenportal ein: bis zu fünf benannte Zeitpläne, je Wochentag bis zu zwei Zeitfenster („rund um die Uhr“ und „nach Vereinbarung“ sind möglich), Abweichungen an Feiertagen, Betriebsurlaub und Sondertage. Die Website zeigt diese Zeiten überall gleich an und berechnet daraus den Live-Status („Jetzt geöffnet · schließt um 18 Uhr“).
(2) An den Stellen der Website, die nach Nr. 3 Abs. 2 dafür vorgesehen sind, erscheinen die Zeiten als Tabelle, als Status oder als Kurzanzeige („Geöffnet“, „Geschlossen“, „Nach Vereinbarung“). Dazu gehören, soweit der Kunde sie einrichtet:
- eine Hinweis-Leiste am Seitenanfang für befristete Hinweise (Info, Urlaub, Aktion, Wichtig) mit Beginn und Ende und auf Wunsch einem Knopf (Link oder Anruf),
- auf dem Handy eine Kontakt-Leiste am unteren Rand mit den Knöpfen „Anrufen“, „Route“ und, wenn der Kunde sie einrichtet, „WhatsApp“,
- die Öffnungszeiten im Seitencode in maschinenlesbarer Form (strukturierte Daten), die Suchmaschinen lesen können,
- eine E-Mail vor Feiertagen, die den Kunden bittet, seine Zeiten für diese Tage zu prüfen, und
- im Kundenportal der Knopf „Für Google kopieren“, der die Zeiten in der Reihenfolge von Google in die Zwischenablage legt, damit der Kunde sie in sein Google-Unternehmensprofil einträgt.
(3) Die Funktion rechnet die Feiertage des Bundeslands ein, das der Kunde wählt; je Feiertag kann der Kunde eigene Zeiten eintragen. Feiertage, die nur örtlich gelten, sowie Heiligabend und Silvester zeigt das Kundenportal als „bitte prüfen“. Alle Zeiten gelten in der Zeitzone Europe/Berlin.
(4) Beim Aufruf der Website lädt der Browser des Besuchers ein kleines Programm und die Öffnungszeiten von project.webklar.com, einem Server der WEBklar GbR bei der Hetzner Online GmbH in Falkenstein (Deutschland). Dabei werden keine Cookies gesetzt, und es wird nichts im Browser gespeichert. Das Programm lädt nichts von anderen Anbietern. WhatsApp und Google Maps werden erst nach dem Klick des Besuchers auf „WhatsApp“ oder „Route“ geöffnet; vorher werden keine Daten an diese Dienste übertragen.
(5) Das Team von WEBklar kann die Hinweis-Leiste und die Kontakt-Leiste für eine Website abschalten, etwa wenn die Seite schon eine eigene Anruf-Leiste hat. Auf Wunsch des Kunden können Mitarbeiter die Öffnungszeiten in der Kundenansicht für ihn ändern; ein Anspruch darauf besteht nicht (Nr. 5 Abs. 1). Den Haken für den WhatsApp-Hinweis (Nr. 6 Abs. 3) setzen sie nicht.
## 3. Einbau und Freischaltung
(1) Der Einbau des Programms erfolgt automatisch nur bei Websites, die WEBklar hostet und über den eigenen Veröffentlichungsablauf bereitstellt: Beim nächsten Veröffentlichen setzt WEBklar die Einbau-Zeile in jede Seite ein, und Hinweis-Leiste, Kontakt-Leiste und Angaben für Suchmaschinen erscheinen ohne weiteren Aufwand. In allen anderen Fällen, etwa bei einer Website bei einem anderen Anbieter, setzt das Team die Einbau-Zeile von Hand ein. Hat WEBklar keinen Zugriff auf die Website, erhält der Kunde die Zeile und setzt sie selbst oder durch seinen Anbieter ein.
(2) Die Tabelle, den Status und die Kurzanzeige setzt das Team an den Stellen ein, an denen auf der Website feste Öffnungszeiten stehen oder stehen sollen, und ersetzt dort die fest eingetragenen Zeiten. In jedem dieser Platzhalter bleibt eine statische Ersatz-Tabelle stehen. Sie erscheint, wenn der Browser kein JavaScript ausführt, wenn die Daten nicht erreichbar oder älter als 24 Stunden sind und nachdem die Funktion abgeschaltet wurde. Sie zeigt den Wochenplan zum Zeitpunkt des Einbaus.
(3) WEBklar schaltet die Funktion frei, sobald der Einbau abgeschlossen ist und der Kunde Öffnungszeiten eingetragen hat (mindestens ein geöffneter Tag im Hauptplan). Solange der Kunde keine Zeiten eingetragen hat, kann das Team die Tabelle nicht einbauen.
(4) Der Einbau dauert erfahrungsgemäß meist wenige Tage. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins.
## 4. Einrichtung
(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.
(2) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.
(3) Im Umfang der Einrichtung ist ein Einbau je Website enthalten, einschließlich der Aktualisierung der Ersatz-Tabelle beim Abschalten (Nr. 9 Abs. 4). Weitere Wünsche, etwa zusätzliche Stellen für die Anzeige, eine Sondergestaltung oder andere Änderungen an der Website, gehören nicht zur Einrichtung. Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.
## 5. Nicht enthalten
(1) Nicht enthalten sind:
- die Übertragung der Öffnungszeiten zu Google: WEBklar schreibt nichts automatisch in das Google-Unternehmensprofil des Kunden; der Kunde trägt die Zeiten dort selbst ein, der Knopf „Für Google kopieren“ und die Erinnerung vor Feiertagen helfen dabei,
- Feiertage einer Gemeinde und andere örtliche Besonderheiten (zum Beispiel Brückentage oder Stadtfeste); der Kunde trägt sie als Ausnahmen selbst ein,
- eine Zusage, dass Suchmaschinen die Angaben im Seitencode übernehmen oder wie sie sie anzeigen,
- die Pflege der Zeiten und der Hinweise durch WEBklar und
- Änderungen an der Website, die über den Einbau nach Nr. 3 hinausgehen (Nr. 4 Abs. 3).
## 6. Pflichten des Kunden
(1) Der Kunde prüft seine Zeiten, Feiertage und Hinweistexte vor dem Speichern und hält sie aktuell. Er wählt das richtige Bundesland und trägt örtliche Feiertage und Abweichungen selbst ein.
(2) Preise und Rabatte in Hinweisen gibt er rechtmäßig an, zum Beispiel als Endpreise und, bei Preisermäßigungen für Waren, mit dem niedrigsten Preis der letzten 30 Tage. Hinweise zu Preisen sind Angaben des Kunden, nicht von WEBklar.
(3) Die Kontaktangaben (Telefonnummer, Anschrift, WhatsApp-Nummer) sind auf der Website öffentlich sichtbar. Der Kunde trägt nur Angaben ein, die er veröffentlichen darf. Die WhatsApp-Schaltfläche schaltet er erst ein, nachdem er den WhatsApp-Hinweis in seine Datenschutzerklärung aufgenommen hat, und bestätigt das im Kundenportal mit einem Haken.
(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz zu Öffnungszeiten live ein; das gehört zum Einbau. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.
(5) Der Kunde prüft nach dem Einbau, ob die Anzeige auf seiner Website stimmt, und meldet Fehler unverzüglich im Chat des Kundenportals.
(6) Er entfernt oder verändert das Programm von WEBklar nicht. Wünscht er die Hinweis-Leiste oder die Kontakt-Leiste nicht, teilt er das dem Team mit (Nr. 2 Abs. 5).
## 7. Verfügbarkeit und Aktualität
(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Live-Anzeige. Störungen der Anzeige behebt WEBklar in angemessener Frist.
(2) Ist die Anzeige nicht erreichbar oder sind die Daten älter als 24 Stunden, bleibt auf der Website die Ersatz-Tabelle stehen (Nr. 3 Abs. 2); Hinweis-Leiste und Kontakt-Leiste erscheinen dann nicht.
(3) Änderungen, die der Kunde im Kundenportal speichert, erscheinen nicht sofort auf der Website, sondern mit einer Verzögerung von einigen Minuten (bis zu etwa zehn Minuten). Der Status folgt der Zeitzone Europe/Berlin; geht die Uhr des Geräts des Besuchers um mehr als fünf Minuten falsch, rechnet das Programm mit der Serverzeit von WEBklar.
## 8. Haftung
(1) Die Live-Anzeige ist ein Hilfsmittel. Die Richtigkeit der Zeiten, Ausnahmen, Hinweise und Kontaktangaben liegt beim Kunden: WEBklar zeigt die Angaben des Kunden an und prüft sie nicht auf Richtigkeit.
(2) Die Feiertage stammen aus einer Feiertagsliste für das gewählte Bundesland. WEBklar beseitigt Fehler in den Feiertagsdaten und andere Softwarefehler nach Meldung in angemessener Frist (Nr. 7 Abs. 1). Für Schäden daraus gelten die Absätze 3 bis 5.
(3) Für entgangenen Gewinn und Folgeschäden, etwa weil ein Besucher wegen einer falschen oder fehlenden Angabe vor verschlossener Tür steht, haftet WEBklar nur im Rahmen der Absätze 4 und 5.
(4) 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.
(5) 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.
(6) Diese Regeln gelten auch zugunsten der Mitarbeiter und Beauftragten von WEBklar. Dieser Abschnitt entspricht § 13 Abs. 1 und 2 der Abo-Bedingungen und gilt unabhängig davon, nach welcher Fassung der Abo-Bedingungen das Wartungs-Abo besteht.
(7) Gesetzliche Rechte des Kunden, die sich nicht ausschließen lassen, bleiben unberührt. Der Haftungsrahmen des Kunden gegenüber seinen Besuchern und Kunden bleibt ebenfalls unberührt.
## 9. Ende der Leistung
(1) Auf Wunsch des Kunden in Textform (im Chat des Kundenportals oder per E-Mail) schaltet WEBklar Öffnungszeiten live jederzeit ab; für den Widerruf gilt Nr. 11.
(2) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo, läuft die Funktion noch 30 Tage unverändert weiter; danach schaltet WEBklar sie ab. Das Gleiche gilt beim Widerruf eines Wechsels ins Wartungs-Abo. WEBklar informiert den Kunden per E-Mail. Die Eingaben des Kunden bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt schon in den 30 Tagen Weiterlauf wieder ein Wartungs-Abo, läuft die Funktion einfach weiter, beginnt es in den 30 Tagen danach, schaltet WEBklar die Funktion mit den vorhandenen Eingaben ohne weitere Kosten wieder ein.
(3) Sobald ein neues Wartungs-Abo beginnt, solange die Eingaben des Kunden noch gespeichert sind (Absatz 4), läuft die Funktion mit den vorhandenen Eingaben weiter oder WEBklar schaltet sie ohne weitere Kosten wieder ein.
(4) Mit dem Abschalten verschwinden Hinweis-Leiste, Kontakt-Leiste, Live-Status und die Angaben für Suchmaschinen; auf der Website steht wieder die Ersatz-Tabelle (Nr. 3 Abs. 2). Hat sich der Wochenplan seit dem Einbau geändert, aktualisiert WEBklar die Ersatz-Tabelle einmal beim Abschalten; das gehört zum Einbau nach Nr. 4 Abs. 3. Hat WEBklar keinen Zugriff auf die Website, erhält der Kunde den aktuellen Text und setzt ihn selbst ein. Nach dem Abschalten wegen des Endes des Abos bleiben die Eingaben des Kunden noch weitere 30 Tage gespeichert und werden danach gelöscht; bis dahin kann der Kunde sie im Kundenportal ansehen.
(5) Schaltet WEBklar die Funktion auf Wunsch des Kunden oder aus einem anderen Grund als dem Ende des Abos ab, löscht WEBklar die Eingaben des Kunden auf sein Verlangen in Textform, spätestens mit dem Löschen des Kundenkontos.
## 10. Änderungen und Einstellung
(1) WEBklar darf die Technik von Öffnungszeiten live verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten die Funktion verarbeitet, passt WEBklar die Datenschutzerklärung an und informiert den Kunden, weil sich dann sein Datenschutz-Absatz (Nr. 6 Abs. 4) ändern kann.
(2) WEBklar darf die Funktion für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn
- ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung dieser Leistungsbeschreibung nicht zustimmt, oder
- eine Sicherheitslücke sich nicht anders beheben lässt.
Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.
(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Funktion ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.
(4) Ist der Kunde Unternehmer, darf WEBklar die Funktion außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Wartungs-Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.
(5) Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.
## 11. Widerruf
(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.
(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.
## 12. Hinweise
(1) **Keine Rechtsberatung.** Der Datenschutz-Absatz, der WhatsApp-Hinweis und die Hinweise des Kundenportals zu Preisangaben sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall, etwa zu Preisangaben, zum Wettbewerbsrecht oder zum Datenschutz der Website des Kunden, schuldet WEBklar nicht.
(2) **Datenschutz.** Wie WEBklar im Kundenportal Daten zu Öffnungszeiten live verarbeitet, steht in der Datenschutzerklärung von WEBklar (Abschnitt 6e). Was beim Aufruf der Website des Kunden übertragen wird, beschreibt Nr. 2 Abs. 4; die Information der Besucher gehört in die Datenschutzerklärung der Website (Nr. 6 Abs. 4).
Anbieter: WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler. Es gelten ergänzend die Abo-Bedingungen und die Datenschutzerklärung.
+465
View File
@@ -0,0 +1,465 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<title>Vertragsbedingungen – WEBklar-Abos</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
h2 { font-size: 20px; margin: 40px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
h3 { font-size: 16.5px; margin: 26px 0 6px; font-weight: 600; }
p, li { margin: 6px 0; }
ol, ul { padding-left: 24px; }
ol.abs > li { padding-left: 4px; }
ol.abs > li::marker { content: "(" counter(list-item) ") "; font-weight: 600; }
.meta { color: #5e6678; font-size: 14px; }
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 20px 0; }
.box > strong:first-child { display: block; margin-bottom: 6px; }
table { border-collapse: collapse; width: 100%; font-size: 15px; }
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
a:hover { background: #f6f8fa; }
nav.top { font-size: 14px; margin-bottom: 22px; }
.toc { columns: 2; column-gap: 28px; font-size: 14.5px; }
.toc a { text-decoration: none; }
.toc li { break-inside: avoid; }
.anlage-kopf { font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; color: #5e6678; margin-top: 44px; }
.anlage-kopf + h2 { margin-top: 4px; border-top: none; padding-top: 0; }
.hinweis { font-size: 14px; color: #5e6678; }
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
@media (max-width: 640px) { .toc { columns: 1; } h1 { font-size: 25px; } }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar-Abos · Vertragsunterlagen"; font: 8pt sans-serif; color: #8a93a3; }
@top-right { content: "Version 6 · Stand 16.10.2026"; font: 8pt sans-serif; color: #8a93a3; }
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
body { background: #fff; font-size: 10pt; line-height: 1.45; }
main { max-width: none; padding: 0; }
nav.top, .screen-only { display: none; }
h1 { font-size: 20pt; }
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
h3 { font-size: 11pt; break-after: avoid; }
.box { padding: 8pt 0; }
.neue-seite { break-before: page; }
a { color: inherit; text-decoration: none; }
.box, tr, ol.abs > li { break-inside: avoid; }
li, p { orphans: 3; widows: 3; }
}
</style>
</head>
<body>
<main>
<nav class="top"><a href="/dashboard.html">← Zurück zum Kundenportal</a> · <a href="/kuendigen.html">Verträge hier kündigen</a> · <a href="/widerrufen.html">Vertrag widerrufen</a> · <a href="/datenschutz.html">Datenschutz</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Vertragsbedingungen für die WEBklar-Abos</h1>
<p class="meta">Hosting-Abo und Wartungs-Abo · Vertragsbedingungen mit Leistungsbeschreibungen, Widerrufsbelehrung und Auftragsverarbeitungsvertrag · Version 6 · Stand 16.10.2026</p>
<p class="hinweis offen">Entwurf – Prüfung durch Kanzlei offen</p>
<p class="screen-only meta"><a href="/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf">Als PDF herunterladen und ausdrucken</a></p>
<div class="box">
<strong>Das Wichtigste auf einen Blick</strong>
<table>
<tr><td>Anbieter</td><td>WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein (jeder einzeln vertretungsberechtigt) · Telefon 0176 23726355 · E-Mail support@webklar.com</td></tr>
<tr><td>Hosting-Abo</td><td>20,00 € pro Monat · Bereitstellung Ihrer statischen Website auf unserem Server in Deutschland · 1 Domain · 1 E-Mail-Postfach mit 5 GB · Änderungen an der Website nur nach Ihrer Freigabe, 100 € pro Stunde (Leistungsbeschreibung A)</td></tr>
<tr><td>Wartungs-Abo</td><td>50,00 € pro Monat · Betrieb und Pflege Ihrer Website bzw. Web-Anwendung auf unserem Server in Deutschland · 1 Domain · beliebig viele E-Mail-Postfächer mit zusammen 5 GB · 1 Stunde Arbeitszeit pro Monat, nicht genutzte Zeit bleibt 12 Monate verfügbar; Mehraufwand nur nach Ihrer Freigabe, 100 € pro Stunde (Leistungsbeschreibung B)</td></tr>
<tr><td>Funktionen</td><td>Das Hosting-Abo enthält die Website-Analyse, das Wartungs-Abo zusätzlich News-Pinnwand, Terminplaner, Webshop, Buchungskalender und Öffnungszeiten live. Das Einschalten kostet nichts extra; die Einrichtung geht vom Stundenkontingent ab (§ 4 Abs. 8).</td></tr>
<tr><td>E-Mail-Zusatzspeicher</td><td>In beiden Abos: je Paket von 5 GB 1,00 € pro Monat, bis zu 10 Pakete. Im Kundenportal bestellbar und reduzierbar nach Leistungsbeginn, solange das Abo nicht gekündigt ist und keine Zahlung offen ist; ganz oder teilweise kündbar zum Ende des Abrechnungsmonats.</td></tr>
<tr><td>Preise</td><td>Alle Preise sind Endpreise, zahlbar monatlich im Voraus. Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.</td></tr>
<tr><td>Zahlung</td><td>Kreditkarte oder SEPA-Lastschrift über Stripe</td></tr>
<tr><td>Laufzeit</td><td>Mindestlaufzeit 12 Monate ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach unbefristet und jederzeit zum Ende des Abrechnungsmonats kündbar. Bei Geschäftsaufgabe Sonderkündigung mit einem Monat Frist.</td></tr>
<tr><td>Wechsel</td><td>Vom Hosting- ins Wartungs-Abo im Kundenportal nach Leistungsbeginn, auch während der Mindestlaufzeit, solange das Hosting-Abo nicht gekündigt ist und keine Zahlung offen ist; der Wechsel steht unter der Bedingung, dass die Wechselrechnung bezahlt wird (§ 9 Abs. 3). Mit dem Wechseltag beginnt eine neue Mindestlaufzeit von 12 Monaten. Vom Wartungs- ins Hosting-Abo auf Anfrage nach Ablauf der Mindestlaufzeit.</td></tr>
<tr><td>Kündigen</td><td>project.webklar.com/kuendigen.html (ohne Anmeldung; Auswahl: das Abo oder nur der E-Mail-Zusatzspeicher, ganz oder mit der Zahl der Pakete) oder per E-Mail</td></tr>
<tr><td>Widerruf</td><td>Verbraucher können jede Bestellung (Abo, Wechsel, Zusatzspeicher) innerhalb von 14 Tagen ab Vertragsschluss widerrufen: project.webklar.com/widerrufen.html (siehe Widerrufsbelehrung und Hinweise zum Widerruf)</td></tr>
</table>
</div>
<div class="box">
<strong>Inhalt</strong>
<ol class="toc">
<li><a href="#p1">Anbieter und Geltungsbereich</a></li>
<li><a href="#p2">Vertragsschluss und Vertragstext</a></li>
<li><a href="#p3">Leistungen</a></li>
<li><a href="#p4">Arbeitszeit und Stundenkontingent</a></li>
<li><a href="#p5">Verfügbarkeit und Wartung</a></li>
<li><a href="#p6">Pflichten des Kunden</a></li>
<li><a href="#p7">Preise und Zahlung</a></li>
<li><a href="#p8">Laufzeit und Kündigung</a></li>
<li><a href="#wechsel">Wechsel zwischen den Abos</a></li>
<li><a href="#zusatzspeicher">E-Mail-Zusatzspeicher</a></li>
<li><a href="#p11">Vertragsende</a></li>
<li><a href="#p12">Mängel</a></li>
<li><a href="#p13">Haftung</a></li>
<li><a href="#p14">Datenschutz</a></li>
<li><a href="#p15">Änderungen dieser Bedingungen</a></li>
<li><a href="#p16">Schlussbestimmungen</a></li>
</ol>
<p class="meta" style="margin-bottom:0">Außerdem: <a href="#hosting">Leistungsbeschreibung A: Hosting-Abo</a> · <a href="#wartung">Leistungsbeschreibung B: Wartungs-Abo</a> · <a href="#widerruf">Widerrufsbelehrung und Muster-Widerrufsformular</a> · <a href="#widerruf-hinweise">Hinweise zum Widerruf</a> · <a href="#avv">Anlage 1: Auftragsverarbeitungsvertrag</a> · <a href="#tom">Anlage 2: Technische und organisatorische Maßnahmen</a> · <a href="#sub">Anlage 3: Unterauftragsverarbeiter</a></p>
</div>
<h2 id="p1">§ 1 Anbieter und Geltungsbereich</h2>
<ol class="abs">
<li>Diese Bedingungen regeln das Hosting-Abo und das Wartungs-Abo (zusammen „Abos“) zwischen der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler („WEBklar“ oder „wir“), und dem Kunden. Welches Abo der Kunde bestellt hat, ergibt sich aus seiner Bestellung und unserer Bestätigungs-E-Mail. Den Leistungsumfang beschreiben Leistungsbeschreibung A (Hosting-Abo) und Leistungsbeschreibung B (Wartungs-Abo); sie sind Teil dieser Bedingungen. Zu beiden Abos kann der Kunde E-Mail-Zusatzspeicher buchen (§ 10).</li>
<li>Sie gelten für Verbraucher (§ 13 BGB) und Unternehmer (§ 14 BGB). Gilt eine Regel nur für eine der beiden Gruppen, ist das ausdrücklich angegeben. Die Bestellseiten richten sich nach der Kundenart, die wir im Kundenkonto hinterlegt haben („Betrieb“ oder „privat“). Wir hinterlegen sie nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat. Ob der Kunde Verbraucher oder Unternehmer ist, richtet sich nach dem Zweck, zu dem er den Vertrag abschließt, nicht nach dieser Angabe.</li>
<li>Für die Abos gehen diese Bedingungen den allgemeinen Geschäftsbedingungen von WEBklar (webklar.com/agb) vor. Die dortige Regelung zum jährlichen Hosting (Leistungsmodul C) gilt für die Abos nicht. Die Erstellung einer Website ist nicht Teil der Abos und wird gesondert vereinbart.</li>
<li>Abweichende Geschäftsbedingungen des Kunden werden nicht Vertragsbestandteil.</li>
</ol>
<h2 id="p2">§ 2 Vertragsschluss und Vertragstext</h2>
<ol class="abs">
<li>Die Abos werden im Kundenportal unter project.webklar.com bestellt. Den Zugang zum Kundenportal richten wir für den Kunden ein.</li>
<li>Der Abschluss eines Abos läuft so ab:
<ol type="a">
<li>Der Kunde wählt im Kundenportal im Bereich „Verträge“ bei dem gewünschten Abo „Abo abschließen“.</li>
<li>Eine Bestellübersicht zeigt Leistung, Preis, Laufzeit und Zahlungsweise.</li>
<li>Direkt über der Schaltfläche steht der Hinweis, dass diese Bedingungen mit den Leistungsbeschreibungen, dem Auftragsverarbeitungsvertrag (Anlage 1) und der Widerrufsbelehrung gelten, jeweils mit Fassung und Link; die Unterlagen erhält der Kunde nach der Bestellung zusätzlich als PDF per E-Mail. Ist die Kundenart „privat“ hinterlegt, kann der Kunde dort durch ein Kästchen verlangen, dass wir sofort mit der Leistung beginnen; ohne das Kästchen beginnen wir erst nach Ablauf der 14-tägigen Widerrufsfrist. Ist die Kundenart „Betrieb“ hinterlegt, beginnen wir mit Vertragsschluss, ohne dass der Kunde etwas markieren muss.</li>
<li>Mit dem Klick auf „Zahlungspflichtig bestellen“ gibt er ein verbindliches Angebot ab.</li>
<li>Anschließend hinterlegt er beim Zahlungsdienstleister Stripe eine Zahlungsart (Kreditkarte oder SEPA-Lastschrift) und schließt den Vorgang dort ab.</li>
</ol></li>
<li>Der Vertrag kommt zustande, sobald der Kunde den Vorgang bei Stripe abgeschlossen hat. Bei sofortigem Beginn setzt das die erfolgreiche Zahlung der ersten Monatsgebühr voraus; scheitert sie, kommt kein Vertrag zustande. Wir bestätigen den Vertragsschluss unverzüglich per E-Mail und senden diese Bedingungen mit Widerrufsbelehrung als PDF mit.</li>
<li>Bis zum Abschluss des Vorgangs bei Stripe kann der Kunde jederzeit abbrechen und seine Angaben im Kundenportal korrigieren.</li>
<li>Der Wechsel vom Hosting- ins Wartungs-Abo (§ 9) und die Bestellung von E-Mail-Zusatzspeicher (§ 10) sind eigene Bestellungen. Der Kunde gibt sie im Kundenportal im Bereich „Verträge“ auf. Direkt über der Schaltfläche zeigt eine Übersicht die Leistung, den Preis pro Monat, beim Wechsel den sofort fälligen Betrag, beim Zusatzspeicher den anteiligen Betrag, der mit der nächsten Abbuchung eingezogen wird, den Tag der nächsten Abbuchung sowie Laufzeit und Kündigungsmöglichkeit. Mit dem Klick auf „Zahlungspflichtig bestellen“ kommt der Vertrag zustande; beim Wechsel steht er unter der Bedingung des § 9 Abs. 3 (Zahlung der Wechselrechnung). Bezahlt wird mit der bei Stripe hinterlegten Zahlungsart; ein neuer Vorgang bei Stripe ist nicht nötig. Verlangt die Bank des Kunden eine Bestätigung der Zahlung, leitet das Kundenportal ihn dafür auf die Zahlungsseite von Stripe weiter. Bis zum Klick kann der Kunde abbrechen und seine Angaben korrigieren. Wir bestätigen die Bestellung unverzüglich per E-Mail und senden diese Bedingungen mit Widerrufsbelehrung als PDF mit.</li>
<li>Vertragssprache ist Deutsch. Wir speichern den Vertragstext sowie Zeitpunkt, Version und bestelltes Abo jeder Zustimmung des Kunden. Wir speichern außerdem den Wortlaut der angezeigten Hinweise und Kästchen, die hinterlegte Kundenart sowie Zeit, IP-Adresse und Browserkennung des Klicks. Die aktuelle Fassung dieser Bedingungen ist unter project.webklar.com/abo-bedingungen.html abrufbar. Die bei Vertragsschluss gültige Fassung erhält der Kunde mit der Bestätigungs-E-Mail; er kann sie außerdem jederzeit im Kundenportal unter „Verträge“ abrufen.</li>
</ol>
<h2 id="p3">§ 3 Leistungen</h2>
<h3>(1) Leistungsbeschreibungen</h3>
<p>Den Umfang des Hosting-Abos beschreibt Leistungsbeschreibung A, den des Wartungs-Abos Leistungsbeschreibung B. Beide Abos umfassen den Betrieb der Website auf unserem Server, eine Domain, E-Mail-Postfächer, die tägliche Datensicherung und die Servicezeiten nach den Absätzen 2 bis 6. Das Wartungs-Abo umfasst zusätzlich die Pflege der Website und ein Stundenkontingent (§ 4). Das Hosting-Abo ist nur für statische Websites bestimmt (Leistungsbeschreibung A Nr. 2).</p>
<h3>(2) Betrieb der Website</h3>
<ul>
<li>Wir stellen die Website bzw. Web-Anwendung, die wir für den Kunden erstellt oder zur Betreuung übernommen haben, auf einem von uns betriebenen Server bereit. Der Server steht im Rechenzentrum der Hetzner Online GmbH (im Folgenden „Hetzner“) in Falkenstein (Deutschland).</li>
<li>Speicherplatz und Datenverkehr stehen im Umfang einer üblichen Firmen-Website zur Verfügung. Ungewöhnlich hohe Last, etwa durch große Downloads oder Streaming, ist nicht umfasst.</li>
<li>Wir spielen Sicherheitsupdates für Betriebssystem und Server-Software ein, kritische Sicherheitsupdates in der Regel innerhalb von 3 Werktagen nach ihrer Veröffentlichung, übrige Updates regelmäßig. Aktualisierungen, die nötig sind, damit unsere Leistungen vertragsgemäß bleiben, stellen wir während der gesamten Vertragslaufzeit bereit. Updates der Software-Komponenten der Website selbst gehören nur zum Wartungs-Abo (Leistungsbeschreibung B Nr. 2).</li>
<li>Wir richten ein SSL/TLS-Zertifikat (HTTPS) ein und verlängern es.</li>
<li>Wir überwachen die Erreichbarkeit automatisch und beheben Störungen in unserem Verantwortungsbereich (§ 5).</li>
</ul>
<h3>(3) Domain</h3>
<ul>
<li>Enthalten ist eine Domain mit einer der Endungen .de, .com, .eu, .net oder .org: Registrierung oder Übernahme einer bestehenden Domain, Verlängerung und DNS-Verwaltung. Premium-Domains und andere Endungen nur nach gesonderter Vereinbarung.</li>
<li>Inhaber der Domain wird der Kunde, wir verwalten sie technisch. Den Umzugscode (AuthCode) erhält der Kunde jederzeit auf Anfrage.</li>
<li>Für die Wahl der Domain und dafür, dass sie keine Rechte Dritter verletzt, ist der Kunde verantwortlich.</li>
</ul>
<h3>(4) E-Mail-Postfächer</h3>
<ul>
<li>Wir richten E-Mail-Postfächer unter der Domain des Kunden ein, mit verschlüsseltem Zugang per IMAP und SMTP sowie über Webmail, mit Spam- und Virenfilter. Wir stellen sie über Hetzner (Rechenzentrum Nürnberg, Deutschland) bereit. Das Hosting-Abo umfasst ein Postfach, das Wartungs-Abo beliebig viele Postfächer.</li>
<li>Für die Postfächer des Kunden stehen zusammen 5 GB Speicher zur Verfügung, zuzüglich gebuchtem Zusatzspeicher (§ 10). Hetzner setzt für jedes Postfach eine Speichergrenze. Die Grenzen aller Postfächer des Kunden ergeben zusammen höchstens den verfügbaren Speicher. Wie der Speicher auf die Postfächer verteilt wird, bestimmt der Kunde; ohne Vorgabe teilen wir ihn gleichmäßig auf.</li>
<li>Ist die Speichergrenze eines Postfachs erreicht, nimmt es keine neuen E-Mails mehr an, bis wieder Platz ist, etwa durch Löschen von E-Mails, eine andere Aufteilung oder Zusatzspeicher.</li>
<li>Das Anlegen und Löschen von Postfächern, neue Passwörter und eine andere Aufteilung des Speichers beauftragt der Kunde über den Chat im Kundenportal (Schaltfläche „Postfach anfragen“) oder per E-Mail. Wir setzen das in der Regel innerhalb eines Werktags um. Diese Arbeiten sind im Abo enthalten.</li>
<li>Zugangsdaten zu den Postfächern geben wir nur an den Kunden oder an Personen, die er uns in Textform benennt.</li>
<li>Die Postfächer dürfen nur der Kunde, seine Beschäftigten und andere für ihn tätige Personen für seine Organisation nutzen. Nicht erlaubt sind die Überlassung an Dritte, auch unentgeltlich, sowie Newsletter- und Massenversand. Es gelten die Versandgrenzen von Hetzner.</li>
<li>Die Inhalte der Postfächer sichert Hetzner nach seinen Bedingungen, nicht wir selbst. Wichtige E-Mails sollte der Kunde zusätzlich bei sich aufbewahren.</li>
<li>Die Zustellung von E-Mails hängt auch von Dritten ab, etwa von Empfangsservern und Spamfiltern, und kann nicht garantiert werden.</li>
</ul>
<h3>(5) Datensicherung</h3>
<ul>
<li>Der Server wird täglich automatisch vollständig gesichert (Backup-Funktion von Hetzner). Die Sicherungen liegen getrennt vom Server in einem anderen Brandabschnitt am selben Rechenzentrumsstandort in Deutschland. Verfügbar sind jeweils die letzten sieben Tagessicherungen.</li>
<li>Datenbanken einer Web-Anwendung exportieren wir zusätzlich täglich vor der Serversicherung, damit sie sich in einem einheitlichen Stand wiederherstellen lassen.</li>
<li>Eine Wiederherstellung nach technischen Störungen oder nach Fehlern, die wir zu vertreten haben, ist inklusive. Wünscht der Kunde eine Wiederherstellung aus anderen Gründen, etwa nach eigenen Änderungen, wird sie als Arbeitszeit nach § 4 abgerechnet.</li>
<li>Die Sicherung dient der Wiederherstellung des Betriebs. Ein Archiv oder die Herausgabe von Sicherungskopien ist nicht geschuldet.</li>
</ul>
<h3>(6) Servicezeiten und Reaktion</h3>
<ul>
<li>Servicezeiten sind Montag bis Freitag von 9 bis 17 Uhr, ausgenommen gesetzliche Feiertage in Rheinland-Pfalz. Anfragen außerhalb der Servicezeiten gelten zu Beginn der nächsten Servicezeit als eingegangen.</li>
<li>Auf Anfragen und Störungsmeldungen antworten wir spätestens bis zum Ende des nächsten Werktags. Gemeint ist eine erste fachliche Rückmeldung, nicht zwingend schon die Behebung.</li>
<li>Fällt die Website oder ein Postfach vollständig aus, beginnen wir innerhalb der Servicezeiten unverzüglich mit der Behebung. Unser Ziel ist eine Behebung innerhalb von 4 Stunden. Eine Garantie ist damit nicht verbunden.</li>
<li>Beauftragte Änderungen setzen wir in der Regel innerhalb von 5 Werktagen um.</li>
<li>Betriebsferien kündigen wir mindestens zwei Wochen vorher per E-Mail an.</li>
</ul>
<h3>(7) Technische Änderungen</h3>
<p>Wir dürfen die technische Umsetzung ändern, etwa das Rechenzentrum, die Server-Software oder den Anbieter der E-Mail-Postfächer wechseln. Voraussetzung ist, dass der vereinbarte Leistungsumfang nicht schlechter wird, dem Kunden keine Kosten entstehen und wir ihn mindestens vier Wochen vorher in Textform informieren. Beeinträchtigt eine Änderung den Zugang zur Leistung oder ihre Nutzbarkeit mehr als nur unerheblich, kann der Kunde den Vertrag innerhalb von 30 Tagen nach der Information bzw. der Änderung unentgeltlich beenden (§ 327r BGB).</p>
<h3>(8) Funktionen</h3>
<p>Zum Abo gehören Funktionen, die der Kunde im Kundenportal unter „Funktionen“ einschaltet. Das Hosting-Abo enthält die Website-Analyse. Das Wartungs-Abo enthält News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse. Umfang, Voraussetzungen und Pflichten jeder Funktion beschreiben ihre Leistungsbeschreibung und, soweit die Funktion personenbezogene Daten im Auftrag des Kunden verarbeitet, ihr Auftragsverarbeitungsvertrag, jeweils in der Fassung, die das Kundenportal beim Einschalten anzeigt. Eine Funktion kostet keine zusätzliche Gebühr; die Einrichtung richtet sich nach § 4 Abs. 8 und 9.</p>
<p>Vorbehaltlich § 11 Abs. 4 läuft eine Funktion nur, solange das Abo läuft, das sie enthält. Das gilt auch für eine Funktion, die wir auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut haben. Funktionen, die der Kunde vor Veröffentlichung dieser Fassung einzeln gekauft hat, laufen nach den Bedingungen weiter, unter denen er sie gekauft hat.</p>
<h2 id="p4">§ 4 Arbeitszeit und Stundenkontingent</h2>
<ol class="abs">
<li>Im Wartungs-Abo gehört zum Leistungsumfang ein Kontingent von 60 Minuten Arbeitszeit je bezahltem Abrechnungsmonat. Das erste Kontingent steht mit der ersten Zahlung bereit, nach einem Wechsel aus dem Hosting-Abo mit der Zahlung der Wechselrechnung (§ 9 Abs. 3). Das Kontingent ist Teil der Leistungsbeschreibung und kein Geldguthaben.</li>
<li>Nicht genutzte Zeit bleibt verfügbar: Jedes Monatskontingent kann zwölf Monate lang ab seiner Bereitstellung genutzt werden und entfällt danach. Genutzt wird immer das älteste noch verfügbare Kontingent zuerst. Mit dem Ende des Wartungs-Abos, auch durch einen Wechsel ins Hosting-Abo, entfällt nicht genutzte Zeit. Sie wird weder ausgezahlt noch erstattet und ist nicht übertragbar. Einen Vorschuss nach Absatz 8 gleichen die folgenden Kontingente zuerst aus.</li>
<li>Das Kontingent gilt für Änderungen, Erweiterungen, Beratung und Unterstützung rund um die im Abo betreute Website, Domain und E-Mail. Aufträge erteilt der Kunde über den Chat im Kundenportal oder per E-Mail. Aufträge von mehr als 4 Stunden in einem Monat setzen wir nach Terminabsprache um.</li>
<li>Angerechnet wird die tatsächlich aufgewendete Zeit je Auftrag, aufgerundet auf volle 15 Minuten. Dazu zählen die Umsetzung und die Abstimmung zum Auftrag, soweit sie länger als 10 Minuten dauert. Nicht angerechnet werden die laufenden Leistungen nach § 3 Abs. 2 bis 5 und die Sicherheitsupdates nach Leistungsbeschreibung B Nr. 2 (Betrieb, Updates, Überwachung, Sicherung, Verwaltung von Domain und Postfächern), die Behebung von Störungen (§ 3 Abs. 6, § 5) sowie die Behebung von Fehlern, die wir zu vertreten haben. Wirkt an einem Arbeitsschritt eine weitere Person unterstützend mit, etwa bei Gestaltung, Abstimmung oder Prüfung, rechnen wir für sie die Hälfte der Arbeitszeit dieses Arbeitsschritts an, aufgerundet auf volle 15 Minuten; bei mehreren unterstützenden Personen gilt das für jede von ihnen. Im Kundenportal weisen wir jeden Arbeitsschritt mit seiner Arbeitszeit und jede Unterstützung mit Person, Tätigkeit und angerechneter Zeit einzeln aus.</li>
<li>Verfügbare Zeit, Bereitstellungen, Verbrauch und Ablaufdaten sieht der Kunde jederzeit im Kundenportal.</li>
<li>Reicht die verfügbare Zeit für einen Auftrag voraussichtlich nicht aus, nennen wir dem Kunden vorher den geschätzten Mehraufwand. Wir führen ihn nur nach seiner Freigabe aus, etwa im Chat oder per E-Mail, und berechnen 100 € pro Stunde (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer), abgerechnet in Schritten von 15 Minuten. Aufträge mit einem Umfang von mehr als 10 Stunden bieten wir gesondert an.</li>
<li>Im Hosting-Abo gibt es kein Stundenkontingent. Aufträge, etwa Änderungen an der Website, führen wir nur nach Freigabe des Kunden aus. Vorher nennen wir ihm den geschätzten Aufwand. Wir berechnen 100 € pro Stunde (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer), abgerechnet in Schritten von 15 Minuten; Absatz 4 Satz 1, 2 und 4 gelten entsprechend. Aufträge mit einem Umfang von mehr als 10 Stunden bieten wir gesondert an. Die Behebung von Störungen an Server, Domain und Postfach in unserem Verantwortungsbereich ist auch im Hosting-Abo inklusive.</li>
<li>Die Einrichtung einer Funktion rechnen wir als Arbeitszeit auf das Stundenkontingent an. Angerechnet wird die tatsächlich aufgewendete Zeit nach Absatz 4. Reicht das verfügbare Kontingent nicht aus, führen wir die Einrichtung trotzdem ohne weitere Rückfrage aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen, sobald sie bereitstehen. Dafür berechnen wir nichts, weder als Mehraufwand nach Absatz 6 noch in einer gesonderten Rechnung. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest; der Kunde schuldet ihn nicht.</li>
<li>Im Hosting-Abo ist die Einrichtung der Website-Analyse für den Kunden kostenlos; ein Stundenkontingent gibt es dort nicht (Absatz 7). Lässt der Kunde eine Funktion auf seinen Wunsch schon bei der Erstellung seiner Website einbauen, rechnen wir die Einrichtung nicht über das Stundenkontingent ab, sondern über die Kosten des Projekts, wie wir es mit ihm vereinbart haben.</li>
</ol>
<h2 id="p5">§ 5 Verfügbarkeit und Wartung</h2>
<ol class="abs">
<li>Wir streben eine Erreichbarkeit der Website von 99 % im Jahresmittel an, gemessen am Übergang des Rechenzentrums ins Internet. Eine ununterbrochene Erreichbarkeit ist technisch nicht möglich und wird nicht geschuldet.</li>
<li>Nicht eingerechnet werden:
<ul>
<li>angekündigte Wartungsarbeiten, möglichst zwischen 22 und 6 Uhr und mindestens 24 Stunden vorher angekündigt; dringende Sicherheitsupdates auch kurzfristig,</li>
<li>Störungen außerhalb unseres Einflussbereichs, etwa im Internet, bei der Domain-Vergabestelle oder bei vorgelagerten Netzbetreibern,</li>
<li>Angriffe wie DDoS, sofern wir angemessene Schutzmaßnahmen getroffen haben,</li>
<li>höhere Gewalt sowie Störungen, die der Kunde verursacht hat.</li>
</ul></li>
<li>Die gesetzlichen Rechte des Kunden bei Ausfällen, die wir zu vertreten haben, bleiben unberührt. Die Haftung richtet sich nach § 13.</li>
</ol>
<h2 id="p6">§ 6 Pflichten des Kunden</h2>
<ol class="abs">
<li>Der Kunde hält seine Zugangsdaten zum Kundenportal und zu allen Postfächern geheim und informiert uns unverzüglich, wenn er einen Missbrauch vermutet. Gibt er die Zugangsdaten eines Postfachs an Beschäftigte oder andere für ihn tätige Personen weiter, geschieht das in seiner Verantwortung; er sorgt dafür, dass auch diese Personen die Zugangsdaten geheim halten und die Regeln nach § 3 Abs. 4 einhalten. Ein neues Passwort kann er jederzeit über den Chat im Kundenportal anfordern.</li>
<li>Der Kunde stellt nur Inhalte bereit, die er verwenden darf und die nicht gegen Gesetze verstoßen. Das gilt auch für E-Mails, die über seine Postfächer versendet werden. Offensichtlich rechtswidrige Inhalte dürfen wir nach einem Hinweis vorübergehend sperren. Wir informieren den Kunden darüber unverzüglich.</li>
<li>Für Impressum, Datenschutzerklärung und Inhalte seiner Website ist der Kunde verantwortlich. Die Angaben zum Hosting, die er für seine Datenschutzerklärung braucht, stehen in den Anlagen 1 bis 3.</li>
<li>Der Kunde bewahrt Kopien der Inhalte auf, die er uns übergibt oder selbst einpflegt, und teilt uns Änderungen seiner Kontaktdaten mit.</li>
<li>Im Hosting-Abo stellt der Kunde keine Inhalte oder Funktionen bereit, die nach Leistungsbeschreibung A Nr. 2 im Hosting-Abo nicht möglich sind, und beauftragt keine. Braucht seine Website solche Teile, wechselt er vorher ins Wartungs-Abo (§ 9).</li>
<li>Schaltet der Kunde die Website-Analyse ein, ist er für die Messung auf seiner Website Verantwortlicher. Er entfernt oder umgeht das Cookie-Banner nicht, bindet ohne Abstimmung mit uns keine weiteren Mess- oder Tracking-Werkzeuge ein und ergänzt seine Datenschutzerklärung um einen Hinweis auf die von uns erzeugte Zusatzseite „/datenschutz-analyse.html“.</li>
</ol>
<h2 id="p7">§ 7 Preise und Zahlung</h2>
<ol class="abs">
<li>Die Monatsgebühr beträgt im Hosting-Abo 20,00 € und im Wartungs-Abo 50,00 €. E-Mail-Zusatzspeicher kostet je Paket von 5 GB 1,00 € pro Monat (§ 10). Alle Preise sind Endpreise. Als Kleinunternehmer im Sinne von § 19 UStG berechnen wir keine Umsatzsteuer.</li>
<li>Die Monatsgebühr ist monatlich im Voraus fällig, erstmals mit Beginn der Leistung (bei sofortigem Beginn mit Vertragsschluss, sonst nach Ablauf der Widerrufsfrist), danach jeweils am selben Kalendertag der Folgemonate (Abrechnungsmonat). Gebuchter Zusatzspeicher wird zusammen mit der Monatsgebühr abgebucht. Die Gebühren werden per Kreditkarte oder SEPA-Lastschrift über die Stripe Payments Europe, Limited (Irland) eingezogen. Rechnungen erhält der Kunde per E-Mail und im Kundenportal über „Abo verwalten &amp; Rechnungen“.</li>
<li>Bestellt der Kunde Zusatzspeicher, berechnen wir für den Rest des laufenden Abrechnungsmonats einen anteiligen Betrag. Er wird in der Bestellübersicht angezeigt und mit der nächsten Abbuchung eingezogen. Ab dem folgenden Abrechnungsmonat gilt der volle Monatspreis. Pakete, die für den laufenden Abrechnungsmonat schon bezahlt sind, berechnen wir nicht noch einmal. Verringert der Kunde den Zusatzspeicher, wirkt das zum Ende des laufenden Abrechnungsmonats; die Gebühr für den laufenden Monat wird nicht anteilig erstattet.</li>
<li>Beim Wechsel vom Hosting- ins Wartungs-Abo (§ 9) wird die Monatsgebühr des Wartungs-Abos am Wechseltag fällig, zusammen mit gebuchtem Zusatzspeicher. Die im Voraus bezahlten Gebühren für die nicht genutzten Tage des laufenden Abrechnungsmonats schreiben wir auf dieser Rechnung gut. Die folgenden Abrechnungsmonate beginnen jeweils am Kalendertag des Wechsels. Den fälligen Betrag und den neuen Abbuchungstag zeigt die Bestellübersicht vor der Bestellung. Wird die Wechselrechnung nicht bezahlt, gilt § 9 Abs. 3.</li>
<li>Schlägt eine Zahlung fehl, versucht Stripe den Einzug automatisch erneut und informiert den Kunden per E-Mail.</li>
<li>Ist der Kunde mit einem Betrag in Höhe von zwei Monatsgebühren seines Abos in Verzug, dürfen wir die Leistungen vorübergehend sperren, etwa die Website offline nehmen. Vorher kündigen wir die Sperre in Textform an und setzen eine Frist von mindestens zwei Wochen. Das gilt nicht, solange der Kunde die Forderung mit nachvollziehbaren Gründen bestreitet. Während einer Sperre löschen wir keine Daten. Nach Zahlung heben wir die Sperre unverzüglich auf. Ist der Kunde mit zwei aufeinanderfolgenden Monatsgebühren in Verzug, dürfen wir außerordentlich kündigen.</li>
<li>Preisänderungen kündigen wir mindestens 6 Wochen vor ihrem Wirksamwerden in Textform an. Sie gelten nur, wenn der Kunde zustimmt. Ohne Zustimmung läuft der Vertrag zum bisherigen Preis weiter, und beide Seiten können nach § 8 ordentlich kündigen.</li>
</ol>
<h2 id="p8">§ 8 Laufzeit und Kündigung</h2>
<ol class="abs">
<li>Jedes Abo hat eine Mindestlaufzeit von zwölf Monaten ab Leistungsbeginn, bei sofortigem Beginn also ab Vertragsschluss, sonst ab Ablauf der Widerrufsfrist. Nach einem Wechsel vom Hosting- ins Wartungs-Abo beginnt eine neue Mindestlaufzeit von zwölf Monaten ab dem Wechseltag (§ 9 Abs. 3). Das Ende der laufenden Mindestlaufzeit zeigt das Kundenportal unter „Verträge“.</li>
<li>Beide Seiten können zum Ende der Mindestlaufzeit mit einer Frist von einem Monat kündigen. Wird nicht fristgerecht gekündigt, läuft das Abo auf unbestimmte Zeit weiter.</li>
<li>Nach Ablauf der Mindestlaufzeit kann der Kunde jederzeit zum Ende des laufenden Abrechnungsmonats kündigen. Wir können dann mit einer Frist von einem Monat zum Ende eines Abrechnungsmonats kündigen.</li>
<li>Gibt der Kunde den Geschäftsbetrieb, für den die Website bestimmt ist, dauerhaft auf, kann er auch während der Mindestlaufzeit mit einer Frist von einem Monat zum Ende eines Abrechnungsmonats kündigen. Auf Verlangen weist er die Geschäftsaufgabe nach, etwa durch die Gewerbeabmeldung.</li>
<li>Eine vorzeitige Beendigung im gegenseitigen Einvernehmen ist jederzeit möglich.</li>
<li>Das Recht zur außerordentlichen Kündigung aus wichtigem Grund bleibt unberührt.</li>
<li>Der E-Mail-Zusatzspeicher hat keine Mindestlaufzeit. Der Kunde kann ihn jederzeit zum Ende des laufenden Abrechnungsmonats ganz oder teilweise (mit einer geringeren Zahl von Paketen) kündigen (§ 10 Abs. 5). Er endet spätestens mit dem Abo.</li>
<li>Der Kunde kann über die Schaltfläche „Verträge hier kündigen“ unter project.webklar.com/kuendigen.html kündigen (ohne Anmeldung) oder in Textform, etwa per E-Mail. Dort wählt er, ob er sein Abo einschließlich des Zusatzspeichers kündigt oder mit der Auswahl „Nur den E-Mail-Zusatzspeicher“ allein den Zusatzspeicher, ganz oder mit der Zahl der Pakete, die er kündigt; das geht auch, wenn das Abo schon gekündigt ist. Den Zeitpunkt, zu dem die Kündigung wirkt, bestätigen wir unverzüglich per E-Mail. Bis dahin bleiben alle Leistungen verfügbar. Die Gebühr für den laufenden Monat wird nicht anteilig erstattet.</li>
</ol>
<h2 id="wechsel">§ 9 Wechsel zwischen den Abos</h2>
<ol class="abs">
<li>Ein Kunde mit laufendem Hosting-Abo kann jederzeit ins Wartungs-Abo wechseln, auch während der Mindestlaufzeit des Hosting-Abos. Voraussetzung ist, dass die Leistung des Hosting-Abos begonnen hat, keine Zahlung offen ist und das Hosting-Abo nicht gekündigt ist.</li>
<li>Der Wechsel ist ein neuer Vertrag über das Wartungs-Abo. Der Kunde bestellt ihn im Bereich „Verträge“ des Kundenportals (§ 2 Abs. 5). Die Bestellübersicht zeigt den Preis des Wartungs-Abos, gebuchten Zusatzspeicher, die Gutschrift für die nicht genutzten Tage des Hosting-Abos, den sofort fälligen Betrag, den neuen Abbuchungstag, das Ende der neuen Mindestlaufzeit und die Kündigungsmöglichkeit nach Ablauf der neuen Mindestlaufzeit. Der Kunde stimmt dabei diesen Bedingungen einschließlich des Auftragsverarbeitungsvertrags (Anlage 1) zu. Wird der Wechsel wirksam, endet das Hosting-Abo im gegenseitigen Einvernehmen.</li>
<li>Der Wechsel steht unter der Bedingung, dass die Wechselrechnung bezahlt wird. Bei Zahlung per Kreditkarte wird er erst mit der erfolgreichen Zahlung wirksam. Gelingt die Zahlung nicht, etwa weil der Kunde eine von seiner Bank verlangte Freigabe nicht bestätigt, wird er nicht wirksam; spätestens 23 Stunden nach der Bestellung verfällt die Wechselrechnung. Bei SEPA-Lastschrift beginnt das Wartungs-Abo schon mit der Bestellung; wird die Lastschrift nicht eingelöst, gilt der Wechsel als nicht erfolgt. In diesen Fällen läuft das Hosting-Abo mit Preis und Mindestlaufzeit wie vor dem Wechsel weiter; bei SEPA-Lastschrift bleibt der Abbuchungstag der Wechseltag; wir informieren den Kunden per E-Mail. Das Wartungs-Abo beginnt, sobald der Wechsel wirksam ist; einen späteren Beginn bietet der Wechsel nicht. Der Kunde verlangt bei der Bestellung ausdrücklich, dass wir vor Ablauf der Widerrufsfrist beginnen (siehe Hinweise zum Widerruf). Ab Beginn des Wartungs-Abos gilt Leistungsbeschreibung B. Wechseltag ist der Tag der Bestellung; er ist der neue Abbuchungstag (§ 7 Abs. 4), und mit ihm beginnt eine neue Mindestlaufzeit von zwölf Monaten (§ 8 Abs. 1). Das erste Stundenkontingent stellen wir bereit, sobald die Wechselrechnung bezahlt ist, danach monatlich (§ 4). Postfächer und gebuchter Zusatzspeicher bleiben unverändert; ab Beginn des Wartungs-Abos kann der Kunde weitere Postfächer beauftragen; wir richten sie ein, sobald die Wechselrechnung bezahlt ist.</li>
<li>Die Abrechnung des Wechsels richtet sich nach § 7 Abs. 4. Ein laufender Rabatt aus einem persönlichen Angebot läuft bis zu seinem Ende weiter. Guthaben aus einer Übernahmegutschrift wird weiter verrechnet.</li>
<li>Ein Wechsel vom Wartungs- ins Hosting-Abo ist nach Ablauf der Mindestlaufzeit des Wartungs-Abos zum Ende eines Abrechnungsmonats möglich. Der Kunde teilt uns den Wunsch in Textform mit, etwa im Chat des Kundenportals oder per E-Mail; eine Bestellung im Kundenportal gibt es dafür nicht. Voraussetzung ist, dass die Website statisch ist (Leistungsbeschreibung A Nr. 2). Vor dem Wechsel legt der Kunde fest, welches Postfach bleibt; die Inhalte der übrigen Postfächer stellen wir ihm auf Wunsch vorher als Export bereit, danach löschen wir sie. Nicht genutzte Arbeitszeit entfällt mit dem Wechsel (§ 4 Abs. 2). Es beginnt keine neue Mindestlaufzeit; das Hosting-Abo ist danach nach § 8 Abs. 3 kündbar. Ab dem Wechsel gilt die Monatsgebühr des Hosting-Abos; eine anteilige Verrechnung für den laufenden Abrechnungsmonat gibt es nicht. Wir bestätigen den Wechsel per E-Mail.</li>
<li>Will der Kunde im Hosting-Abo eine Funktion einschalten, die nur das Wartungs-Abo enthält, führt ihn das Kundenportal zum Wechsel. Nach Wirksamkeit des Wechsels schaltet er die Funktion in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung nach Absatz 2.</li>
</ol>
<h2 id="zusatzspeicher">§ 10 E-Mail-Zusatzspeicher</h2>
<ol class="abs">
<li>In beiden Abos kann der Kunde den Speicher seiner Postfächer erweitern. Ein Paket Zusatzspeicher umfasst 5 GB und kostet 1,00 € pro Monat (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer). Der Kunde kann bis zu 10 Pakete buchen. Der Zusatzspeicher erhöht den Speicher, der allen Postfächern des Kunden zusammen zur Verfügung steht (§ 3 Abs. 4). Bestellungen und Änderungen des Zusatzspeichers im Kundenportal sind möglich, sobald die Leistung des Abos begonnen hat, und nur, solange keine Zahlung offen ist und das Abo nicht gekündigt ist.</li>
<li>Der Kunde bestellt Zusatzspeicher im Kundenportal im Bereich „Verträge“, indem er die Zahl der Pakete erhöht (§ 2 Abs. 5). Die Bestellübersicht zeigt die neue Gesamtgröße, den Mehrpreis pro Monat, den anteiligen Betrag für den Rest des laufenden Abrechnungsmonats, den Tag der nächsten Abbuchung und die Laufzeit (monatlich, zum Ende jedes Abrechnungsmonats kündbar, endet spätestens mit dem Abo). Mit dem Klick auf „Zahlungspflichtig bestellen“ kommt der Vertrag über den Zusatzspeicher zustande.</li>
<li>Wir stellen den zusätzlichen Speicher in der Regel innerhalb eines Werktags bereit und verteilen ihn nach den Vorgaben des Kunden auf die Postfächer (§ 3 Abs. 4). Bis dahin zeigt das Kundenportal die Bestellung als „wird eingerichtet“ an. Wir beginnen mit der Bereitstellung sofort, auch während der Widerrufsfrist; die Folgen eines Widerrufs stehen in den Hinweisen zum Widerruf.</li>
<li>Die Abrechnung richtet sich nach § 7 Abs. 3.</li>
<li>Der Kunde kann die Zahl der Pakete verringern, im Kundenportal im Bereich „Verträge“ mit der Schaltfläche „Speichern“, solange das Abo nicht gekündigt ist. Den Zusatzspeicher ganz oder teilweise kündigen kann er jederzeit über „Verträge hier kündigen“ mit der Auswahl „Nur den E-Mail-Zusatzspeicher“ und der Zahl der Pakete oder in Textform (§ 8 Abs. 8); nach einer Kündigung des Abos kann der Kunde die Zahl der Pakete nur noch über „Verträge hier kündigen“ oder in Textform verringern. Die Änderung wirkt zum Ende des laufenden Abrechnungsmonats; bis dahin zeigt das Kundenportal den Zeitpunkt und die künftige Größe an. Wir bestätigen die Änderung per E-Mail. Passen die gespeicherten E-Mails dann nicht mehr in den verbleibenden Speicher, nimmt das betroffene Postfach keine neuen E-Mails mehr an, bis wieder Platz ist (§ 3 Abs. 4). Der Kunde sollte vorher E-Mails löschen oder sichern.</li>
<li>Der Zusatzspeicher endet spätestens mit dem Abo. Bei einem Wechsel ins Wartungs-Abo läuft er unverändert weiter.</li>
</ol>
<h2 id="p11">§ 11 Vertragsende</h2>
<ol class="abs">
<li>Während der Laufzeit und bis 30 Tage nach Vertragsende stellen wir dem Kunden auf Wunsch eine Kopie seiner Website bzw. Anwendungsdaten (Dateien und Datenbank-Export) und der Inhalte aller seiner Postfächer bereit. Wem Quellcode und Nutzungsrechte an der Website zustehen, richtet sich nach dem Vertrag über ihre Erstellung.</li>
<li>Wir geben den AuthCode der Domain heraus und helfen beim Umzug zu einem anderen Anbieter. Ist die Domain 30 Tage nach Vertragsende nicht umgezogen, dürfen wir ihre Registrierung beenden. Das kündigen wir vorher in Textform an und setzen eine Frist von 14 Tagen.</li>
<li>Nach Ablauf der Frist aus Absatz 1 löschen wir die Daten, die wir für den Kunden gespeichert haben (Website bzw. Anwendungsdaten und alle Postfächer). Vertrags- und Abrechnungsdaten bewahren wir auf, wie es die Datenschutzerklärung beschreibt (project.webklar.com/datenschutz.html). Sicherungskopien werden spätestens mit Ablauf ihrer Aufbewahrungszeit (§ 3 Abs. 5) überschrieben.</li>
<li>Mit dem Ende des Abos, das eine Funktion enthält, endet die Funktion; abweichend davon läuft Öffnungszeiten live noch 30 Tage unverändert weiter. Wir schalten sie ab und löschen ihre Daten nach den Fristen in ihrer Leistungsbeschreibung; die Messdaten der Website-Analyse löschen wir 60 Tage nach der Abschaltung. Beginnt innerhalb dieser Fristen wieder ein Abo, das die Funktion enthält, schalten wir sie mit den vorhandenen Daten wieder ein. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; die übrigen Funktionen enden. Das Gleiche gilt, wenn der Kunde den Wechsel ins Wartungs-Abo widerruft.</li>
</ol>
<h2 id="p12">§ 12 Mängel</h2>
<p>Es gelten die gesetzlichen Regeln, für die digitalen Leistungen (Bereitstellung der Website, E-Mail-Postfächer, Zusatzspeicher) insbesondere das gesetzliche Mängelhaftungsrecht für digitale Dienstleistungen (§§ 327 ff. BGB). Mängel unserer Leistungen beheben wir nach Meldung innerhalb angemessener Frist.</p>
<h2 id="p13">§ 13 Haftung</h2>
<ol class="abs">
<li>Wir haften unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit wir eine Garantie übernommen haben.</li>
<li>Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haften wir 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.</li>
<li>Außer in den Fällen von Absatz 1 haften wir für den Verlust von Daten nur in Höhe des Aufwands, der zur Wiederherstellung nötig gewesen wäre, wenn der Kunde seine Pflicht aus § 6 Abs. 4 erfüllt hätte.</li>
<li>Die verschuldensunabhängige Haftung für Mängel, die schon bei Vertragsschluss vorhanden waren (§ 536a Abs. 1 BGB), ist ausgeschlossen.</li>
<li>Diese Regeln gelten auch zugunsten unserer Mitarbeiter und Beauftragten.</li>
</ol>
<h2 id="p14">§ 14 Datenschutz</h2>
<ol class="abs">
<li>Wie wir personenbezogene Daten des Kunden verarbeiten, beschreibt unsere Datenschutzerklärung unter project.webklar.com/datenschutz.html.</li>
<li>Soweit wir im Rahmen eines Abos personenbezogene Daten im Auftrag des Kunden verarbeiten, etwa E-Mails in seinen Postfächern, Eingaben in Kontaktformulare oder Server-Protokolle seiner Website, gilt der Auftragsverarbeitungsvertrag in Anlage 1. Er wird mit der Bestellung des Abos vereinbart und beim Wechsel ins Wartungs-Abo erneut bestätigt. Für Funktionen, die personenbezogene Daten im Auftrag des Kunden verarbeiten, gilt zusätzlich der Auftragsverarbeitungsvertrag der Funktion, dem der Kunde beim Einschalten mit dem Klick auf „Einschalten“ zustimmt.</li>
</ol>
<h2 id="p15">§ 15 Änderungen dieser Bedingungen</h2>
<ol class="abs">
<li>Änderungen teilen wir mindestens 6 Wochen vor ihrem Wirksamwerden in Textform mit. Sie werden nur mit Zustimmung des Kunden wirksam. Ohne Zustimmung gelten die bisherigen Bedingungen weiter, und beide Seiten können nach § 8 kündigen.</li>
<li>Fassung 6 gilt für Abos, die ab ihrer Veröffentlichung abgeschlossen werden. Für Abos, die nach einer früheren Fassung abgeschlossen wurden, gelten diese Fassungen weiter. Schaltet der Kunde mit einem solchen Abo eine Funktion ein, gelten für die Funktion ihre Leistungsbeschreibung und ihr Auftragsverarbeitungsvertrag in der angezeigten Fassung, einschließlich der Regeln zur Einrichtung und zum Stundenkontingent.</li>
</ol>
<h2 id="p16">§ 16 Schlussbestimmungen</h2>
<ol class="abs">
<li>Es gilt deutsches Recht unter Ausschluss des UN-Kaufrechts. Gegenüber Verbrauchern gilt diese Rechtswahl nur, soweit ihnen dadurch nicht der Schutz zwingender Vorschriften des Staates entzogen wird, in dem sie ihren gewöhnlichen Aufenthalt haben.</li>
<li>Ist der Kunde Kaufmann, juristische Person des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen, ist Gerichtsstand der Sitz von WEBklar.</li>
<li>Wir sind nicht verpflichtet und nicht bereit, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</li>
</ol>
<p class="anlage-kopf neue-seite" id="hosting">Leistungsbeschreibung A</p>
<h2>Hosting-Abo</h2>
<p class="hinweis">20,00 € pro Monat · Endpreis, gemäß § 19 UStG ohne Umsatzsteuer · Mindestlaufzeit 12 Monate · für statische Websites</p>
<h3>1. Umfang</h3>
<ul>
<li>Bereitstellung der Website auf unserem Server in Deutschland mit SSL-Zertifikat, Überwachung der Erreichbarkeit und Updates von Betriebssystem und Server-Software (§ 3 Abs. 2)</li>
<li>1 Domain mit Verwaltung (§ 3 Abs. 3)</li>
<li>1 E-Mail-Postfach mit 5 GB Speicher; mehr Speicher über den Zusatzspeicher (§ 3 Abs. 4, § 10)</li>
<li>tägliche Datensicherung (§ 3 Abs. 5)</li>
<li>Servicezeiten und Reaktion (§ 3 Abs. 6); die Behebung von Störungen an Server, Domain und Postfach in unserem Verantwortungsbereich ist inklusive</li>
<li>Website-Analyse im Kundenportal (§ 3 Abs. 8)</li>
</ul>
<h3>2. Nur statische Websites</h3>
<ul>
<li>Das Hosting-Abo ist für statische Websites bestimmt, die wir erstellt oder zur Betreuung übernommen haben. Statisch heißt: Der Webserver liefert die Dateien der Website (HTML, CSS, JavaScript, Bilder) unverändert aus. Dazu gehören auch die von uns erstellten React- und Vite-Seiten.</li>
<li>Nicht möglich sind ein Content-Management-System, ein Login, eine Datenbank und eigener serverseitiger Code (etwa PHP). Solche Teile brauchen laufende Updates; ohne sie wären sie ein Sicherheitsrisiko für den Server, auf dem auch andere Websites und unser Kundenportal laufen.</li>
<li>Kontakt über E-Mail-Links auf der Website ist möglich. Formulare, die Eingaben an einen Server übermitteln, sind im Hosting-Abo nicht enthalten; eine Lösung dafür bieten wir auf Anfrage gesondert an.</li>
<li>Braucht die Website später dynamische Teile, bieten wir den Wechsel ins Wartungs-Abo an (§ 9).</li>
</ul>
<h3>3. Keine Pflege der Website</h3>
<ul>
<li>Updates und Pflege der Website selbst sind nicht enthalten. Weil statische Websites keinen eigenen Code auf dem Server ausführen, sind sie für den sicheren Betrieb in der Regel nicht nötig.</li>
<li>Änderungen an der Website und andere Aufträge führen wir nur nach Freigabe des Kunden aus und berechnen 100 € pro Stunde (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer) in Schritten von 15 Minuten (§ 4 Abs. 7). Ein Stundenkontingent gibt es im Hosting-Abo nicht.</li>
</ul>
<h3>4. Nicht enthalten</h3>
<ul>
<li>Änderungen, neue Funktionen, Redesign oder neue Seiten ohne gesonderten Auftrag (Nr. 3).</li>
<li>Texte, Bilder und Rechtstexte (Impressum, Datenschutzerklärung) für die Website des Kunden, Suchmaschinenoptimierung und Werbung.</li>
<li>Unterstützung für Endgeräte, Betriebssysteme oder E-Mail-Programme des Kunden.</li>
<li>Behebung von Fehlern, die der Kunde oder Dritte durch eigene Änderungen verursacht haben. Das ist als Auftrag nach Nr. 3 möglich.</li>
</ul>
<p class="anlage-kopf" id="wartung">Leistungsbeschreibung B</p>
<h2>Wartungs-Abo</h2>
<p class="hinweis">50,00 € pro Monat · Endpreis, gemäß § 19 UStG ohne Umsatzsteuer · Mindestlaufzeit 12 Monate, auch nach einem Wechsel · für alle Websites und Web-Anwendungen, die wir erstellt oder zur Betreuung übernommen haben</p>
<h3>1. Umfang</h3>
<ul>
<li>Betrieb der Website bzw. Web-Anwendung auf unserem Server in Deutschland mit SSL-Zertifikat, Überwachung der Erreichbarkeit und Updates von Betriebssystem und Server-Software (§ 3 Abs. 2)</li>
<li>Pflege der Website (Nr. 2)</li>
<li>1 Domain mit Verwaltung (§ 3 Abs. 3)</li>
<li>beliebig viele Postfächer, zusammen 5 GB Speicher; mehr Speicher über den Zusatzspeicher (§ 3 Abs. 4, § 10)</li>
<li>tägliche Datensicherung (§ 3 Abs. 5)</li>
<li>1 Stunde Arbeitszeit je bezahltem Monat; nicht genutzte Zeit bleibt 12 Monate verfügbar (§ 4)</li>
<li>Servicezeiten und Reaktion (§ 3 Abs. 6)</li>
<li>Funktionen im Kundenportal: News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse (§ 3 Abs. 8)</li>
</ul>
<h3>2. Pflege der Website</h3>
<ul>
<li>Zusätzlich zu den Updates nach § 3 Abs. 2 spielen wir Sicherheitsupdates für die von uns eingesetzten Software-Komponenten der Website bzw. Web-Anwendung ein, kritische in der Regel innerhalb von 3 Werktagen nach ihrer Veröffentlichung, übrige regelmäßig. Aktualisierungen, die für den vertragsgemäßen Betrieb nötig sind, stellen wir während der gesamten Vertragslaufzeit bereit.</li>
<li>Das Wartungs-Abo eignet sich für alle Websites und Web-Anwendungen, die wir erstellt oder zur Betreuung übernommen haben, auch mit Content-Management-System, Login, Datenbank oder serverseitigem Code.</li>
<li>Änderungswünsche setzen wir über das Stundenkontingent um (§ 4), in der Regel innerhalb von 5 Werktagen.</li>
</ul>
<h3>3. Nicht enthalten</h3>
<ul>
<li>Neue Funktionen, Redesign oder neue Seiten, soweit sie nicht über das Stundenkontingent beauftragt werden.</li>
<li>Texte, Bilder und Rechtstexte (Impressum, Datenschutzerklärung) für die Website des Kunden, Suchmaschinenoptimierung und Werbung.</li>
<li>Unterstützung für Endgeräte, Betriebssysteme oder E-Mail-Programme des Kunden.</li>
<li>Behebung von Fehlern, die der Kunde oder Dritte durch eigene Änderungen verursacht haben. Das ist über das Stundenkontingent möglich.</li>
</ul>
<div class="neue-seite"></div>
<h2 id="widerruf">Widerrufsbelehrung</h2>
<p class="hinweis">Das Widerrufsrecht gilt nur für Verbraucher. Die folgende Belehrung entspricht dem gesetzlichen Muster. Sie gilt für jede Bestellung: den Abschluss eines Abos, den Wechsel ins Wartungs-Abo und die Bestellung von Zusatzspeicher. Hinweise dazu stehen nach dem Muster-Widerrufsformular.</p>
<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. 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>
<p class="meta">Ende der Widerrufsbelehrung</p>
<h3 id="formular">Muster-Widerrufsformular</h3>
<div class="box">
<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 class="meta">(*) Unzutreffendes streichen.</p>
</div>
<h2 id="widerruf-hinweise">Hinweise zum Widerruf</h2>
<p class="hinweis">Diese Hinweise gehören zu diesen Bedingungen, nicht zur gesetzlichen Widerrufsbelehrung. Sie erklären, wie sich das Widerrufsrecht auf die einzelnen Bestellungen auswirkt.</p>
<ol class="abs">
<li>Das Widerrufsrecht gilt für jede Bestellung gesondert: für den Abschluss eines Abos, für den Wechsel ins Wartungs-Abo und für jede Bestellung von Zusatzspeicher. Die Frist von vierzehn Tagen beginnt jeweils mit dem Vertragsschluss (§ 2 Abs. 3 bzw. Abs. 5). Unter project.webklar.com/widerrufen.html wählt der Kunde aus, welche Bestellung er widerruft. Solange eine Widerrufsfrist läuft, zeigt das Kundenportal unter „Verträge“ den letzten Tag der Frist an.</li>
<li>Widerruf des Abos: Widerruft der Kunde den Abschluss seines Abos, endet damit auch der dazu bestellte Zusatzspeicher. Was der Kunde für den Zusatzspeicher gezahlt hat, erstatten wir vollständig; Wertersatz verlangen wir dafür nicht (Nr. 4).</li>
<li>Widerruf des Wechsels: Widerruft der Kunde den Wechsel ins Wartungs-Abo, läuft sein Hosting-Abo zu den bisherigen Bedingungen weiter, als hätte es den Wechsel nicht gegeben, einschließlich seiner ursprünglichen Mindestlaufzeit. Wir erstatten den für das Wartungs-Abo gezahlten Mehrbetrag. Mehrbetrag ist, was der Kunde seit dem Wechsel mehr gezahlt hat, als er ohne den Wechsel für das Hosting-Abo gezahlt hätte. Weil der Kunde beim Wechsel den sofortigen Beginn verlangt hat, ziehen wir davon den Wertersatz für die bis zum Widerruf erbrachte Leistung ab (§ 357a Abs. 2 BGB): den anteiligen Mehrpreis des Wartungs-Abos gegenüber dem Hosting-Abo für die Tage vom Beginn des Wartungs-Abos bis zum Widerruf.</li>
<li>Widerruf des Zusatzspeichers: Wir stellen bestellten Zusatzspeicher sofort bereit, ohne dass der Kunde das ausdrücklich verlangen muss. Widerruft er die Bestellung, nehmen wir die Pakete zurück und erstatten alles, was er dafür gezahlt hat. Wertersatz verlangen wir nicht. Passen die gespeicherten E-Mails danach nicht mehr in den verbleibenden Speicher, nimmt das betroffene Postfach keine neuen E-Mails mehr an, bis wieder Platz ist (§ 3 Abs. 4).</li>
<li>Das Einschalten einer Funktion ist kein eigener Vertrag, sondern Teil des Abos. Es verlängert die Widerrufsfrist des Abos nicht. Widerruft ein Verbraucher das Abo, endet auch die Funktion (§ 11 Abs. 4).</li>
</ol>
<p class="anlage-kopf neue-seite" id="avv">Anlage 1</p>
<h2>Auftragsverarbeitungsvertrag nach Art. 28 DSGVO</h2>
<p class="hinweis">zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR als Auftragsverarbeiter („WEBklar“). Der Vertrag wird mit der Bestellung des Hosting-Abos oder des Wartungs-Abos vereinbart; beim Wechsel ins Wartungs-Abo bestätigt der Kunde ihn erneut.</p>
<h3>1. Gegenstand und Dauer</h3>
<p>WEBklar verarbeitet personenbezogene Daten im Auftrag des Kunden, soweit das für die Leistungen des Hosting-Abos oder des Wartungs-Abos (Bereitstellung bzw. Betrieb und Pflege der Website, Domain, E-Mail-Postfächer einschließlich Zusatzspeicher, Unterstützung) sowie für gesondert vereinbarte Zusatzleistungen zum Abo, etwa eine Formularlösung, nötig ist. Der Vertrag gilt für die Dauer des Abos, auch über einen Wechsel zwischen den Abos hinweg, und darüber hinaus, bis alle Daten nach Nummer 8 gelöscht oder zurückgegeben sind.</p>
<h3>2. Art und Zweck der Verarbeitung, Datenarten, Betroffene</h3>
<table>
<tr><td>Art der Verarbeitung</td><td>Speichern, Bereitstellen, Übermitteln (Empfang und Versand von E-Mails, Weiterleiten von Kontaktformular-Eingaben per E-Mail an den Kunden), Sichern, Wiederherstellen und Löschen; Einsicht, soweit sie für Wartung und Unterstützung nötig ist</td></tr>
<tr><td>Zweck</td><td>Betrieb der Website bzw. Web-Anwendung und der E-Mail-Postfächer des Kunden sowie gesondert vereinbarter Zusatzleistungen</td></tr>
<tr><td>Datenarten</td><td>Inhalte der Website; Eingaben in Formulare (z. B. Name, Kontaktdaten, Nachricht), soweit die Website solche vorsieht; Inhalte der Postfächer (E-Mails mit Anhängen, Absender, Empfänger, Zeitpunkte); Verbindungsdaten in Server-Protokollen (IP-Adresse, Zeitpunkt, aufgerufene Seite, Browserkennung); Benutzerkonten, soweit die Anwendung solche vorsieht</td></tr>
<tr><td>Betroffene</td><td>Besucher der Website; Nutzer der Postfächer des Kunden, insbesondere seine Beschäftigten; Absender und Empfänger von E-Mails; Kunden, Interessenten und Beschäftigte des Kunden; weitere Personen, deren Daten der Kunde über seine Website oder seine Postfächer verarbeitet</td></tr>
</table>
<p>Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) verarbeitet der Kunde über seine Website oder seine Postfächer nur nach vorheriger Mitteilung an WEBklar.</p>
<h3>3. Weisungen</h3>
<p>WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch bei Übermittlungen in Drittländer, es sei denn, eine gesetzliche Pflicht verlangt etwas anderes. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es erlaubt. Die Weisungen ergeben sich aus diesem Vertrag und den Bedingungen der Abos. Weitere Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin.</p>
<h3>4. Vertraulichkeit</h3>
<p>WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.</p>
<h3>5. Sicherheit der Verarbeitung</h3>
<p>WEBklar trifft die technischen und organisatorischen Maßnahmen nach Art. 32 DSGVO, die in Anlage 2 beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.</p>
<h3>6. Unterauftragsverarbeiter</h3>
<p>Der Kunde genehmigt die in Anlage 3 genannten Unterauftragsverarbeiter. Will WEBklar einen Unterauftragsverarbeiter hinzuziehen oder ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann aus wichtigem datenschutzrechtlichem Grund widersprechen. Einigen sich die Parteien nicht, können beide das Abo zu dem Zeitpunkt kündigen, zu dem der neue Unterauftragsverarbeiter eingesetzt werden soll. WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf dieselben Datenschutzpflichten, die sich aus diesem Vertrag ergeben.</p>
<h3>7. Unterstützung des Kunden</h3>
<ul>
<li>WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter und unterstützt ihn bei der Beantwortung, soweit das mit den eingesetzten Systemen möglich ist.</li>
<li>WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO (Sicherheit, Meldung von Datenschutzverletzungen, Datenschutz-Folgenabschätzung).</li>
<li>Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.</li>
</ul>
<h3>8. Löschung und Rückgabe</h3>
<p>Nach Ende des Abos gibt WEBklar die Daten nach Wahl des Kunden zurück (§ 11 Abs. 1 der Bedingungen) oder löscht sie, soweit keine gesetzliche Pflicht zur Speicherung besteht. Sicherungskopien werden im Rahmen der regulären Rotation überschrieben.</p>
<h3>9. Nachweise und Kontrollen</h3>
<p>WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind. WEBklar ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Rechenzentrumsbetreibers (Zertifizierung nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.</p>
<h3>10. Schlussbestimmungen</h3>
<p>Für die Haftung gilt Art. 82 DSGVO. Bei Widersprüchen geht dieser Vertrag in Datenschutzfragen den Bedingungen der Abos vor.</p>
<p class="anlage-kopf neue-seite" id="tom">Anlage 2</p>
<h2>Technische und organisatorische Maßnahmen (Art. 32 DSGVO)</h2>
<table>
<tr><th style="width:28%">Bereich</th><th>Maßnahmen</th></tr>
<tr><td>Rechenzentrum und Zutritt</td><td>Betrieb im Rechenzentrum der Hetzner Online GmbH in Falkenstein (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert. Zutritt, Stromversorgung, Klimatisierung und Brandschutz regeln die Maßnahmen von Hetzner.</td></tr>
<tr><td>Zugang zu den Systemen</td><td>Serververwaltung nur über verschlüsselte SSH-Verbindung, Zugang nur für die Gesellschafter. Automatische Sperre bei wiederholten Fehlversuchen. Firewall des Rechenzentrumsbetreibers, die nur die benötigten Dienste zulässt. Zwei-Faktor-Anmeldung für die Verwaltungskonten bei Hetzner und Stripe.</td></tr>
<tr><td>Zugriff auf Daten</td><td>Zugriff nur für die Gesellschafter und beauftragte Mitarbeiter, soweit ihre Aufgabe es erfordert. Im Kundenportal sieht jeder Kunde nur seine eigenen Daten. Datenbanken sind nur für die jeweilige Anwendung erreichbar. Zugangsdaten zu E-Mail-Postfächern erhalten nur der Kunde und die von ihm in Textform benannten Personen.</td></tr>
<tr><td>Übertragung</td><td>Verschlüsselte Übertragung (TLS) für alle Websites, das Kundenportal und die Verwaltung. E-Mail-Versand und -Abruf nur verschlüsselt.</td></tr>
<tr><td>Nachvollziehbarkeit</td><td>Änderungen an Websites laufen über eine Versionsverwaltung (Git) und sind nachvollziehbar. Aufträge und Weisungen werden im Ticketsystem bzw. im Chat des Kundenportals dokumentiert.</td></tr>
<tr><td>Verfügbarkeit und Wiederherstellung</td><td>Tägliche Sicherung des gesamten Servers, verfügbar sind die letzten sieben Tage. Tägliche Datenbank-Exporte vor der Serversicherung. Automatische Überwachung der Erreichbarkeit. Automatische Sicherheitsupdates des Betriebssystems. Wiederherstellung wird regelmäßig getestet.</td></tr>
<tr><td>Trennung</td><td>Websites der Kunden in getrennten Verzeichnissen bzw. Containern. Vorschau- und Testversionen sind nur nach Anmeldung erreichbar. Postfächer und Speichergrenzen werden je Kunde getrennt verwaltet.</td></tr>
<tr><td>Organisation</td><td>Verpflichtung aller Beteiligten auf Vertraulichkeit. Festgelegter Ablauf für die Meldung von Datenschutzverletzungen. Löschung nach Vertragsende (§ 11 der Bedingungen). Server-Protokolle werden nach 14 Tagen gelöscht. Die Maßnahmen werden mindestens einmal im Jahr überprüft.</td></tr>
</table>
<p class="anlage-kopf" id="sub">Anlage 3</p>
<h2>Unterauftragsverarbeiter</h2>
<table>
<tr><th>Unternehmen</th><th>Leistung</th><th>Ort der Verarbeitung</th></tr>
<tr><td>Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen</td><td>Server-Hosting und Datensicherung</td><td>Deutschland (Rechenzentrum Falkenstein)</td></tr>
<tr><td>Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen</td><td>E-Mail-Postfächer (Webhosting)</td><td>Deutschland (Rechenzentrum Nürnberg)</td></tr>
<tr><td>Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen</td><td>Server für die Website-Analyse (Messdienst, nur bei eingeschalteter Funktion)</td><td>Deutschland (Rechenzentrum Falkenstein)</td></tr>
</table>
<p class="hinweis">Die Domain registrieren wir über die Hetzner Online GmbH als Domain-Registrar. Für die Daten des Domaininhabers sind Registrar und Vergabestelle (bei .de-Domains die DENIC eG) eigenständig verantwortlich; sie sind insoweit keine Unterauftragsverarbeiter. Die Zahlungsabwicklung über Stripe betrifft das Vertragsverhältnis zwischen Kunde und WEBklar und ist nicht Gegenstand dieses Auftragsverarbeitungsvertrags.</p>
</main>
</body>
</html>
@@ -0,0 +1,199 @@
<!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-16">
<meta name="robots" content="noindex">
<title>Auftragsverarbeitungsvertrag Terminplaner – WEBklar</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #5e6678; font-size: 14px; }
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
table { border-collapse: collapse; width: 100%; font-size: 15px; }
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
a:hover { background: #f6f8fa; }
nav.top { font-size: 14px; margin-bottom: 22px; }
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar · AVV Terminplaner"; font: 8pt sans-serif; color: #8a93a3; }
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
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="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.html">Leistungsbeschreibung Terminplaner</a> · <a href="/neu/rechtliches/avv-terminplaner-2026-10-16.html">AVV Terminplaner</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Auftragsverarbeitungsvertrag Terminplaner (Art. 28 DSGVO)</h1>
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen</p>
<p>zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).</p>
<h2>Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen</h2>
<p>(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Terminplaner“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Terminplaner“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.</p>
<p>(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.</p>
<p>(3) Dieser Vertrag ist ein eigener Vertrag für den Terminplaner. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Terminplaner geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.</p>
<h2>1. Gegenstand und Dauer</h2>
<p>(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Verwaltung im Kundenportal („Terminplaner“) und verarbeitet dabei personenbezogene Daten der Gäste und der Beschäftigten des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.</p>
<p>(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Terminplaner schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 8 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.</p>
<h2>2. Art und Zweck der Verarbeitung</h2>
<p>(1) Art: Erheben über das Widget auf der Website des Kunden, Übermitteln an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen eines Kalender-Links und einer CSV-Datei und Löschen.</p>
<p>(2) Zwecke: Der Kunde soll Termine seiner Gäste verwalten. Dazu nimmt der Terminplaner Buchungen und Terminanfragen entgegen, verhindert Doppelbuchungen, bestätigt, ändert und sagt Termine ab, erinnert Gäste auf deren Wunsch und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Terminplaner für den Kunden vor Missbrauch (Mengenbegrenzung, Erkennen automatisierter Buchungen).</p>
<p>(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der gelöschten Buchungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.</p>
<h2>3. Datenarten und Betroffene</h2>
<p>(1) Daten der Gäste: Name und E-Mail-Adresse; auf Einstellung des Kunden außerdem Telefonnummer, eine Nachricht und Zusatzfelder, die der Kunde im Formular festlegt; die gebuchte Leistung, die Person oder der Platz und der Termin; der Status der Buchung mit den Zeitpunkten (Anlegen, Bestätigung, Annahme, Absage, Widerruf); Absagegrund und Vermerke zu einer Absage oder einem Widerruf (Freitext, auch mit dem Namen aus einer Widerrufserklärung); Preis, Zahlart, Zahlungsstatus und die Angabe, ob als Verbraucher gebucht wurde; der Nachweis der Zustimmungen (Fassung der Gästetexte, Zeitpunkt, Text des Buchungsknopfs, Anrede und Hashes (Prüfsummen) der gezeigten Texte); der Wunsch nach einer Erinnerung; der Zustellstatus der E-Mails an den Gast.</p>
<p>(2) Daten der Beschäftigten des Kunden: Namen oder Bezeichnungen der Personen oder Plätze, deren Arbeitszeiten und Abwesenheiten sowie ein geheimer Kalender-Link je Person oder Platz.</p>
<p>(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Terminplaner ist nicht für den Gesundheitsbereich bestimmt; der Kunde fordert von seinen Gästen keine Gesundheitsangaben (Nummer 11 Abs. 4).</p>
<p>(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse nur im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Buchung wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. In den Server-Protokollen stehen sie 14 Tage (Anlage 2 der Abo-Bedingungen).</p>
<p>(5) Der Terminplaner setzt keine Cookies und speichert nichts im Browser des Gastes.</p>
<p>(6) Betroffene: Gäste, die über die Website des Kunden Termine buchen oder anfragen, und Beschäftigte des Kunden, die im Terminplaner geführt werden.</p>
<h2>4. Weisungen</h2>
<p>(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.</p>
<p>(2) Mit dem Einschalten weist der Kunde WEBklar an, den Terminplaner nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Leistungen, Personen oder Plätze, Zeiten, Felder des Formulars, Aufbewahrungsdauer, Erinnerungen, Einschalten und Ausschalten). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Terminplaner abschalten lassen (Nr. 8 Abs. 6 der Leistungsbeschreibung). Die Löschung auf Weisung nach Nummer 9 Abs. 6 setzt WEBklar immer um.</p>
<p>(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.</p>
<h2>5. Vertraulichkeit</h2>
<p>WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.</p>
<h2>6. Technische und organisatorische Maßnahmen</h2>
<p>(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.</p>
<p>(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.</p>
<h2>7. Unterauftragsverarbeiter</h2>
<p>(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Terminplaner läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Buchungs-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.</p>
<p>(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Terminplaner abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).</p>
<p>(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).</p>
<p>(4) Die Zahlung der Einrichtung über Stripe betrifft nur den Vertrag zwischen dem Kunden und WEBklar. Daten von Gästen oder Beschäftigten des Kunden gehen nicht an Stripe. Stripe ist deshalb kein Unterauftragsverarbeiter dieses Vertrags.</p>
<h2>8. Unterstützung des Kunden, Betroffenenrechte</h2>
<p>(1) WEBklar leitet Anfragen von Gästen und Beschäftigten, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.</p>
<p>(2) Dafür bietet das Kundenportal die Aktion „Gastdaten löschen“ und den Export der Termine als CSV-Datei. Mit „Gastdaten löschen“ leert der Kunde Name, E-Mail-Adresse, Telefonnummer, Nachricht und Zusatzfelder einer Buchung; Status und Zeiten bleiben. Dabei löscht WEBklar die E-Mails zu dieser Buchung, die noch auf den Versand warten, und die Links des Gastes werden ungültig. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Eine noch nicht bestätigte Buchung verfällt von selbst (Nummer 9 Abs. 1).</p>
<p>(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.</p>
<p>(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.</p>
<p>(5) Die Buchung speichert als Nachweis der Zustimmungen die Fassung der Gästetexte, den Zeitpunkt, den Text des Buchungsknopfs, die Anrede und Hashes der übrigen Texte, die der Gast gesehen hat, nicht diese Texte selbst. Damit sich der Wortlaut nachweisen lässt, bewahrt WEBklar jede Fassung der Gästetexte (Hinweis am Formular, Texte der Haken, Datenschutz-Absatz und Widerrufsbelehrung für Gäste) mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende des Terminplaners. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung.</p>
<p>(6) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).</p>
<h2>9. Löschung und Rückgabe</h2>
<p>(1) Nicht bestätigte Buchungen löscht WEBklar 48 Stunden nach Ablauf der Bestätigungsfrist von 30 Minuten.</p>
<p>(2) Abgesagte und abgelehnte Termine löscht WEBklar 30 Tage nach der Absage oder Ablehnung. Terminanfragen, die der Kunde bis zum angefragten Terminbeginn nicht beantwortet, behandelt der Terminplaner als abgelehnt; die 30 Tage laufen dann ab diesem Zeitpunkt.</p>
<p>(3) Bestätigte und erledigte Termine löscht WEBklar nach der Aufbewahrungsdauer, gerechnet ab dem Ende des Termins. Der Kunde stellt sie im Kundenportal ein; die Vorgabe sind 90 Tage, einstellbar sind 14 bis 365 Tage.</p>
<p>(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.</p>
<p>(5) Der Löschlauf läuft täglich; gelöscht wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu löschen, als ein Lauf schafft, folgt der Rest im nächsten Lauf.</p>
<p>(6) Ist der Terminplaner abgeschaltet oder das Wartungs-Abo beendet, löscht WEBklar alle Termindaten und die Einstellungen 30 Tage danach. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen (zum Beispiel Betriebsangaben, Namen der Leistungen, die ersten acht Namen der Personen oder Plätze und die Wünsche des Kunden zum Einbau) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Bis zur Löschung kann der Kunde die Termine im Kundenportal als CSV-Datei exportieren. Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen wieder ein (Nr. 8 Abs. 4 der Leistungsbeschreibung). Früher löscht der Kunde einzelne Gastdaten selbst mit der Aktion „Gastdaten löschen“ (Nummer 8 Abs. 2); alle Daten löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.</p>
<p>(7) Entwürfe aus dem Fragebogen, aus denen die Funktion nicht eingeschaltet wurde, löscht WEBklar 90 Tage nach der letzten Änderung.</p>
<p>(8) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.</p>
<p>(9) Gesetzliche Aufbewahrungspflichten bleiben unberührt.</p>
<h2>10. Nachweise und Kontrollen</h2>
<p>WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.</p>
<h2>11. Pflichten des Kunden</h2>
<p>(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste und seiner Beschäftigten im Terminplaner.</p>
<p>(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für die Online-Terminbuchung in seine Datenschutzerklärung auf (Nr. 4 Abs. 9 der Leistungsbeschreibung) und lässt den Datenschutz-Hinweis am Buchungsformular unverändert.</p>
<p>(3) Der Kunde informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt (Nr. 4 Abs. 7 der Leistungsbeschreibung).</p>
<p>(4) Der Kunde setzt den Terminplaner nicht im Gesundheitsbereich ein und fordert von seinen Gästen keine Angaben zu ihrer Gesundheit, auch nicht über Zusatzfelder oder das Nachrichtenfeld (Nr. 4 Abs. 3 der Leistungsbeschreibung).</p>
<p>(5) Der Kunde verändert den Haken für die Erinnerung und die Texte der übrigen Haken nicht (Nr. 4 Abs. 6 der Leistungsbeschreibung). Erinnerungen versendet der Terminplaner nur, wenn der Gast den Haken gesetzt hat.</p>
<h2>12. Haftung, Änderungen, Schlussbestimmungen</h2>
<p>(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.</p>
<p>(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.</p>
<p>(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Terminplaner für seine Website nach Nr. 9 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 9 Abs. 3 der Leistungsbeschreibung aussetzen. Das Wartungs-Abo bleibt davon unberührt.</p>
<p>(4) Im Übrigen gilt § 16 der Abo-Bedingungen.</p>
<h2>Anlage: Technische und organisatorische Maßnahmen des Terminplaners (Art. 32 DSGVO)</h2>
<table>
<thead><tr><th>Bereich</th><th>Maßnahmen</th></tr></thead>
<tbody>
<tr><td>Übertragung</td><td>Das Widget, die Seiten der Gäste und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://.</td></tr>
<tr><td>Speicher im Browser</td><td>Es werden keine Cookies gesetzt, und der Terminplaner speichert nichts im Browser des Gastes (kein lokaler Speicher, kein Sitzungsspeicher). Das Widget sendet seine Anfragen ohne Cookies, liest weder den Referrer noch die Zeitzone des Browsers und ruft nur die Schnittstelle des Terminplaners auf.</td></tr>
<tr><td>Zugriffskontrolle</td><td>Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen eingeschalteten Terminplaner Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Buchungen nimmt sie nur von Seiten des Kunden und des Portals an. Der Kalender-Link enthält ein geheimes Token, das mit einem Vergleich in konstanter Zeit geprüft wird.</td></tr>
<tr><td>Missbrauchsschutz</td><td>Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Buchungen. Grenzen je IP-Adresse, je Website und je E-Mail-Adresse sowie das Mailbudget je Stunde (insgesamt und je Funktion) gelten nur im Arbeitsspeicher; ein Neustart leert sie.</td></tr>
<tr><td>Datenminimierung</td><td>Pflicht sind immer nur Name und E-Mail-Adresse; ob Telefonnummer, Nachricht und Zusatzfelder abgefragt werden, legt der Kunde fest. Mit der Buchung werden keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Der Nachweis der Zustimmungen besteht aus der Fassung der Gästetexte, dem Zeitpunkt, dem Text des Buchungsknopfs, der Anrede und Hashes der übrigen gezeigten Texte, nicht aus diesen Texten. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert.</td></tr>
<tr><td>Trennung</td><td>Alle Buchungen, Belegungen und Warteschlangeneinträge tragen die Kennung der Funktion (eines Terminplaners je Website-Projekt); das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten der jeweiligen Funktion. Die Hintergrundläufe (Löschung, Versand der E-Mails, Erinnerungen) gehen die Dokumente aller Funktionen nacheinander durch und schreiben jeweils nur in das Dokument, um das es geht. Die Einstellungen liegen je Funktion in einem eigenen Dokument.</td></tr>
<tr><td>Löschung</td><td>Täglicher Löschlauf für Buchungen, Belegungen, Warteschlange und Entwürfe nach den Fristen der Nummer 9; Termindaten und Einstellungen abgeschalteter Funktionen 30 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6). Vor der Löschung einer Buchung, einer abgeschalteten Funktion oder eines nicht bestellten Fragebogens wird der Zustand frisch gelesen; bei einem Lesefehler wird nichts gelöscht.</td></tr>
<tr><td>Protokolle</td><td>Die Programmprotokolle des Terminplaners und der Tagesbericht enthalten nur Zählungen und Buchungsnummern, keine Namen, E-Mail-Adressen oder Nachrichten von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen).</td></tr>
<tr><td>Datensicherung</td><td>Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen).</td></tr>
<tr><td>Rechenzentrum</td><td>Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren.</td></tr>
<tr><td>Organisation</td><td>Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Archiv der Fassungen der Gästetexte (Nummer 8 Abs. 5), Überprüfung der Maßnahmen mindestens einmal im Jahr.</td></tr>
</tbody>
</table>
</main>
</body>
</html>
@@ -0,0 +1,214 @@
<!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-16">
<meta name="robots" content="noindex">
<title>Auftragsverarbeitungsvertrag Buchungskalender – WEBklar</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #5e6678; font-size: 14px; }
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
table { border-collapse: collapse; width: 100%; font-size: 15px; }
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
a:hover { background: #f6f8fa; }
nav.top { font-size: 14px; margin-bottom: 22px; }
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar · AVV Buchungskalender"; font: 8pt sans-serif; color: #8a93a3; }
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
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="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-unterkunft-2026-10-16.html">Leistungsbeschreibung Buchungskalender</a> · <a href="/neu/rechtliches/avv-unterkunft-2026-10-16.html">AVV Buchungskalender</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Auftragsverarbeitungsvertrag Buchungskalender (Art. 28 DSGVO)</h1>
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen</p>
<p>zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Vermieter“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).</p>
<h2>Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen</h2>
<p>(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Buchungskalender“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Buchungskalender“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.</p>
<p>(2) Ein Mitarbeiter von WEBklar darf die Funktion auch ohne Bestellung im Portal einschalten (zum Beispiel für einen Kunden, dem WEBklar sie als Teil seines Website-Projekts bereitstellt), aber nur, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.</p>
<p>(3) Dieser Vertrag ist ein eigener Vertrag für den Buchungskalender. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Buchungskalender geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.</p>
<p>(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Gäste im Buchungskalender. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4). Die Plattformen, deren Kalender der Kunde einbindet (zum Beispiel Booking.com oder Airbnb), sind weder Auftragsverarbeiter noch Unterauftragsverarbeiter von WEBklar.</p>
<h2>1. Gegenstand und Dauer</h2>
<p>(1) WEBklar betreibt für die öffentliche Website des Kunden ein Buchungs-Widget mit einer Buchungsseite und einer Verwaltung im Kundenportal („Buchungskalender“) und verarbeitet dabei personenbezogene Daten der Gäste des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.</p>
<p>(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Buchungskalender schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.</p>
<h2>2. Art und Zweck der Verarbeitung</h2>
<p>(1) Art: Übermitteln der Anfrage und der Buchung vom Widget und von der Buchungsseite an den Server von WEBklar, Speichern, Verwalten und Anzeigen im Kundenportal, Berechnen des Preises, Erzeugen von Rechnungen und Stornorechnungen im Namen des Kunden (Nr. 3 Abs. 6 der Leistungsbeschreibung), Abgleichen von Kalendern, Versenden von E-Mails, Übernehmen von Zahlungsstatus aus Stripe, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.</p>
<p>(2) Zwecke: Der Kunde soll Anfragen und Buchungen seiner Gäste abwickeln. Dazu nimmt der Buchungskalender Anfragen entgegen, reserviert Zeiträume nach der Freigabe durch den Kunden, nimmt die verbindliche Buchung des Gastes entgegen, stellt die Rechnung aus, gleicht Online-Zahlungen mit Stripe ab, bildet Zahlungen vor Ort ab, bildet Stornierungen ab und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Buchungskalender für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der Anfragen je IP-Adresse, Erkennen automatisierter Anfragen).</p>
<p>(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Gäste. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Anfragen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.</p>
<h2>3. Datenarten und Betroffene</h2>
<p>(1) Daten der Gäste, die WEBklar aus dem Widget, von der Buchungsseite und aus Stripe erhält und speichert:</p>
<ul>
<li>bei der Anfrage: Name, E-Mail-Adresse, auf Wunsch Firma, Telefonnummer und eine Nachricht, die gewünschte Unterkunft, der Zeitraum, die Zahl der Personen, die Anfragenummer, ein Hashwert der IP-Adresse (Absatz 4) und der Zeitpunkt;</li>
<li>bei der Freigabe: die Posten und Summen des Preises (Text, Menge, Einzelbetrag, Steuersatz), die Reservierungsfrist, eine Nachricht des Kunden an den Gast, die Stornobedingungen, die galten, und die Fassung der Rechtstexte, der der Gast gesehen hat, jeweils als Schnappschuss;</li>
<li>bei der Buchung: die Rechnungsadresse (Name, Firma, Straße, Postleitzahl, Ort, Land), die Zahlart, der Zeitpunkt der Buchung und die Rechnungsnummer;</li>
<li>bei der Zahlung und der Erstattung: Status, Zeitpunkt, Art (online über Stripe oder vor Ort), die Kennungen der Zahlung bei Stripe, erstattete Beträge und der Grund einer Stornierung;</li>
<li>die Rechnung und die Stornorechnung als unveränderlicher Schnappschuss (Angaben des Ausstellers, Empfänger, Unterkunft, Zeitraum, Posten, Summen, Steuerart, Bezug), aus dem WEBklar das PDF bei jedem Abruf erzeugt;</li>
<li>ein Hashwert des geheimen Zugangs zur Buchungsseite (der Link in der E-Mail wird nur als Prüfsumme gespeichert);</li>
<li>der Zustellstatus der E-Mails an den Gast.</li>
</ul>
<p><strong>Kartendaten des Gastes erhält und speichert WEBklar nicht;</strong> die gibt der Gast nur bei Stripe ein. Online-Zahlungen sind derzeit nur per Karte (einschließlich Apple Pay und Google Pay) möglich, nicht per SEPA-Lastschrift. Die Bankverbindung des Kunden selbst (Angabe auf der Rechnung) und seine Firmen- und Steuerangaben gehören nicht zu den Gästedaten.</p>
<p>(2) Von Kalendern anderer Plattformen übernimmt WEBklar nur die belegten Zeiträume (Datum von und bis), nicht Namen, Beschreibungen oder andere Angaben der dortigen Buchungen, auch wenn der Kalender sie enthält. Die iCal-Adresse, die WEBklar dem Kunden bereitstellt, enthält nur belegte Nächte, keine Angaben zu Gästen. Wer die geheime Adresse kennt, sieht die belegten Nächte; der Kunde kann sie jederzeit erneuern.</p>
<p>(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Der Kunde fordert von seinen Gästen keine entsprechenden Angaben und bittet sie, solche Angaben nicht in das Nachrichtenfeld zu schreiben (Nummer 11 Abs. 4). Meldedaten nach dem Bundesmeldegesetz (Meldeschein) und Daten zur Kurtaxe verarbeitet der Buchungskalender nicht.</p>
<p>(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit der Anfrage werden weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein <strong>Hashwert der IP-Adresse („IP-Hash“)</strong>: ein SHA-256-Wert, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz. Er dient nicht dazu, Gäste zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Anfrage. WEBklar entfernt ihn aus dem Datensatz, sobald der Kunde die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 9 Abs. 1). In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).</p>
<p>(5) Der Buchungskalender setzt keine Cookies. Das Widget speichert nichts im Browser. Auch die Buchungsseite legt weder Cookies noch Daten im lokalen oder im Sitzungsspeicher des Browsers ab.</p>
<p>(6) Betroffene: Gäste, die über die Website des Kunden Anfragen stellen oder buchen, und Personen, deren Angaben der Gast in der Anfrage oder Buchung macht (zum Beispiel Mitreisende oder abweichende Rechnungsempfänger). Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.</p>
<h2>4. Weisungen</h2>
<p>(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.</p>
<p>(2) Mit dem Einschalten weist der Kunde WEBklar an, den Buchungskalender nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben, in seinem Namen Rechnungen auszustellen und Zahlungen, die nach einer Stornierung oder nach dem Ablauf einer Reservierung eingehen, sowie zweite Zahlungen auf eine bereits bezahlte Buchung ohne weitere Anweisung in voller Höhe über sein Stripe-Konto zu erstatten (dauerhafte Weisung; der Stand der Buchung ändert sich dabei nicht). Bei einer Stornierung schließt WEBklar eine noch offene Zahlungsseite bei Stripe. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Unterkünfte, Preise, Sperren, Kalenderverbindungen, Freigabe und Ablehnung von Anfragen, Zahlungseingang vor Ort, Stornierungen und Erstattungen, Online-Zahlung, Öffnen und Schließen des Buchungskalenders). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Buchungskalender abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um, soweit Aufbewahrungspflichten dem nicht entgegenstehen (Nummer 9 Abs. 8).</p>
<p>(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.</p>
<h2>5. Vertraulichkeit</h2>
<p>WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.</p>
<h2>6. Technische und organisatorische Maßnahmen</h2>
<p>(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.</p>
<p>(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.</p>
<h2>7. Unterauftragsverarbeiter</h2>
<p>(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Buchungskalender läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.</p>
<p>(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Buchungskalender abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).</p>
<p>(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).</p>
<p>(4) <strong>Stripe ist kein Unterauftragsverarbeiter von WEBklar.</strong> Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 3 der Leistungsbeschreibung); die Zahlung des Gastes fließt direkt auf dieses Konto. Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs- und Kartendaten (derzeit nur Karte, einschließlich Apple Pay und Google Pay), die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden legt WEBklar bei Stripe die Zahlung an, ruft die Angaben zu Zahlungen ab, die der Buchungskalender braucht (Nummer 2 Abs. 1), schließt offene Zahlungsseiten und veranlasst Erstattungen (auch die automatischen nach Nummer 4 Abs. 2). An Stripe gehen dabei von WEBklar nur: die Bezeichnung des Aufenthalts mit Rechnungsnummer als Zahlungsposition, der Betrag, die Kennungen der Buchung und der Zahlung (Metadaten), die E-Mail-Adresse des Gastes zur Vorbelegung der Zahlungsseite, soweit WEBklar sie übergibt, die Erstattung und der <strong>geheime Link zur Buchungsseite mit dem Token als Rücksprungadresse</strong> (bei Erfolg und bei Abbruch). Dieser Link ist damit für Stripe und im Stripe-Dashboard des Kunden sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.</p>
<p>(5) Plattformen, deren iCal-Kalender der Kunde einbindet oder denen er die iCal-Adresse des Buchungskalenders mitteilt, sind Empfänger nach seiner Entscheidung. WEBklar übermittelt ihnen keine Daten von Gästen.</p>
<h2>8. Unterstützung des Kunden, Betroffenenrechte</h2>
<p>(1) WEBklar leitet Anfragen von Gästen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.</p>
<p>(2) Dafür bietet das Kundenportal die Anzeige der Anfragen und Buchungen, mit denen der Kunde Auskunft erteilen kann, und den Abruf der Rechnungen als PDF. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Gästedaten einer Anfrage oder Buchung unverzüglich: Name, Firma, E-Mail-Adresse, Telefonnummer und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt in der Regel längst entfernt); Anfragenummer, Zeitraum, Beträge und Status bleiben. Rechnungen und Rechnungsadresse einer gebuchten Anfrage bleiben, solange eine Aufbewahrungspflicht besteht (Nummer 9 Abs. 3). Dabei löscht WEBklar die E-Mails zu dieser Anfrage, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 5 löscht (48 Stunden nach dem Fehlschlag).</p>
<p>(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.</p>
<p>(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.</p>
<p>(5) Die Gäste informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).</p>
<h2>9. Löschung und Rückgabe</h2>
<p>(1) Den IP-Hash entfernt WEBklar aus dem Datensatz einer Anfrage, sobald der Kunde sie freigibt oder ablehnt, spätestens 30 Tage nach dem Anlegen.</p>
<p>(2) Anfragen, aus denen keine Buchung wurde (abgelehnt, abgelaufen, oder neu und nicht innerhalb von 60 Tagen entschieden), anonymisiert WEBklar nach 6 Monaten: Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht werden gelöscht. Anfragenummer, Zeitraum, Status und, soweit vorhanden, der Preisvorschlag bleiben ohne Personenbezug erhalten.</p>
<p>(3) Bei gebuchten Aufenthalten anonymisiert WEBklar die Gästedaten (Name, Firma, E-Mail-Adresse, Telefonnummer und Nachricht) 3 Jahre nach dem Ende des Kalenderjahres, in dem der Gast abgereist ist (Abreisejahr; maßgeblich ist die Zeit in Berlin). <strong>Rechnungen, Stornorechnungen und die Rechnungsadresse bleiben 8 Jahre erhalten</strong>, gerechnet ab dem Ende des Kalenderjahres der Rechnung, solange der Buchungskalender läuft (Buchungsbelege, § 147 AO); sie sind für den Kunden als Aussteller bestimmt. Anfragenummer, Zeitraum, Beträge und Status bleiben ebenfalls.</p>
<p>(4) Die Anonymisierung nach den Absätzen 2 und 3 sowie die Entfernung des IP-Hash laufen im täglichen Löschlauf; anonymisiert oder entfernt wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.</p>
<p>(5) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.</p>
<p>(6) Ist der Buchungskalender abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Anfragen, Buchungen und Rechnungen 90 Tage lang im Kundenportal lesen und die Rechnungen einzeln als PDF herunterladen; WEBklar weist ihn nach dem Abschalten per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern. Danach löscht WEBklar alle Daten des Buchungskalenders: Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen. Damit endet auch die Aufbewahrung der Rechnungen nach Absatz 3; sie bewahrt der Kunde dann selbst auf. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Gästen; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 6 (bei Abschaltung 90 Tage später); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird. alle Daten des Buchungskalenders löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.</p>
<p>(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.</p>
<p>(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Rechnungen und Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung. Die Aufbewahrung der Rechnungen nach Absatz 3 geschieht als Dienst für den Kunden und für die Dauer des Buchungskalenders. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Löschung nach Absatz 6 durch den Download. Soweit eine gesetzliche Aufbewahrungspflicht besteht, löscht oder anonymisiert WEBklar auch auf Weisung des Kunden nicht früher; stattdessen sperrt es die Daten für die weitere Verarbeitung.</p>
<h2>10. Nachweise und Kontrollen</h2>
<p>WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.</p>
<h2>11. Pflichten des Kunden</h2>
<p>(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Gäste im Buchungskalender.</p>
<p>(2) Der Kunde informiert seine Gäste nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Buchungskalender in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.</p>
<p>(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst, wenn er die Online-Zahlung nutzt, und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).</p>
<p>(4) Der Kunde fordert von seinen Gästen keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten und weist sie auf der Website darauf hin, solche Angaben nicht in das Nachrichtenfeld zu schreiben.</p>
<p>(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht (Nr. 5 Abs. 9 der Leistungsbeschreibung).</p>
<p>(6) Der Kunde erfüllt seine Pflichten zu Meldeschein, Kurtaxe und sonstigen Meldungen an Behörden selbst und verarbeitet die dafür nötigen Daten außerhalb des Buchungskalenders (Nr. 5 Abs. 6 der Leistungsbeschreibung).</p>
<h2>12. Haftung, Änderungen, Schlussbestimmungen</h2>
<p>(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.</p>
<p>(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.</p>
<p>(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Buchungskalender für seine Website nach Nr. 12 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 12 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.</p>
<p>(4) Im Übrigen gilt § 16 der Abo-Bedingungen.</p>
<h2>Anlage: Technische und organisatorische Maßnahmen des Buchungskalenders (Art. 32 DSGVO)</h2>
<table>
<thead><tr><th>Bereich</th><th>Maßnahmen</th></tr></thead>
<tbody>
<tr><td>Übertragung</td><td>Das Widget, die Buchungsseite und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe. Kalender anderer Plattformen ruft WEBklar nur über https ab.</td></tr>
<tr><td>Speicher im Browser</td><td>Es werden keine Cookies gesetzt. Das Widget sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Buchungskalenders auf, keine Dienste Dritter.</td></tr>
<tr><td>Zugriffskontrolle</td><td>Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Buchungskalender Daten und ist über den Website-Schlüssel (Site-Key) adressiert; die belegten Nächte nennt sie ohne Quelle und ohne Daten von Gästen. Die Buchungsseite ruft der Gast nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert und zeitkonstant verglichen wird. Der Token wird nur als Prüfsumme gespeichert; er wird nur als Teil der Rücksprungadresse an Stripe übergeben (Nummer 7 Abs. 4) und öffnet nur diese eine Buchungsseite; er gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage), danach antwortet die Buchungsseite wie bei einem unbekannten Link. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server. Die iCal-Adresse des Buchungskalenders ist ein geheimer, erneuerbarer Zufallswert.</td></tr>
<tr><td>Missbrauchsschutz</td><td>Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und Grenzen je IP-Adresse und je Buchungskalender schützen vor automatisierten Anfragen. Die Grenzen gelten im Arbeitsspeicher; ein Neustart leert sie. Mit der Anfrage wird nur ein gesalzener IP-Hash gespeichert (Nummer 3 Abs. 4). Beim Abruf von Kalendern anderer Plattformen sind nur Adressen mit https erlaubt, private und lokale Ziele sind gesperrt, und Größe und Dauer des Abrufs sind begrenzt.</td></tr>
<tr><td>Datenminimierung</td><td>Kartendaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Preisen und Posten, nie aus Angaben des Browsers. Von anderen Kalendern werden nur Zeiträume übernommen. Mit der Anfrage werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert; der IP-Hash wird mit der Entscheidung des Kunden entfernt. Die Gästedaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert.</td></tr>
<tr><td>Integrität der Rechnungen</td><td>Rechnungen und Stornorechnungen werden als unveränderlicher Schnappschuss gespeichert; das PDF entsteht bei jedem Abruf aus diesem Schnappschuss. Rechnungsnummern werden unter Sperre lückenlos je Kunde und Jahr vergeben. Freigabe und Buchung prüfen unter Sperre erneut, dass der Zeitraum frei ist.</td></tr>
<tr><td>Trennung</td><td>Alle Unterkünfte, Anfragen und Rechnungen tragen die Kennung des Kunden, des Projekts und der Funktion; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Buchungskalenders. Die Hintergrundläufe (Ablauf von Reservierungen, Kalenderabgleich, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht.</td></tr>
<tr><td>Löschung</td><td>Täglicher Löschlauf für Anfragen, Buchungen und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Buchungskalender 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6).</td></tr>
<tr><td>Protokolle</td><td>Die Programmprotokolle des Buchungskalenders und der Tagesbericht enthalten nur Zählungen und Anfragenummern, keine Namen, E-Mail-Adressen oder Adressen von Gästen. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen).</td></tr>
<tr><td>Datensicherung</td><td>Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen).</td></tr>
<tr><td>Rechenzentrum</td><td>Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren.</td></tr>
<tr><td>Organisation</td><td>Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr.</td></tr>
</tbody>
</table>
</main>
</body>
</html>
@@ -0,0 +1,199 @@
<!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-16">
<meta name="robots" content="noindex">
<title>Auftragsverarbeitungsvertrag Webshop – WEBklar</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #5e6678; font-size: 14px; }
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
table { border-collapse: collapse; width: 100%; font-size: 15px; }
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
a:hover { background: #f6f8fa; }
nav.top { font-size: 14px; margin-bottom: 22px; }
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar · AVV Webshop"; font: 8pt sans-serif; color: #8a93a3; }
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
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="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-webshop-2026-10-16.html">Leistungsbeschreibung Webshop</a> · <a href="/neu/rechtliches/avv-webshop-2026-10-16.html">AVV Webshop</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Auftragsverarbeitungsvertrag Webshop (Art. 28 DSGVO)</h1>
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen</p>
<p>zwischen dem Kunden als Verantwortlichem („Kunde“; in der Leistungsbeschreibung „Händler“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).</p>
<h2>Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen</h2>
<p>(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Webshop“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Webshop“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.</p>
<p>(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. Das Portal verlangt dafür das Datum und den Weg der Erklärung; fehlt eine Angabe, schaltet es nicht ein. WEBklar speichert vor dem Einschalten einen Zustimmungsnachweis mit der Fassung der beiden Texte, dem Datum und dem Weg dieser Erklärung, dem Zeitpunkt des Einschaltens und dem Namen des Mitarbeiters (ohne IP-Adresse und Browser-Kennzeichen, weil der Kunde nicht im Portal zugestimmt hat). Der Kunde erhält anschließend eine E-Mail mit diesen beiden Texten als PDF.</p>
<p>(3) Dieser Vertrag ist ein eigener Vertrag für den Webshop. Er ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für den Webshop geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung vor.</p>
<p>(4) Die Rollen: Der Kunde ist Verantwortlicher für die personenbezogenen Daten seiner Käufer im Webshop. WEBklar ist sein Auftragsverarbeiter. Stripe ist nicht Auftragsverarbeiter von WEBklar, sondern Vertragspartner des Kunden (Nummer 7 Abs. 4).</p>
<h2>1. Gegenstand und Dauer</h2>
<p>(1) WEBklar betreibt für die öffentliche Website des Kunden ein Shop-Widget mit einer Bestellverwaltung im Kundenportal („Webshop“) und verarbeitet dabei personenbezogene Daten der Käufer des Kunden im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung.</p>
<p>(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Abschalten, spätestens mit dem Ende des Wartungs-Abos; den Webshop schaltet WEBklar dann spätestens am folgenden Tag ab (Nr. 10 Abs. 1 der Leistungsbeschreibung). Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.</p>
<h2>2. Art und Zweck der Verarbeitung</h2>
<p>(1) Art: Übermitteln der Bestellung vom Widget an den Server von WEBklar, Übernehmen der Zahlungs- und Adressangaben aus der Checkout-Sitzung bei Stripe, Speichern, Verwalten und Anzeigen im Kundenportal, Versenden von E-Mails, Bereitstellen einer CSV-Datei, Weitergeben von Erstattungen an Stripe im Auftrag des Kunden, Anonymisieren und Löschen.</p>
<p>(2) Zwecke: Der Kunde soll Bestellungen seiner Käufer abwickeln. Dazu nimmt der Webshop Bestellungen entgegen, reserviert den Bestand, gleicht Zahlungen mit Stripe ab, bestätigt Bestellungen (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, zunächst nur mit einer Eingangsmail ohne Vertragsschluss), bildet Versand und Abholung ab, nimmt Widerrufe entgegen, erstattet Zahlungen im Auftrag des Kunden und benachrichtigt den Kunden. WEBklar versendet die E-Mails im Auftrag des Kunden. Außerdem schützt WEBklar den Webshop für den Kunden vor Missbrauch (Mengenbegrenzung, Begrenzung der offenen Bestellungen je IP-Adresse, Erkennen automatisierter Bestellungen und Widerrufe).</p>
<p>(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Käufer. Zählungen ohne Personenbezug, die WEBklar für den sicheren Betrieb braucht (etwa die Zahl der anonymisierten Bestellungen je Tag oder die Länge der Versandwarteschlange), sind keine eigenen Zwecke.</p>
<h2>3. Datenarten und Betroffene</h2>
<p>(1) Daten der Käufer, die WEBklar aus der Checkout-Sitzung bei Stripe und aus dem Widget erhält und speichert: Name, E-Mail-Adresse, Lieferadresse und Rechnungsadresse (die Rechnungsadresse nur, soweit die Zahlungsseite von Stripe sie für die gewählte Zahlungsart abfragt); die Bestellung (Bestellnummer, Positionen mit Namen, Mengen und Preisen zum Zeitpunkt der Bestellung, Versandkosten, Lieferart, Steuerausweis, Gesamtbetrag); der Status der Bestellung mit den Zeitpunkten (bestellt, bezahlt, versendet, abholbereit, abgeschlossen, storniert, erstattet); Zahlungsstatus, Zahlungsart (zum Beispiel Karte oder SEPA-Lastschrift) und Kennungen der Zahlung bei Stripe; Versanddienst und Sendungsnummer, wenn der Kunde sie einträgt; Erstattungen mit Betrag, Zeitpunkt und Grund; die Prüfsumme (Hash) der Rechtstexte des Kunden, die der Käufer bei der Bestellung gesehen hat (den Wortlaut der Fassung verwahrt das Archiv nach Nummer 8 Abs. 5); ein Hashwert der IP-Adresse (Absatz 4); der Zustellstatus der E-Mails an den Käufer, auch der Eingangsmail bei ausstehender Zahlung. <strong>Kartendaten und Kontodaten (IBAN) erhält und speichert WEBklar nicht;</strong> die gibt der Käufer nur bei Stripe ein. <strong>Eine Telefonnummer des Käufers erhebt und speichert WEBklar nicht:</strong> Das Widget hat kein Telefonfeld, die Zahlungsseite von Stripe ist so eingestellt, dass sie keine Telefonnummer verlangt, und eine Telefonnummer, die Stripe in den Kundendaten der Sitzung dennoch übermitteln sollte, übernimmt WEBklar nicht in die Bestellung. Die Telefonnummer des Kunden selbst (Anbieterangaben in seinen Einstellungen) gehört nicht zu den Käuferdaten.</p>
<p>(2) Daten von Personen, die über die Widerrufsfunktion einen Vertrag widerrufen (Käufer und Interessenten, auch ohne passende Bestellung): Name, E-Mail-Adresse, Bestellnummer, die Erklärung als Freitext und der Zeitpunkt des Eingangs, außerdem der Zeitpunkt der Eingangsbestätigung und der Bearbeitung durch den Kunden.</p>
<p>(3) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht verarbeitet. Waren, aus deren Kauf sich besondere Kategorien ergeben können, etwa Arzneimittel, sind nach Nr. 3 der Leistungsbeschreibung ausgeschlossen; der Kunde fordert von seinen Käufern keine entsprechenden Angaben (Nummer 11 Abs. 4).</p>
<p>(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Widgets und jeder Anfrage. Die Mengenbegrenzung (Missbrauchsschutz) verwendet die IP-Adresse im Arbeitsspeicher des Servers; ein Neustart leert sie. Mit dem Widerruf wird keine IP-Adresse und kein Browser-Kennzeichen gespeichert. Mit der Bestellung werden ebenfalls weder eine IP-Adresse im Klartext noch ein Browser-Kennzeichen gespeichert, sondern nur ein <strong>Hashwert der IP-Adresse („IP-Hash“)</strong>: die ersten 16 Hexadezimalzeichen eines SHA-256-Werts, den WEBklar mit einem geheimen Schlüssel des Servers salzt. Der IP-Hash ist ein pseudonymes technisches Datum und bleibt ein personenbezogenes Datum, weil WEBklar mit dem Schlüssel für dieselbe IP-Adresse denselben Wert berechnen kann; ohne den Schlüssel lässt sich aus ihm keine IP-Adresse ableiten. WEBklar verwendet ihn ausschließlich zum Missbrauchsschutz: Je IP-Hash und Webshop sind höchstens drei offene (noch nicht bezahlte) Bestellungen innerhalb von 30 Minuten zulässig. Er dient nicht dazu, Käufer zu identifizieren, wiederzuerkennen oder zu profilieren, wird dem Kunden weder angezeigt noch exportiert und steht nur im Datensatz der Bestellung. WEBklar löscht ihn aus dem Datensatz der Bestellung, sobald die Bestellung bezahlt oder storniert wird, spätestens mit der Löschung nicht bezahlter Bestellungen nach 30 Tagen (Nummer 9 Abs. 1); bei bezahlten Bestellungen bleibt er nicht bis zur Anonymisierung (Nummer 9 Abs. 2) erhalten. In den Server-Protokollen stehen IP-Adresse und Browser-Kennzeichen 14 Tage (Anlage 2 der Abo-Bedingungen).</p>
<p>(5) Der Webshop setzt keine Cookies. Das Widget speichert im Browser des Besuchers nur den Warenkorb (Kennungen der Artikel und Mengen, ohne Personenbezug) im Sitzungsspeicher (sessionStorage) des Tabs, solange der Tab geöffnet ist; das ist für den Warenkorb technisch erforderlich. Andere Daten legt das Widget im Browser nicht ab.</p>
<p>(6) Betroffene: Käufer, die über die Website des Kunden Waren bestellen, und Personen, die über die Widerrufsfunktion einen Vertrag widerrufen. Beschäftigte des Kunden sind von diesem Vertrag nicht betroffen.</p>
<h2>4. Weisungen</h2>
<p>(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.</p>
<p>(2) Mit dem Einschalten weist der Kunde WEBklar an, den Webshop nach seinen Einstellungen und der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen und Aktionen im Kundenportal (Artikel, Versand, Abholung, Zahlungsarten, Rechtstexte, Bestellstatus, Erstattungen, Öffnen und Schließen des Webshops, Export). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die über diese Einstellungen hinausgehen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde den Webshop abschalten lassen (Nr. 10 Abs. 7 der Leistungsbeschreibung). Die Anonymisierung und Löschung auf Weisung nach Nummer 8 Abs. 2 und Nummer 9 Abs. 6 setzt WEBklar immer um.</p>
<p>(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.</p>
<h2>5. Vertraulichkeit</h2>
<p>WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.</p>
<h2>6. Technische und organisatorische Maßnahmen</h2>
<p>(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.</p>
<p>(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.</p>
<h2>7. Unterauftragsverarbeiter</h2>
<p>(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Webshop läuft, und für dessen Sicherung (Rechenzentrum Falkenstein, Deutschland) sowie für den Versand der Bestell-E-Mails über den SMTP-Dienst des Webhostings (Rechenzentrum Nürnberg, Deutschland). Eine Übermittlung in Drittländer findet nicht statt.</p>
<p>(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde den Webshop abschalten lassen und WEBklar ihn für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Weisung des Kunden früher (Nummer 9 Abs. 6).</p>
<p>(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).</p>
<p>(4) <strong>Stripe ist kein Unterauftragsverarbeiter von WEBklar.</strong> Der Kunde verwendet sein eigenes Stripe-Konto und hat den Vertrag darüber unmittelbar mit Stripe geschlossen (Nr. 4 der Leistungsbeschreibung). Die Zahlungsseite (Stripe Checkout), die Erhebung von Zahlungs-, Karten- und Kontodaten, die Prüfung von Zahlungen und die Auszahlungen verantworten der Kunde und Stripe nach ihrem Vertrag; WEBklar erhält diese Daten nicht. Auf Weisung des Kunden ruft WEBklar bei Stripe die Angaben zu Zahlungen und Bestellungen ab, die der Webshop braucht (Nummer 2 Abs. 1), und veranlasst Erstattungen. Daten von Käufern gehen dabei nicht von WEBklar an Stripe, außer der Erstattung selbst und der Kennung der Zahlung. Die Weitergabe von Daten durch Stripe, auch an Stellen außerhalb der Europäischen Union, regeln der Kunde und Stripe; dafür ist WEBklar nicht verantwortlich.</p>
<h2>8. Unterstützung des Kunden, Betroffenenrechte</h2>
<p>(1) WEBklar leitet Anfragen von Käufern, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.</p>
<p>(2) Dafür bietet das Kundenportal den Export der Bestellungen als CSV-Datei und die Anzeige der Bestellungen und Widerrufe, mit denen der Kunde Auskunft erteilen kann. Auf Weisung des Kunden in Textform (etwa im Chat des Kundenportals) anonymisiert WEBklar die Käuferdaten einer Bestellung oder eines Widerrufs unverzüglich: Name, E-Mail-Adresse, Liefer- und Rechnungsadresse und die Freitexte werden gelöscht oder unkenntlich gemacht (ein IP-Hash ist zu diesem Zeitpunkt längst gelöscht); Bestellnummer, Positionen, Beträge und Status bleiben. Dabei löscht WEBklar die E-Mails zu dieser Bestellung, die noch auf den Versand warten. E-Mails, deren Versand endgültig fehlgeschlagen ist, behalten Empfängeradresse und Inhalt, bis WEBklar sie nach Nummer 9 Abs. 4 löscht (48 Stunden nach dem Fehlschlag). Gesetzliche Aufbewahrungspflichten des Kunden (Nummer 9 Abs. 8) prüft der Kunde vor seiner Weisung selbst.</p>
<p>(3) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.</p>
<p>(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht.</p>
<p>(5) Bestätigt der Kunde seine Rechtstexte im Kundenportal (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular, Versand und Zahlung, Gewährleistungshinweis sowie die Adressen von Impressum und Datenschutzerklärung), legt WEBklar jede bestätigte Fassung einmal im Archiv der Rechtstexte ab: mit ihrer Prüfsumme (Hash), dem Zeitpunkt und der Angabe, welche angemeldete Person des Kunden (Name oder E-Mail-Adresse) sie bestätigt hat. Die Bestellung speichert die Prüfsumme der Fassung, die der Käufer bei der Bestellung gesehen hat. Die Bestellbestätigung (G09) zeigt die archivierte Fassung zu dieser Prüfsumme, nicht die Fassung, die beim Versand der E-Mail gerade gilt; fehlt die archivierte Fassung ausnahmsweise, verwendet WEBklar die aktuell bestätigte Fassung. Das Archiv enthält keine Daten von Käufern. WEBklar löscht es mit den übrigen Daten des Webshops (Nummer 9 Abs. 6), nicht früher. Das Archiv hilft dem Kunden, die Fassung nachzuweisen, die ein Käufer erhalten hat; es ersetzt nicht seine eigene Aufbewahrung (Nummer 9 Abs. 8). Die Bestellbestätigung enthält diese Texte; WEBklar löscht ihren Inhalt nach dem Versand aus der Versandwarteschlange (Nummer 9 Abs. 4).</p>
<p>(6) Die Käufer informiert der Kunde (Art. 13 DSGVO; Nummer 11 Abs. 2).</p>
<h2>9. Löschung und Rückgabe</h2>
<p>(1) Eine Bestellung, die nicht bezahlt wird, verfällt 30 Minuten nach dem Anlegen und gilt als storniert. WEBklar löscht nicht bezahlte Bestellungen (offen oder storniert) 30 Tage nach dem Anlegen. Den IP-Hash löscht WEBklar aus dem Datensatz, sobald die Bestellung bezahlt oder storniert wird, spätestens mit dieser Löschung.</p>
<p>(2) Bei bezahlten Bestellungen anonymisiert WEBklar die Käuferdaten 3 Jahre nach dem Ende des Kalenderjahres, in dem die Bestellung angelegt wurde (Bestelljahr; maßgeblich ist die Zeit in Berlin): Name, E-Mail-Adresse sowie Liefer- und Rechnungsadresse werden gelöscht (der IP-Hash ist mit der Zahlung bereits gelöscht; eine Telefonnummer des Käufers wird nicht gespeichert). Bestellnummer, Positionen, Beträge, Steuerausweis, Status, Versanddienst und Sendungsnummer bleiben für die Buchhaltung des Kunden erhalten.</p>
<p>(3) Widerrufe nach Nummer 3 Abs. 2 behandelt WEBklar wie bezahlte Bestellungen: Name, E-Mail-Adresse und Erklärung werden 3 Jahre nach dem Ende des Kalenderjahres des Eingangs gelöscht; die Bestellnummer, die die Person angegeben hat, und der Zeitpunkt des Eingangs bleiben.</p>
<p>(4) In der Versandwarteschlange für E-Mails leert WEBklar die Empfängeradresse und den Inhalt einer E-Mail nach dem Versand und löscht den Eintrag 7 Tage nach dem Versand. E-Mails, die nicht zugestellt werden können, löscht WEBklar mit Empfängeradresse und Inhalt 48 Stunden nach dem endgültigen Fehlschlag.</p>
<p>(5) Der Löschlauf läuft täglich; gelöscht oder anonymisiert wird frühestens nach Ablauf der jeweiligen Frist. Ist an einem Tag mehr zu bearbeiten, als ein Lauf schafft, folgt der Rest im nächsten Lauf.</p>
<p>(6) Ist der Webshop abgeschaltet oder das Wartungs-Abo beendet, kann der Kunde die Bestellungen und Widerrufe 90 Tage lang im Kundenportal lesen und als CSV-Datei exportieren. Danach löscht WEBklar alle Daten des Webshops: Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen der Rechtstexte (Nummer 8 Abs. 5). Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen (zum Beispiel Adresse der Website, Einbau-Code und die Angaben des Kunden aus der Anfrage) und enthält keine Daten von Käufern; dafür gilt die Datenschutzerklärung von WEBklar (Abschnitt 6). Beginnt innerhalb dieser Frist wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Kunden mit den vorhandenen Daten wieder ein (Nr. 10 Abs. 5 der Leistungsbeschreibung). Früher anonymisiert WEBklar einzelne Daten auf Weisung des Kunden (Nummer 8 Abs. 2); alle Daten des Webshops löscht WEBklar auf Weisung des Kunden in Textform unverzüglich.</p>
<p>(7) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte oder anonymisierte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen und Anonymisierungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.</p>
<p>(8) Gesetzliche Aufbewahrungspflichten bleiben unberührt. Der Kunde ist für seine eigenen Aufbewahrungspflichten (zum Beispiel für Buchungsbelege) selbst verantwortlich; WEBklar ist kein Archiv im Sinne der Buchführung, und das Archiv der Rechtstexte dient nur dem Nachweis der Texte. Der Kunde sichert die Belege, die er braucht, rechtzeitig vor der Anonymisierung oder Löschung durch den Export und die Belege von Stripe.</p>
<h2>10. Nachweise und Kontrollen</h2>
<p>WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.</p>
<h2>11. Pflichten des Kunden</h2>
<p>(1) Der Kunde ist Verantwortlicher für die Verarbeitung der Daten seiner Käufer im Webshop.</p>
<p>(2) Der Kunde informiert seine Käufer nach Art. 13 DSGVO. Dazu nimmt er den Datenschutz-Absatz für den Webshop in seine Datenschutzerklärung auf (Nr. 5 Abs. 10 der Leistungsbeschreibung) und nennt in seinen Rechtstexten (Datenschutzerklärung, Impressum) die Angaben, die das Gesetz verlangt.</p>
<p>(3) Der Kunde schließt mit Stripe den Vertrag über sein Stripe-Konto selbst und verantwortet die Verarbeitung von Daten durch Stripe, soweit sie nicht in diesem Vertrag geregelt ist (Nummer 7 Abs. 4).</p>
<p>(4) Der Kunde verkauft keine ausgeschlossenen Waren (Nr. 3 der Leistungsbeschreibung) und fordert von seinen Käufern keine Angaben zu ihrer Gesundheit oder zu anderen besonderen Kategorien personenbezogener Daten, auch nicht im Freitext der Widerrufserklärung.</p>
<p>(5) Der Kunde verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht und entfernt den Link „Vertrag widerrufen“ nicht (Nr. 5 Abs. 6 und 9 der Leistungsbeschreibung).</p>
<h2>12. Haftung, Änderungen, Schlussbestimmungen</h2>
<p>(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.</p>
<p>(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung, auch wegen geänderter Datenerhebung, teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.</p>
<p>(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar den Webshop für seine Website nach Nr. 11 Abs. 2 der Leistungsbeschreibung beenden oder nach Nr. 11 Abs. 3 der Leistungsbeschreibung aussetzen. Das Abo bleibt davon unberührt.</p>
<p>(4) Im Übrigen gilt § 16 der Abo-Bedingungen.</p>
<h2>Anlage: Technische und organisatorische Maßnahmen des Webshops (Art. 32 DSGVO)</h2>
<table>
<thead><tr><th>Bereich</th><th>Maßnahmen</th></tr></thead>
<tbody>
<tr><td>Übertragung</td><td>Das Widget, die Seiten der Käufer und das Kundenportal werden nur über TLS (HTTPS) übertragen. Der Einbau-Code verweist auf eine Adresse mit https://. Die Zahlung geschieht auf der Zahlungsseite von Stripe.</td></tr>
<tr><td>Speicher im Browser</td><td>Es werden keine Cookies gesetzt. Das Widget speichert nur den Warenkorb (Kennungen der Artikel und Mengen) im Sitzungsspeicher des Tabs. Es sendet seine Anfragen ohne Cookies und ruft nur die Schnittstelle des Webshops auf, keine Dienste Dritter.</td></tr>
<tr><td>Zugriffskontrolle</td><td>Das Kundenportal ist nur nach Anmeldung erreichbar; jeder Kunde sieht nur die Daten seiner eigenen Projekte. Die öffentliche Schnittstelle des Widgets liefert nur für einen vorhandenen Webshop Daten und ist über den Website-Schlüssel (Site-Key) adressiert; Bestellungen und Widerrufe nimmt sie nur von Seiten des Kunden und des Portals an. Den Stand einer Bestellung ruft der Käufer nur mit einem geheimen Token ab, das nur als Prüfsumme gespeichert wird. Benachrichtigungen von Stripe werden nur nach Prüfung ihrer Signatur verarbeitet; die Schlüssel für Stripe liegen nur auf dem Server.</td></tr>
<tr><td>Missbrauchsschutz</td><td>Ein verstecktes Feld (Honeypot), eine Mindestzeit bis zum Absenden des Formulars mit signiertem Formular-Token und eine Mindestantwortzeit schützen vor automatisierten Bestellungen und Widerrufen. Grenzen je IP-Adresse und je Webshop gelten im Arbeitsspeicher; ein Neustart leert sie. Die Obergrenze offener Bestellungen je IP-Adresse (drei in 30 Minuten) arbeitet mit einem gesalzenen Hashwert der IP-Adresse in der Bestellung (Nummer 3 Abs. 4).</td></tr>
<tr><td>Datenminimierung</td><td>Kartendaten und Kontodaten erhält und speichert WEBklar nicht. Preise berechnet der Server aus den gespeicherten Artikeln, nie aus Angaben des Browsers. Eine Telefonnummer wird weder abgefragt noch gespeichert, die Rechnungsadresse nur, soweit Stripe sie für die Zahlungsart braucht. Mit der Bestellung und mit dem Widerruf werden keine IP-Adresse im Klartext und kein Browser-Kennzeichen gespeichert, bei der Bestellung nur der gesalzene IP-Hash, und der nur, solange die Bestellung offen ist (gelöscht mit Zahlung oder Stornierung). Die Käuferdaten werden nach den Fristen der Nummer 9 anonymisiert. Nach dem Versand einer E-Mail werden Empfängeradresse und Inhalt in der Warteschlange geleert.</td></tr>
<tr><td>Trennung</td><td>Alle Artikel, Bestellungen und Widerrufe tragen die Kennung des Webshops und des Kunden; das Kundenportal und die öffentliche Schnittstelle lesen immer nur die Daten des jeweiligen Webshops. Artikelbilder liegen in einem eigenen Ordner je Webshop. Die Hintergrundläufe (Abgleich offener Bestellungen, Versand der E-Mails, Anonymisierung und Löschung) schreiben jeweils nur in das Dokument, um das es geht.</td></tr>
<tr><td>Löschung</td><td>Täglicher Löschlauf für Bestellungen, Widerrufe und Warteschlange nach den Fristen der Nummer 9; die Daten abgeschalteter Webshops einschließlich des Archivs der bestätigten Rechtstexte 90 Tage nach dem Abschalten; das Einrichtungs-Ticket bleibt als Arbeitsnachweis mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6).</td></tr>
<tr><td>Protokolle</td><td>Die Programmprotokolle des Webshops und der Tagesbericht enthalten nur Zählungen und Bestellnummern, keine Namen, E-Mail-Adressen oder Adressen von Käufern. Server-Protokolle des Webservers werden nach 14 Tagen gelöscht (Anlage 2 der Abo-Bedingungen).</td></tr>
<tr><td>Datensicherung</td><td>Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen).</td></tr>
<tr><td>Rechenzentrum</td><td>Hetzner Online GmbH, Rechenzentren Falkenstein und Nürnberg (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren.</td></tr>
<tr><td>Organisation</td><td>Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 3), Überprüfung der Maßnahmen mindestens einmal im Jahr.</td></tr>
</tbody>
</table>
</main>
</body>
</html>
@@ -0,0 +1,192 @@
<!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-16">
<meta name="robots" content="noindex">
<title>Auftragsverarbeitungsvertrag Website-Analyse – WEBklar</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #5e6678; font-size: 14px; }
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
table { border-collapse: collapse; width: 100%; font-size: 15px; }
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
a:hover { background: #f6f8fa; }
nav.top { font-size: 14px; margin-bottom: 22px; }
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar · AVV Website-Analyse"; font: 8pt sans-serif; color: #8a93a3; }
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
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="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-website-analyse-2026-10-16.html">Leistungsbeschreibung Website-Analyse</a> · <a href="/neu/rechtliches/avv-website-analyse-2026-10-16.html">AVV Website-Analyse</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Auftragsverarbeitungsvertrag Website-Analyse (Art. 28 DSGVO)</h1>
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen</p>
<p>zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein, als Auftragsverarbeiter („WEBklar“).</p>
<h2>Vorbemerkung: Abschluss und Verhältnis zu anderen Verträgen</h2>
<p>(1) Der Kunde schließt diesen Vertrag, wenn er im Kundenportal die Funktion „Website-Analyse“ einschaltet und die dort angezeigte Zeile „Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16)“ mit dem Klick auf „Einschalten“ bestätigt; diesem Vertrag und der Leistungsbeschreibung „Website-Analyse“ („Leistungsbeschreibung“) in der angezeigten Fassung stimmt er damit zu. Das Portal speichert den Wortlaut der angezeigten Zeile, die Fassungen, den Zeitpunkt, das Kundenkonto und die angemeldete Person. Anschließend sendet WEBklar dem Kunden eine E-Mail, die beide Texte in dieser Fassung als PDF enthält und auf sie verlinkt.</p>
<p>(2) Ein Mitarbeiter von WEBklar darf die Funktion nur einschalten, wenn der Kunde vorher in Textform (etwa im Chat des Kundenportals oder per E-Mail) erklärt hat, dass er diesem Vertrag und der Leistungsbeschreibung in der aktuellen Fassung zustimmt. WEBklar speichert die Fassung, das Datum und den Weg dieser Erklärung, den Zeitpunkt des Einschaltens und den Namen des Mitarbeiters. Der Kunde erhält die E-Mail nach Absatz 1.</p>
<p>(3) Dieser Vertrag ergänzt den Auftragsverarbeitungsvertrag in Anlage 1 der Abo-Bedingungen. Für die Website-Analyse geht er Anlage 1 vor; im Übrigen gilt Anlage 1 ergänzend. In Datenschutzfragen geht er den Abo-Bedingungen und der Leistungsbeschreibung „Website-Analyse“ vor.</p>
<h2>1. Gegenstand und Dauer</h2>
<p>(1) WEBklar betreibt für die öffentliche Website des Kunden eine Reichweitenmessung mit Einwilligungsbanner („Website-Analyse“) und verarbeitet dabei personenbezogene Daten der Besucher der Website im Auftrag des Kunden. Den Umfang beschreibt die Leistungsbeschreibung „Website-Analyse“.</p>
<p>(2) Der Vertrag beginnt mit dem Einschalten der Funktion. Er endet mit dem Ausschalten, spätestens mit dem Ende des Abos, das die Website-Analyse enthält (Hosting-Abo oder Wartungs-Abo); endet nur das Wartungs-Abo und läuft ein Hosting-Abo weiter, läuft dieser Vertrag weiter. Den Messdienst schaltet WEBklar bei Vertragsende spätestens am folgenden Tag ab. Die Pflichten aus diesem Vertrag gelten fort, bis alle Daten nach Nummer 9 gelöscht sind.</p>
<h2>2. Art und Zweck der Verarbeitung</h2>
<p>(1) Art: Erheben über ein Skript, das WEBklar beim Veröffentlichen in die Website des Kunden einfügt, Übermitteln an den Messdienst von WEBklar, Speichern, Verdichten zu Tageswerten, Auswerten im Kundenportal (Kennzahlen, Heatmap) und Löschen.</p>
<p>(2) Zwecke: Der Kunde soll erkennen, wie seine Website genutzt wird, um sie zu verbessern. Außerdem schützt WEBklar den Messdienst für den Kunden vor Missbrauch und verfälschten Zahlen (Mengenbegrenzung, Erkennen von Bots).</p>
<p>(3) WEBklar verwendet die Daten für keine eigenen Zwecke, auch nicht in anonymisierter Form und nicht für Auswertungen über mehrere Kunden. Es findet keine Werbung und keine Zusammenführung mit anderen Daten statt. WEBklar bildet keine Profile einzelner Besucher. Die Besucher-Kennung dient nur dazu, die Besucher eines Tages zu zählen.</p>
<h2>3. Datenarten und Betroffene</h2>
<p>(1) Solange der Besucher im Banner keine Wahl getroffen hat und sein Browser kein Signal nach Absatz 2 Satz 2 sendet: Pfad der aufgerufenen Seite (ohne Suchparameter und Anker), auch bei einem Seitenwechsel ohne Neuladen; Land (aus der IP-Adresse abgeleitet); Geräteklasse (Handy, Tablet oder Computer; aus dem Browser-Kennzeichen abgeleitet); Zeitpunkt (auf die volle Stunde gerundet); ein Zähler dafür, dass das Banner angezeigt wurde.</p>
<p>(2) Nach „Ablehnen“ sendet das Skript einmalig einen Zähler „abgelehnt“ und danach nichts mehr. Sendet der Browser die Signale „Global Privacy Control“ oder „Do Not Track“, zeigt das Skript kein Banner und sendet nichts.</p>
<p>(3) Nach „Annehmen“ zusätzlich: Domain der Website, von der der Besucher kam; Größenklasse des Browserfensters; eine täglich wechselnde pseudonyme Besucher-Kennung; Verweildauer (ab dem Laden der Seite) und Scrolltiefe je Seite; Klickpositionen mit einem Elementpfad aus Tag-Namen und Position (ohne Texte, Kennungen und Klassennamen); die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp oder Link auf eine andere Website, dann mit deren Domain); ein Zähler „angenommen“.</p>
<p>(4) IP-Adresse und Browser-Kennzeichen (User-Agent) erhält der Server von WEBklar technisch bei jedem Abruf des Skripts und bei jeder Messanfrage, auch nach „Ablehnen“. Der Messdienst verarbeitet sie nur kurz im Arbeitsspeicher, um Land, Geräteklasse, Bots, die Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung zu bestimmen. Er speichert und protokolliert sie nicht. Nicht erhoben werden Cookies, Formularinhalte, Seitentexte, Mausbewegungen, Tastatureingaben und Sitzungsaufzeichnungen.</p>
<p>(5) Tageswerte sind Summen je Tag und Merkmal (etwa Aufrufe je Seite, je Land oder je Geräteklasse), ohne Besucher-Kennung. Enthalten die Pfade der Website personenbezogene Angaben, können auch Tageswerte personenbezogen sein (Nummer 11 Abs. 2).</p>
<p>(6) Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) werden nicht gezielt verarbeitet. Je nach Inhalt der Website können sich aus Pfaden oder Domains aber Rückschlüsse ergeben (Nummer 11 Abs. 3).</p>
<p>(7) Betroffene: Besucher der Website des Kunden.</p>
<h2>4. Weisungen</h2>
<p>(1) WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch in Bezug auf eine Übermittlung in ein Drittland oder an eine internationale Organisation. Das gilt nicht, wenn das Recht der Europäischen Union oder Deutschlands WEBklar zu einer Verarbeitung verpflichtet. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es nicht verbietet.</p>
<p>(2) Mit dem Einschalten weist der Kunde WEBklar an, die Website-Analyse in der Standardkonfiguration nach der Leistungsbeschreibung zu betreiben. Weitere Weisungen ergeben sich aus den Einstellungen im Kundenportal (Einschalten, Ausschalten, Akzentfarbe des Banners). Sonstige Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Weisungen, die von der Standardkonfiguration abweichen, setzt WEBklar nur um, wenn das technisch vorgesehen ist; sonst kann der Kunde die Funktion ausschalten.</p>
<p>(3) Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin. WEBklar darf die Ausführung aussetzen, bis der Kunde die Weisung ändert oder die Rechtslage geklärt ist.</p>
<h2>5. Vertraulichkeit</h2>
<p>WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.</p>
<h2>6. Technische und organisatorische Maßnahmen</h2>
<p>(1) WEBklar trifft die Maßnahmen nach Art. 32 DSGVO, die in der Anlage zu diesem Vertrag beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.</p>
<p>(2) Für den Server gelten ergänzend die Maßnahmen der Anlage 2 der Abo-Bedingungen, soweit die Anlage zu diesem Vertrag nichts anderes regelt.</p>
<h2>7. Unterauftragsverarbeiter</h2>
<p>(1) Der Kunde genehmigt den Einsatz der Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen, für den Betrieb des Servers, auf dem der Messdienst läuft, und für dessen Sicherung. Ort der Verarbeitung ist das Rechenzentrum Falkenstein (Deutschland). Eine Übermittlung in Drittländer findet nicht statt.</p>
<p>(2) Will WEBklar einen weiteren Unterauftragsverarbeiter hinzuziehen oder einen ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann innerhalb von zwei Wochen nach Zugang der Mitteilung aus wichtigem datenschutzrechtlichem Grund in Textform widersprechen. WEBklar setzt den neuen Unterauftragsverarbeiter nicht vor Ablauf dieser Frist ein. Kündigt ein Unterauftragsverarbeiter von WEBklar eine eigene Änderung mit kürzerer Frist an, informiert WEBklar den Kunden unverzüglich, nachdem die Ankündigung eingegangen ist; der Kunde kann dann unverzüglich widersprechen. Einigen sich die Parteien nicht, kann der Kunde die Website-Analyse ausschalten und WEBklar sie für diese Website zu dem Zeitpunkt beenden, zu dem der neue Unterauftragsverarbeiter eingesetzt wird. Die Daten werden dann nach Nummer 9 gelöscht, auf Verlangen des Kunden sofort.</p>
<p>(3) WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf die Datenschutzpflichten nach Art. 28 Abs. 4 DSGVO. Bei der Hetzner Online GmbH ist das deren Vertrag zur Auftragsverarbeitung, den WEBklar abgeschlossen hat. Kommt ein Unterauftragsverarbeiter seinen Datenschutzpflichten nicht nach, haftet WEBklar dem Kunden für die Einhaltung dieser Pflichten (Art. 28 Abs. 4 Satz 2 DSGVO).</p>
<p>(4) Die Länderzuordnung nutzt eine Datenbank, die WEBklar auf dem eigenen Server speichert („IP to Country Lite“ von DB-IP.com, Lizenz CC BY 4.0). Daten der Besucher werden dabei nicht an DB-IP übermittelt. DB-IP ist kein Unterauftragsverarbeiter.</p>
<h2>8. Unterstützung des Kunden, Betroffenenrechte</h2>
<p>(1) WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter. WEBklar unterstützt ihn mit geeigneten technischen und organisatorischen Maßnahmen bei der Beantwortung.</p>
<p>(2) Die gespeicherten Messdaten enthalten keine IP-Adresse, keinen Namen und keine Kennung, die länger als einen Tag gilt; Zeitpunkte sind auf die volle Stunde gerundet. Einzelne Besucher kann WEBklar daher in der Regel nicht identifizieren (Art. 11 DSGVO). Ein Abgleich kann aber möglich sein, solange die Zugriffsprotokolle der Website (Anlage 1 und 2 der Abo-Bedingungen, 14 Tage) vorhanden sind. Nennt eine betroffene Person zusätzliche Angaben, etwa Datum, Uhrzeit, besuchte Seiten oder ihre IP-Adresse, prüft WEBklar auf Weisung des Kunden, auch anhand dieser Protokolle, ob eine Zuordnung möglich ist, und teilt ihm das Ergebnis mit.</p>
<p>(3) Der Messung können Besucher technisch widersprechen (Art. 21 DSGVO): Nach „Ablehnen“ und bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“ sendet das Skript nichts mehr.</p>
<p>(4) WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO. Dazu gehören die Angaben, die er für sein Verzeichnis von Verarbeitungstätigkeiten und für eine etwaige Datenschutz-Folgenabschätzung braucht, und eine kurze Interessenabwägung für die Zählung nach Art. 6 Abs. 1 lit. f DSGVO, die WEBklar mit jeder Fassung dieses Vertrags bereitstellt.</p>
<p>(5) Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.</p>
<p>(6) Zum Nachweis der Einwilligungen bewahrt WEBklar jede Fassung des Banner-Textes und der Zusatzseite mit dem Zeitraum ihrer Verwendung auf, bis drei Jahre nach dem Ende der Website-Analyse. WEBklar stellt sie dem Kunden auf Verlangen zur Verfügung. Ein Protokoll je Besucher führt WEBklar nicht.</p>
<p>(7) Die Besucher informiert der Kunde (Art. 13 DSGVO). WEBklar stellt ihm dafür die Zusatzseite „/datenschutz-analyse.html“ als Muster bereit. Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar dort einen Hinweis auf die Zusatzseite ein; sonst erhält der Kunde den Hinweis als Textbaustein (Leistungsbeschreibung Nr. 4 Abs. 4).</p>
<h2>9. Löschung und Rückgabe</h2>
<p>(1) Einzelne Ereignisse und Klickpositionen löscht WEBklar automatisch nach 30 Tagen. Der Löschlauf läuft täglich, die Löschung erfolgt also in der Regel spätestens am 31. Tag; fällt ein Lauf aus, holt der nächste ihn nach.</p>
<p>(2) Tageswerte löscht WEBklar 25 Monate nach dem jeweiligen Tag. Nach dem Ende der Website-Analyse (Ausschalten oder Ende des Abos, das sie enthält) löscht WEBklar alle Daten der Website am 61. Tag. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse verlangen; WEBklar stellt sie innerhalb von zwei Wochen als CSV-Datei bereit. Auf Weisung des Kunden löscht WEBklar die Daten früher.</p>
<p>(3) Die Daten sind in der täglichen Vollsicherung des Servers enthalten (§ 3 Abs. 5 der Abo-Bedingungen). Gelöschte Daten bleiben dort bis zu sieben Tage erhalten und werden dann überschrieben. Wird eine Sicherung zurückgespielt, holt der tägliche Löschlauf die fälligen Löschungen nach; Löschungen auf Weisung des Kunden wiederholt WEBklar.</p>
<p>(4) Gesetzliche Aufbewahrungspflichten bleiben unberührt.</p>
<h2>10. Nachweise und Kontrollen</h2>
<p>WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind, und ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Betreibers (Zertifizierung der Hetzner Online GmbH nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.</p>
<h2>11. Pflichten des Kunden</h2>
<p>(1) Der Kunde ist Verantwortlicher für die Messung. Er entfernt, verdeckt oder umgeht das Banner nicht. Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess- oder Tracking-Werkzeuge ein.</p>
<p>(2) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.</p>
<p>(3) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren oder lassen ihre Inhalte auf besondere Kategorien personenbezogener Daten schließen (etwa Gesundheit, Religion oder politische Meinung), gilt Nr. 3 Abs. 4 der Leistungsbeschreibung.</p>
<h2>12. Haftung, Änderungen, Schlussbestimmungen</h2>
<p>(1) Gegenüber betroffenen Personen gilt Art. 82 DSGVO. Für die Haftung zwischen Kunde und WEBklar gilt § 13 der Abo-Bedingungen.</p>
<p>(2) Eine neue Fassung dieses Vertrags oder der Leistungsbeschreibung teilt WEBklar dem Kunden mindestens sechs Wochen vor ihrem Wirksamwerden in Textform mit. Sie wird wirksam, wenn der Kunde ihr im Kundenportal oder in Textform zustimmt. Stimmt er nicht zu, gilt die bisherige Fassung weiter.</p>
<p>(3) Ist die neue Fassung nötig, weil ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form nicht mehr zulässt, und stimmt der Kunde ihr nicht zu, darf WEBklar die Website-Analyse für seine Website nach Nr. 8 Abs. 2 der Leistungsbeschreibung beenden oder aussetzen. Das Abo bleibt davon unberührt.</p>
<p>(4) Im Übrigen gilt § 16 der Abo-Bedingungen.</p>
<h2>Anlage: Technische und organisatorische Maßnahmen der Website-Analyse (Art. 32 DSGVO)</h2>
<table>
<thead><tr><th>Bereich</th><th>Maßnahmen</th></tr></thead>
<tbody>
<tr><td>Übertragung</td><td>Skript und Messdaten werden nur über TLS (HTTPS) übertragen.</td></tr>
<tr><td>Speicher im Browser</td><td>Es werden keine Cookies gesetzt, und der Messdienst wertet keine Cookies aus. Messanfragen sendet das Skript ohne Cookies. Im lokalen Speicher des Browsers liegt nur die Banner-Wahl (Eintrag „wk-consent“ mit Wahl, Zeitpunkt und Fassung des Banner-Textes). Sie gilt 183 Tage; danach und bei einer neuen Fassung des Banner-Textes fragt das Banner erneut.</td></tr>
<tr><td>Einwilligung</td><td>Solange keine Wahl getroffen ist und kein Browser-Signal vorliegt, sendet das Skript nur den Pfad der Seite, auch beim Seitenwechsel ohne Neuladen, und den Zähler „Banner angezeigt“. Nach „Ablehnen“ sendet es einmalig den Zähler „abgelehnt“ und danach nichts mehr, auch nicht aus anderen geöffneten Tabs. Bei „Global Privacy Control“ oder „Do Not Track“ zeigt es kein Banner und sendet nichts. Klick-, Scroll- und Verweildauer-Ereignisse registriert es erst nach „Annehmen“. Die Schaltflächen „Ablehnen“ und „Annehmen“ sind gleich gestaltet.</td></tr>
<tr><td>Datenminimierung</td><td>IP-Adresse und Browser-Kennzeichen werden weder gespeichert noch protokolliert. Der Messdienst verwendet sie nur im Arbeitsspeicher für Land, Geräteklasse, Bot-Erkennung, Mengenbegrenzung und, nur nach Einwilligung, die Besucher-Kennung. Zeitpunkte werden auf die volle Stunde gerundet gespeichert. Pfade werden ohne Suchparameter und Anker gespeichert; Abschnitte, die wie Kennungen aussehen (lange Ziffernfolgen, UUIDs, E-Mail-Adressen), werden vorher ersetzt. Elementpfade bestehen nur aus Tag-Namen und Position. Formulareingaben und Seitentexte werden nicht erfasst.</td></tr>
<tr><td>Pseudonymisierung</td><td>Die Besucher-Kennung (nur nach Einwilligung) ist ein auf 64 Bit gekürzter HMAC-SHA256 aus Website-Schlüssel, IP-Adresse und Browser-Kennzeichen mit einem Tagesschlüssel. Auch der Schlüssel der Mengenbegrenzung wird mit dem Tagesschlüssel gebildet. Der Tagesschlüssel liegt nur im Arbeitsspeicher; die Auslagerung auf die Festplatte ist für den Messdienst gesperrt. Er wird beim ersten Ereignis eines neuen Kalendertags (Europe/Berlin) und bei jedem Neustart neu erzeugt und nie gesichert. Ältere Kennungen lassen sich danach nicht mehr zuordnen.</td></tr>
<tr><td>Zugriffskontrolle</td><td>Die Verwaltungsschnittstelle des Messdienstes (Auswertung, Website-Verwaltung) läuft auf einem internen Port, der nicht öffentlich erreichbar ist, und ist durch ein Zugriffstoken geschützt. Zugriff haben nur das Kundenportal und die Verwaltung von WEBklar. Das Portal zeigt jedem Kunden nur die Daten seiner eigenen Websites.</td></tr>
<tr><td>Heatmap</td><td>Die Heatmap öffnet ein signierter Link, den nur der angemeldete Kunde im Kundenportal erzeugen kann. Der Link gilt 30 Minuten und liefert nur Klickpunkte der gewählten Seite und Ansicht, ohne Zeitpunkte und Kennungen.</td></tr>
<tr><td>Eingangsprüfung</td><td>Messdaten werden nur angenommen, wenn die Website registriert und eingeschaltet ist und Origin bzw. Referer zu ihren Domains passen. Bots werden verworfen. Größe und Menge der Anfragen sind je Website und Absender begrenzt (Zähler nur im Arbeitsspeicher). Für ausgeschaltete Websites liefert der Messdienst ein leeres Skript aus.</td></tr>
<tr><td>Löschung</td><td>Täglicher Löschlauf um 02:30 Uhr (Europe/Berlin): Ereignisse und Klicks nach 30 Tagen, Tageswerte nach 25 Monaten, alle Daten einer Website 60 Tage nach dem Ausschalten. Gelöschte Datensätze werden in der Datenbankdatei überschrieben und aus dem Schreibprotokoll (WAL) entfernt.</td></tr>
<tr><td>Trennung</td><td>Der Messdienst läuft in einem eigenen Container mit eigenem Datenverzeichnis und hat keinen Zugriff auf andere Kundendaten. Alle Datensätze tragen den Website-Schlüssel; Abfragen erfolgen nur je Website.</td></tr>
<tr><td>Protokolle</td><td>Der Reverse-Proxy führt kein Zugriffsprotokoll, und der Messdienst protokolliert keine Anfragen. Wird ein Zugriffsprotokoll eingeschaltet, dann nicht für die Adresse des Messdienstes. Meldungen der Container werden nach 14 Tagen gelöscht.</td></tr>
<tr><td>Datensicherung</td><td>Tägliche Vollsicherung des Servers durch Hetzner; verfügbar sind die letzten sieben Tagessicherungen (§ 3 Abs. 5 der Abo-Bedingungen). Einen gesonderten Datenbank-Export des Messdienstes gibt es nicht.</td></tr>
<tr><td>Rechenzentrum</td><td>Hetzner Online GmbH, Rechenzentrum Falkenstein (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert; der Geltungsbereich umfasst die Hosting-Dienstleistungen und die Rechenzentren.</td></tr>
<tr><td>Organisation</td><td>Verpflichtung aller Beteiligten auf Vertraulichkeit, Ablauf für Datenschutzverletzungen (Nummer 8 Abs. 5), Archiv der Fassungen von Banner-Text und Zusatzseite (Nummer 8 Abs. 6), Überprüfung der Maßnahmen mindestens einmal im Jahr.</td></tr>
</tbody>
</table>
</main>
</body>
</html>
@@ -0,0 +1,96 @@
<!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-16">
<meta name="robots" content="noindex">
<title>Leistungsbeschreibung News-Pinnwand – WEBklar</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #5e6678; font-size: 14px; }
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
table { border-collapse: collapse; width: 100%; font-size: 15px; }
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
a:hover { background: #f6f8fa; }
nav.top { font-size: 14px; margin-bottom: 22px; }
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar · Leistungsbeschreibung News-Pinnwand"; font: 8pt sans-serif; color: #8a93a3; }
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
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="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-news-2026-10-16.html">Leistungsbeschreibung News-Pinnwand</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Leistungsbeschreibung News-Pinnwand</h1>
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo</p>
<h2>1. Geltung</h2>
<p>(1) Funktion im Sinne dieser Leistungsbeschreibung ist die News-Pinnwand. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.</p>
<p>(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die News-Pinnwand diese Leistungsbeschreibung vor.</p>
<h2>2. Leistung</h2>
<p>(1) WEBklar baut die News-Pinnwand in die Website ein, die der Kunde für das gewählte Projekt bei WEBklar im Kundenportal führt. Die Pinnwand zeigt die Beiträge des Kunden als Zettel mit Titel, Text und Foto; pro Seite erscheinen sechs Beiträge, die neuesten zuerst.</p>
<p>(2) Die Beiträge schreibt, ändert und löscht der Kunde selbst im Kundenportal im Bereich „Funktionen“. Er wählt je Beitrag Zettelfarbe und Anzeigedauer; danach verschwindet der Beitrag von seiner Website. Hochgeladene Fotos werden verkleinert, alle Metadaten einschließlich Aufnahmeort (GPS) werden entfernt.</p>
<p>(3) Änderungen an der Website, die über den Einbau der Pinnwand hinausgehen, sind nicht Teil dieser Leistung.</p>
<h2>3. Einrichtung</h2>
<p>(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.</p>
<p>(2) Der Einbau ist erfahrungsgemäß meist innerhalb von 1–2 Wochen abgeschlossen. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins. Nach Abschluss des Einbaus schaltet WEBklar die Funktion frei; ab dann erscheinen die Beiträge auf der Website.</p>
<p>(3) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.</p>
<p>(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.</p>
<h2>4. Pflichten des Kunden</h2>
<p>(1) Der Kunde ist für die Inhalte seiner Beiträge und Fotos verantwortlich. Er versichert, dass er die nötigen Rechte an Texten und Fotos hat und dass die Inhalte keine Rechte Dritter verletzen, etwa Urheberrechte, Persönlichkeitsrechte oder das Recht am eigenen Bild abgebildeter Personen.</p>
<h2>5. Ende der Leistung</h2>
<p>(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.</p>
<p>(2) Auf Wunsch des Kunden schaltet WEBklar die Pinnwand jederzeit ab; für den Widerruf gilt Nr. 6.</p>
<p>(3) Beiträge und Fotos bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt in dieser Zeit wieder ein Wartungs-Abo, schaltet WEBklar die Pinnwand mit den vorhandenen Beiträgen ohne weitere Kosten wieder ein. Der Kunde kann seine Beiträge und Fotos jederzeit selbst löschen; andernfalls gelten die Fristen der Datenschutzerklärung (Abschnitt 6a).</p>
<h2>6. Widerruf</h2>
<p>(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.</p>
<p>(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.</p>
</main>
</body>
</html>
@@ -0,0 +1,179 @@
<!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-16">
<meta name="robots" content="noindex">
<title>Leistungsbeschreibung Terminplaner – WEBklar</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #5e6678; font-size: 14px; }
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
table { border-collapse: collapse; width: 100%; font-size: 15px; }
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
a:hover { background: #f6f8fa; }
nav.top { font-size: 14px; margin-bottom: 22px; }
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar · Leistungsbeschreibung Terminplaner"; font: 8pt sans-serif; color: #8a93a3; }
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
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="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.html">Leistungsbeschreibung Terminplaner</a> · <a href="/neu/rechtliches/avv-terminplaner-2026-10-16.html">AVV Terminplaner</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Leistungsbeschreibung Terminplaner</h1>
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo</p>
<h2>1. Geltung</h2>
<p>(1) Funktion im Sinne dieser Leistungsbeschreibung ist der Terminplaner. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Terminplaner.</p>
<p>(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Terminplaner diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Terminplaner.</p>
<p>(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für den Datenschutz-Absatz nach Nr. 4 Abs. 9 und die Gästetexte nach Nr. 2 Abs. 6. Beides ist Teil des Terminplaners.</p>
<h2>2. Leistung</h2>
<p>(1) Der Terminplaner ist ein Buchungs-Widget für die Website des Kunden mit einer Verwaltung im Kundenportal. WEBklar baut je Website-Projekt ein Widget an einer Stelle der Website ein, entweder eingebettet in die Seite oder hinter einem Knopf. Die Gestaltung richtet sich nach dem Fragebogen, den der Kunde vor dem Einschalten im Kundenportal beantwortet.</p>
<p>(2) Der Kunde legt seine Leistungen, seine Personen oder Plätze, seine Zeiten und seine Regeln für Buchungen fest. Gäste buchen im Widget einen Termin; je nach Einstellung ist die Buchung verbindlich oder eine Anfrage.</p>
<p>(3) Der Terminplaner versendet im Auftrag des Kunden E-Mails an die Gäste: den Bestätigungslink, die Bestätigung, Änderungen, Absagen, auf Wunsch des Gastes eine Erinnerung (nur mit seinem Haken) und die Eingangsbestätigung eines Widerrufs. Den Kunden benachrichtigt er über neue und geänderte Buchungen.</p>
<p>(4) Der Kunde sieht und verwaltet seine Termine im Kundenportal und kann sie als CSV-Datei exportieren. Dazu erhält er einen geheimen Kalender-Link (ICS), mit dem er seine Termine in einem Kalenderprogramm abonnieren kann.</p>
<p>(5) Der Terminplaner enthält einen Missbrauchsschutz gegen automatisierte Buchungen. Die Löschfristen für Buchungsdaten nennt Nummer 9 des AVV Terminplaner.</p>
<p>(6) Bezahlt wird nur vor Ort. Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz für die Website des Kunden und die Widerrufsbelehrung für Gäste sind Mustertexte der Standardkonfiguration (Nr. 11 Abs. 1).</p>
<h2>3. Nicht enthalten</h2>
<p>(1) Nicht enthalten sind SMS, Online-Zahlung, ein Kalender-Abgleich (es gibt nur das Abonnement des Kalender-Links nach Nr. 2 Abs. 4), Konten für Gäste, Marketing-E-Mails, Gruppentermine, Mehrsprachigkeit und der Einsatz im Gesundheitsbereich (Nr. 4 Abs. 3).</p>
<p>(2) Zusätzliche Wünsche, etwa ein zweites Widget, Knöpfe je Leistung oder ein Sonderdesign, gehören nicht zur Einrichtung nach Nr. 5. WEBklar führt sie nach Stunden aus (§ 4 der Abo-Bedingungen).</p>
<h2>4. Voraussetzungen und Pflichten des Kunden</h2>
<p>(1) Der Kunde hat ein laufendes Wartungs-Abo.</p>
<p>(2) Der Kunde trägt vor dem Einschalten seine Betriebsangaben ein, dazu die Adresse seines Impressums und die Adresse seiner Datenschutzerklärung.</p>
<p>(3) Der Terminplaner ist nicht für Angebote im Gesundheitsbereich bestimmt, weil dort besondere Kategorien personenbezogener Daten anfallen können (Art. 9 DSGVO). Der Kunde setzt ihn dort nicht ein und fordert von seinen Gästen keine Gesundheitsangaben. WEBklar darf die Einrichtung für eine solche Website ablehnen oder den Terminplaner dort abschalten. Der Fragebogen fragt die Branche ab; wählt der Kunde den Gesundheitsbereich, ist der Terminplaner für ihn nicht verfügbar.</p>
<p>(4) Richtet sich das Angebot des Kunden gezielt an Kinder unter 16 Jahren, nutzt der Kunde den Terminplaner nicht.</p>
<p>(5) Der Kunde nennt eine Telefonnummer, unter der Gäste Termine vereinbaren können, wenn das Widget nicht erreichbar ist (Nr. 6 Abs. 2).</p>
<p>(6) Er lässt die Texte der Haken und Hinweise im Widget unverändert.</p>
<p>(7) Er informiert seine Beschäftigten darüber, dass ihre Namen oder Bezeichnungen, ihre Arbeitszeiten und Abwesenheiten im Terminplaner stehen und dass es einen geheimen Kalender-Link gibt.</p>
<p>(8) Die Pflichten gegenüber seinen Gästen trägt der Kunde: Preisangaben, Allgemeine Geschäftsbedingungen und Stornobedingungen sowie die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung.</p>
<p>(9) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für die Online-Terminbuchung ein; das gehört zur Einrichtung. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.</p>
<h2>5. Einrichtung</h2>
<p>(1) Die Einrichtung umfasst die Einrichtung nach dem Fragebogen, den Einbau des Widgets in die Website, den Datenschutz-Absatz nach Nr. 4 Abs. 9, die Abnahme durch den Kunden und die Freischaltung durch WEBklar.</p>
<p>(2) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.</p>
<p>(3) Die voraussichtliche Dauer der Einrichtung beträgt meist 1–2 Wochen. Sie ist eine Einschätzung und keine verbindliche Zusage eines Termins.</p>
<p>(4) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.</p>
<p>(5) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen. Für den Betrieb des Terminplaners berechnet WEBklar neben dem Wartungs-Abo nichts weiter; er läuft, solange das Wartungs-Abo läuft (Nr. 8).</p>
<h2>6. Verfügbarkeit und Störungen</h2>
<p>(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Terminplaner. Störungen des Terminplaners beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).</p>
<p>(2) Ist der Terminplaner nicht erreichbar, zeigt das Widget einen Ausweichtext mit der Telefonnummer des Kunden (Nr. 4 Abs. 5).</p>
<p>(3) Alle Zeiten gelten in der Zeitzone Europe/Berlin.</p>
<p>(4) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.</p>
<p>(5) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.</p>
<h2>7. Haftung für verpasste Termine</h2>
<p>(1) Der Terminplaner ist ein Hilfsmittel. Die Einhaltung der Termine bleibt Sache des Kunden.</p>
<p>(2) WEBklar übernimmt keine Gewähr für die Zustellung der E-Mails (Nr. 6 Abs. 4).</p>
<p>(3) Für entgangenen Gewinn und Folgeschäden aus verpassten Terminen haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.</p>
<p>(4) Der Haftungsrahmen des Kunden gegenüber seinen Gästen bleibt unberührt.</p>
<h2>8. Ende der Leistung</h2>
<p>(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Kunden per E-Mail.</p>
<p>(2) Mit dem Abschalten nimmt der Terminplaner keine neuen Buchungen mehr an, und das Widget zeigt den Ausweichtext. Die Termine bleiben im Kundenportal sichtbar, und der Kunde kann sie als CSV-Datei exportieren. Gäste können über ihren Link absagen. Bereits angeforderte Erinnerungen laufen weiter. Der Kunde informiert seine Gäste.</p>
<p>(3) WEBklar löscht alle Termindaten und die Einstellungen 30 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Einstellungen bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Terminplaner).</p>
<p>(4) Beginnt innerhalb dieser 30 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Terminplaner mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Einrichtung nötig (neuer Fragebogen, Einrichtung nach Nr. 5).</p>
<p>(5) Endet das Wartungs-Abo, bevor WEBklar den Terminplaner freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 5 Abs. 2 verfällt.</p>
<p>(6) Auf Wunsch des Kunden in Textform schaltet WEBklar den Terminplaner jederzeit ab; für den Widerruf gilt Nr. 10.</p>
<h2>9. Änderungen und Einstellung</h2>
<p>(1) WEBklar darf die Technik des Terminplaners verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Terminplaner.</p>
<p>(2) WEBklar darf den Terminplaner für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn</p>
<ul>
<li>ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Terminplaner Nummer 12), oder</li>
<li>eine Sicherheitslücke sich nicht anders beheben lässt.</li>
</ul>
<p>Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.</p>
<p>(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Terminplaner ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.</p>
<p>(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.</p>
<h2>10. Widerruf</h2>
<p>(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.</p>
<p>(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.</p>
<h2>11. Hinweise</h2>
<p>(1) <strong>Keine Rechtsberatung.</strong> Der Hinweis am Buchungsformular, die Texte der Haken, der Datenschutz-Absatz und die Widerrufsbelehrung für Gäste sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Die Einstufung seiner Leistungen als Leistung für Verbraucher oder Unternehmer und als Freizeitleistung trägt der Kunde. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht.</p>
<p>(2) <strong>Barrierefreiheit.</strong> Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Kunde als Dienstleistungserbringer unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Kunde.</p>
</main>
</body>
</html>
@@ -0,0 +1,255 @@
<!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-16">
<meta name="robots" content="noindex">
<title>Leistungsbeschreibung Buchungskalender – WEBklar</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #5e6678; font-size: 14px; }
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
table { border-collapse: collapse; width: 100%; font-size: 15px; }
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
a:hover { background: #f6f8fa; }
nav.top { font-size: 14px; margin-bottom: 22px; }
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar · Leistungsbeschreibung Buchungskalender"; font: 8pt sans-serif; color: #8a93a3; }
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
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="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-unterkunft-2026-10-16.html">Leistungsbeschreibung Buchungskalender</a> · <a href="/neu/rechtliches/avv-unterkunft-2026-10-16.html">AVV Buchungskalender</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Leistungsbeschreibung Buchungskalender</h1>
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo</p>
<h2>1. Geltung</h2>
<p>(1) Diese Leistungsbeschreibung regelt die Funktion „Buchungskalender“. Mit dem Buchungskalender nimmt der Kunde über seine Website Anfragen für die Vermietung von Unterkünften entgegen (Ferienwohnungen, Monteurzimmer, kleine Pensionen) und rechnet die Aufenthalte ab („Vermieter“); Gäste sind die Besucher seiner Website, die im Buchungskalender anfragen und buchen („Gäste“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung vermieten.</p>
<p>(2) Diese Leistungsbeschreibung gilt, wenn der Vermieter die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Vermieter die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Buchungskalender. Einschalten kann der Vermieter erst, nachdem WEBklar seine Anfrage für den Buchungskalender geprüft und angenommen hat (Nr. 2 Abs. 1). Für einen Vermieter, dem WEBklar die Funktion als Teil seines Website-Projekts bereitstellt, schaltet WEBklar sie nach seiner Zustimmung in Textform frei; die Bedingungen dieser Leistungsbeschreibung gelten dann ebenso, die Einrichtung rechnet WEBklar nach Nr. 9 Abs. 3 ab.</p>
<p>(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Buchungskalender diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Buchungskalender.</p>
<p>(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Buchungskalender nicht entgegen. Das Widget des Buchungskalenders läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und der Muster-Text zur Stornierung nach Nr. 6 ist Teil des Buchungskalenders.</p>
<h2>2. Leistung</h2>
<p>(1) Der Vermieter fragt den Buchungskalender im Kundenportal an und beschreibt seine Unterkünfte (Art der Unterkunft, Zahl der Einheiten, ob er Booking.com oder Airbnb nutzt). WEBklar prüft die Anfrage und nimmt sie an oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Annahme besteht nicht. Erst nach der Annahme kann der Vermieter den Buchungskalender einschalten.</p>
<p>(2) Die Einrichtung umfasst den Einbau eines Kalender-Widgets an einer Stelle der Website des Vermieters, die Bereitstellung der Verwaltung im Kundenportal, den Einbau des Datenschutz-Absatzes für den Buchungskalender (Nr. 5 Abs. 10) und die Freischaltung durch WEBklar. Für jede Website wird ein Buchungskalender eingerichtet; er kann mehrere Unterkünfte desselben Vermieters enthalten. Zusätzlich gibt es für jeden freigegebenen Aufenthalt eine Buchungsseite bei WEBklar, die der Gast über den Link in seiner E-Mail erreicht.</p>
<p>(3) So läuft eine Buchung ab:</p>
<ul>
<li>Das Widget zeigt dem Gast die belegten Nächte. Er wählt Anreise und Abreise, gibt Personenzahl, Namen, E-Mail-Adresse und auf Wunsch Firma, Telefonnummer und Nachricht an und sendet eine <strong>unverbindliche Anfrage</strong>. Er erhält eine Eingangsbestätigung, der Vermieter eine Benachrichtigung.</li>
<li>Der Vermieter prüft die Anfrage im Kundenportal, sieht den vorgerechneten Preis, kann die Posten ändern und gibt die Anfrage mit einer Reservierungsfrist frei oder lehnt sie ab. Gibt er sie frei, reserviert der Buchungskalender die Nächte bis zum Ablauf der Frist, und der Gast erhält eine E-Mail mit Preis, Stornobedingungen und dem Link zur Buchungsseite.</li>
<li>Auf der Buchungsseite sieht der Gast alle Angaben zum Aufenthalt, trägt seine Rechnungsadresse ein, wählt die Zahlart (online über Stripe, wenn der Vermieter sie eingeschaltet hat, oder vor Ort) und bucht mit der Schaltfläche zur zahlungspflichtigen Buchung. Erst damit kommt der Vertrag zwischen Vermieter und Gast zustande, nicht schon mit der Anfrage oder der Freigabe.</li>
<li>Mit der Buchung erzeugt der Buchungskalender eine Rechnung mit lückenloser Nummer und sendet dem Gast eine Buchungsbestätigung mit der Rechnung als PDF; der Vermieter wird benachrichtigt.</li>
<li>Verstreicht die Frist ohne Buchung, läuft die Reservierung ab und die Nächte werden frei; der Gast erhält darüber eine E-Mail.</li>
<li>Der Vermieter kann einen Aufenthalt stornieren. Dann erhält der Gast eine Stornorechnung mit eigener Nummer und, soweit der Vermieter eine Online-Zahlung erstattet, eine Mitteilung über den Erstattungsbetrag. Eine Stornierung durch den Gast gibt es im Widget nicht; er wendet sich an den Vermieter.</li>
</ul>
<p>(4) Im Kundenportal verwaltet der Vermieter:</p>
<ul>
<li>Unterkünfte mit Beschreibung, Höchstzahl der Personen, Mindestnächten und Check-in- und Check-out-Zeiten,</li>
<li>Preise (Staffel nach Aufenthaltsdauer, Endreinigung, Zusatzpersonen, Direktbuchungsrabatt, Steuerart und Steuersätze) und seine Stornobedingungen,</li>
<li>Anfragen und Buchungen mit Status, die Eintragung „bezahlt (vor Ort)“ und Stornierungen,</li>
<li>Kalender mit manuellen Sperren,</li>
<li>die Verbindungen zu anderen Kalendern (Nr. 4),</li>
<li>seine Rechnungen als PDF,</li>
<li>seine Angaben als Aussteller der Rechnungen (Name, Anschrift, Steuernummer oder Umsatzsteuer-Identifikationsnummer, Bankverbindung, Rechnungspräfix) und die Verbindung mit seinem Stripe-Konto (Nr. 3).</li>
</ul>
<p>(5) Der Buchungskalender versendet im Namen des Vermieters E-Mails an die Gäste: Eingang der Anfrage, Freigabe mit Buchungslink, Ablehnung, Buchungsbestätigung mit Rechnung, Zahlungseingang, Ablauf der Reservierung und Stornierung. Absender ist die Versandadresse von WEBklar mit dem Namen des Vermieters, als Antwortadresse dient seine E-Mail-Adresse. Den Vermieter benachrichtigt der Buchungskalender über neue Anfragen und neue Buchungen sowie, wenn der Abgleich mit einem anderen Kalender seit mindestens sechs Stunden fehlschlägt.</p>
<p>(6) Der Buchungskalender enthält einen Missbrauchsschutz gegen automatisierte Anfragen (verstecktes Feld, Mindest-Ausfüllzeit, Obergrenzen je IP-Adresse). Dafür speichert WEBklar mit der Anfrage nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird entfernt, sobald der Vermieter die Anfrage freigibt oder ablehnt, spätestens nach 30 Tagen (Nummer 3 Abs. 4 des AVV Buchungskalender).</p>
<p>(7) Nicht enthalten sind:</p>
<ul>
<li>der Meldeschein nach dem Bundesmeldegesetz und die Verwaltung der Meldedaten,</li>
<li>Kurtaxe, Gästebeitrag und Beherbergungsabgaben,</li>
<li>Anzahlungen und Teilzahlungen (der Gast zahlt den Gesamtbetrag in einer Zahlung, online oder vor Ort),</li>
<li>Sofortbuchung ohne Freigabe,</li>
<li>saisonale Preise (die Preise richten sich nur nach der Aufenthaltsdauer),</li>
<li>Gutscheine, Channel-Manager-Anbindungen außer dem Kalenderabgleich nach Nr. 4 und Mehrsprachigkeit (der Buchungskalender ist deutschsprachig),</li>
<li>die Buchführung des Vermieters.</li>
</ul>
<p>Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Buchungskalender oder eine über das übliche Maß hinausgehende Zahl von Unterkünften, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).</p>
<h2>3. Zahlungen über Stripe und Rechnungen im Namen des Vermieters</h2>
<p>(1) Online-Zahlungen im Buchungskalender sind optional und laufen über Stripe Connect. Der Vermieter verbindet dazu sein eigenes Stripe-Konto mit dem Buchungskalender. Hat er für den Webshop von WEBklar bereits ein Stripe-Konto verbunden, kann er es auch für den Buchungskalender verwenden. Den Vertrag über dieses Konto schließt der Vermieter unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Vermieters durch Stripe und die Auszahlungen gelten die Bedingungen von Stripe. Ohne verbundenes und freigeschaltetes Stripe-Konto ist nur die Zahlart „vor Ort“ möglich.</p>
<p>(2) Das Geld der Gäste fließt direkt an den Vermieter (Zahlung unmittelbar auf dem Konto des Vermieters, sogenannte Direct Charge). WEBklar erhält und verwahrt kein Geld der Gäste, leitet keines weiter und beteiligt sich nicht am Umsatz des Vermieters. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Vermieter; sie fallen nur zwischen dem Vermieter und Stripe an.</p>
<p>(3) Der Vermieter prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt ihm im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Gästen und Zahlungsausfälle regeln der Vermieter und Stripe.</p>
<p>(4) Erstattungen bei einer Stornierung veranlasst der Vermieter im Kundenportal; WEBklar führt die Erstattung über das Stripe-Konto des Vermieters aus. Er bestimmt den Betrag bis zur Höhe der Zahlung des Gastes. Storniert der Vermieter einen Aufenthalt, schließt WEBklar eine noch offene Zahlungsseite bei Stripe. <strong>Automatische Erstattung:</strong> Geht eine Zahlung ein, nachdem die Buchung storniert oder die Reservierung abgelaufen ist, oder zahlt der Gast eine bereits bezahlte Buchung ein zweites Mal, erstattet WEBklar den Betrag ohne weitere Anweisung im Namen des Vermieters in voller Höhe über dessen Stripe-Konto. Dazu erteilt der Vermieter WEBklar mit der Zustimmung zum Buchungskalender eine dauerhafte Weisung; der Stand der Buchung ändert sich dadurch nicht. Erstattungen aus dem Stripe-Dashboard trägt WEBklar in die Buchung nach, soweit Stripe sie meldet.</p>
<p>(5) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Online-Zahlungen sind derzeit nur per Karte möglich, einschließlich Apple Pay und Google Pay; eine SEPA-Lastschrift bietet der Buchungskalender nicht an. Kartendaten gibt der Gast nur dort ein; WEBklar erhält sie nicht. Beim Start der Zahlung übergibt WEBklar Stripe als Rücksprungadresse (bei Erfolg und bei Abbruch) den geheimen Link zur Buchungsseite des Gastes samt Token. Dieser Link ist damit für Stripe und im Stripe-Dashboard des Vermieters sichtbar; Der Link gilt bis 90 Tage nach der Abreise (bei nicht zustande gekommenen Buchungen 30 Tage) und öffnet nur diese eine Buchungsseite.</p>
<p>(6) <strong>Rechnungen im Namen des Vermieters.</strong> Der Vermieter beauftragt WEBklar mit dem Rechnungsauftrag in den Einstellungen des Buchungskalenders, in seinem Namen und für seine Rechnung Rechnungen und Stornorechnungen an seine Gäste auszustellen (Rechnung durch einen Dritten im Namen und im Auftrag des Leistenden, § 14 Abs. 2 Satz 4 UStG). Aussteller der Rechnung ist der Vermieter; sie trägt seinen Namen, seine Anschrift, seine Steuernummer oder Umsatzsteuer-Identifikationsnummer und keinen Hinweis auf WEBklar. WEBklar erzeugt die Rechnung aus den Angaben, die der Vermieter hinterlegt und bei der Freigabe bestätigt hat. Er verantwortet die Richtigkeit seiner Angaben, der Preise, der Steuerart und der Steuersätze; WEBklar prüft sie nicht steuerrechtlich. Die Rechnung wird nach ihrer Ausstellung nicht mehr verändert; Fehler korrigiert der Vermieter durch Stornierung und neue Buchung nach den Möglichkeiten des Kundenportals. Ohne diesen Auftrag stellt WEBklar keine Rechnungen aus. Widerruft er den Auftrag in Textform, stellt der Buchungskalender keine Rechnungen mehr aus und ist für ihn nicht mehr nutzbar.</p>
<p>(7) Die Rechnungsnummern sind je Vermieter und Jahr lückenlos fortlaufend; sie bestehen aus dem Präfix, das der Vermieter vor der ersten Rechnung festlegt, dem Jahr und einer laufenden Nummer.</p>
<h2>4. Verfügbarkeit und Kalenderabgleich (iCal)</h2>
<p>(1) Der Vermieter kann Kalender anderer Plattformen (zum Beispiel Booking.com oder Airbnb) über eine iCal-Adresse einlesen lassen; die eingelesenen Zeiträume blockiert der Buchungskalender. Umgekehrt stellt der Buchungskalender dem Vermieter eine geheime iCal-Adresse bereit, die er bei anderen Plattformen einträgt; sie enthält die belegten Nächte ohne Daten der Gäste. Der Vermieter kann diese Adresse jederzeit erneuern; die bisherige Adresse funktioniert dann nicht mehr.</p>
<p>(2) Der Abgleich läuft regelmäßig, aber zeitversetzt: WEBklar liest die Kalender anderer Plattformen etwa alle 15 Minuten ein, und die anderen Plattformen lesen die Adresse des Buchungskalenders nach ihrem eigenen Takt, oft nur alle paar Stunden. <strong>Zwischen einer Buchung hier und ihrem Eingang dort können deshalb Minuten bis Stunden vergehen, und umgekehrt.</strong> WEBklar sagt keine Echtzeit-Verfügbarkeit zu und übernimmt keine Gewähr dafür, dass eine Unterkunft nicht doppelt gebucht wird.</p>
<p>(3) Dieses Risiko mindert die Freigabe: Jede Anfrage im Buchungskalender braucht die Freigabe des Vermieters. Er prüft vor der Freigabe seine anderen Plattformen und seinen Kalender. Beim Freigeben und beim Buchen prüft der Buchungskalender zusätzlich, ob die Nächte nach seinem Stand frei sind. Eine Doppelbuchung entsteht trotzdem, wenn eine andere Plattform eine Buchung noch nicht gemeldet hat oder der Abgleich ausfällt. Der Vermieter löst eine Doppelbuchung selbst (etwa durch Stornierung) und trägt die Folgen gegenüber seinen Gästen und den Plattformen.</p>
<p>(4) Fehlerhafte oder nicht erreichbare iCal-Adressen kann WEBklar nicht beheben. Schlägt der Abruf fehl, bleibt der zuletzt gelesene Stand erhalten, und der Vermieter wird benachrichtigt, wenn der Abruf seit mindestens sechs Stunden nicht gelungen ist (höchstens einmal je 24 Stunden und Kalender). Eingelesen werden nur Zeiträume von gestern bis 18 Monate in die Zukunft, höchstens 1 Megabyte je Kalender; nur Adressen mit https werden abgerufen.</p>
<h2>5. Pflichten des Vermieters</h2>
<p>(1) Der Vermieter hat ein laufendes Wartungs-Abo und ist Unternehmer. Will er Online-Zahlungen nutzen, stellt er sein eigenes Stripe-Konto bereit und schließt die Anmeldung bei Stripe selbst ab. Der Vermieter vermietet in eigenem Namen; für Preise, Stornobedingungen, Rechnungen und Meldepflichten ist er verantwortlich. Online-Zahlungen seiner Gäste laufen, sofern er sie nutzt, über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Gäste und erhebt keine Umsatzbeteiligung. Rechnungen an Gäste stellt WEBklar nur aus, wenn der Vermieter in den Einstellungen des Buchungskalenders den Rechnungsauftrag erteilt hat (§ 14 Abs. 2 Satz 4 UStG); für die Richtigkeit von Preisen und Steuerangaben bleibt er verantwortlich.</p>
<p>(2) Er verantwortet seine Unterkünfte, Preise und Angaben zum Angebot. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer, für die Beherbergung und die Endreinigung nach den dafür geltenden Steuersätzen, oder Hinweis auf § 19 UStG), Check-in- und Check-out-Zeiten, Höchstbelegung, Mindestnächte und alle Angaben, die er seinen Gästen vor der Buchung geben muss. Die Steuerart und die Steuersätze trägt er selbst im Kundenportal ein; WEBklar berät dazu nicht.</p>
<p>(3) Er legt vor der Freischaltung seine Stornobedingungen fest und verantwortet sie. Der Muster-Text nach Nr. 6 ist nur ein Vorschlag.</p>
<p>(4) Er verantwortet die Verfügbarkeit seiner Unterkünfte, pflegt Sperren und Kalenderverbindungen im Kundenportal und prüft regelmäßig auf neue Anfragen und Buchungen. Die Folgen von Angaben, die er setzt (zum Beispiel einer fälschlich freien oder gesperrten Zeit), trägt er.</p>
<p>(5) Er gibt Anfragen nur frei, wenn er die Unterkunft für den Zeitraum zu den angegebenen Bedingungen bereitstellen kann, und erfüllt die gebuchten Aufenthalte. Er trägt alle Pflichten eines Vermieters gegenüber seinen Gästen, auch Gewährleistung, Kundenservice und Beschwerden.</p>
<p>(6) Er erfüllt seine gesetzlichen Pflichten rund um die Beherbergung selbst. Das betrifft insbesondere den Meldeschein nach dem Bundesmeldegesetz, die Kurtaxe und andere kommunale Abgaben, Anzeige- und Genehmigungspflichten für die Vermietung (zum Beispiel Zweckentfremdungs- oder Gewerberecht) sowie die Pflichten aus seinem Steuerrecht. Der Buchungskalender nimmt ihm davon nichts ab.</p>
<p>(7) Anzahlungen und Teilzahlungen bietet der Buchungskalender nicht an. Vereinbart der Vermieter außerhalb des Buchungskalenders etwas anderes mit seinem Gast, ist das nicht Gegenstand dieser Leistungsbeschreibung.</p>
<p>(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem. WEBklar bewahrt die Rechnungen des Vermieters im Buchungskalender 8 Jahre auf, solange der Buchungskalender läuft und nach Nr. 10 bis zur Löschung, hält sie ihm zum Abruf als PDF bereit und anonymisiert personenbezogene Daten der Gäste nach Nummer 9 des AVV Buchungskalender. Der Vermieter lädt seine Rechnungen deshalb rechtzeitig vor der Löschung nach Nr. 10 Abs. 3 herunter und bewahrt sie selbst auf.</p>
<p>(9) Er hält die Vorgaben dieser Leistungsbeschreibung ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets und der Buchungsseite nicht.</p>
<p>(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Buchungskalender ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Vermieter den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Vermieter darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.</p>
<p>(11) Der Vermieter informiert seine Gäste nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Buchungskalender: Der Vermieter ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Vermieters.</p>
<h2>6. Muster-Text zur Stornierung, keine Rechtsberatung</h2>
<p>(1) WEBklar stellt dem Vermieter einen Muster-Text für Stornobedingungen bereit. Er ist eine <strong>Vorlage ohne Gewähr</strong> und keine Rechtsberatung (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes). Er passt zur Technik des Buchungskalenders, kann aber auf den Betrieb des Vermieters nicht zugeschnitten sein.</p>
<p>(2) WEBklar empfiehlt, die Stornobedingungen und die Angaben zum Angebot vor der Freischaltung von einer Rechtsanwältin oder einem Rechtsanwalt prüfen zu lassen.</p>
<p>(3) Der Vermieter bleibt für seine Stornobedingungen und alle seine Texte verantwortlich, auch wenn er den Muster-Text unverändert übernimmt (Nr. 5 Abs. 3). Die Stornobedingungen, die bei der Freigabe galten, speichert der Buchungskalender mit der Anfrage; die Buchungsseite und die Buchungsbestätigung zeigen dem Gast diese Fassung.</p>
<h2>7. Kein Widerrufsrecht des Gastes, Hinweise auf der Buchungsseite</h2>
<p>(1) Für die Buchung von Beherbergungsleistungen zu einem bestimmten Zeitpunkt oder in einem bestimmten Zeitraum besteht nach § 312g Abs. 2 Satz 1 Nr. 9 BGB kein gesetzliches Widerrufsrecht. Die Buchungsseite weist den Gast darauf hin; der Vermieter kann ihm vertraglich ein weitergehendes Recht einräumen, der Buchungskalender bietet dafür aber keine Funktion.</p>
<p>(2) Die Buchungsseite zeigt dem Gast vor der Schaltfläche zur zahlungspflichtigen Buchung die wesentlichen Angaben nach Art. 246a EGBGB, soweit der Buchungskalender sie kennt (Anbieter, Unterkunft, Zeitraum, Leistung, Preis insgesamt, Zahlart, Stornobedingungen, Hinweis auf das fehlende Widerrufsrecht). Der Vermieter ergänzt, was darüber hinausgeht, in seinen Texten selbst (Nr. 5 Abs. 2).</p>
<p>(3) Die Buchungsbestätigung erhält der Gast als E-Mail mit der Rechnung als PDF; sie enthält die Angaben zum Vertrag und die Stornobedingungen auf einem dauerhaften Datenträger.</p>
<h2>8. Verfügbarkeit und Störungen</h2>
<p>(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Buchungskalender. WEBklar sagt für den Buchungskalender keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Buchungskalenders beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).</p>
<p>(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Buchungskalender kurzzeitig unterbrechen. Während einer Unterbrechung sind Anfragen und Buchungen nicht möglich; das Widget zeigt dann einen Ausweichtext.</p>
<p>(3) Zahlungen übernimmt der Buchungskalender über Benachrichtigungen von Stripe und einen Abgleich beim Aufruf der Buchungsseite. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Zahlung verzögern. Eine Zahlung, die nach einer Stornierung oder nach dem Ablauf der Reservierung eingeht, und eine zweite Zahlung auf eine bereits bezahlte Buchung erstattet WEBklar automatisch und in voller Höhe (Nr. 3 Abs. 4); die Buchung bleibt dabei unverändert, und die Erstattung braucht einige Tage, bis sie beim Gast ankommt. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.</p>
<p>(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.</p>
<p>(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.</p>
<p>(6) Die gesetzlichen Rechte des Vermieters sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.</p>
<h2>9. Einrichtung, Abrechnung und Widerruf</h2>
<p>(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Vermieters. Für den Betrieb des Buchungskalenders berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz oder an den Buchungen.</p>
<p>(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 1 bis 2 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Vermieter Unterkünfte, Preise und gegebenenfalls das Stripe-Konto bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.</p>
<p>(3) Hat WEBklar die Funktion auf Wunsch des Vermieters schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.</p>
<p>(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Vermieters in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.</p>
<p>(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Vermieter die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat. Das Widerrufsrecht gegenüber WEBklar ist vom Widerrufsrecht der Gäste gegenüber dem Vermieter (Nr. 7) zu unterscheiden.</p>
<h2>10. Laufzeit und Ende</h2>
<p>(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Vermieter per E-Mail und weist ihn darauf hin, seine Rechnungen als PDF herunterzuladen.</p>
<p>(2) Mit dem Abschalten nimmt der Buchungskalender keine neuen Anfragen und Buchungen mehr an, und das Widget zeigt den Ausweichtext. Reservierungen laufen aus. Bereits gebuchte Aufenthalte bleiben Verträge zwischen dem Vermieter und seinen Gästen; der Vermieter erfüllt sie und informiert seine Gäste, wenn er den Buchungskalender nicht mehr anbietet. Anfragen, Buchungen und Rechnungen bleiben im Kundenportal 90 Tage lesbar; Rechnungen kann er einzeln als PDF herunterladen.</p>
<p>(3) WEBklar löscht alle Daten des Buchungskalenders, also Unterkünfte, Preise, Anfragen, Buchungen, Rechnungen, Kalenderverbindungen, Sperren und Einstellungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Gästen (Nummer 9 Abs. 6 des AVV Buchungskalender). Die Aufbewahrungsfrist der Rechnungen (Nr. 5 Abs. 8) endet damit mit der Löschung; der Vermieter bewahrt sie danach selbst auf. Nach dem Abschalten weist WEBklar den Vermieter per E-Mail mit dem Löschdatum darauf hin, seine Rechnungen zu sichern; bis dahin sind sie im Kundenportal abrufbar. Wird das Kundenkonto gelöscht, schaltet WEBklar den Buchungskalender ab und löscht seine Daten wie nach Absatz 3 (90 Tage nach dem Abschalten); einen Hinweis per E-Mail gibt es dann nicht, weil mit dem Konto auch die E-Mail-Adresse gelöscht wird.</p>
<p>(4) Das Stripe-Konto bleibt beim Vermieter. Das Ende des Buchungskalenders berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Vermieter kann die Verbindung zwischen dem Konto und dem Buchungskalender im Stripe-Dashboard beenden.</p>
<p>(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Buchungskalender auf Wunsch des Vermieters in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).</p>
<p>(6) Endet das Wartungs-Abo, bevor WEBklar den Buchungskalender freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.</p>
<p>(7) Auf Wunsch des Vermieters in Textform schaltet WEBklar den Buchungskalender jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.</p>
<h2>11. Haftung</h2>
<p>(1) Der Buchungskalender ist ein Hilfsmittel für die Vermietung des Vermieters. Der Vertrag über die Unterkunft besteht allein zwischen dem Vermieter und dem Gast; WEBklar ist daran nicht beteiligt.</p>
<p>(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Buchungskalenders, aus Verzögerungen oder Fehlern beim Kalenderabgleich (Nr. 4), aus Doppelbuchungen und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.</p>
<p>(3) WEBklar haftet nicht für Inhalte des Vermieters, insbesondere nicht für seine Unterkünfte, Preise, Steuerangaben, Stornobedingungen und Rechtstexte, auch nicht für den Muster-Text, den er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Gästen und den Behörden (Nr. 5).</p>
<p>(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 3 Abs. 3), und nicht für Ausfälle, Änderungen und Entscheidungen anderer Plattformen, deren Kalender der Vermieter einbindet.</p>
<p>(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Vermieters gegenüber seinen Gästen bleibt unberührt.</p>
<h2>12. Änderungen und Einstellung</h2>
<p>(1) WEBklar darf die Technik des Buchungskalenders verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Buchungskalender.</p>
<p>(2) WEBklar darf den Buchungskalender für eine Website mit einer Frist von vier Wochen beenden oder für alle Vermieter mit einer Frist von drei Monaten einstellen, wenn</p>
<ul>
<li>ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Vermieter einer dafür nötigen neuen Fassung nicht zustimmt (AVV Buchungskalender Nummer 12),</li>
<li>Stripe die Anbindung nach Nr. 3 beendet oder wesentlich ändert, so dass die Online-Zahlung in der bisherigen Form nicht weiterbetrieben werden kann (dann entfällt nur die Online-Zahlung, soweit der Buchungskalender ohne sie weiterläuft), oder</li>
<li>eine Sicherheitslücke sich nicht anders beheben lässt.</li>
</ul>
<p>Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.</p>
<p>(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Buchungskalender ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Vermieter darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.</p>
<p>(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.</p>
<h2>13. Hinweise</h2>
<p>(1) <strong>Keine Rechtsberatung.</strong> Der Buchungskalender und der Muster-Text sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche und steuerliche Einordnung seiner Vermietung, seiner Preisangaben und seines Angebots trägt der Vermieter.</p>
<p>(2) <strong>Barrierefreiheit.</strong> Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen der Tage, Bedienung mit Pfeiltasten, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Vermieter als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Vermieter.</p>
</main>
</body>
</html>
@@ -0,0 +1,239 @@
<!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-16">
<meta name="robots" content="noindex">
<title>Leistungsbeschreibung Webshop – WEBklar</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #5e6678; font-size: 14px; }
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
table { border-collapse: collapse; width: 100%; font-size: 15px; }
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
a:hover { background: #f6f8fa; }
nav.top { font-size: 14px; margin-bottom: 22px; }
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar · Leistungsbeschreibung Webshop"; font: 8pt sans-serif; color: #8a93a3; }
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
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="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-webshop-2026-10-16.html">Leistungsbeschreibung Webshop</a> · <a href="/neu/rechtliches/avv-webshop-2026-10-16.html">AVV Webshop</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Leistungsbeschreibung Webshop</h1>
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo</p>
<h2>1. Geltung</h2>
<p>(1) Diese Leistungsbeschreibung regelt die Funktion „Webshop“. Mit dem Webshop verkauft der Kunde Waren über seine Website („Händler“); Käufer sind die Besucher seiner Website, die im Webshop bestellen („Käufer“). Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht. Sie richtet sich nur an Unternehmer, die in eigenem Namen und für eigene Rechnung verkaufen.</p>
<p>(2) Diese Leistungsbeschreibung gilt, wenn der Händler die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Händler die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Webshop. Einschalten kann der Händler erst, nachdem WEBklar seine Anfrage für den Webshop geprüft und freigegeben hat (Nr. 2 Abs. 1).</p>
<p>(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für den Webshop diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Webshop.</p>
<p>(4) Leistungsbeschreibung A Nr. 2 Abs. 3 und Nr. 4 sowie Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen (Formulare mit Server, „Rechtstexte … nicht enthalten“) stehen dem Webshop nicht entgegen. Das Widget des Webshops läuft auf dem Server von WEBklar, nicht auf dem Server der Website, und die Muster-Rechtstexte nach Nr. 6 sind Teil des Webshops.</p>
<h2>2. Leistung</h2>
<p>(1) Der Händler fragt den Webshop im Kundenportal an und beschreibt, was er verkauft. WEBklar prüft die Anfrage, vor allem nach Nr. 3, und gibt sie frei oder lehnt sie mit einer Begründung ab. Ein Anspruch auf Freigabe besteht nicht. Erst nach der Freigabe kann der Händler den Webshop einschalten.</p>
<p>(2) Die Einrichtung umfasst den Einbau eines Shop-Widgets an einer Stelle der Website des Händlers, die Seite des Webshops mit einem Verweis im Menü und den Link „Vertrag widerrufen“ in der Fußzeile der Seiten der Website, außerdem die Freischaltung durch WEBklar. Für jede Website wird ein Webshop eingerichtet. Zusätzlich steht dem Händler eine eigenständige Seite des Webshops bei WEBklar zur Verfügung (Standalone-Seite), etwa als Vorschau.</p>
<p>(3) Im Kundenportal verwaltet der Händler:</p>
<ul>
<li>Artikel mit Beschreibung, Bildern, Preis, Varianten, Bestand und den Pflichtangaben zum Artikel,</li>
<li>Versand (nur nach Deutschland und Österreich) und Abholung mit Versandkosten und Lieferzeiten,</li>
<li>Bestellungen mit Status (zum Beispiel bezahlt, versendet, abholbereit, abgeschlossen), die er als CSV-Datei exportieren kann,</li>
<li>Erstattungen, ganz oder teilweise, über sein Stripe-Konto (Nr. 4),</li>
<li>die Widerrufe, die Käufer über die Widerrufsfunktion erklärt haben,</li>
<li>die Verbindung mit seinem Stripe-Konto, seine Einstellungen und seine Rechtstexte, deren bestätigte Fassungen WEBklar archiviert (Nr. 6 Abs. 5).</li>
</ul>
<p>(4) Der Webshop enthält die Widerrufsfunktion nach § 356a BGB: Das Widget zeigt in seiner Fußzeile und die Standalone-Seite zeigt unter einer eigenen Adresse die Schaltfläche „Vertrag widerrufen“. Sie führt zu einem Formular und danach zur Bestätigungsschaltfläche „Widerruf bestätigen“. WEBklar speichert den Eingang, sendet dem Erklärenden sofort eine Eingangsbestätigung mit Inhalt, Datum und Uhrzeit und benachrichtigt den Händler. Die Funktion ist ohne Anmeldung erreichbar, auch wenn zur Erklärung keine Bestellung gefunden wird. Der Händler bearbeitet jeden Widerruf selbst (Nr. 5 Abs. 6).</p>
<p>(5) Der Webshop versendet im Namen des Händlers E-Mails an die Käufer: Eingang der Bestellung (bei einer Zahlung, die noch aussteht, etwa einer SEPA-Lastschrift, vor dem Vertragsschluss), Bestellbestätigung, Versand, Abholbereitschaft, Eingangsbestätigung eines Widerrufs, Erstattung und Hinweis auf eine fehlgeschlagene Zahlung. Absender ist die Versandadresse von WEBklar mit dem Namen des Shops, als Antwortadresse dient die E-Mail-Adresse des Händlers. Den Händler benachrichtigt der Webshop über neue Bestellungen und eingegangene Widerrufe.</p>
<p>(6) Der Webshop enthält einen Missbrauchsschutz gegen automatisierte Bestellungen und Widerrufe, darunter eine Obergrenze von drei offenen Bestellungen je IP-Adresse in 30 Minuten. Dafür speichert WEBklar mit der offenen Bestellung nur einen Hashwert der IP-Adresse, nicht die IP-Adresse selbst; er wird gelöscht, sobald die Bestellung bezahlt oder storniert wird (Nummer 3 Abs. 4 des AVV Webshop). Eine Telefonnummer des Käufers wird nicht erhoben und nicht gespeichert. Die Löschfristen für Bestelldaten nennt Nummer 9 des AVV Webshop.</p>
<p>(7) Nicht enthalten sind der Verkauf an Verbraucher außerhalb von Deutschland und Österreich, Rechnungen (Nr. 5 Abs. 7), die Verwaltung mehrerer Lager, Rabattcodes, Marktplatzfunktionen, die Anbindung an Warenwirtschaft oder Versanddienstleister, Mehrsprachigkeit und die Zahlung per PayPal (Nr. 4 Abs. 5). Zusätzliche Wünsche, etwa ein Sonderdesign, ein zweiter Webshop oder eine größere Zahl von Artikeln als vorgesehen, gehören nicht zur Einrichtung nach Nr. 9. WEBklar führt sie nach Absprache nach Stunden aus (Nr. 9 Abs. 4).</p>
<h2>3. Ausgeschlossene Waren und Leistungen</h2>
<p>(1) Über den Webshop dürfen nicht verkauft werden:</p>
<ul>
<li>alkoholische Getränke,</li>
<li>Tabakwaren, E-Zigaretten und Zubehör,</li>
<li>Arzneimittel und Medizinprodukte,</li>
<li>Waffen, Munition, Feuerwerk und Sprengstoffe,</li>
<li>Waren mit erotischem oder pornografischem Inhalt,</li>
<li>lebende Tiere,</li>
<li>digitale Inhalte und Downloads,</li>
<li>Gutscheine, Geschenkkarten und Guthaben,</li>
<li>Abonnements und andere wiederkehrende Zahlungen,</li>
<li>Waren und Leistungen, die nach der Liste der verbotenen und eingeschränkten Geschäfte von Stripe nicht erlaubt sind, und</li>
<li>Waren, deren Verkauf oder Versand nach deutschem, österreichischem oder europäischem Recht verboten ist.</li>
</ul>
<p>(2) Der Händler liefert nur nach Deutschland und Österreich. Eine Lieferung in andere Länder bietet der Webshop nicht an.</p>
<p>(3) Der Händler bestätigt bei der Anfrage und beim Einschalten, dass er keine dieser Waren und Leistungen verkauft. WEBklar darf eine Anfrage ablehnen, wenn der Händler Waren nach Absatz 1 verkaufen will oder die Angaben dazu nicht ausreichen. Verkauft der Händler später Waren nach Absatz 1, darf WEBklar den Webshop nach einem Hinweis in Textform abschalten; bei einem Verstoß, der Rechte Dritter oder Käufer gefährdet, darf WEBklar ihn auch ohne vorherigen Hinweis abschalten und informiert den Händler unverzüglich. Für das Abschalten gilt Nr. 10 Abs. 2 und 3. Die bereits auf das Stundenkontingent angerechnete Einrichtungszeit wird daraus nicht gutgeschrieben, wenn der Händler den Verstoß zu vertreten hat.</p>
<h2>4. Zahlungen über Stripe</h2>
<p>(1) Zahlungen im Webshop laufen über Stripe Connect. Der Händler verbindet dazu sein eigenes Stripe-Konto mit dem Webshop. Den Vertrag über dieses Konto schließt der Händler unmittelbar mit Stripe; WEBklar ist daran nicht beteiligt. Für das Konto, die Prüfung des Händlers durch Stripe (zum Beispiel zur Identität) und die Auszahlungen gelten die Bedingungen von Stripe.</p>
<p>(2) Das Geld der Käufer fließt direkt an den Händler. WEBklar erhält und verwahrt kein Geld der Käufer, leitet keines weiter und beteiligt sich nicht am Umsatz des Händlers. WEBklar ist kein Zahlungsdienstleister. Die Gebühren von Stripe trägt der Händler; sie fallen nur zwischen dem Händler und Stripe an.</p>
<p>(3) Der Händler prüft selbst, ob sein Stripe-Konto Zahlungen annehmen und Auszahlungen leisten kann. WEBklar zeigt dem Händler im Kundenportal den Stand, den Stripe meldet, hat aber keinen Einfluss darauf, ob und wann Stripe ein Konto freischaltet, einschränkt oder sperrt, und haftet dafür nicht. Rückbuchungen, Streitfälle mit Käufern und Zahlungsausfälle regeln der Händler und Stripe.</p>
<p>(4) Der Händler erstattet Zahlungen im Kundenportal oder im Stripe-Dashboard. WEBklar trägt Erstattungen aus dem Stripe-Dashboard in die Bestellung nach. Eine Erstattung führt der Händler rechtzeitig aus (Nr. 5 Abs. 6).</p>
<p>(5) Der Webshop bietet die Zahlungsarten an, die Stripe im Checkout für das Konto des Händlers bereitstellt und der Händler in seinen Einstellungen wählt, zum Beispiel Karte und SEPA-Lastschrift. PayPal ist über Stripe für diese Art der Anbindung nicht verfügbar und deshalb nicht Teil des Webshops.</p>
<p>(6) Die Zahlungsseite selbst (Stripe Checkout) betreibt Stripe. Kartendaten und Kontodaten gibt der Käufer nur dort ein; WEBklar erhält sie nicht.</p>
<h2>5. Pflichten des Händlers</h2>
<p>(1) Der Händler hat ein laufendes Wartungs-Abo, ist Unternehmer und stellt sein eigenes Stripe-Konto bereit; die Anmeldung bei Stripe schließt er selbst ab. Der Webshop steht nur Kunden zur Verfügung, deren Kundenart „Betrieb“ ist. Der Händler verkauft in eigenem Namen. Für seine Produkte, Preise, Produktangaben und Rechtstexte (Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Impressum, Datenschutzerklärung) ist er selbst verantwortlich; WEBklar stellt Muster bereit und berät nicht rechtlich. Er verkauft keine der in Nr. 3 ausgeschlossenen Waren. Die Zahlungen seiner Käufer laufen über sein eigenes Stripe-Konto zu den Bedingungen von Stripe; WEBklar erhält und verwahrt kein Geld seiner Käufer und erhebt keine Umsatzbeteiligung.</p>
<p>(2) Er verantwortet seine Rechtstexte: Impressum, Datenschutzerklärung, Allgemeine Geschäftsbedingungen, Widerrufsbelehrung, Muster-Widerrufsformular sowie die Angaben zu Versand und Zahlung. Die Mustertexte nach Nr. 6 prüft er, passt sie an seinen Betrieb an und bestätigt sie im Kundenportal, bevor er den Webshop öffnet. Er trägt vor dem Einschalten die Adressen von Impressum und Datenschutzerklärung seiner Website ein.</p>
<p>(3) Er verantwortet seine Artikel, Preise und Produktangaben. Dazu gehören richtige Preise einschließlich Steuerausweis (Umsatzsteuer oder Hinweis auf § 19 UStG), Grundpreise, Versandkosten und Lieferzeiten, die Pflichtangaben für Lebensmittel (zum Beispiel Zutaten, Allergene, Füllmenge, verantwortlicher Lebensmittelunternehmer), die Angaben zur Produktsicherheit (zum Beispiel Hersteller, Warn- und Sicherheitshinweise), Widerrufsausschlüsse bei einzelnen Artikeln und die Rechte an seinen Texten und Bildern.</p>
<p>(4) Er liefert die Waren, wie bestellt und beschrieben, und trägt alle Pflichten eines Verkäufers gegenüber seinen Käufern, auch Gewährleistung, Kundenservice und Beschwerden.</p>
<p>(5) Er pflegt Bestand, Bestellstatus und Versandstatus im Kundenportal. Die Folgen falscher Angaben oder Statusmeldungen, die der Händler setzt, trägt er.</p>
<p>(6) Er bearbeitet Widerrufe und Erstattungen selbst, fristgerecht und nach dem Gesetz. Er entfernt den Link „Vertrag widerrufen“ nicht und verändert die Widerrufsfunktion nicht. Er prüft das Kundenportal regelmäßig auf neue Bestellungen und Widerrufe und lässt sich über die Benachrichtigungen informieren.</p>
<p>(7) Rechnungen und Belege an die Käufer erstellt der Händler selbst; WEBklar stellt sie nicht aus. Die Quittung von Stripe und die Bestellbestätigung des Webshops ersetzen keine Rechnung nach dem Umsatzsteuergesetz, soweit der Händler eine ausstellen muss.</p>
<p>(8) Er erfüllt seine Aufbewahrungspflichten selbst, etwa nach der Abgabenordnung und dem Handelsgesetzbuch. WEBklar ist kein Archiv für die Buchführung und kein Buchführungssystem (das Archiv der Rechtstexte nach Nr. 6 Abs. 5 dient nur dem Nachweis der Texte): Personenbezogene Käuferdaten werden nach Nummer 9 des AVV Webshop anonymisiert und nach dem Ende des Webshops gelöscht. Der Händler exportiert seine Bestellungen deshalb rechtzeitig als CSV-Datei und bewahrt sie mit den Belegen von Stripe auf.</p>
<p>(9) Er hält die Vorgaben nach Nr. 3 ein und verändert den Einbau-Code und die gesetzlich vorgeschriebenen Hinweise des Widgets nicht.</p>
<p>(10) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz für den Webshop ein; das gehört zur Einrichtung. Liegt sie woanders, erhält der Händler den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt er verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Händler darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.</p>
<p>(11) Der Händler informiert seine Käufer nach Art. 13 DSGVO. Die Rollen im Datenschutz regelt der AVV Webshop: Der Händler ist Verantwortlicher, WEBklar Auftragsverarbeiter, Stripe Vertragspartner des Händlers.</p>
<h2>6. Muster-Rechtstexte, keine Rechtsberatung</h2>
<p>(1) WEBklar stellt dem Händler Mustertexte bereit: eine Muster-Widerrufsbelehrung, ein Muster-Widerrufsformular, Allgemeine Geschäftsbedingungen und den Text zu Versand und Zahlung. Das Widget zeigt außerdem die Hinweise, die für die Kasse und die Bestellbestätigung vorgesehen sind, darunter den Hinweis auf die gesetzliche Gewährleistung.</p>
<p>(2) Die Mustertexte sind <strong>Muster ohne Gewähr</strong>. Sie passen zur Standardkonfiguration des Webshops und können auf den Betrieb des Händlers nicht zugeschnitten sein. WEBklar sorgt dafür, dass sie die Technik des Webshops richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht (kein Rechtsrat im Sinne des Rechtsdienstleistungsgesetzes).</p>
<p>(3) WEBklar empfiehlt, die Rechtstexte vor der Öffnung des Webshops durch eine Rechtsanwältin oder einen Rechtsanwalt prüfen zu lassen, besonders bei besonderen Waren (zum Beispiel Lebensmitteln, Kosmetik, Textilien mit Pflegekennzeichnung, Elektrogeräten).</p>
<p>(4) Der Händler bleibt für seine Rechtstexte verantwortlich, auch wenn er Mustertexte unverändert übernimmt (Nr. 5 Abs. 2).</p>
<p>(5) Bestätigt der Händler seine Rechtstexte im Kundenportal, legt WEBklar jede bestätigte Fassung mit Prüfsumme, Zeitpunkt und der bestätigenden Person im Archiv ab. Die Bestellbestätigung zeigt dem Käufer die archivierte Fassung, die zum Zeitpunkt seiner Bestellung galt. WEBklar löscht das Archiv mit den übrigen Daten des Webshops (Nr. 10 Abs. 3; Nummer 8 Abs. 5 des AVV Webshop). Der Händler bewahrt die Fassungen, die er für eigene Aufbewahrungspflichten braucht, selbst auf (Nr. 5 Abs. 8).</p>
<h2>7. Verfügbarkeit und Störungen</h2>
<p>(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für den Webshop. WEBklar sagt für den Webshop keine bestimmte Verfügbarkeit zu, die über den Betrieb der Website im Abo hinausgeht. Störungen des Webshops beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen).</p>
<p>(2) Angekündigte Wartungsfenster (§ 5 der Abo-Bedingungen) können den Webshop kurzzeitig unterbrechen. Während einer Unterbrechung sind Bestellungen im Widget nicht möglich; das Widget zeigt dann einen Ausweichtext.</p>
<p>(3) Zahlungen übernimmt der Webshop über Benachrichtigungen von Stripe und einen regelmäßigen Abgleich in die Bestellung. Fällt die Verbindung zu Stripe oder zum Server aus, kann sich die Bestätigung einer Bestellung verzögern. Ausfälle von Stripe, von Zahlungsarten und von Banken liegen nicht im Einflussbereich von WEBklar.</p>
<p>(4) Alle Zeiten gelten in der Zeitzone Europe/Berlin.</p>
<p>(5) WEBklar übernimmt keine Gewähr dafür, dass E-Mails zugestellt und gelesen werden. Das hängt vom Postfach des Empfängers und von den Mailgrenzen seines Anbieters ab.</p>
<p>(6) Die gesetzlichen Rechte des Händlers sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.</p>
<h2>8. Haftung</h2>
<p>(1) Der Webshop ist ein Hilfsmittel für den Verkauf des Händlers. Der Vertrag über die Ware besteht allein zwischen dem Händler und dem Käufer; WEBklar ist daran nicht beteiligt.</p>
<p>(2) Für entgangenen Gewinn und Folgeschäden aus Störungen des Webshops, aus Fehlern bei Bestand oder Bestellstatus und aus nicht zugestellten E-Mails haftet WEBklar nur im Rahmen von § 13 Abs. 1 und 2 der Abo-Bedingungen.</p>
<p>(3) WEBklar haftet nicht für Inhalte des Händlers, insbesondere nicht für seine Artikel, Preise, Produktangaben und Rechtstexte, auch nicht für Mustertexte, die er übernommen hat (Nr. 6), und nicht für die Erfüllung seiner Pflichten gegenüber den Käufern (Nr. 5).</p>
<p>(4) WEBklar haftet nicht für Ausfälle, Sperren oder Entscheidungen von Stripe und der Zahlungsarten sowie für Rückbuchungen und Zahlungsausfälle (Nr. 4 Abs. 3).</p>
<p>(5) Die Haftung nach § 13 Abs. 1 der Abo-Bedingungen (unter anderem bei Vorsatz, grober Fahrlässigkeit, Verletzung von Leben, Körper oder Gesundheit und nach dem Produkthaftungsgesetz) bleibt unberührt. Der Haftungsrahmen des Händlers gegenüber seinen Käufern bleibt unberührt.</p>
<h2>9. Einrichtung, Abrechnung und Widerruf</h2>
<p>(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Händlers. Für den Betrieb des Webshops berechnet WEBklar nichts weiter; es gibt keine laufende Gebühr und keine Beteiligung am Umsatz.</p>
<p>(2) Die Einrichtung dauert nach der Erfahrung von WEBklar meist 2 bis 3 Wochen ab dem Einschalten. Sie hängt davon ab, wie schnell der Händler Stripe-Konto, Artikel und Rechtstexte bereitstellt und wie umfangreich der Einbau ist. Die Dauer ist eine Einschätzung und keine verbindliche Zusage eines Termins.</p>
<p>(3) Hat WEBklar die Funktion auf Wunsch des Händlers schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.</p>
<p>(4) Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Händlers in Textform aus; es gilt § 4 der Abo-Bedingungen. Welche Wünsche nicht zur Einrichtung gehören, beschreibt Nr. 2 Abs. 7.</p>
<p>(5) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion. Hat der Händler die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.</p>
<h2>10. Laufzeit und Ende</h2>
<p>(1) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo oder durch den Widerruf eines Wechsels, schaltet WEBklar die Funktion spätestens am folgenden Tag ab. WEBklar informiert den Händler per E-Mail.</p>
<p>(2) Mit dem Abschalten nimmt der Webshop keine neuen Bestellungen mehr an, und das Widget zeigt den Ausweichtext. Bestellungen, die noch nicht bezahlt sind, laufen aus. Die Bestellungen und Widerrufe bleiben im Kundenportal 90 Tage lesbar, und der Händler kann sie als CSV-Datei exportieren. Die Widerrufsfunktion bleibt bis zur Löschung nach Absatz 3 erreichbar, soweit das Widget noch eingebunden ist. Der Händler erfüllt seine Pflichten gegenüber den Käufern weiter (Nr. 5) und informiert sie, wenn er den Webshop nicht mehr anbietet.</p>
<p>(3) WEBklar löscht alle Daten des Webshops, also Artikel, Bilder, Bestellungen, Widerrufe, Einstellungen, Rechtstexte und das Archiv der bestätigten Fassungen, 90 Tage nach dem Abschalten. Eine Ausnahme ist das Einrichtungs-Ticket, in dem WEBklar die Einrichtung als Arbeitsnachweis festhält: Es bleibt mit einer Kurzfassung der Angaben zur Einrichtung bestehen und enthält keine Daten von Käufern (Nummer 9 Abs. 6 des AVV Webshop).</p>
<p>(4) Das Stripe-Konto bleibt beim Händler. Das Ende des Webshops berührt weder das Konto noch die Auszahlungen noch den Vertrag mit Stripe. Der Händler kann die Verbindung zwischen dem Konto und dem Webshop im Stripe-Dashboard beenden.</p>
<p>(5) Beginnt innerhalb der 90 Tage wieder ein Wartungs-Abo, schaltet WEBklar den Webshop auf Wunsch des Händlers in Textform mit den vorhandenen Daten und Einstellungen ohne weitere Kosten wieder ein. Nach der Löschung ist eine neue Anfrage und eine neue Einrichtung nötig (Einrichtung nach Nr. 9).</p>
<p>(6) Endet das Wartungs-Abo, bevor WEBklar den Webshop freigeschaltet hat, bricht WEBklar die Einrichtung ab; ein Vorschuss nach Nr. 9 Abs. 1 verfällt.</p>
<p>(7) Auf Wunsch des Händlers in Textform schaltet WEBklar den Webshop jederzeit ab; für den Widerruf gilt Nr. 9 Abs. 5.</p>
<h2>11. Änderungen und Einstellung</h2>
<p>(1) WEBklar darf die Technik des Webshops verbessern, solange der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Webshop.</p>
<p>(2) WEBklar darf den Webshop für eine Website mit einer Frist von vier Wochen beenden oder für alle Händler mit einer Frist von drei Monaten einstellen, wenn</p>
<ul>
<li>ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Händler einer dafür nötigen neuen Fassung nicht zustimmt (AVV Webshop Nummer 12),</li>
<li>Stripe die Anbindung nach Nr. 4 beendet oder wesentlich ändert, so dass der Webshop in der bisherigen Form nicht weiterbetrieben werden kann, oder</li>
<li>eine Sicherheitslücke sich nicht anders beheben lässt.</li>
</ul>
<p>Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.</p>
<p>(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar den Webshop ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Händler darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.</p>
<p>(4) Eine Einstellung ohne einen Grund nach Absatz 2 ist ausgeschlossen. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.</p>
<h2>12. Hinweise</h2>
<p>(1) <strong>Keine Rechtsberatung.</strong> Der Webshop und die Mustertexte sind technische Hilfsmittel, keine Rechtsberatung (Nr. 6). Die rechtliche Einordnung seiner Waren, seiner Preisangaben und seines Angebots trägt der Händler.</p>
<p>(2) <strong>Barrierefreiheit.</strong> Das Widget ist für die Bedienung mit Tastatur und Hilfstechniken gestaltet (Beschriftungen, Fokusführung, Fehlermeldungen als Live-Region). Eine Konformität nach WCAG 2.1 AA oder EN 301 549 sagt WEBklar nicht zu. Ob der Händler als Anbieter von Dienstleistungen im elektronischen Geschäftsverkehr unter das BFSG fällt (Ausnahme für Kleinstunternehmen, § 3 Abs. 3 BFSG), entscheidet und prüft der Händler.</p>
</main>
</body>
</html>
@@ -0,0 +1,169 @@
<!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-16">
<meta name="robots" content="noindex">
<title>Leistungsbeschreibung Website-Analyse – WEBklar</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #5e6678; font-size: 14px; }
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
table { border-collapse: collapse; width: 100%; font-size: 15px; }
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
a:hover { background: #f6f8fa; }
nav.top { font-size: 14px; margin-bottom: 22px; }
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar · Leistungsbeschreibung Website-Analyse"; font: 8pt sans-serif; color: #8a93a3; }
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
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="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-website-analyse-2026-10-16.html">Leistungsbeschreibung Website-Analyse</a> · <a href="/neu/rechtliches/avv-website-analyse-2026-10-16.html">AVV Website-Analyse</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Leistungsbeschreibung Website-Analyse</h1>
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Hosting-Abo und zum Wartungs-Abo</p>
<h2>1. Geltung</h2>
<p>(1) Funktion im Sinne dieser Leistungsbeschreibung ist die Website-Analyse. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist in beiden Abos enthalten. Sie setzt ein laufendes Hosting-Abo oder Wartungs-Abo voraus. Sie wird zusammen mit dem Auftragsverarbeitungsvertrag Website-Analyse („AVV Website-Analyse“) vereinbart. Wie die Zustimmung abläuft, regelt die Vorbemerkung des AVV Website-Analyse.</p>
<p>(2) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für die Website-Analyse diese Leistungsbeschreibung vor, in Datenschutzfragen der AVV Website-Analyse.</p>
<p>(3) Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Rechtstexte … nicht enthalten“) gilt nicht für die Zusatzseite nach Nr. 2 Abs. 2 und den Hinweis in der Datenschutzerklärung nach Nr. 4 Abs. 4. Beides ist Teil der Website-Analyse.</p>
<p>(4) Das Einschalten der Website-Analyse ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen.</p>
<h2>2. Leistung</h2>
<p>(1) Die Website-Analyse misst, wie die öffentliche Website des Kunden genutzt wird. Sie ist im Hosting-Abo und im Wartungs-Abo ohne Aufpreis enthalten. Die Einrichtung der Website-Analyse ist für den Kunden kostenlos. WEBklar rechnet sie weder auf ein Stundenkontingent an noch berechnet WEBklar sie gesondert.</p>
<p>(2) Beim Veröffentlichen der Website fügt WEBklar Folgendes ein:</p>
<ul>
<li>ein Messskript in jede HTML-Datei der Website,</li>
<li>ein Einwilligungsbanner am unteren Rand mit den gleich gestalteten Schaltflächen „Ablehnen“ und „Annehmen“ in der Akzentfarbe, die der Kunde beim Einschalten wählt (ist sie für weiße Schrift zu hell, nimmt das Banner Schwarz),</li>
<li>eine Schaltfläche „Datenschutz-Einstellungen“ unten rechts, über die Besucher ihre Wahl jederzeit ändern können (hat die Website ein eigenes Element mit dem Attribut <code>data-webklar-consent</code>, übernimmt dieses Element die Aufgabe),</li>
<li>eine Zusatzseite „/datenschutz-analyse.html“ mit den Datenschutzhinweisen zur Messung (Verantwortlicher: der Kunde).</li>
</ul>
<p>(3) Gemessen wird:</p>
<ul>
<li><strong>solange der Besucher keine Wahl getroffen hat und sein Browser kein Signal nach dem nächsten Spiegelstrich sendet:</strong> Seitenaufrufe mit Pfad (ohne Suchparameter), Land (aus der IP-Adresse, die nicht gespeichert wird), Geräteklasse und ein Zähler dafür, dass das Banner angezeigt wurde;</li>
<li><strong>bei den Browser-Signalen „Global Privacy Control“ und „Do Not Track“:</strong> nichts, und es erscheint kein Banner;</li>
<li><strong>nach „Ablehnen“:</strong> nichts mehr, außer einmal dem Zähler „abgelehnt“;</li>
<li><strong>nach „Annehmen“ zusätzlich:</strong> die Domain der Website, von der der Besucher kam, die Größenklasse des Browserfensters, eine täglich wechselnde pseudonyme Besucher-Kennung, Verweildauer, Scrolltiefe, Klickpositionen (Heatmap), die Art von Kontakt-Klicks (Telefon, E-Mail, WhatsApp, Link auf eine andere Website mit deren Domain) und ein Zähler „angenommen“.</li>
</ul>
<p>Die einzelnen Datenarten nennt der AVV Website-Analyse in Nummer 3.</p>
<p>(4) Es werden keine Cookies gesetzt. Gespeichert wird auf dem Gerät des Besuchers nur seine Banner-Wahl im lokalen Speicher des Browsers, für 183 Tage.</p>
<p>(5) Die Auswertung steht im Kundenportal zur Verfügung: Kennzahlen und Diagramme für die letzten 7, 30 oder 90 Tage und eine Heatmap je Seite und Ansicht (Handy, Tablet, Computer) aus den bis zu 5.000 jüngsten Klicks der letzten 30 Tage. Die Daten liegen auf dem Server von WEBklar bei der Hetzner Online GmbH in Deutschland; sie werden nicht in Drittländer übermittelt. Einzelne Ereignisse und Klicks werden nach 30 Tagen gelöscht, Tageswerte nach 25 Monaten, alle Daten rund 60 Tage nach dem Ende der Website-Analyse (AVV Website-Analyse Nummer 9).</p>
<p>(6) Die Zahlen sind Näherungswerte. Besucher mit Blockern, abgeschaltetem JavaScript, Browser-Signal oder Ablehnung werden nur eingeschränkt oder gar nicht erfasst. Besucherzahlen, Herkunftsseiten, Verweildauer, Scrolltiefe, Klicks und Kontakt-Klicks stammen nur von Besuchern, die „Annehmen“ gewählt haben.</p>
<p>(7) Nicht enthalten sind Sitzungsaufzeichnungen, die Auswertung von Formularinhalten, die Messung von Ladezeiten und Fehlern, Kampagnen- und Trichteranalysen sowie ein Datenexport außer nach Nr. 7 Abs. 3.</p>
<p>(8) Das Banner fragt nur nach der Einwilligung in die Website-Analyse von WEBklar. Andere einwilligungspflichtige Inhalte der Website, etwa eingebettete Karten, Videos oder Schriften von Drittanbietern, deckt es nicht ab.</p>
<h2>3. Voraussetzungen</h2>
<p>(1) Der Kunde hat ein laufendes Hosting-Abo oder Wartungs-Abo.</p>
<p>(2) Die Website ist öffentlich erreichbar und wird bei WEBklar gehostet (Adresse unter project.webklar.com oder eigene Domain, die auf den Server von WEBklar zeigt). Nicht öffentliche Websites und Vorschauen, die nur nach einer Anmeldung erreichbar sind, werden nicht gemessen, ebenso wenig Websites, die im Dev-Modus auf einem Entwicklungsserver laufen. Enthält eine öffentliche Website Bereiche, die erst nach einer Anmeldung zugänglich sind (etwa eine Web-Anwendung mit Kundenkonto), werden auch dort Seitenaufrufe gezählt; dafür gilt Nr. 4 Abs. 5.</p>
<p>(3) Auf der Zusatzseite erscheinen als Verantwortlicher die Firma (sonst der Name), die Anschrift, soweit sie im Kundenkonto hinterlegt ist, und die E-Mail-Adresse aus dem Kundenkonto. Das Portal zeigt diese Angaben vor dem Einschalten an. Der Kunde prüft sie und teilt Änderungen mit. Fehlen Firma und Name oder fehlt eine gültige E-Mail-Adresse, lässt sich die Funktion nicht einschalten.</p>
<p>(4) Richtet sich die Website auch gezielt an Kinder unter 16 Jahren, schaltet der Kunde die Website-Analyse nicht ein, weil eine Einwilligung über das Banner dann nicht genügt (Art. 8 DSGVO). Lassen Inhalte oder Adressen der Website auf besondere Kategorien personenbezogener Daten schließen, etwa Gesundheit, Religion oder politische Meinung, teilt der Kunde das WEBklar vor dem Einschalten mit. WEBklar darf das Einschalten für eine solche Website ablehnen oder die Website-Analyse dort ausschalten.</p>
<h2>4. Pflichten des Kunden</h2>
<p>(1) Der Kunde ist für die Messung auf seiner Website Verantwortlicher im Sinne der DSGVO.</p>
<p>(2) Er entfernt, verdeckt oder umgeht das Banner nicht und verändert das Skript nicht. Text und Gestaltung des Banners legt WEBklar fest.</p>
<p>(3) Er bindet ohne Abstimmung mit WEBklar keine weiteren Mess-, Tracking- oder Marketing-Werkzeuge ein (zum Beispiel Google Analytics oder Meta Pixel), weil sie eine eigene Einwilligung erfordern können.</p>
<p>(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einschalten dort einen kurzen Hinweis mit Link auf die Zusatzseite ein und entfernt ihn beim Ausschalten wieder; das wird nicht auf das Stundenkontingent angerechnet. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Hinweis als Textbaustein und fügt ihn selbst ein und entfernt ihn nach dem Ausschalten selbst. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf (etwa „Wir setzen keine Analyse-Werkzeuge ein“), weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.</p>
<p>(5) Enthalten die Pfade seiner Website personenbezogene Angaben, etwa Namen, Kundennummern oder E-Mail-Adressen, teilt der Kunde das WEBklar vor dem Einschalten mit.</p>
<p>(6) Er nutzt die Auswertung nur für eigene Zwecke, versucht nicht, einzelne Besucher zu identifizieren, und gibt Heatmap-Links nicht weiter.</p>
<h2>5. Verfügbarkeit und Störungen</h2>
<p>(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Messung. Störungen der Messung beheben wir in angemessener Frist (§ 12 der Abo-Bedingungen). Daten, die während einer Störung nicht erfasst wurden, lassen sich nicht nachträglich erheben.</p>
<p>(2) Fällt der Messdienst aus, wird die Website weiter ausgeliefert, nur ohne Banner und Messung.</p>
<p>(3) Die gesetzlichen Rechte des Kunden sowie §§ 12 und 13 der Abo-Bedingungen bleiben unberührt.</p>
<h2>6. Hinweis zur Rechtslage</h2>
<p>Für die Zählung, solange der Besucher keine Wahl getroffen hat (Nr. 2 Abs. 3), übermittelt das Skript nur die Adresse der aufgerufenen Seite und die Angabe, dass das Banner angezeigt wurde. Land und Geräteklasse bestimmt der Messdienst aus den Daten, die jeder Browser bei einer Anfrage ohnehin mitsendet. Ob auch dafür eine Einwilligung nach § 25 TDDDG nötig ist, haben Gerichte und Aufsichtsbehörden nicht entschieden; die Aufsichtsbehörden legen den Begriff des Zugriffs auf das Endgerät eher weit aus. Ändert sich die Rechtslage, passt WEBklar die Messung an (Nr. 8).</p>
<h2>7. Ende der Leistung</h2>
<p>(1) Der Kunde kann die Website-Analyse jederzeit im Kundenportal ausschalten; das Portal bestätigt die Abschaltung. Die Website-Analyse läuft, solange das Abo läuft, das sie enthält. Endet es, schaltet WEBklar sie spätestens am folgenden Tag ab. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; das Gleiche gilt, wenn er den Wechsel ins Wartungs-Abo widerruft. WEBklar informiert den Kunden per E-Mail.</p>
<p>(2) Mit dem Ausschalten speichert der Messdienst keine Messdaten mehr, und die Heatmap ist nicht mehr abrufbar. Das Messskript liefert dann kein Banner mehr aus, sobald die Website einmal mit der aktuellen Fassung des Skripts veröffentlicht wurde. Skript, Zusatzseite und, soweit WEBklar ihn eingefügt hat, den Hinweis in der Datenschutzerklärung entfernt WEBklar mit der nächsten Veröffentlichung der Website. WEBklar löst sie in der Regel sofort aus, spätestens innerhalb von fünf Werktagen.</p>
<p>(3) Die gespeicherten Daten löscht WEBklar nach Nummer 9 des AVV Website-Analyse. Bis zum 46. Tag nach dem Ende kann der Kunde eine Kopie der Tageswerte und der noch vorhandenen Einzelereignisse als CSV-Datei verlangen.</p>
<h2>8. Änderungen und Einstellung</h2>
<p>(1) WEBklar darf die Technik der Website-Analyse verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten erhoben werden, gilt Nummer 12 des AVV Website-Analyse.</p>
<p>(2) WEBklar darf die Website-Analyse für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn</p>
<ul>
<li>ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung nicht zustimmt (AVV Website-Analyse Nummer 12 Abs. 3), oder</li>
<li>eine Sicherheitslücke sich nicht anders beheben lässt.</li>
</ul>
<p>Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.</p>
<p>(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Messung ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt die Messung wieder auf, sobald das zulässig und technisch möglich ist.</p>
<p>(4) Gegenüber Unternehmern darf WEBklar die Website-Analyse außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.</p>
<p>(5) Weil für die Website-Analyse kein Aufpreis berechnet wird, ändert ihre Einstellung den Preis des Abos nicht. Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.</p>
<h2>9. Hinweise</h2>
<p>(1) <strong>Keine Rechtsberatung.</strong> Die Zusatzseite und der Hinweis in der Datenschutzerklärung sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine Prüfung, ob die Website des Kunden insgesamt datenschutzkonform ist, und eine rechtliche Beratung im Einzelfall schuldet WEBklar nicht. Abstimmungen nach dieser Leistungsbeschreibung betreffen nur die Technik.</p>
<p>(2) <strong>Nachweis der Einwilligung.</strong> Ein Protokoll je Besucher wird nicht geführt. Als Nachweis dienen die Dokumentation der Technik und das Archiv der Fassungen von Banner-Text und Zusatzseite (AVV Website-Analyse Nummer 8 Abs. 6).</p>
<p>(3) Die Länderzuordnung nutzt die Datenbank „IP to Country Lite“ von DB-IP.com (Lizenz CC BY 4.0). Im Kundenportal steht dazu unter der Auswertung der Hinweis „Länderdaten: IP-Geolocation by DB-IP“ mit Link auf db-ip.com.</p>
</main>
</body>
</html>
@@ -0,0 +1,195 @@
<!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-16">
<meta name="robots" content="noindex">
<title>Leistungsbeschreibung Öffnungszeiten live – WEBklar</title>
<style>
:root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #5e6678; font-size: 14px; }
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
table { border-collapse: collapse; width: 100%; font-size: 15px; }
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
a:hover { background: #f6f8fa; }
nav.top { font-size: 14px; margin-bottom: 22px; }
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar · Leistungsbeschreibung Öffnungszeiten live"; font: 8pt sans-serif; color: #8a93a3; }
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
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="/neu/rechtliches/abo-bedingungen.html">Abo-Bedingungen</a> · <a href="/neu/rechtliches/leistungsbeschreibung-zeiten-2026-10-16.html">Leistungsbeschreibung Öffnungszeiten live</a> · <a href="/datenschutz.html">Datenschutzerklärung</a> · <a href="https://webklar.com/impressum">Impressum</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Leistungsbeschreibung Öffnungszeiten live</h1>
<p class="meta"><strong>Fassung 2026-10-16</strong> · Entwurf – Prüfung durch Kanzlei offen · Zusatzbedingungen zum Wartungs-Abo</p>
<h2>1. Geltung</h2>
<p>(1) Funktion im Sinne dieser Leistungsbeschreibung ist Öffnungszeiten live. Diese Leistungsbeschreibung gilt, wenn der Kunde die Funktion im Kundenportal einschaltet und die dort angezeigte Zeile mit dem Klick auf „Einschalten“ bestätigt. Sie gilt für Funktionen, die ab ihrer Veröffentlichung eingeschaltet werden; hat der Kunde die Funktion zuvor einzeln gekauft, gilt für sie weiter die Leistungsbeschreibung, unter der er sie gekauft hat. Die Funktion ist Teil des Wartungs-Abos. Sie setzt ein laufendes Wartungs-Abo voraus; ein Hosting-Abo genügt nicht.</p>
<p>(2) Die Funktion ist im Wartungs-Abo ohne Aufpreis enthalten (§ 3 Abs. 8 der Abo-Bedingungen). Leistungsbeschreibung B Nr. 3 der Abo-Bedingungen („Nicht enthalten“: neue Funktionen und Rechtstexte) gilt dafür nicht: Die Funktion und der Datenschutz-Absatz nach Nr. 6 Abs. 4 sind Teil dieser Leistung.</p>
<p>(3) Im Übrigen gelten die Abo-Bedingungen. Bei Widersprüchen geht für Öffnungszeiten live diese Leistungsbeschreibung vor. Die Haftungsregeln stehen in Nr. 8 ausgeschrieben da, damit sie auch für Abos früherer Fassungen der Abo-Bedingungen gelten.</p>
<h2>2. Leistung</h2>
<p>(1) Der Kunde trägt seine Öffnungszeiten einmal im Kundenportal ein: bis zu fünf benannte Zeitpläne, je Wochentag bis zu zwei Zeitfenster („rund um die Uhr“ und „nach Vereinbarung“ sind möglich), Abweichungen an Feiertagen, Betriebsurlaub und Sondertage. Die Website zeigt diese Zeiten überall gleich an und berechnet daraus den Live-Status („Jetzt geöffnet · schließt um 18 Uhr“).</p>
<p>(2) An den Stellen der Website, die nach Nr. 3 Abs. 2 dafür vorgesehen sind, erscheinen die Zeiten als Tabelle, als Status oder als Kurzanzeige („Geöffnet“, „Geschlossen“, „Nach Vereinbarung“). Dazu gehören, soweit der Kunde sie einrichtet:</p>
<ul>
<li>eine Hinweis-Leiste am Seitenanfang für befristete Hinweise (Info, Urlaub, Aktion, Wichtig) mit Beginn und Ende und auf Wunsch einem Knopf (Link oder Anruf),</li>
<li>auf dem Handy eine Kontakt-Leiste am unteren Rand mit den Knöpfen „Anrufen“, „Route“ und, wenn der Kunde sie einrichtet, „WhatsApp“,</li>
<li>die Öffnungszeiten im Seitencode in maschinenlesbarer Form (strukturierte Daten), die Suchmaschinen lesen können,</li>
<li>eine E-Mail vor Feiertagen, die den Kunden bittet, seine Zeiten für diese Tage zu prüfen, und</li>
<li>im Kundenportal der Knopf „Für Google kopieren“, der die Zeiten in der Reihenfolge von Google in die Zwischenablage legt, damit der Kunde sie in sein Google-Unternehmensprofil einträgt.</li>
</ul>
<p>(3) Die Funktion rechnet die Feiertage des Bundeslands ein, das der Kunde wählt; je Feiertag kann der Kunde eigene Zeiten eintragen. Feiertage, die nur örtlich gelten, sowie Heiligabend und Silvester zeigt das Kundenportal als „bitte prüfen“. Alle Zeiten gelten in der Zeitzone Europe/Berlin.</p>
<p>(4) Beim Aufruf der Website lädt der Browser des Besuchers ein kleines Programm und die Öffnungszeiten von project.webklar.com, einem Server der WEBklar GbR bei der Hetzner Online GmbH in Falkenstein (Deutschland). Dabei werden keine Cookies gesetzt, und es wird nichts im Browser gespeichert. Das Programm lädt nichts von anderen Anbietern. WhatsApp und Google Maps werden erst nach dem Klick des Besuchers auf „WhatsApp“ oder „Route“ geöffnet; vorher werden keine Daten an diese Dienste übertragen.</p>
<p>(5) Das Team von WEBklar kann die Hinweis-Leiste und die Kontakt-Leiste für eine Website abschalten, etwa wenn die Seite schon eine eigene Anruf-Leiste hat. Auf Wunsch des Kunden können Mitarbeiter die Öffnungszeiten in der Kundenansicht für ihn ändern; ein Anspruch darauf besteht nicht (Nr. 5 Abs. 1). Den Haken für den WhatsApp-Hinweis (Nr. 6 Abs. 3) setzen sie nicht.</p>
<h2>3. Einbau und Freischaltung</h2>
<p>(1) Der Einbau des Programms erfolgt automatisch nur bei Websites, die WEBklar hostet und über den eigenen Veröffentlichungsablauf bereitstellt: Beim nächsten Veröffentlichen setzt WEBklar die Einbau-Zeile in jede Seite ein, und Hinweis-Leiste, Kontakt-Leiste und Angaben für Suchmaschinen erscheinen ohne weiteren Aufwand. In allen anderen Fällen, etwa bei einer Website bei einem anderen Anbieter, setzt das Team die Einbau-Zeile von Hand ein. Hat WEBklar keinen Zugriff auf die Website, erhält der Kunde die Zeile und setzt sie selbst oder durch seinen Anbieter ein.</p>
<p>(2) Die Tabelle, den Status und die Kurzanzeige setzt das Team an den Stellen ein, an denen auf der Website feste Öffnungszeiten stehen oder stehen sollen, und ersetzt dort die fest eingetragenen Zeiten. In jedem dieser Platzhalter bleibt eine statische Ersatz-Tabelle stehen. Sie erscheint, wenn der Browser kein JavaScript ausführt, wenn die Daten nicht erreichbar oder älter als 24 Stunden sind und nachdem die Funktion abgeschaltet wurde. Sie zeigt den Wochenplan zum Zeitpunkt des Einbaus.</p>
<p>(3) WEBklar schaltet die Funktion frei, sobald der Einbau abgeschlossen ist und der Kunde Öffnungszeiten eingetragen hat (mindestens ein geöffneter Tag im Hauptplan). Solange der Kunde keine Zeiten eingetragen hat, kann das Team die Tabelle nicht einbauen.</p>
<p>(4) Der Einbau dauert erfahrungsgemäß meist wenige Tage. Das ist eine Einschätzung und keine verbindliche Zusage eines Termins.</p>
<h2>4. Einrichtung</h2>
<p>(1) Die Einrichtung der Funktion rechnet WEBklar als Arbeitszeit auf das Stundenkontingent des Wartungs-Abos an (§ 4 der Abo-Bedingungen). Reicht das verfügbare Kontingent nicht aus, führt WEBklar die Einrichtung trotzdem aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen. Eine zusätzliche Rechnung gibt es dafür nicht. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest zugunsten des Kunden.</p>
<p>(2) Hat WEBklar die Funktion auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut, richtet sich die Abrechnung der Einrichtung nach der Vereinbarung zum Projekt und nicht nach dem Stundenkontingent. Auch dann läuft die Funktion nur, solange das Wartungs-Abo läuft.</p>
<p>(3) Im Umfang der Einrichtung ist ein Einbau je Website enthalten, einschließlich der Aktualisierung der Ersatz-Tabelle beim Abschalten (Nr. 9 Abs. 4). Weitere Wünsche, etwa zusätzliche Stellen für die Anzeige, eine Sondergestaltung oder andere Änderungen an der Website, gehören nicht zur Einrichtung. Arbeiten, die nicht zur Einrichtung gehören, führt WEBklar nur nach Absprache vor der Ausführung und nach Freigabe des Kunden in Textform aus; es gilt § 4 der Abo-Bedingungen.</p>
<h2>5. Nicht enthalten</h2>
<p>(1) Nicht enthalten sind:</p>
<ul>
<li>die Übertragung der Öffnungszeiten zu Google: WEBklar schreibt nichts automatisch in das Google-Unternehmensprofil des Kunden; der Kunde trägt die Zeiten dort selbst ein, der Knopf „Für Google kopieren“ und die Erinnerung vor Feiertagen helfen dabei,</li>
<li>Feiertage einer Gemeinde und andere örtliche Besonderheiten (zum Beispiel Brückentage oder Stadtfeste); der Kunde trägt sie als Ausnahmen selbst ein,</li>
<li>eine Zusage, dass Suchmaschinen die Angaben im Seitencode übernehmen oder wie sie sie anzeigen,</li>
<li>die Pflege der Zeiten und der Hinweise durch WEBklar und</li>
<li>Änderungen an der Website, die über den Einbau nach Nr. 3 hinausgehen (Nr. 4 Abs. 3).</li>
</ul>
<h2>6. Pflichten des Kunden</h2>
<p>(1) Der Kunde prüft seine Zeiten, Feiertage und Hinweistexte vor dem Speichern und hält sie aktuell. Er wählt das richtige Bundesland und trägt örtliche Feiertage und Abweichungen selbst ein.</p>
<p>(2) Preise und Rabatte in Hinweisen gibt er rechtmäßig an, zum Beispiel als Endpreise und, bei Preisermäßigungen für Waren, mit dem niedrigsten Preis der letzten 30 Tage. Hinweise zu Preisen sind Angaben des Kunden, nicht von WEBklar.</p>
<p>(3) Die Kontaktangaben (Telefonnummer, Anschrift, WhatsApp-Nummer) sind auf der Website öffentlich sichtbar. Der Kunde trägt nur Angaben ein, die er veröffentlichen darf. Die WhatsApp-Schaltfläche schaltet er erst ein, nachdem er den WhatsApp-Hinweis in seine Datenschutzerklärung aufgenommen hat, und bestätigt das im Kundenportal mit einem Haken.</p>
<p>(4) Liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügt WEBklar beim Einbau dort den Datenschutz-Absatz zu Öffnungszeiten live ein; das gehört zum Einbau. Liegt sie woanders, etwa bei einem externen Generator, erhält der Kunde den Absatz als Textbaustein und fügt ihn selbst ein. Für den übrigen Inhalt seiner Datenschutzerklärung bleibt der Kunde verantwortlich (§ 6 Abs. 3 der Abo-Bedingungen). Fällt WEBklar beim Einfügen ein offensichtlicher Widerspruch auf, weist WEBklar den Kunden darauf hin; eine Pflicht zur Prüfung der Datenschutzerklärung entsteht daraus nicht.</p>
<p>(5) Der Kunde prüft nach dem Einbau, ob die Anzeige auf seiner Website stimmt, und meldet Fehler unverzüglich im Chat des Kundenportals.</p>
<p>(6) Er entfernt oder verändert das Programm von WEBklar nicht. Wünscht er die Hinweis-Leiste oder die Kontakt-Leiste nicht, teilt er das dem Team mit (Nr. 2 Abs. 5).</p>
<h2>7. Verfügbarkeit und Aktualität</h2>
<p>(1) Das Verfügbarkeitsziel nach § 5 der Abo-Bedingungen gilt für die Website, nicht für die Live-Anzeige. Störungen der Anzeige behebt WEBklar in angemessener Frist.</p>
<p>(2) Ist die Anzeige nicht erreichbar oder sind die Daten älter als 24 Stunden, bleibt auf der Website die Ersatz-Tabelle stehen (Nr. 3 Abs. 2); Hinweis-Leiste und Kontakt-Leiste erscheinen dann nicht.</p>
<p>(3) Änderungen, die der Kunde im Kundenportal speichert, erscheinen nicht sofort auf der Website, sondern mit einer Verzögerung von einigen Minuten (bis zu etwa zehn Minuten). Der Status folgt der Zeitzone Europe/Berlin; geht die Uhr des Geräts des Besuchers um mehr als fünf Minuten falsch, rechnet das Programm mit der Serverzeit von WEBklar.</p>
<h2>8. Haftung</h2>
<p>(1) Die Live-Anzeige ist ein Hilfsmittel. Die Richtigkeit der Zeiten, Ausnahmen, Hinweise und Kontaktangaben liegt beim Kunden: WEBklar zeigt die Angaben des Kunden an und prüft sie nicht auf Richtigkeit.</p>
<p>(2) Die Feiertage stammen aus einer Feiertagsliste für das gewählte Bundesland. WEBklar beseitigt Fehler in den Feiertagsdaten und andere Softwarefehler nach Meldung in angemessener Frist (Nr. 7 Abs. 1). Für Schäden daraus gelten die Absätze 3 bis 5.</p>
<p>(3) Für entgangenen Gewinn und Folgeschäden, etwa weil ein Besucher wegen einer falschen oder fehlenden Angabe vor verschlossener Tür steht, haftet WEBklar nur im Rahmen der Absätze 4 und 5.</p>
<p>(4) 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>(5) 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>(6) Diese Regeln gelten auch zugunsten der Mitarbeiter und Beauftragten von WEBklar. Dieser Abschnitt entspricht § 13 Abs. 1 und 2 der Abo-Bedingungen und gilt unabhängig davon, nach welcher Fassung der Abo-Bedingungen das Wartungs-Abo besteht.</p>
<p>(7) Gesetzliche Rechte des Kunden, die sich nicht ausschließen lassen, bleiben unberührt. Der Haftungsrahmen des Kunden gegenüber seinen Besuchern und Kunden bleibt ebenfalls unberührt.</p>
<h2>9. Ende der Leistung</h2>
<p>(1) Auf Wunsch des Kunden in Textform (im Chat des Kundenportals oder per E-Mail) schaltet WEBklar Öffnungszeiten live jederzeit ab; für den Widerruf gilt Nr. 11.</p>
<p>(2) Die Funktion läuft, solange das Wartungs-Abo läuft. Endet es, auch durch einen Wechsel ins Hosting-Abo, läuft die Funktion noch 30 Tage unverändert weiter; danach schaltet WEBklar sie ab. Das Gleiche gilt beim Widerruf eines Wechsels ins Wartungs-Abo. WEBklar informiert den Kunden per E-Mail. Die Eingaben des Kunden bleiben nach dem Abschalten 30 Tage gespeichert und werden danach gelöscht; beginnt schon in den 30 Tagen Weiterlauf wieder ein Wartungs-Abo, läuft die Funktion einfach weiter, beginnt es in den 30 Tagen danach, schaltet WEBklar die Funktion mit den vorhandenen Eingaben ohne weitere Kosten wieder ein.</p>
<p>(3) Sobald ein neues Wartungs-Abo beginnt, solange die Eingaben des Kunden noch gespeichert sind (Absatz 4), läuft die Funktion mit den vorhandenen Eingaben weiter oder WEBklar schaltet sie ohne weitere Kosten wieder ein.</p>
<p>(4) Mit dem Abschalten verschwinden Hinweis-Leiste, Kontakt-Leiste, Live-Status und die Angaben für Suchmaschinen; auf der Website steht wieder die Ersatz-Tabelle (Nr. 3 Abs. 2). Hat sich der Wochenplan seit dem Einbau geändert, aktualisiert WEBklar die Ersatz-Tabelle einmal beim Abschalten; das gehört zum Einbau nach Nr. 4 Abs. 3. Hat WEBklar keinen Zugriff auf die Website, erhält der Kunde den aktuellen Text und setzt ihn selbst ein. Nach dem Abschalten wegen des Endes des Abos bleiben die Eingaben des Kunden noch weitere 30 Tage gespeichert und werden danach gelöscht; bis dahin kann der Kunde sie im Kundenportal ansehen.</p>
<p>(5) Schaltet WEBklar die Funktion auf Wunsch des Kunden oder aus einem anderen Grund als dem Ende des Abos ab, löscht WEBklar die Eingaben des Kunden auf sein Verlangen in Textform, spätestens mit dem Löschen des Kundenkontos.</p>
<h2>10. Änderungen und Einstellung</h2>
<p>(1) WEBklar darf die Technik von Öffnungszeiten live verbessern, wenn der Leistungsumfang nicht schlechter wird (§ 3 Abs. 7 der Abo-Bedingungen). Ändert sich dabei, welche Daten die Funktion verarbeitet, passt WEBklar die Datenschutzerklärung an und informiert den Kunden, weil sich dann sein Datenschutz-Absatz (Nr. 6 Abs. 4) ändern kann.</p>
<p>(2) WEBklar darf die Funktion für eine Website mit einer Frist von vier Wochen beenden oder für alle Kunden mit einer Frist von drei Monaten einstellen, wenn</p>
<ul>
<li>ein Gesetz, eine Gerichtsentscheidung oder eine Vorgabe einer Aufsichtsbehörde den Betrieb in der bisherigen Form unzulässig macht oder nur mit wesentlich anderer Technik erlaubt und der Kunde einer dafür nötigen neuen Fassung dieser Leistungsbeschreibung nicht zustimmt, oder</li>
<li>eine Sicherheitslücke sich nicht anders beheben lässt.</li>
</ul>
<p>Die Mitteilung erfolgt in Textform und nennt den Grund, die Merkmale der Änderung, den Zeitpunkt und bei Verbrauchern das Recht, den Vertrag nach § 327r Abs. 3 BGB zu beenden, mit seiner Frist.</p>
<p>(3) Ist der Weiterbetrieb aus einem Grund nach Absatz 2 schon vor Ablauf der Frist nicht zulässig, darf WEBklar die Funktion ohne Vorankündigung aussetzen, solange das nötig ist. WEBklar informiert den Kunden darüber unverzüglich in Textform mit dem Grund und nimmt den Betrieb wieder auf, sobald das zulässig und technisch möglich ist.</p>
<p>(4) Ist der Kunde Unternehmer, darf WEBklar die Funktion außerdem ohne besonderen Grund mit einer Frist von drei Monaten in Textform für alle Kunden einstellen. Der Kunde kann das Wartungs-Abo dann innerhalb von 30 Tagen nach der Mitteilung zum Zeitpunkt der Einstellung kündigen; die Mindestlaufzeit steht dem nicht entgegen.</p>
<p>(5) Gesetzliche Rechte von Verbrauchern bei Änderungen digitaler Produkte (§ 327r BGB) bleiben unberührt.</p>
<h2>11. Widerruf</h2>
<p>(1) Das Einschalten der Funktion ist Teil des Abos und kein eigener Vertrag. Für den Widerruf gilt die Widerrufsbelehrung der Abo-Bedingungen. Widerruft ein Verbraucher das Abo oder den Wechsel ins Wartungs-Abo, endet die Funktion.</p>
<p>(2) Hat der Kunde die Funktion zuvor einzeln gekauft, bleibt die Widerrufsbelehrung maßgeblich, die er bei der Bestellung erhalten hat.</p>
<h2>12. Hinweise</h2>
<p>(1) <strong>Keine Rechtsberatung.</strong> Der Datenschutz-Absatz, der WhatsApp-Hinweis und die Hinweise des Kundenportals zu Preisangaben sind Mustertexte für die Standardkonfiguration. WEBklar sorgt dafür, dass sie die Technik richtig beschreiben, und passt sie an, wenn sich die Technik ändert. Eine rechtliche Beratung im Einzelfall, etwa zu Preisangaben, zum Wettbewerbsrecht oder zum Datenschutz der Website des Kunden, schuldet WEBklar nicht.</p>
<p>(2) <strong>Datenschutz.</strong> Wie WEBklar im Kundenportal Daten zu Öffnungszeiten live verarbeitet, steht in der Datenschutzerklärung von WEBklar (Abschnitt 6e). Was beim Aufruf der Website des Kunden übertragen wird, beschreibt Nr. 2 Abs. 4; die Information der Besucher gehört in die Datenschutzerklärung der Website (Nr. 6 Abs. 4).</p>
<p>Anbieter: WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler. Es gelten ergänzend die Abo-Bedingungen und die Datenschutzerklärung.</p>
</main>
</body>
</html>
@@ -0,0 +1,445 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vertragsbedingungen – WEBklar-Abos</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: 40px 0 10px; padding-top: 14px; border-top: 1px solid #e3e0da; }
h3 { font-size: 16.5px; margin: 26px 0 6px; }
p, li { margin: 6px 0; }
ol, ul { padding-left: 24px; }
ol.abs > li { padding-left: 4px; }
ol.abs > li::marker { content: "(" counter(list-item) ") "; font-weight: 600; }
.meta { color: #6b6b70; font-size: 14px; }
.box { background: #fff; border: 1px solid #e3e0da; border-radius: 14px; padding: 16px 20px; margin: 18px 0; }
.box > strong:first-child { display: block; margin-bottom: 6px; }
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; }
.toc { columns: 2; column-gap: 28px; font-size: 14.5px; }
.toc a { text-decoration: none; }
.toc li { break-inside: avoid; }
.anlage-kopf { font-size: 13px; letter-spacing: 0.08em; text-transform: uppercase; color: #6d1fd1; margin-top: 44px; }
.anlage-kopf + h2 { margin-top: 4px; border-top: none; padding-top: 0; }
.hinweis { font-size: 14px; color: #555; }
.offen { background: #fff3cd; padding: 0 4px; border-radius: 4px; }
@media (max-width: 640px) { .toc { columns: 1; } h1 { font-size: 25px; } }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar-Abos · Vertragsunterlagen"; font: 8pt sans-serif; color: #888; }
@top-right { content: "Version 5 · Stand 30.09.2026"; 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, .screen-only { display: none; }
h1 { font-size: 20pt; }
h2 { font-size: 13pt; margin-top: 16pt; break-after: avoid; }
h3 { font-size: 11pt; break-after: avoid; }
.box { border-radius: 4pt; padding: 8pt 10pt; }
.neue-seite { break-before: page; }
a { color: inherit; text-decoration: none; }
.box, tr, ol.abs > li { break-inside: avoid; }
li, p { orphans: 3; widows: 3; }
}
</style>
</head>
<body>
<main>
<nav class="top"><a href="/dashboard.html">← Zurück zum Kundenportal</a> · <a href="/kuendigen.html">Verträge hier kündigen</a> · <a href="/widerrufen.html">Vertrag widerrufen</a> · <a href="/datenschutz.html">Datenschutz</a></nav>
<div class="brand">WEBKLAR</div>
<h1>Vertragsbedingungen für die WEBklar-Abos</h1>
<p class="meta">Hosting-Abo und Wartungs-Abo · Vertragsbedingungen mit Leistungsbeschreibungen, Widerrufsbelehrung und Auftragsverarbeitungsvertrag · Version 5 · Stand 30.09.2026</p>
<p class="screen-only meta"><a href="/vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf">Als PDF herunterladen und ausdrucken</a></p>
<div class="box">
<strong>Das Wichtigste auf einen Blick</strong>
<table>
<tr><td>Anbieter</td><td>WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler, vertreten durch die Gesellschafter Kenso Grimm und Justin Klein (jeder einzeln vertretungsberechtigt) · Telefon 0176 23726355 · E-Mail support@webklar.com</td></tr>
<tr><td>Hosting-Abo</td><td>20,00 € pro Monat · Bereitstellung Ihrer statischen Website auf unserem Server in Deutschland · 1 Domain · 1 E-Mail-Postfach mit 5 GB · Änderungen an der Website nur nach Ihrer Freigabe, 100 € pro Stunde (Leistungsbeschreibung A)</td></tr>
<tr><td>Wartungs-Abo</td><td>50,00 € pro Monat · Betrieb und Pflege Ihrer Website bzw. Web-Anwendung auf unserem Server in Deutschland · 1 Domain · beliebig viele E-Mail-Postfächer mit zusammen 5 GB · 1 Stunde Arbeitszeit pro Monat, nicht genutzte Zeit bleibt 12 Monate verfügbar; Mehraufwand nur nach Ihrer Freigabe, 100 € pro Stunde (Leistungsbeschreibung B)</td></tr>
<tr><td>E-Mail-Zusatzspeicher</td><td>In beiden Abos: je Paket von 5 GB 1,00 € pro Monat, bis zu 10 Pakete. Im Kundenportal bestellbar und reduzierbar nach Leistungsbeginn, solange das Abo nicht gekündigt ist und keine Zahlung offen ist; ganz oder teilweise kündbar zum Ende des Abrechnungsmonats.</td></tr>
<tr><td>Preise</td><td>Alle Preise sind Endpreise, zahlbar monatlich im Voraus. Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.</td></tr>
<tr><td>Zahlung</td><td>Kreditkarte oder SEPA-Lastschrift über Stripe</td></tr>
<tr><td>Laufzeit</td><td>Mindestlaufzeit 12 Monate ab Leistungsbeginn, kündbar zum Ende der Mindestlaufzeit bis einen Monat vorher; danach unbefristet und jederzeit zum Ende des Abrechnungsmonats kündbar. Bei Geschäftsaufgabe Sonderkündigung mit einem Monat Frist.</td></tr>
<tr><td>Wechsel</td><td>Vom Hosting- ins Wartungs-Abo im Kundenportal nach Leistungsbeginn, auch während der Mindestlaufzeit, solange das Hosting-Abo nicht gekündigt ist und keine Zahlung offen ist; der Wechsel steht unter der Bedingung, dass die Wechselrechnung bezahlt wird (§ 9 Abs. 3). Mit dem Wechseltag beginnt eine neue Mindestlaufzeit von 12 Monaten. Vom Wartungs- ins Hosting-Abo auf Anfrage nach Ablauf der Mindestlaufzeit.</td></tr>
<tr><td>Kündigen</td><td>project.webklar.com/kuendigen.html (ohne Anmeldung; Auswahl: das Abo oder nur der E-Mail-Zusatzspeicher, ganz oder mit der Zahl der Pakete) oder per E-Mail</td></tr>
<tr><td>Widerruf</td><td>Verbraucher können jede Bestellung (Abo, Wechsel, Zusatzspeicher) innerhalb von 14 Tagen ab Vertragsschluss widerrufen: project.webklar.com/widerrufen.html (siehe Widerrufsbelehrung und Hinweise zum Widerruf)</td></tr>
</table>
</div>
<div class="box">
<strong>Inhalt</strong>
<ol class="toc">
<li><a href="#p1">Anbieter und Geltungsbereich</a></li>
<li><a href="#p2">Vertragsschluss und Vertragstext</a></li>
<li><a href="#p3">Leistungen</a></li>
<li><a href="#p4">Arbeitszeit und Stundenkontingent</a></li>
<li><a href="#p5">Verfügbarkeit und Wartung</a></li>
<li><a href="#p6">Pflichten des Kunden</a></li>
<li><a href="#p7">Preise und Zahlung</a></li>
<li><a href="#p8">Laufzeit und Kündigung</a></li>
<li><a href="#wechsel">Wechsel zwischen den Abos</a></li>
<li><a href="#zusatzspeicher">E-Mail-Zusatzspeicher</a></li>
<li><a href="#p11">Vertragsende</a></li>
<li><a href="#p12">Mängel</a></li>
<li><a href="#p13">Haftung</a></li>
<li><a href="#p14">Datenschutz</a></li>
<li><a href="#p15">Änderungen dieser Bedingungen</a></li>
<li><a href="#p16">Schlussbestimmungen</a></li>
</ol>
<p class="meta" style="margin-bottom:0">Außerdem: <a href="#hosting">Leistungsbeschreibung A: Hosting-Abo</a> · <a href="#wartung">Leistungsbeschreibung B: Wartungs-Abo</a> · <a href="#widerruf">Widerrufsbelehrung und Muster-Widerrufsformular</a> · <a href="#widerruf-hinweise">Hinweise zum Widerruf</a> · <a href="#avv">Anlage 1: Auftragsverarbeitungsvertrag</a> · <a href="#tom">Anlage 2: Technische und organisatorische Maßnahmen</a> · <a href="#sub">Anlage 3: Unterauftragsverarbeiter</a></p>
</div>
<h2 id="p1">§ 1 Anbieter und Geltungsbereich</h2>
<ol class="abs">
<li>Diese Bedingungen regeln das Hosting-Abo und das Wartungs-Abo (zusammen „Abos“) zwischen der WEBklar GbR, Am Schwimmbad 10, 67722 Winnweiler („WEBklar“ oder „wir“), und dem Kunden. Welches Abo der Kunde bestellt hat, ergibt sich aus seiner Bestellung und unserer Bestätigungs-E-Mail. Den Leistungsumfang beschreiben Leistungsbeschreibung A (Hosting-Abo) und Leistungsbeschreibung B (Wartungs-Abo); sie sind Teil dieser Bedingungen. Zu beiden Abos kann der Kunde E-Mail-Zusatzspeicher buchen (§ 10).</li>
<li>Sie gelten für Verbraucher (§ 13 BGB) und Unternehmer (§ 14 BGB). Gilt eine Regel nur für eine der beiden Gruppen, ist das ausdrücklich angegeben.</li>
<li>Für die Abos gehen diese Bedingungen den allgemeinen Geschäftsbedingungen von WEBklar (webklar.com/agb) vor. Die dortige Regelung zum jährlichen Hosting (Leistungsmodul C) gilt für die Abos nicht. Die Erstellung einer Website ist nicht Teil der Abos und wird gesondert vereinbart.</li>
<li>Abweichende Geschäftsbedingungen des Kunden werden nicht Vertragsbestandteil.</li>
</ol>
<h2 id="p2">§ 2 Vertragsschluss und Vertragstext</h2>
<ol class="abs">
<li>Die Abos werden im Kundenportal unter project.webklar.com bestellt. Den Zugang zum Kundenportal richten wir für den Kunden ein.</li>
<li>Der Abschluss eines Abos läuft so ab:
<ol type="a">
<li>Der Kunde wählt im Kundenportal im Bereich „Verträge“ bei dem gewünschten Abo „Abo abschließen“.</li>
<li>Eine Bestellübersicht zeigt Leistung, Preis, Laufzeit und Zahlungsweise.</li>
<li>Der Kunde bestätigt, dass er diese Bedingungen einschließlich des Auftragsverarbeitungsvertrags (Anlage 1) akzeptiert. Außerdem wählt er, ob wir sofort oder erst nach Ablauf der 14-tägigen Widerrufsfrist mit der Leistung beginnen. Mit der Wahl des sofortigen Beginns verlangt er ausdrücklich, dass wir vor Ablauf der Widerrufsfrist beginnen.</li>
<li>Mit dem Klick auf „Zahlungspflichtig bestellen“ gibt er ein verbindliches Angebot ab.</li>
<li>Anschließend hinterlegt er beim Zahlungsdienstleister Stripe eine Zahlungsart (Kreditkarte oder SEPA-Lastschrift) und schließt den Vorgang dort ab.</li>
</ol></li>
<li>Der Vertrag kommt zustande, sobald der Kunde den Vorgang bei Stripe abgeschlossen hat. Bei sofortigem Beginn setzt das die erfolgreiche Zahlung der ersten Monatsgebühr voraus; scheitert sie, kommt kein Vertrag zustande. Wir bestätigen den Vertragsschluss unverzüglich per E-Mail und senden diese Bedingungen mit Widerrufsbelehrung als PDF mit.</li>
<li>Bis zum Abschluss des Vorgangs bei Stripe kann der Kunde jederzeit abbrechen und seine Angaben im Kundenportal korrigieren.</li>
<li>Der Wechsel vom Hosting- ins Wartungs-Abo (§ 9) und die Bestellung von E-Mail-Zusatzspeicher (§ 10) sind eigene Bestellungen. Der Kunde gibt sie im Kundenportal im Bereich „Verträge“ auf. Direkt über der Schaltfläche zeigt eine Übersicht die Leistung, den Preis pro Monat, beim Wechsel den sofort fälligen Betrag, beim Zusatzspeicher den anteiligen Betrag, der mit der nächsten Abbuchung eingezogen wird, den Tag der nächsten Abbuchung sowie Laufzeit und Kündigungsmöglichkeit. Mit dem Klick auf „Zahlungspflichtig bestellen“ kommt der Vertrag zustande; beim Wechsel steht er unter der Bedingung des § 9 Abs. 3 (Zahlung der Wechselrechnung). Bezahlt wird mit der bei Stripe hinterlegten Zahlungsart; ein neuer Vorgang bei Stripe ist nicht nötig. Verlangt die Bank des Kunden eine Bestätigung der Zahlung, leitet das Kundenportal ihn dafür auf die Zahlungsseite von Stripe weiter. Bis zum Klick kann der Kunde abbrechen und seine Angaben korrigieren. Wir bestätigen die Bestellung unverzüglich per E-Mail und senden diese Bedingungen mit Widerrufsbelehrung als PDF mit.</li>
<li>Vertragssprache ist Deutsch. Wir speichern den Vertragstext sowie Zeitpunkt, Version und bestelltes Abo jeder Zustimmung des Kunden. Die aktuelle Fassung dieser Bedingungen ist unter project.webklar.com/abo-bedingungen.html abrufbar. Die bei Vertragsschluss gültige Fassung erhält der Kunde mit der Bestätigungs-E-Mail; er kann sie außerdem jederzeit im Kundenportal unter „Verträge“ abrufen.</li>
</ol>
<h2 id="p3">§ 3 Leistungen</h2>
<h3>(1) Leistungsbeschreibungen</h3>
<p>Den Umfang des Hosting-Abos beschreibt Leistungsbeschreibung A, den des Wartungs-Abos Leistungsbeschreibung B. Beide Abos umfassen den Betrieb der Website auf unserem Server, eine Domain, E-Mail-Postfächer, die tägliche Datensicherung und die Servicezeiten nach den Absätzen 2 bis 6. Das Wartungs-Abo umfasst zusätzlich die Pflege der Website und ein Stundenkontingent (§ 4). Das Hosting-Abo ist nur für statische Websites bestimmt (Leistungsbeschreibung A Nr. 2).</p>
<h3>(2) Betrieb der Website</h3>
<ul>
<li>Wir stellen die Website bzw. Web-Anwendung, die wir für den Kunden erstellt oder zur Betreuung übernommen haben, auf einem von uns betriebenen Server bereit. Der Server steht im Rechenzentrum der Hetzner Online GmbH (im Folgenden „Hetzner“) in Falkenstein (Deutschland).</li>
<li>Speicherplatz und Datenverkehr stehen im Umfang einer üblichen Firmen-Website zur Verfügung. Ungewöhnlich hohe Last, etwa durch große Downloads oder Streaming, ist nicht umfasst.</li>
<li>Wir spielen Sicherheitsupdates für Betriebssystem und Server-Software ein, kritische Sicherheitsupdates in der Regel innerhalb von 3 Werktagen nach ihrer Veröffentlichung, übrige Updates regelmäßig. Aktualisierungen, die nötig sind, damit unsere Leistungen vertragsgemäß bleiben, stellen wir während der gesamten Vertragslaufzeit bereit. Updates der Software-Komponenten der Website selbst gehören nur zum Wartungs-Abo (Leistungsbeschreibung B Nr. 2).</li>
<li>Wir richten ein SSL/TLS-Zertifikat (HTTPS) ein und verlängern es.</li>
<li>Wir überwachen die Erreichbarkeit automatisch und beheben Störungen in unserem Verantwortungsbereich (§ 5).</li>
</ul>
<h3>(3) Domain</h3>
<ul>
<li>Enthalten ist eine Domain mit einer der Endungen .de, .com, .eu, .net oder .org: Registrierung oder Übernahme einer bestehenden Domain, Verlängerung und DNS-Verwaltung. Premium-Domains und andere Endungen nur nach gesonderter Vereinbarung.</li>
<li>Inhaber der Domain wird der Kunde, wir verwalten sie technisch. Den Umzugscode (AuthCode) erhält der Kunde jederzeit auf Anfrage.</li>
<li>Für die Wahl der Domain und dafür, dass sie keine Rechte Dritter verletzt, ist der Kunde verantwortlich.</li>
</ul>
<h3>(4) E-Mail-Postfächer</h3>
<ul>
<li>Wir richten E-Mail-Postfächer unter der Domain des Kunden ein, mit verschlüsseltem Zugang per IMAP und SMTP sowie über Webmail, mit Spam- und Virenfilter. Wir stellen sie über Hetzner (Rechenzentrum Nürnberg, Deutschland) bereit. Das Hosting-Abo umfasst ein Postfach, das Wartungs-Abo beliebig viele Postfächer.</li>
<li>Für die Postfächer des Kunden stehen zusammen 5 GB Speicher zur Verfügung, zuzüglich gebuchtem Zusatzspeicher (§ 10). Hetzner setzt für jedes Postfach eine Speichergrenze. Die Grenzen aller Postfächer des Kunden ergeben zusammen höchstens den verfügbaren Speicher. Wie der Speicher auf die Postfächer verteilt wird, bestimmt der Kunde; ohne Vorgabe teilen wir ihn gleichmäßig auf.</li>
<li>Ist die Speichergrenze eines Postfachs erreicht, nimmt es keine neuen E-Mails mehr an, bis wieder Platz ist, etwa durch Löschen von E-Mails, eine andere Aufteilung oder Zusatzspeicher.</li>
<li>Das Anlegen und Löschen von Postfächern, neue Passwörter und eine andere Aufteilung des Speichers beauftragt der Kunde über den Chat im Kundenportal (Schaltfläche „Postfach anfragen“) oder per E-Mail. Wir setzen das in der Regel innerhalb eines Werktags um. Diese Arbeiten sind im Abo enthalten.</li>
<li>Zugangsdaten zu den Postfächern geben wir nur an den Kunden oder an Personen, die er uns in Textform benennt.</li>
<li>Die Postfächer dürfen nur der Kunde, seine Beschäftigten und andere für ihn tätige Personen für seine Organisation nutzen. Nicht erlaubt sind die Überlassung an Dritte, auch unentgeltlich, sowie Newsletter- und Massenversand. Es gelten die Versandgrenzen von Hetzner.</li>
<li>Die Inhalte der Postfächer sichert Hetzner nach seinen Bedingungen, nicht wir selbst. Wichtige E-Mails sollte der Kunde zusätzlich bei sich aufbewahren.</li>
<li>Die Zustellung von E-Mails hängt auch von Dritten ab, etwa von Empfangsservern und Spamfiltern, und kann nicht garantiert werden.</li>
</ul>
<h3>(5) Datensicherung</h3>
<ul>
<li>Der Server wird täglich automatisch vollständig gesichert (Backup-Funktion von Hetzner). Die Sicherungen liegen getrennt vom Server in einem anderen Brandabschnitt am selben Rechenzentrumsstandort in Deutschland. Verfügbar sind jeweils die letzten sieben Tagessicherungen.</li>
<li>Datenbanken einer Web-Anwendung exportieren wir zusätzlich täglich vor der Serversicherung, damit sie sich in einem einheitlichen Stand wiederherstellen lassen.</li>
<li>Eine Wiederherstellung nach technischen Störungen oder nach Fehlern, die wir zu vertreten haben, ist inklusive. Wünscht der Kunde eine Wiederherstellung aus anderen Gründen, etwa nach eigenen Änderungen, wird sie als Arbeitszeit nach § 4 abgerechnet.</li>
<li>Die Sicherung dient der Wiederherstellung des Betriebs. Ein Archiv oder die Herausgabe von Sicherungskopien ist nicht geschuldet.</li>
</ul>
<h3>(6) Servicezeiten und Reaktion</h3>
<ul>
<li>Servicezeiten sind Montag bis Freitag von 9 bis 17 Uhr, ausgenommen gesetzliche Feiertage in Rheinland-Pfalz. Anfragen außerhalb der Servicezeiten gelten zu Beginn der nächsten Servicezeit als eingegangen.</li>
<li>Auf Anfragen und Störungsmeldungen antworten wir spätestens bis zum Ende des nächsten Werktags. Gemeint ist eine erste fachliche Rückmeldung, nicht zwingend schon die Behebung.</li>
<li>Fällt die Website oder ein Postfach vollständig aus, beginnen wir innerhalb der Servicezeiten unverzüglich mit der Behebung. Unser Ziel ist eine Behebung innerhalb von 4 Stunden. Eine Garantie ist damit nicht verbunden.</li>
<li>Beauftragte Änderungen setzen wir in der Regel innerhalb von 5 Werktagen um.</li>
<li>Betriebsferien kündigen wir mindestens zwei Wochen vorher per E-Mail an.</li>
</ul>
<h3>(7) Technische Änderungen</h3>
<p>Wir dürfen die technische Umsetzung ändern, etwa das Rechenzentrum, die Server-Software oder den Anbieter der E-Mail-Postfächer wechseln. Voraussetzung ist, dass der vereinbarte Leistungsumfang nicht schlechter wird, dem Kunden keine Kosten entstehen und wir ihn mindestens vier Wochen vorher in Textform informieren. Beeinträchtigt eine Änderung den Zugang zur Leistung oder ihre Nutzbarkeit mehr als nur unerheblich, kann der Kunde den Vertrag innerhalb von 30 Tagen nach der Information bzw. der Änderung unentgeltlich beenden (§ 327r BGB).</p>
<h2 id="p4">§ 4 Arbeitszeit und Stundenkontingent</h2>
<ol class="abs">
<li>Im Wartungs-Abo gehört zum Leistungsumfang ein Kontingent von 60 Minuten Arbeitszeit je bezahltem Abrechnungsmonat. Das erste Kontingent steht mit der ersten Zahlung bereit, nach einem Wechsel aus dem Hosting-Abo mit der Zahlung der Wechselrechnung (§ 9 Abs. 3). Das Kontingent ist Teil der Leistungsbeschreibung und kein Geldguthaben.</li>
<li>Nicht genutzte Zeit bleibt verfügbar: Jedes Monatskontingent kann zwölf Monate lang ab seiner Bereitstellung genutzt werden und entfällt danach. Genutzt wird immer das älteste noch verfügbare Kontingent zuerst. Mit dem Ende des Wartungs-Abos, auch durch einen Wechsel ins Hosting-Abo, entfällt nicht genutzte Zeit. Sie wird weder ausgezahlt noch erstattet und ist nicht übertragbar.</li>
<li>Das Kontingent gilt für Änderungen, Erweiterungen, Beratung und Unterstützung rund um die im Abo betreute Website, Domain und E-Mail. Aufträge erteilt der Kunde über den Chat im Kundenportal oder per E-Mail. Aufträge von mehr als 4 Stunden in einem Monat setzen wir nach Terminabsprache um.</li>
<li>Angerechnet wird die tatsächlich aufgewendete Zeit je Auftrag, aufgerundet auf volle 15 Minuten. Dazu zählen die Umsetzung und die Abstimmung zum Auftrag, soweit sie länger als 10 Minuten dauert. Nicht angerechnet werden die laufenden Leistungen nach § 3 Abs. 2 bis 5 und die Sicherheitsupdates nach Leistungsbeschreibung B Nr. 2 (Betrieb, Updates, Überwachung, Sicherung, Verwaltung von Domain und Postfächern), die Behebung von Störungen (§ 3 Abs. 6, § 5) sowie die Behebung von Fehlern, die wir zu vertreten haben. Wirkt an einem Arbeitsschritt eine weitere Person unterstützend mit, etwa bei Gestaltung, Abstimmung oder Prüfung, rechnen wir für sie die Hälfte der Arbeitszeit dieses Arbeitsschritts an, aufgerundet auf volle 15 Minuten; bei mehreren unterstützenden Personen gilt das für jede von ihnen. Im Kundenportal weisen wir jeden Arbeitsschritt mit seiner Arbeitszeit und jede Unterstützung mit Person, Tätigkeit und angerechneter Zeit einzeln aus.</li>
<li>Verfügbare Zeit, Bereitstellungen, Verbrauch und Ablaufdaten sieht der Kunde jederzeit im Kundenportal.</li>
<li>Reicht die verfügbare Zeit für einen Auftrag voraussichtlich nicht aus, nennen wir dem Kunden vorher den geschätzten Mehraufwand. Wir führen ihn nur nach seiner Freigabe aus, etwa im Chat oder per E-Mail, und berechnen 100 € pro Stunde (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer), abgerechnet in Schritten von 15 Minuten. Aufträge mit einem Umfang von mehr als 10 Stunden bieten wir gesondert an.</li>
<li>Im Hosting-Abo gibt es kein Stundenkontingent. Aufträge, etwa Änderungen an der Website, führen wir nur nach Freigabe des Kunden aus. Vorher nennen wir ihm den geschätzten Aufwand. Wir berechnen 100 € pro Stunde (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer), abgerechnet in Schritten von 15 Minuten; Absatz 4 Satz 1, 2 und 4 gelten entsprechend. Aufträge mit einem Umfang von mehr als 10 Stunden bieten wir gesondert an. Die Behebung von Störungen an Server, Domain und Postfach in unserem Verantwortungsbereich ist auch im Hosting-Abo inklusive.</li>
</ol>
<h2 id="p5">§ 5 Verfügbarkeit und Wartung</h2>
<ol class="abs">
<li>Wir streben eine Erreichbarkeit der Website von 99 % im Jahresmittel an, gemessen am Übergang des Rechenzentrums ins Internet. Eine ununterbrochene Erreichbarkeit ist technisch nicht möglich und wird nicht geschuldet.</li>
<li>Nicht eingerechnet werden:
<ul>
<li>angekündigte Wartungsarbeiten, möglichst zwischen 22 und 6 Uhr und mindestens 24 Stunden vorher angekündigt; dringende Sicherheitsupdates auch kurzfristig,</li>
<li>Störungen außerhalb unseres Einflussbereichs, etwa im Internet, bei der Domain-Vergabestelle oder bei vorgelagerten Netzbetreibern,</li>
<li>Angriffe wie DDoS, sofern wir angemessene Schutzmaßnahmen getroffen haben,</li>
<li>höhere Gewalt sowie Störungen, die der Kunde verursacht hat.</li>
</ul></li>
<li>Die gesetzlichen Rechte des Kunden bei Ausfällen, die wir zu vertreten haben, bleiben unberührt. Die Haftung richtet sich nach § 13.</li>
</ol>
<h2 id="p6">§ 6 Pflichten des Kunden</h2>
<ol class="abs">
<li>Der Kunde hält seine Zugangsdaten zum Kundenportal und zu allen Postfächern geheim und informiert uns unverzüglich, wenn er einen Missbrauch vermutet. Gibt er die Zugangsdaten eines Postfachs an Beschäftigte oder andere für ihn tätige Personen weiter, geschieht das in seiner Verantwortung; er sorgt dafür, dass auch diese Personen die Zugangsdaten geheim halten und die Regeln nach § 3 Abs. 4 einhalten. Ein neues Passwort kann er jederzeit über den Chat im Kundenportal anfordern.</li>
<li>Der Kunde stellt nur Inhalte bereit, die er verwenden darf und die nicht gegen Gesetze verstoßen. Das gilt auch für E-Mails, die über seine Postfächer versendet werden. Offensichtlich rechtswidrige Inhalte dürfen wir nach einem Hinweis vorübergehend sperren. Wir informieren den Kunden darüber unverzüglich.</li>
<li>Für Impressum, Datenschutzerklärung und Inhalte seiner Website ist der Kunde verantwortlich. Die Angaben zum Hosting, die er für seine Datenschutzerklärung braucht, stehen in den Anlagen 1 bis 3.</li>
<li>Der Kunde bewahrt Kopien der Inhalte auf, die er uns übergibt oder selbst einpflegt, und teilt uns Änderungen seiner Kontaktdaten mit.</li>
<li>Im Hosting-Abo stellt der Kunde keine Inhalte oder Funktionen bereit, die nach Leistungsbeschreibung A Nr. 2 im Hosting-Abo nicht möglich sind, und beauftragt keine. Braucht seine Website solche Teile, wechselt er vorher ins Wartungs-Abo (§ 9).</li>
</ol>
<h2 id="p7">§ 7 Preise und Zahlung</h2>
<ol class="abs">
<li>Die Monatsgebühr beträgt im Hosting-Abo 20,00 € und im Wartungs-Abo 50,00 €. E-Mail-Zusatzspeicher kostet je Paket von 5 GB 1,00 € pro Monat (§ 10). Alle Preise sind Endpreise. Als Kleinunternehmer im Sinne von § 19 UStG berechnen wir keine Umsatzsteuer.</li>
<li>Die Monatsgebühr ist monatlich im Voraus fällig, erstmals mit Beginn der Leistung (bei sofortigem Beginn mit Vertragsschluss, sonst nach Ablauf der Widerrufsfrist), danach jeweils am selben Kalendertag der Folgemonate (Abrechnungsmonat). Gebuchter Zusatzspeicher wird zusammen mit der Monatsgebühr abgebucht. Die Gebühren werden per Kreditkarte oder SEPA-Lastschrift über die Stripe Payments Europe, Limited (Irland) eingezogen. Rechnungen erhält der Kunde per E-Mail und im Kundenportal über „Abo verwalten &amp; Rechnungen“.</li>
<li>Bestellt der Kunde Zusatzspeicher, berechnen wir für den Rest des laufenden Abrechnungsmonats einen anteiligen Betrag. Er wird in der Bestellübersicht angezeigt und mit der nächsten Abbuchung eingezogen. Ab dem folgenden Abrechnungsmonat gilt der volle Monatspreis. Pakete, die für den laufenden Abrechnungsmonat schon bezahlt sind, berechnen wir nicht noch einmal. Verringert der Kunde den Zusatzspeicher, wirkt das zum Ende des laufenden Abrechnungsmonats; die Gebühr für den laufenden Monat wird nicht anteilig erstattet.</li>
<li>Beim Wechsel vom Hosting- ins Wartungs-Abo (§ 9) wird die Monatsgebühr des Wartungs-Abos am Wechseltag fällig, zusammen mit gebuchtem Zusatzspeicher. Die im Voraus bezahlten Gebühren für die nicht genutzten Tage des laufenden Abrechnungsmonats schreiben wir auf dieser Rechnung gut. Die folgenden Abrechnungsmonate beginnen jeweils am Kalendertag des Wechsels. Den fälligen Betrag und den neuen Abbuchungstag zeigt die Bestellübersicht vor der Bestellung. Wird die Wechselrechnung nicht bezahlt, gilt § 9 Abs. 3.</li>
<li>Schlägt eine Zahlung fehl, versucht Stripe den Einzug automatisch erneut und informiert den Kunden per E-Mail.</li>
<li>Ist der Kunde mit einem Betrag in Höhe von zwei Monatsgebühren seines Abos in Verzug, dürfen wir die Leistungen vorübergehend sperren, etwa die Website offline nehmen. Vorher kündigen wir die Sperre in Textform an und setzen eine Frist von mindestens zwei Wochen. Das gilt nicht, solange der Kunde die Forderung mit nachvollziehbaren Gründen bestreitet. Während einer Sperre löschen wir keine Daten. Nach Zahlung heben wir die Sperre unverzüglich auf. Ist der Kunde mit zwei aufeinanderfolgenden Monatsgebühren in Verzug, dürfen wir außerordentlich kündigen.</li>
<li>Preisänderungen kündigen wir mindestens 6 Wochen vor ihrem Wirksamwerden in Textform an. Sie gelten nur, wenn der Kunde zustimmt. Ohne Zustimmung läuft der Vertrag zum bisherigen Preis weiter, und beide Seiten können nach § 8 ordentlich kündigen.</li>
</ol>
<h2 id="p8">§ 8 Laufzeit und Kündigung</h2>
<ol class="abs">
<li>Jedes Abo hat eine Mindestlaufzeit von zwölf Monaten ab Leistungsbeginn, bei sofortigem Beginn also ab Vertragsschluss, sonst ab Ablauf der Widerrufsfrist. Nach einem Wechsel vom Hosting- ins Wartungs-Abo beginnt eine neue Mindestlaufzeit von zwölf Monaten ab dem Wechseltag (§ 9 Abs. 3). Das Ende der laufenden Mindestlaufzeit zeigt das Kundenportal unter „Verträge“.</li>
<li>Beide Seiten können zum Ende der Mindestlaufzeit mit einer Frist von einem Monat kündigen. Wird nicht fristgerecht gekündigt, läuft das Abo auf unbestimmte Zeit weiter.</li>
<li>Nach Ablauf der Mindestlaufzeit kann der Kunde jederzeit zum Ende des laufenden Abrechnungsmonats kündigen. Wir können dann mit einer Frist von einem Monat zum Ende eines Abrechnungsmonats kündigen.</li>
<li>Gibt der Kunde den Geschäftsbetrieb, für den die Website bestimmt ist, dauerhaft auf, kann er auch während der Mindestlaufzeit mit einer Frist von einem Monat zum Ende eines Abrechnungsmonats kündigen. Auf Verlangen weist er die Geschäftsaufgabe nach, etwa durch die Gewerbeabmeldung.</li>
<li>Eine vorzeitige Beendigung im gegenseitigen Einvernehmen ist jederzeit möglich.</li>
<li>Das Recht zur außerordentlichen Kündigung aus wichtigem Grund bleibt unberührt.</li>
<li>Der E-Mail-Zusatzspeicher hat keine Mindestlaufzeit. Der Kunde kann ihn jederzeit zum Ende des laufenden Abrechnungsmonats ganz oder teilweise (mit einer geringeren Zahl von Paketen) kündigen (§ 10 Abs. 5). Er endet spätestens mit dem Abo.</li>
<li>Der Kunde kann über die Schaltfläche „Verträge hier kündigen“ unter project.webklar.com/kuendigen.html kündigen (ohne Anmeldung) oder in Textform, etwa per E-Mail. Dort wählt er, ob er sein Abo einschließlich des Zusatzspeichers kündigt oder mit der Auswahl „Nur den E-Mail-Zusatzspeicher“ allein den Zusatzspeicher, ganz oder mit der Zahl der Pakete, die er kündigt; das geht auch, wenn das Abo schon gekündigt ist. Den Zeitpunkt, zu dem die Kündigung wirkt, bestätigen wir unverzüglich per E-Mail. Bis dahin bleiben alle Leistungen verfügbar. Die Gebühr für den laufenden Monat wird nicht anteilig erstattet.</li>
</ol>
<h2 id="wechsel">§ 9 Wechsel zwischen den Abos</h2>
<ol class="abs">
<li>Ein Kunde mit laufendem Hosting-Abo kann jederzeit ins Wartungs-Abo wechseln, auch während der Mindestlaufzeit des Hosting-Abos. Voraussetzung ist, dass die Leistung des Hosting-Abos begonnen hat, keine Zahlung offen ist und das Hosting-Abo nicht gekündigt ist.</li>
<li>Der Wechsel ist ein neuer Vertrag über das Wartungs-Abo. Der Kunde bestellt ihn im Bereich „Verträge“ des Kundenportals (§ 2 Abs. 5). Die Bestellübersicht zeigt den Preis des Wartungs-Abos, gebuchten Zusatzspeicher, die Gutschrift für die nicht genutzten Tage des Hosting-Abos, den sofort fälligen Betrag, den neuen Abbuchungstag, das Ende der neuen Mindestlaufzeit und die Kündigungsmöglichkeit nach Ablauf der neuen Mindestlaufzeit. Der Kunde stimmt dabei diesen Bedingungen einschließlich des Auftragsverarbeitungsvertrags (Anlage 1) zu. Wird der Wechsel wirksam, endet das Hosting-Abo im gegenseitigen Einvernehmen.</li>
<li>Der Wechsel steht unter der Bedingung, dass die Wechselrechnung bezahlt wird. Bei Zahlung per Kreditkarte wird er erst mit der erfolgreichen Zahlung wirksam. Gelingt die Zahlung nicht, etwa weil der Kunde eine von seiner Bank verlangte Freigabe nicht bestätigt, wird er nicht wirksam; spätestens 23 Stunden nach der Bestellung verfällt die Wechselrechnung. Bei SEPA-Lastschrift beginnt das Wartungs-Abo schon mit der Bestellung; wird die Lastschrift nicht eingelöst, gilt der Wechsel als nicht erfolgt. In diesen Fällen läuft das Hosting-Abo mit Preis und Mindestlaufzeit wie vor dem Wechsel weiter; bei SEPA-Lastschrift bleibt der Abbuchungstag der Wechseltag; wir informieren den Kunden per E-Mail. Das Wartungs-Abo beginnt, sobald der Wechsel wirksam ist; einen späteren Beginn bietet der Wechsel nicht. Der Kunde verlangt bei der Bestellung ausdrücklich, dass wir vor Ablauf der Widerrufsfrist beginnen (siehe Hinweise zum Widerruf). Ab Beginn des Wartungs-Abos gilt Leistungsbeschreibung B. Wechseltag ist der Tag der Bestellung; er ist der neue Abbuchungstag (§ 7 Abs. 4), und mit ihm beginnt eine neue Mindestlaufzeit von zwölf Monaten (§ 8 Abs. 1). Das erste Stundenkontingent stellen wir bereit, sobald die Wechselrechnung bezahlt ist, danach monatlich (§ 4). Postfächer und gebuchter Zusatzspeicher bleiben unverändert; ab Beginn des Wartungs-Abos kann der Kunde weitere Postfächer beauftragen; wir richten sie ein, sobald die Wechselrechnung bezahlt ist.</li>
<li>Die Abrechnung des Wechsels richtet sich nach § 7 Abs. 4. Ein laufender Rabatt aus einem persönlichen Angebot läuft bis zu seinem Ende weiter. Guthaben aus einer Übernahmegutschrift wird weiter verrechnet.</li>
<li>Ein Wechsel vom Wartungs- ins Hosting-Abo ist nach Ablauf der Mindestlaufzeit des Wartungs-Abos zum Ende eines Abrechnungsmonats möglich. Der Kunde teilt uns den Wunsch in Textform mit, etwa im Chat des Kundenportals oder per E-Mail; eine Bestellung im Kundenportal gibt es dafür nicht. Voraussetzung ist, dass die Website statisch ist (Leistungsbeschreibung A Nr. 2). Vor dem Wechsel legt der Kunde fest, welches Postfach bleibt; die Inhalte der übrigen Postfächer stellen wir ihm auf Wunsch vorher als Export bereit, danach löschen wir sie. Nicht genutzte Arbeitszeit entfällt mit dem Wechsel (§ 4 Abs. 2). Es beginnt keine neue Mindestlaufzeit; das Hosting-Abo ist danach nach § 8 Abs. 3 kündbar. Ab dem Wechsel gilt die Monatsgebühr des Hosting-Abos; eine anteilige Verrechnung für den laufenden Abrechnungsmonat gibt es nicht. Wir bestätigen den Wechsel per E-Mail.</li>
</ol>
<h2 id="zusatzspeicher">§ 10 E-Mail-Zusatzspeicher</h2>
<ol class="abs">
<li>In beiden Abos kann der Kunde den Speicher seiner Postfächer erweitern. Ein Paket Zusatzspeicher umfasst 5 GB und kostet 1,00 € pro Monat (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer). Der Kunde kann bis zu 10 Pakete buchen. Der Zusatzspeicher erhöht den Speicher, der allen Postfächern des Kunden zusammen zur Verfügung steht (§ 3 Abs. 4). Bestellungen und Änderungen des Zusatzspeichers im Kundenportal sind möglich, sobald die Leistung des Abos begonnen hat, und nur, solange keine Zahlung offen ist und das Abo nicht gekündigt ist.</li>
<li>Der Kunde bestellt Zusatzspeicher im Kundenportal im Bereich „Verträge“, indem er die Zahl der Pakete erhöht (§ 2 Abs. 5). Die Bestellübersicht zeigt die neue Gesamtgröße, den Mehrpreis pro Monat, den anteiligen Betrag für den Rest des laufenden Abrechnungsmonats, den Tag der nächsten Abbuchung und die Laufzeit (monatlich, zum Ende jedes Abrechnungsmonats kündbar, endet spätestens mit dem Abo). Mit dem Klick auf „Zahlungspflichtig bestellen“ kommt der Vertrag über den Zusatzspeicher zustande.</li>
<li>Wir stellen den zusätzlichen Speicher in der Regel innerhalb eines Werktags bereit und verteilen ihn nach den Vorgaben des Kunden auf die Postfächer (§ 3 Abs. 4). Bis dahin zeigt das Kundenportal die Bestellung als „wird eingerichtet“ an. Wir beginnen mit der Bereitstellung sofort, auch während der Widerrufsfrist; die Folgen eines Widerrufs stehen in den Hinweisen zum Widerruf.</li>
<li>Die Abrechnung richtet sich nach § 7 Abs. 3.</li>
<li>Der Kunde kann die Zahl der Pakete verringern, im Kundenportal im Bereich „Verträge“ mit der Schaltfläche „Speichern“, solange das Abo nicht gekündigt ist. Den Zusatzspeicher ganz oder teilweise kündigen kann er jederzeit über „Verträge hier kündigen“ mit der Auswahl „Nur den E-Mail-Zusatzspeicher“ und der Zahl der Pakete oder in Textform (§ 8 Abs. 8); nach einer Kündigung des Abos kann der Kunde die Zahl der Pakete nur noch über „Verträge hier kündigen“ oder in Textform verringern. Die Änderung wirkt zum Ende des laufenden Abrechnungsmonats; bis dahin zeigt das Kundenportal den Zeitpunkt und die künftige Größe an. Wir bestätigen die Änderung per E-Mail. Passen die gespeicherten E-Mails dann nicht mehr in den verbleibenden Speicher, nimmt das betroffene Postfach keine neuen E-Mails mehr an, bis wieder Platz ist (§ 3 Abs. 4). Der Kunde sollte vorher E-Mails löschen oder sichern.</li>
<li>Der Zusatzspeicher endet spätestens mit dem Abo. Bei einem Wechsel ins Wartungs-Abo läuft er unverändert weiter.</li>
</ol>
<h2 id="p11">§ 11 Vertragsende</h2>
<ol class="abs">
<li>Während der Laufzeit und bis 30 Tage nach Vertragsende stellen wir dem Kunden auf Wunsch eine Kopie seiner Website bzw. Anwendungsdaten (Dateien und Datenbank-Export) und der Inhalte aller seiner Postfächer bereit. Wem Quellcode und Nutzungsrechte an der Website zustehen, richtet sich nach dem Vertrag über ihre Erstellung.</li>
<li>Wir geben den AuthCode der Domain heraus und helfen beim Umzug zu einem anderen Anbieter. Ist die Domain 30 Tage nach Vertragsende nicht umgezogen, dürfen wir ihre Registrierung beenden. Das kündigen wir vorher in Textform an und setzen eine Frist von 14 Tagen.</li>
<li>Nach Ablauf der Frist aus Absatz 1 löschen wir die Daten, die wir für den Kunden gespeichert haben (Website bzw. Anwendungsdaten und alle Postfächer). Vertrags- und Abrechnungsdaten bewahren wir auf, wie es die Datenschutzerklärung beschreibt (project.webklar.com/datenschutz.html). Sicherungskopien werden spätestens mit Ablauf ihrer Aufbewahrungszeit (§ 3 Abs. 5) überschrieben.</li>
</ol>
<h2 id="p12">§ 12 Mängel</h2>
<p>Es gelten die gesetzlichen Regeln, für die digitalen Leistungen (Bereitstellung der Website, E-Mail-Postfächer, Zusatzspeicher) insbesondere das gesetzliche Mängelhaftungsrecht für digitale Dienstleistungen (§§ 327 ff. BGB). Mängel unserer Leistungen beheben wir nach Meldung innerhalb angemessener Frist.</p>
<h2 id="p13">§ 13 Haftung</h2>
<ol class="abs">
<li>Wir haften unbeschränkt bei Vorsatz und grober Fahrlässigkeit, bei Verletzung von Leben, Körper oder Gesundheit, nach dem Produkthaftungsgesetz und soweit wir eine Garantie übernommen haben.</li>
<li>Bei leicht fahrlässiger Verletzung einer wesentlichen Vertragspflicht haften wir 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.</li>
<li>Außer in den Fällen von Absatz 1 haften wir für den Verlust von Daten nur in Höhe des Aufwands, der zur Wiederherstellung nötig gewesen wäre, wenn der Kunde seine Pflicht aus § 6 Abs. 4 erfüllt hätte.</li>
<li>Die verschuldensunabhängige Haftung für Mängel, die schon bei Vertragsschluss vorhanden waren (§ 536a Abs. 1 BGB), ist ausgeschlossen.</li>
<li>Diese Regeln gelten auch zugunsten unserer Mitarbeiter und Beauftragten.</li>
</ol>
<h2 id="p14">§ 14 Datenschutz</h2>
<ol class="abs">
<li>Wie wir personenbezogene Daten des Kunden verarbeiten, beschreibt unsere Datenschutzerklärung unter project.webklar.com/datenschutz.html.</li>
<li>Soweit wir im Rahmen eines Abos personenbezogene Daten im Auftrag des Kunden verarbeiten, etwa E-Mails in seinen Postfächern, Eingaben in Kontaktformulare oder Server-Protokolle seiner Website, gilt der Auftragsverarbeitungsvertrag in Anlage 1. Er wird mit der Bestellung des Abos vereinbart und beim Wechsel ins Wartungs-Abo erneut bestätigt.</li>
</ol>
<h2 id="p15">§ 15 Änderungen dieser Bedingungen</h2>
<p>Änderungen teilen wir mindestens 6 Wochen vor ihrem Wirksamwerden in Textform mit. Sie werden nur mit Zustimmung des Kunden wirksam. Ohne Zustimmung gelten die bisherigen Bedingungen weiter, und beide Seiten können nach § 8 kündigen.</p>
<h2 id="p16">§ 16 Schlussbestimmungen</h2>
<ol class="abs">
<li>Es gilt deutsches Recht unter Ausschluss des UN-Kaufrechts. Gegenüber Verbrauchern gilt diese Rechtswahl nur, soweit ihnen dadurch nicht der Schutz zwingender Vorschriften des Staates entzogen wird, in dem sie ihren gewöhnlichen Aufenthalt haben.</li>
<li>Ist der Kunde Kaufmann, juristische Person des öffentlichen Rechts oder öffentlich-rechtliches Sondervermögen, ist Gerichtsstand der Sitz von WEBklar.</li>
<li>Wir sind nicht verpflichtet und nicht bereit, an einem Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.</li>
</ol>
<p class="anlage-kopf neue-seite" id="hosting">Leistungsbeschreibung A</p>
<h2>Hosting-Abo</h2>
<p class="hinweis">20,00 € pro Monat · Endpreis, gemäß § 19 UStG ohne Umsatzsteuer · Mindestlaufzeit 12 Monate · für statische Websites</p>
<h3>1. Umfang</h3>
<ul>
<li>Bereitstellung der Website auf unserem Server in Deutschland mit SSL-Zertifikat, Überwachung der Erreichbarkeit und Updates von Betriebssystem und Server-Software (§ 3 Abs. 2)</li>
<li>1 Domain mit Verwaltung (§ 3 Abs. 3)</li>
<li>1 E-Mail-Postfach mit 5 GB Speicher; mehr Speicher über den Zusatzspeicher (§ 3 Abs. 4, § 10)</li>
<li>tägliche Datensicherung (§ 3 Abs. 5)</li>
<li>Servicezeiten und Reaktion (§ 3 Abs. 6); die Behebung von Störungen an Server, Domain und Postfach in unserem Verantwortungsbereich ist inklusive</li>
</ul>
<h3>2. Nur statische Websites</h3>
<ul>
<li>Das Hosting-Abo ist für statische Websites bestimmt, die wir erstellt oder zur Betreuung übernommen haben. Statisch heißt: Der Webserver liefert die Dateien der Website (HTML, CSS, JavaScript, Bilder) unverändert aus. Dazu gehören auch die von uns erstellten React- und Vite-Seiten.</li>
<li>Nicht möglich sind ein Content-Management-System, ein Login, eine Datenbank und eigener serverseitiger Code (etwa PHP). Solche Teile brauchen laufende Updates; ohne sie wären sie ein Sicherheitsrisiko für den Server, auf dem auch andere Websites und unser Kundenportal laufen.</li>
<li>Kontakt über E-Mail-Links auf der Website ist möglich. Formulare, die Eingaben an einen Server übermitteln, sind im Hosting-Abo nicht enthalten; eine Lösung dafür bieten wir auf Anfrage gesondert an.</li>
<li>Braucht die Website später dynamische Teile, bieten wir den Wechsel ins Wartungs-Abo an (§ 9).</li>
</ul>
<h3>3. Keine Pflege der Website</h3>
<ul>
<li>Updates und Pflege der Website selbst sind nicht enthalten. Weil statische Websites keinen eigenen Code auf dem Server ausführen, sind sie für den sicheren Betrieb in der Regel nicht nötig.</li>
<li>Änderungen an der Website und andere Aufträge führen wir nur nach Freigabe des Kunden aus und berechnen 100 € pro Stunde (Endpreis, gemäß § 19 UStG ohne Umsatzsteuer) in Schritten von 15 Minuten (§ 4 Abs. 7). Ein Stundenkontingent gibt es im Hosting-Abo nicht.</li>
</ul>
<h3>4. Nicht enthalten</h3>
<ul>
<li>Änderungen, neue Funktionen, Redesign oder neue Seiten ohne gesonderten Auftrag (Nr. 3).</li>
<li>Texte, Bilder und Rechtstexte (Impressum, Datenschutzerklärung) für die Website des Kunden, Suchmaschinenoptimierung und Werbung.</li>
<li>Unterstützung für Endgeräte, Betriebssysteme oder E-Mail-Programme des Kunden.</li>
<li>Behebung von Fehlern, die der Kunde oder Dritte durch eigene Änderungen verursacht haben. Das ist als Auftrag nach Nr. 3 möglich.</li>
</ul>
<p class="anlage-kopf" id="wartung">Leistungsbeschreibung B</p>
<h2>Wartungs-Abo</h2>
<p class="hinweis">50,00 € pro Monat · Endpreis, gemäß § 19 UStG ohne Umsatzsteuer · Mindestlaufzeit 12 Monate, auch nach einem Wechsel · für alle Websites und Web-Anwendungen, die wir erstellt oder zur Betreuung übernommen haben</p>
<h3>1. Umfang</h3>
<ul>
<li>Betrieb der Website bzw. Web-Anwendung auf unserem Server in Deutschland mit SSL-Zertifikat, Überwachung der Erreichbarkeit und Updates von Betriebssystem und Server-Software (§ 3 Abs. 2)</li>
<li>Pflege der Website (Nr. 2)</li>
<li>1 Domain mit Verwaltung (§ 3 Abs. 3)</li>
<li>beliebig viele Postfächer, zusammen 5 GB Speicher; mehr Speicher über den Zusatzspeicher (§ 3 Abs. 4, § 10)</li>
<li>tägliche Datensicherung (§ 3 Abs. 5)</li>
<li>1 Stunde Arbeitszeit je bezahltem Monat; nicht genutzte Zeit bleibt 12 Monate verfügbar (§ 4)</li>
<li>Servicezeiten und Reaktion (§ 3 Abs. 6)</li>
</ul>
<h3>2. Pflege der Website</h3>
<ul>
<li>Zusätzlich zu den Updates nach § 3 Abs. 2 spielen wir Sicherheitsupdates für die von uns eingesetzten Software-Komponenten der Website bzw. Web-Anwendung ein, kritische in der Regel innerhalb von 3 Werktagen nach ihrer Veröffentlichung, übrige regelmäßig. Aktualisierungen, die für den vertragsgemäßen Betrieb nötig sind, stellen wir während der gesamten Vertragslaufzeit bereit.</li>
<li>Das Wartungs-Abo eignet sich für alle Websites und Web-Anwendungen, die wir erstellt oder zur Betreuung übernommen haben, auch mit Content-Management-System, Login, Datenbank oder serverseitigem Code.</li>
<li>Änderungswünsche setzen wir über das Stundenkontingent um (§ 4), in der Regel innerhalb von 5 Werktagen.</li>
</ul>
<h3>3. Nicht enthalten</h3>
<ul>
<li>Neue Funktionen, Redesign oder neue Seiten, soweit sie nicht über das Stundenkontingent beauftragt werden.</li>
<li>Texte, Bilder und Rechtstexte (Impressum, Datenschutzerklärung) für die Website des Kunden, Suchmaschinenoptimierung und Werbung.</li>
<li>Unterstützung für Endgeräte, Betriebssysteme oder E-Mail-Programme des Kunden.</li>
<li>Behebung von Fehlern, die der Kunde oder Dritte durch eigene Änderungen verursacht haben. Das ist über das Stundenkontingent möglich.</li>
</ul>
<div class="neue-seite"></div>
<h2 id="widerruf">Widerrufsbelehrung</h2>
<p class="hinweis">Das Widerrufsrecht gilt nur für Verbraucher. Die folgende Belehrung entspricht dem gesetzlichen Muster. Sie gilt für jede Bestellung: den Abschluss eines Abos, den Wechsel ins Wartungs-Abo und die Bestellung von Zusatzspeicher. Hinweise dazu stehen nach dem Muster-Widerrufsformular.</p>
<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. 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>
<p class="meta">Ende der Widerrufsbelehrung</p>
<h3 id="formular">Muster-Widerrufsformular</h3>
<div class="box">
<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 class="meta">(*) Unzutreffendes streichen.</p>
</div>
<h2 id="widerruf-hinweise">Hinweise zum Widerruf</h2>
<p class="hinweis">Diese Hinweise gehören zu diesen Bedingungen, nicht zur gesetzlichen Widerrufsbelehrung. Sie erklären, wie sich das Widerrufsrecht auf die einzelnen Bestellungen auswirkt.</p>
<ol class="abs">
<li>Das Widerrufsrecht gilt für jede Bestellung gesondert: für den Abschluss eines Abos, für den Wechsel ins Wartungs-Abo und für jede Bestellung von Zusatzspeicher. Die Frist von vierzehn Tagen beginnt jeweils mit dem Vertragsschluss (§ 2 Abs. 3 bzw. Abs. 5). Unter project.webklar.com/widerrufen.html wählt der Kunde aus, welche Bestellung er widerruft. Solange eine Widerrufsfrist läuft, zeigt das Kundenportal unter „Verträge“ den letzten Tag der Frist an.</li>
<li>Widerruf des Abos: Widerruft der Kunde den Abschluss seines Abos, endet damit auch der dazu bestellte Zusatzspeicher. Was der Kunde für den Zusatzspeicher gezahlt hat, erstatten wir vollständig; Wertersatz verlangen wir dafür nicht (Nr. 4).</li>
<li>Widerruf des Wechsels: Widerruft der Kunde den Wechsel ins Wartungs-Abo, läuft sein Hosting-Abo zu den bisherigen Bedingungen weiter, als hätte es den Wechsel nicht gegeben, einschließlich seiner ursprünglichen Mindestlaufzeit. Wir erstatten den für das Wartungs-Abo gezahlten Mehrbetrag. Mehrbetrag ist, was der Kunde seit dem Wechsel mehr gezahlt hat, als er ohne den Wechsel für das Hosting-Abo gezahlt hätte. Weil der Kunde beim Wechsel den sofortigen Beginn verlangt hat, ziehen wir davon den Wertersatz für die bis zum Widerruf erbrachte Leistung ab (§ 357a Abs. 2 BGB): den anteiligen Mehrpreis des Wartungs-Abos gegenüber dem Hosting-Abo für die Tage vom Beginn des Wartungs-Abos bis zum Widerruf.</li>
<li>Widerruf des Zusatzspeichers: Wir stellen bestellten Zusatzspeicher sofort bereit, ohne dass der Kunde das ausdrücklich verlangen muss. Widerruft er die Bestellung, nehmen wir die Pakete zurück und erstatten alles, was er dafür gezahlt hat. Wertersatz verlangen wir nicht. Passen die gespeicherten E-Mails danach nicht mehr in den verbleibenden Speicher, nimmt das betroffene Postfach keine neuen E-Mails mehr an, bis wieder Platz ist (§ 3 Abs. 4).</li>
</ol>
<p class="anlage-kopf neue-seite" id="avv">Anlage 1</p>
<h2>Auftragsverarbeitungsvertrag nach Art. 28 DSGVO</h2>
<p class="hinweis">zwischen dem Kunden als Verantwortlichem („Kunde“) und der WEBklar GbR als Auftragsverarbeiter („WEBklar“). Der Vertrag wird mit der Bestellung des Hosting-Abos oder des Wartungs-Abos vereinbart; beim Wechsel ins Wartungs-Abo bestätigt der Kunde ihn erneut.</p>
<h3>1. Gegenstand und Dauer</h3>
<p>WEBklar verarbeitet personenbezogene Daten im Auftrag des Kunden, soweit das für die Leistungen des Hosting-Abos oder des Wartungs-Abos (Bereitstellung bzw. Betrieb und Pflege der Website, Domain, E-Mail-Postfächer einschließlich Zusatzspeicher, Unterstützung) sowie für gesondert vereinbarte Zusatzleistungen zum Abo, etwa eine Formularlösung, nötig ist. Der Vertrag gilt für die Dauer des Abos, auch über einen Wechsel zwischen den Abos hinweg, und darüber hinaus, bis alle Daten nach Nummer 8 gelöscht oder zurückgegeben sind.</p>
<h3>2. Art und Zweck der Verarbeitung, Datenarten, Betroffene</h3>
<table>
<tr><td>Art der Verarbeitung</td><td>Speichern, Bereitstellen, Übermitteln (Empfang und Versand von E-Mails, Weiterleiten von Kontaktformular-Eingaben per E-Mail an den Kunden), Sichern, Wiederherstellen und Löschen; Einsicht, soweit sie für Wartung und Unterstützung nötig ist</td></tr>
<tr><td>Zweck</td><td>Betrieb der Website bzw. Web-Anwendung und der E-Mail-Postfächer des Kunden sowie gesondert vereinbarter Zusatzleistungen</td></tr>
<tr><td>Datenarten</td><td>Inhalte der Website; Eingaben in Formulare (z. B. Name, Kontaktdaten, Nachricht), soweit die Website solche vorsieht; Inhalte der Postfächer (E-Mails mit Anhängen, Absender, Empfänger, Zeitpunkte); Verbindungsdaten in Server-Protokollen (IP-Adresse, Zeitpunkt, aufgerufene Seite, Browserkennung); Benutzerkonten, soweit die Anwendung solche vorsieht</td></tr>
<tr><td>Betroffene</td><td>Besucher der Website; Nutzer der Postfächer des Kunden, insbesondere seine Beschäftigten; Absender und Empfänger von E-Mails; Kunden, Interessenten und Beschäftigte des Kunden; weitere Personen, deren Daten der Kunde über seine Website oder seine Postfächer verarbeitet</td></tr>
</table>
<p>Besondere Kategorien personenbezogener Daten (Art. 9 DSGVO) verarbeitet der Kunde über seine Website oder seine Postfächer nur nach vorheriger Mitteilung an WEBklar.</p>
<h3>3. Weisungen</h3>
<p>WEBklar verarbeitet die Daten nur auf dokumentierte Weisung des Kunden, auch bei Übermittlungen in Drittländer, es sei denn, eine gesetzliche Pflicht verlangt etwas anderes. In diesem Fall teilt WEBklar dem Kunden die Pflicht vorher mit, soweit das Gesetz es erlaubt. Die Weisungen ergeben sich aus diesem Vertrag und den Bedingungen der Abos. Weitere Weisungen erteilt der Kunde in Textform, etwa im Chat des Kundenportals. Hält WEBklar eine Weisung für rechtswidrig, weist es den Kunden unverzüglich darauf hin.</p>
<h3>4. Vertraulichkeit</h3>
<p>WEBklar setzt nur Personen ein, die zur Vertraulichkeit verpflichtet sind oder einer gesetzlichen Verschwiegenheitspflicht unterliegen.</p>
<h3>5. Sicherheit der Verarbeitung</h3>
<p>WEBklar trifft die technischen und organisatorischen Maßnahmen nach Art. 32 DSGVO, die in Anlage 2 beschrieben sind. WEBklar darf sie an den Stand der Technik anpassen, sofern das Schutzniveau nicht sinkt.</p>
<h3>6. Unterauftragsverarbeiter</h3>
<p>Der Kunde genehmigt die in Anlage 3 genannten Unterauftragsverarbeiter. Will WEBklar einen Unterauftragsverarbeiter hinzuziehen oder ersetzen, teilt es das dem Kunden mindestens vier Wochen vorher in Textform mit. Der Kunde kann aus wichtigem datenschutzrechtlichem Grund widersprechen. Einigen sich die Parteien nicht, können beide das Abo zu dem Zeitpunkt kündigen, zu dem der neue Unterauftragsverarbeiter eingesetzt werden soll. WEBklar verpflichtet jeden Unterauftragsverarbeiter vertraglich auf dieselben Datenschutzpflichten, die sich aus diesem Vertrag ergeben.</p>
<h3>7. Unterstützung des Kunden</h3>
<ul>
<li>WEBklar leitet Anfragen betroffener Personen, die an WEBklar gerichtet sind, unverzüglich an den Kunden weiter und unterstützt ihn bei der Beantwortung, soweit das mit den eingesetzten Systemen möglich ist.</li>
<li>WEBklar unterstützt den Kunden bei seinen Pflichten aus Art. 32 bis 36 DSGVO (Sicherheit, Meldung von Datenschutzverletzungen, Datenschutz-Folgenabschätzung).</li>
<li>Verletzungen des Schutzes personenbezogener Daten meldet WEBklar dem Kunden unverzüglich, möglichst innerhalb von 24 Stunden nach Kenntnis, mit den Informationen, die zu diesem Zeitpunkt vorliegen.</li>
</ul>
<h3>8. Löschung und Rückgabe</h3>
<p>Nach Ende des Abos gibt WEBklar die Daten nach Wahl des Kunden zurück (§ 11 Abs. 1 der Bedingungen) oder löscht sie, soweit keine gesetzliche Pflicht zur Speicherung besteht. Sicherungskopien werden im Rahmen der regulären Rotation überschrieben.</p>
<h3>9. Nachweise und Kontrollen</h3>
<p>WEBklar stellt dem Kunden alle Informationen zur Verfügung, die zum Nachweis der Pflichten aus Art. 28 DSGVO nötig sind. WEBklar ermöglicht Überprüfungen einschließlich Inspektionen durch den Kunden oder einen von ihm beauftragten, zur Verschwiegenheit verpflichteten Prüfer. Sie finden nach Anmeldung mit angemessener Frist (in der Regel 14 Tage) zu den Servicezeiten statt, ohne den Betrieb unzumutbar zu stören. Für das Rechenzentrum genügen in der Regel die Nachweise des Rechenzentrumsbetreibers (Zertifizierung nach ISO/IEC 27001). Für mehr als eine Vor-Ort-Kontrolle pro Jahr kann WEBklar eine angemessene Vergütung verlangen.</p>
<h3>10. Schlussbestimmungen</h3>
<p>Für die Haftung gilt Art. 82 DSGVO. Bei Widersprüchen geht dieser Vertrag in Datenschutzfragen den Bedingungen der Abos vor.</p>
<p class="anlage-kopf neue-seite" id="tom">Anlage 2</p>
<h2>Technische und organisatorische Maßnahmen (Art. 32 DSGVO)</h2>
<table>
<tr><th style="width:28%">Bereich</th><th>Maßnahmen</th></tr>
<tr><td>Rechenzentrum und Zutritt</td><td>Betrieb im Rechenzentrum der Hetzner Online GmbH in Falkenstein (Deutschland). Hetzner ist nach ISO/IEC 27001 zertifiziert. Zutritt, Stromversorgung, Klimatisierung und Brandschutz regeln die Maßnahmen von Hetzner.</td></tr>
<tr><td>Zugang zu den Systemen</td><td>Serververwaltung nur über verschlüsselte SSH-Verbindung, Zugang nur für die Gesellschafter. Automatische Sperre bei wiederholten Fehlversuchen. Firewall des Rechenzentrumsbetreibers, die nur die benötigten Dienste zulässt. Zwei-Faktor-Anmeldung für die Verwaltungskonten bei Hetzner und Stripe.</td></tr>
<tr><td>Zugriff auf Daten</td><td>Zugriff nur für die Gesellschafter und beauftragte Mitarbeiter, soweit ihre Aufgabe es erfordert. Im Kundenportal sieht jeder Kunde nur seine eigenen Daten. Datenbanken sind nur für die jeweilige Anwendung erreichbar. Zugangsdaten zu E-Mail-Postfächern erhalten nur der Kunde und die von ihm in Textform benannten Personen.</td></tr>
<tr><td>Übertragung</td><td>Verschlüsselte Übertragung (TLS) für alle Websites, das Kundenportal und die Verwaltung. E-Mail-Versand und -Abruf nur verschlüsselt.</td></tr>
<tr><td>Nachvollziehbarkeit</td><td>Änderungen an Websites laufen über eine Versionsverwaltung (Git) und sind nachvollziehbar. Aufträge und Weisungen werden im Ticketsystem bzw. im Chat des Kundenportals dokumentiert.</td></tr>
<tr><td>Verfügbarkeit und Wiederherstellung</td><td>Tägliche Sicherung des gesamten Servers, verfügbar sind die letzten sieben Tage. Tägliche Datenbank-Exporte vor der Serversicherung. Automatische Überwachung der Erreichbarkeit. Automatische Sicherheitsupdates des Betriebssystems. Wiederherstellung wird regelmäßig getestet.</td></tr>
<tr><td>Trennung</td><td>Websites der Kunden in getrennten Verzeichnissen bzw. Containern. Vorschau- und Testversionen sind nur nach Anmeldung erreichbar. Postfächer und Speichergrenzen werden je Kunde getrennt verwaltet.</td></tr>
<tr><td>Organisation</td><td>Verpflichtung aller Beteiligten auf Vertraulichkeit. Festgelegter Ablauf für die Meldung von Datenschutzverletzungen. Löschung nach Vertragsende (§ 11 der Bedingungen). Server-Protokolle werden nach 14 Tagen gelöscht. Die Maßnahmen werden mindestens einmal im Jahr überprüft.</td></tr>
</table>
<p class="anlage-kopf" id="sub">Anlage 3</p>
<h2>Unterauftragsverarbeiter</h2>
<table>
<tr><th>Unternehmen</th><th>Leistung</th><th>Ort der Verarbeitung</th></tr>
<tr><td>Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen</td><td>Server-Hosting und Datensicherung</td><td>Deutschland (Rechenzentrum Falkenstein)</td></tr>
<tr><td>Hetzner Online GmbH, Industriestraße 25, 91710 Gunzenhausen</td><td>E-Mail-Postfächer (Webhosting)</td><td>Deutschland (Rechenzentrum Nürnberg)</td></tr>
</table>
<p class="hinweis">Die Domain registrieren wir über die Hetzner Online GmbH als Domain-Registrar. Für die Daten des Domaininhabers sind Registrar und Vergabestelle (bei .de-Domains die DENIC eG) eigenständig verantwortlich; sie sind insoweit keine Unterauftragsverarbeiter. Die Zahlungsabwicklung über Stripe betrifft das Vertragsverhältnis zwischen Kunde und WEBklar und ist nicht Gegenstand dieses Auftragsverarbeitungsvertrags.</p>
</main>
</body>
</html>
+122 -12
View File
@@ -18,18 +18,25 @@
// dasselbe für die Widerrufsbelehrung der Projektaufträge (Wortlaut aus server/services/auftraege/texte.js)
// und die AGB-Kopie (Quelle docs/rechtliches/agb.md)
//
// node scripts/build-legal-html.mjs funktionen6 [--check] [--nur=<doc,doc>]
// die zehn Funktionsunterlagen der Fassung 6 (Leistungsbeschreibungen und AVV, Quellen docs/rechtliches/<slug>-v6.md)
// im neuen Stil (meta.stil 'neu') unter public/neu/rechtliches/<slug>-<Fassung>.html; --nur= begrenzt auf die
// genannten Dokumente (news-lb, termine-lb, termine-avv, shop-lb, shop-avv, unterkunft-lb, unterkunft-avv, zeiten-lb,
// analyse-lb, analyse-avv; auch der Dateiname ohne Fassung, z. B. avv-webshop). Solange ENTWURF_V6 gilt, tragen die Seiten meta robots noindex.
//
// Fassung = config.termine.avvVersion (Satz termine), config.shop.fassung (Satz shop) bzw. config.unterkunft.fassung (Satz unterkunft) 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,
// damit nie eine Seite mit falsch dargestelltem Rechtstext entsteht.
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
import { basename, join } from 'node:path'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { basename, dirname, 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'
import { FUNKTIONEN_UNTERLAGEN_V6 } from '../server/services/products.js'
const ROOT = fileURLToPath(new URL('..', import.meta.url))
const FASSUNG_RE = /^\d{4}-\d{2}-\d{2}$/
@@ -190,6 +197,44 @@ const STYLE = (seitenkopf) => ` :root { color-scheme: light; }
}
`
/**
* Neue Seitenhülle (Fassung 6, Spec 5.1): weiße Fläche, Text #1a1f36, Zweittext #5e6678, Linien #ebeef1/#d8dde3, Akzent #111, Systemschrift,
* keine Karten, kein Lila, kein Gewicht 800, kein Dunkelmodus. Dieselben Tokens wie die Abo-Bedingungen unter public/neu/rechtliches/.
*/
const STYLE_NEU = (seitenkopf) => ` :root { color-scheme: light; }
* { box-sizing: border-box; }
body { margin: 0; background: #fff; color: #1a1f36; font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-variant-numeric: tabular-nums; }
main { max-width: 780px; margin: 0 auto; padding: 44px 20px 80px; }
.brand { font-weight: 600; letter-spacing: 0.12em; font-size: 13px; color: #111; }
h1 { font-size: 30px; line-height: 1.2; margin: 6px 0 6px; font-weight: 600; }
h2 { font-size: 20px; margin: 36px 0 10px; padding-top: 14px; border-top: 1px solid #ebeef1; font-weight: 600; }
h3 { font-size: 16.5px; margin: 22px 0 6px; font-weight: 600; }
p, li { margin: 6px 0; }
ul { padding-left: 24px; }
.meta { color: #5e6678; font-size: 14px; }
.box { border-top: 1px solid #d8dde3; border-bottom: 1px solid #d8dde3; padding: 14px 0; margin: 18px 0; }
.box.widerspruch { border-top: 2px solid #111; border-bottom: 2px solid #111; }
table { border-collapse: collapse; width: 100%; font-size: 15px; }
td, th { padding: 6px 10px 6px 0; border-bottom: 1px solid #ebeef1; vertical-align: top; text-align: left; }
th { font-size: 13px; text-transform: uppercase; letter-spacing: 0.04em; color: #5e6678; background: #f8f9fb; padding-left: 8px; }
a { color: #111; text-decoration: underline; text-decoration-color: #d8dde3; text-underline-offset: 2px; }
a:hover { background: #f6f8fa; }
nav.top { font-size: 14px; margin-bottom: 22px; }
.offen { display: inline-block; background: #fcedb9; color: #a82c00; padding: 1px 10px; border-radius: 6px; font-weight: 600; }
@media print {
@page { size: A4; margin: 18mm 17mm 18mm;
@top-left { content: "WEBklar · ${cssText(seitenkopf)}"; font: 8pt sans-serif; color: #8a93a3; }
@bottom-right { content: "Seite " counter(page) " von " counter(pages); font: 8pt sans-serif; color: #8a93a3; } }
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; }
}
`
/** Adresse eines Links der Navigation: relativ zur Wurzel ("/…", nicht "//") oder https; kein Rückwärtsschrägstrich (Browser lesen ihn wie "/"). */
function pruefeHref(href) {
if (typeof href !== 'string' || !(/^\/(?!\/)/.test(href) || /^https:\/\/[^\s"'<>]+$/.test(href)) || /[\s"'<>\\]/.test(href)) {
@@ -231,8 +276,10 @@ function bloeckeHtml(bloecke) {
/**
* Rendert ein Dokument in die Seitenhülle der Rechtstexte.
* @param {string} md Markdown (Teilmenge, siehe Kopf); erste Zeile "# Titel", danach der Absatz "**Fassung JJJJ-MM-TT**[ · Zusatz]"
* @param {{ fassung: string, titel: string, seitenkopf: string, nav: Array<{ href: string, text: string }> }} meta
* titel = Seitentitel (vor " – WEBklar"), seitenkopf = Kopfzeile jeder gedruckten Seite hinter "WEBklar · "
* @param {{ fassung: string, titel: string, seitenkopf: string, nav: Array<{ href: string, text: string }>, stil?: 'neu', noindex?: boolean }} meta
* titel = Seitentitel (vor " – WEBklar"), seitenkopf = Kopfzeile jeder gedruckten Seite hinter "WEBklar · ";
* stil = 'neu' wählt die weiße Hülle der Fassung 6 (ohne stil bleibt die Ausgabe bytegleich zur bisherigen Hülle);
* noindex = true ergänzt meta robots noindex (Entwurfsseiten)
* @returns {string} HTML mit abschließendem Zeilenumbruch
*/
export function render(md, meta) {
@@ -241,6 +288,7 @@ export function render(md, meta) {
if (typeof meta[feld] !== 'string' || meta[feld].trim() === '') throw new Error(`render: meta.${feld} fehlt`)
}
if (!Array.isArray(meta.nav) || meta.nav.length === 0) throw new Error('render: meta.nav fehlt')
if (meta.stil !== undefined && meta.stil !== 'neu') throw new Error(`render: meta.stil ist unbekannt (erlaubt: 'neu' oder weglassen): ${String(meta.stil).slice(0, 30)}`)
const bloecke = parse(md)
if (bloecke[0]?.art !== 'h' || bloecke[0].ebene !== 1) throw new MarkdownFehler(1, 'die erste Zeile muss "# Titel" sein')
if (bloecke.filter((b) => b.art === 'h' && b.ebene === 1).length !== 1) throw new MarkdownFehler(1, 'genau eine Überschrift #')
@@ -254,9 +302,9 @@ export function render(md, meta) {
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="webklar-fassung" content="${esc(meta.fassung)}">
<title>${esc(meta.titel)} – WEBklar</title>
${meta.noindex === true ? ' <meta name="robots" content="noindex">\n' : ''} <title>${esc(meta.titel)} – WEBklar</title>
<style>
${STYLE(meta.seitenkopf)} </style>
${(meta.stil === 'neu' ? STYLE_NEU : STYLE)(meta.seitenkopf)} </style>
</head>
<body>
<main>
@@ -338,7 +386,49 @@ export function unterkunftDokumente(fassung = config.unterkunft.fassung) {
)
}
/** Sätze der Kommandozeile: Name => Dokumente. */
/** Namen der Funktionen in Titel und Kopfzeile der Fassung 6 (Schlüssel wie in FUNKTIONEN_UNTERLAGEN_V6). */
const FUNKTIONEN6_NAMEN = {
news: 'News-Pinnwand',
termine: 'Terminplaner',
shop: 'Webshop',
unterkunft: 'Buchungskalender',
zeiten: 'Öffnungszeiten live',
analyse: 'Website-Analyse',
}
/**
* Entwurfsstand der Fassung 6: solange true, tragen die Funktionsseiten meta robots noindex (Nachtrag N45). Bei der Freigabe auf false setzen,
* zusammen mit dem Entfernen der Entwurfsmarke aus den Quellen (Ablauf in docs/rechtliches/kundenansicht-entscheidungen.md).
*/
export const ENTWURF_V6 = true
/**
* Die zehn Funktionsunterlagen der Fassung 6 (Leistungsbeschreibungen und AVV, Reihenfolge wie FUNKTIONEN_UNTERLAGEN_V6) im neuen Stil.
* doc = '<key>-lb' | '<key>-avv'; Quelle docs/rechtliches/<slug>-v6.md, Ziel public/neu/rechtliches/<slug>-<Fassung>.html (Adressen aus products.js).
* Die Navigation verweist nur auf Seiten im Vorschaupfad, nie auf die eingefrorenen Seiten unter /rechtliches/.
*/
export function funktionen6Dokumente() {
return Object.entries(FUNKTIONEN_UNTERLAGEN_V6).flatMap(([key, unterlagen]) => {
const name = FUNKTIONEN6_NAMEN[key]
const dokumente = [
{ art: 'lb', d: unterlagen.lb, titel: `Leistungsbeschreibung ${name}`, seitenkopf: `Leistungsbeschreibung ${name}` },
...(unterlagen.avv ? [{ art: 'avv', d: unterlagen.avv, titel: `Auftragsverarbeitungsvertrag ${name}`, seitenkopf: `AVV ${name}` }] : []),
]
const nav = [
{ href: '/dashboard.html', text: '← Kundenportal' },
{ href: '/neu/rechtliches/abo-bedingungen.html', text: 'Abo-Bedingungen' },
...dokumente.map((x) => ({ href: x.d.url, text: x.seitenkopf })),
{ href: '/datenschutz.html', text: 'Datenschutzerklärung' },
{ href: 'https://webklar.com/impressum', text: 'Impressum' },
]
return dokumente.map((x) => ({
doc: `${key}-${x.art}`,
md: `docs/rechtliches/${basename(x.d.url, `-${x.d.version}.html`)}-v6.md`,
html: `public${x.d.url}`,
meta: { fassung: x.d.version, titel: x.titel, seitenkopf: x.seitenkopf, nav, stil: 'neu', noindex: ENTWURF_V6 },
}))
})
}
/** Markdown eines Dokuments; trägt die Quelle eine andere Fassung (d.stempel), wird die Fassungszeile auf die des Dokuments gesetzt. */
export function quelleVon(d, root = ROOT) {
@@ -407,23 +497,42 @@ export function auftraegeDokumente(texteFassung = TEXTE_FASSUNG, agbFassung = AG
}
/** Sätze der Kommandozeile: Name => Dokumente. */
const SAETZE = { termine: termineDokumente, shop: shopDokumente, unterkunft: unterkunftDokumente, auftraege: auftraegeDokumente }
const SAETZE = { termine: termineDokumente, shop: shopDokumente, unterkunft: unterkunftDokumente, auftraege: auftraegeDokumente, funktionen6: funktionen6Dokumente }
// ---------------------------------------------------------------------------------------------------------- Aufruf
/**
* Kommandozeile: `termine|shop|unterkunft|auftraege [--check]`. root = Wurzel des Projekts (Tests übergeben ein Verzeichnis mit Kopien).
* Kommandozeile: `termine|shop|unterkunft|auftraege [--check]` und `funktionen6 [--check] [--nur=<doc,doc>]`. 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|unterkunft [--check]\n node scripts/build-legal-html.mjs auftraege [--check]')
const nurOptionen = optionen.filter((o) => o.startsWith('--nur='))
const usage = () => fehler(
'Aufruf: node scripts/build-legal-html.mjs termine|shop|unterkunft [--check]\n'
+ ' node scripts/build-legal-html.mjs auftraege [--check]\n'
+ ' node scripts/build-legal-html.mjs funktionen6 [--check] [--nur=<doc,doc>]',
)
if (!Object.hasOwn(SAETZE, satz ?? '') || optionen.some((o) => o !== '--check' && !o.startsWith('--nur=')) || nurOptionen.length > 1 || (nurOptionen.length === 1 && satz !== 'funktionen6')) {
usage()
return 2
}
let dokumente = SAETZE[satz]()
if (nurOptionen.length === 1) {
// erlaubt sind der Name des Dokuments (doc, z. B. shop-avv) und der Name seiner Datei ohne Fassung und Endung (z. B. avv-webshop)
const namen = (d) => [d.doc, basename(d.html).replace(/-\d{4}-\d{2}-\d{2}\.html$/, '')]
const gewuenscht = nurOptionen[0].slice('--nur='.length).split(',').map((x) => x.trim())
if (gewuenscht.some((x) => !dokumente.some((d) => namen(d).includes(x)))) {
fehler(`Unbekanntes Dokument in --nur=. Erlaubt: ${dokumente.map((d) => d.doc).join(', ')} (oder der Dateiname ohne Fassung)`)
usage()
return 2
}
dokumente = dokumente.filter((d) => namen(d).some((n) => gewuenscht.includes(n)))
}
let abweichungen = 0
for (const d of SAETZE[satz]()) {
for (const d of dokumente) {
const html = render(d.markdown ? d.markdown() : quelleVon(d, root), d.meta)
const ziel = join(root, d.html)
if (pruefen) {
@@ -431,6 +540,7 @@ export function lauf(argv, { root = ROOT, log = console.log, fehler = console.er
log(`${gleich ? 'ok ' : 'ABWEICHT'} ${d.html}`)
if (!gleich) abweichungen += 1
} else {
mkdirSync(dirname(ziel), { recursive: true })
writeFileSync(ziel, html)
log(`-> ${d.html}`)
}
+46 -1
View File
@@ -15,6 +15,11 @@
# 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.
# abo6 = Abo-Bedingungen Fassung 6 (public/neu/rechtliches/abo-bedingungen.html -> WEBklar-Abo-Vertragsunterlagen.pdf daneben, <PDF>.sha256 mit der SHA-256 der
# Seite, Neudruck nur bei geaenderter Seite, FORCE=1 druckt neu). Entwurf; erst nach der Freigabe drucken, vorher `free -m`, mit `nice`. `all` druckt diese Datei nicht.
# funktionen6 = die zehn Funktionsunterlagen der Fassung 6 (Leistungsbeschreibungen und AVV, Fassung FUNKTIONEN6_VERSION, Standard: FUNKTIONEN_FASSUNG_V6 aus
# server/services/products.js) aus public/neu/rechtliches/<slug>-<Fassung>.html nach <slug>-<Fassung>.pdf; die HTML-Seiten kommen vorher aus
# `node scripts/build-legal-html.mjs funktionen6`, Pruefsumme und Neudruck wie bei abo6. Entwurf; erst nach der Freigabe drucken, vorher `free -m`, mit `nice`.
# 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.
@@ -82,6 +87,16 @@ agb_version() {
echo "$v"
}
# Fassung der Funktionsunterlagen 6: FUNKTIONEN6_VERSION, sonst FUNKTIONEN_FASSUNG_V6 aus server/services/products.js
funktionen6_version() {
local v="${FUNKTIONEN6_VERSION:-}"
if [ -z "$v" ]; then
v="$(timeout 60 node --max-old-space-size=256 --input-type=module -e "const { FUNKTIONEN_FASSUNG_V6 } = await import('./server/services/products.js'); console.log(FUNKTIONEN_FASSUNG_V6)")"
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
@@ -173,6 +188,36 @@ datenschutz)
echo "$summe" > "$ziel.sha256"
fi
;;
abo6)
# Entwurf Fassung 6 der Abo-Bedingungen: Seite und PDF liegen im Vorschaupfad; erst nach der Freigabe drucken
quelle="public/neu/rechtliches/abo-bedingungen.html"
ziel="public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf"
[ -f "$quelle" ] || { echo "HTML fehlt: $quelle"; 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
;;
funktionen6)
# Entwurf Fassung 6 der Funktionsunterlagen (zehn Seiten): Leistungsbeschreibungen und AVV, nacheinander, nie parallel
fassung="$(funktionen6_version)"
for slug in leistungsbeschreibung-news leistungsbeschreibung-terminplaner avv-terminplaner leistungsbeschreibung-webshop avv-webshop \
leistungsbeschreibung-unterkunft avv-unterkunft leistungsbeschreibung-zeiten leistungsbeschreibung-website-analyse avv-website-analyse; do
quelle="public/neu/rechtliches/$slug-$fassung.html"
ziel="public/neu/rechtliches/$slug-$fassung.pdf"
[ -f "$quelle" ] || { echo "HTML fehlt: $quelle (zuerst node scripts/build-legal-html.mjs funktionen6)"; 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
;;
all)
pdf docs/leitfaden-empfehlungsgeber.html docs/WEBklar-Leitfaden-Empfehlungsgeber.pdf
# Fassung 3 (public/WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf, Archiv unter public/vertragsunterlagen/v3/) wird nicht neu erzeugt
@@ -182,6 +227,6 @@ all)
auftraege_pdfs
;;
*)
echo "Usage: $0 [all|analyse|termine|shop|unterkunft|datenschutz]"; echo " $0 auftraege (Widerrufsbelehrung der Projektauftraege und AGB)"; exit 1
echo "Usage: $0 [all|analyse|termine|shop|unterkunft|datenschutz]"; echo " $0 auftraege (Widerrufsbelehrung der Projektauftraege und AGB)"; echo " $0 abo6|funktionen6 (Entwurf Fassung 6: Abo-Bedingungen bzw. Funktionsunterlagen, erst nach der Freigabe)"; exit 1
;;
esac
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env node
/**
* Kundenansicht neu (Paket P5, Abo-Modell Kern): legt die neuen Attribute an. Der Code schreibt sie nur, wenn ein
* Aufrufer die zugehoerigen Parameter uebergibt; ohne dieses Skript bleibt die alte Ansicht unberuehrt.
* - featureConsents.kundenart: Kundenart zum Klickzeitpunkt im Nachweis ('privat' | 'unternehmen')
* - workorders.einrichtungFuer: Schluessel der Funktion, die das Einrichtungs-Ticket betrifft (laengster: 'unterkunft')
* - workorders.einrichtungAbrechnung: 'abo' | 'projekt' | '' (Abrechnung der Einrichtung am Ticket)
* - portalFeatures.einrichtungAbrechnung: dieselbe Abrechnung am Funktionsdokument (Nachlegen und Heilen setzen
* den Marker am Ticket erneut)
* - portalFeatures.unlockedBy: 'kauf' | 'abo' | 'team' | 'projekt' (steht schon im Zeiten-Schema; fehlt es, legt
* dieses Skript es an)
* Idempotent: Vorhandenes wird uebersprungen. Das Skript loescht nichts und laeuft in Entwicklung und Tests nie
* gegen Produktion; dort nur auf Anweisung des Controllers nach OK des Inhabers, vorher immer mit --pruefen.
* scripts/setup-abo-collections.mjs, feature-setup.mjs und zeiten-setup.mjs bleiben unveraendert.
* Usage:
* node scripts/kundenansicht-setup.mjs --pruefen (Alias --check; nur melden, nichts anlegen; Exit 1 bei Luecken)
* node scripts/kundenansicht-setup.mjs (Fehlendes anlegen)
*
* config.js und der Appwrite-Client werden erst in main() geladen: test/support/schema.js importiert die
* Attributlisten, bevor die Tests ihre Umgebung (useTestEnv) setzen koennen.
*/
import { pathToFileURL } from 'node:url'
import { ZEITEN_FEATURE_ATTRIBUTES } from '../server/services/zeiten/schema.js'
const str = (key, size) => ({ key, type: 'string', size, required: false })
const unlockedBy = ZEITEN_FEATURE_ATTRIBUTES.find((a) => a.key === 'unlockedBy')
export const KUNDENANSICHT_ATTRIBUTE = {
featureConsents: [str('kundenart', 12)],
workorders: [str('einrichtungFuer', 16), str('einrichtungAbrechnung', 8)],
portalFeatures: [unlockedBy, str('einrichtungAbrechnung', 8)],
}
export async function main({ pruefen = false, client, log = console.log } = {}) {
const { config } = await import('../server/config.js')
let api = client
if (!api) {
const { createAppwriteClient } = await import('./analyse-setup.mjs')
api = createAppwriteClient()
}
const missing = []
const plan = []
// Erst alles pruefen, dann (ohne --pruefen) anlegen, damit --pruefen garantiert nichts veraendert
for (const [name, attributes] of Object.entries(KUNDENANSICHT_ATTRIBUTE)) {
const id = config.collections[name]
const exists = await api.collectionExists(id)
if (!exists && !pruefen) throw new Error(`Collection ${id} fehlt – zuerst das jeweilige Setup-Skript ausfuehren`)
if (!exists) log(`- ${id} FEHLT`)
const have = exists ? new Set((await api.listAttributes(id)).map((a) => a.key)) : new Set()
for (const attr of attributes) {
if (have.has(attr.key)) log(`= ${id}.${attr.key} (existiert)`)
else if (pruefen) log(`- ${id}.${attr.key} FEHLT`)
}
const absent = attributes.filter((a) => !have.has(a.key))
absent.forEach((a) => missing.push({ ...a, collection: id }))
plan.push({ id, absent })
}
const result = { missing }
if (pruefen) {
log(missing.length ? `${missing.length} Eintrag/Eintraege fehlen.` : 'Alles vorhanden.')
return result
}
for (const { id, absent } of plan) {
for (const attr of absent) {
await api.createAttribute(id, attr)
log(`+ Attribut ${id}.${attr.key}`)
}
}
for (const { id, absent } of plan) {
for (const attr of absent) await api.waitForAttribute(id, attr.key)
}
log('Fertig.')
return result
}
const invokedDirectly = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
if (invokedDirectly) {
const pruefen = process.argv.includes('--pruefen') || process.argv.includes('--check')
main({ pruefen })
.then(({ missing }) => process.exit(pruefen && missing.length ? 1 : 0))
.catch((e) => {
console.error(e.message)
process.exit(1)
})
}
+325
View File
@@ -0,0 +1,325 @@
#!/usr/bin/env node
// Lädt die Rechtstexte der Fassung 6 (Kundenansicht neu) nach der Freigabe in WOMS „Rechtliches“ hoch (Bucket legal-files, Collection
// legalVersions), wie es die WOMS-Oberfläche tut: neue Fassung "current", die bisherige gültige wird "superseded" mit validTo. Ein Dokument
// je Aufruf, nur mit --freigabe=JJJJ-MM-TT; schon hochgeladene Fassungen (gleiche source) werden übersprungen.
//
// node scripts/rechtliches-hochladen.mjs --probe nur lesen: Schlüssel, Bucket, Versionen und Karten; jede fehlende Karte ist ein Stopp
// node scripts/rechtliches-hochladen.mjs --karten-anlegen legt die fehlenden Karten in legalDocuments an (vorhandene nie), einmal vor dem ersten Hochladen
// node scripts/rechtliches-hochladen.mjs <documentKey> --freigabe=JJJJ-MM-TT
// lädt genau ein Dokument hoch (validFrom = Freigabedatum); ohne Karte Abbruch vor dem Schreiben
// node scripts/rechtliches-hochladen.mjs --pruefen nur lesen: Nachkontrolle, Exit 0 nur, wenn alle elf Dokumente eine aktuelle Version
// mit passender source haben
//
// Schlüssel: kurzlebiger Appwrite-Schlüssel mit files.read/files.write/documents.read/documents.write in /root/.appwrite-rechtliches.key (chmod 600).
// Der Import dieser Datei liest weder den Schlüssel noch nutzt er das Netz; beides geschieht nur in main (deps.schluessel, deps.fetch).
// Dieses Skript wird im Paket P8 nur geschrieben und getestet (mit Nachbau-deps), nie ausgeführt.
import { createHash } from 'node:crypto'
import { execFileSync } from 'node:child_process'
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { FUNKTIONEN_UNTERLAGEN_V6 } from '../server/services/products.js'
const ENDPOINT = 'https://ticket.webklar.com/v1'
const PROJECT = '6a1058610003c5a13a05'
const DB = 'woms-database'
const BUCKET = 'legal-files'
const KEY_DATEI = '/root/.appwrite-rechtliches.key'
const ROOT = fileURLToPath(new URL('..', import.meta.url))
const LIVE = 'https://project.webklar.com/'
const USAGE = 'Aufruf: node scripts/rechtliches-hochladen.mjs --probe | --karten-anlegen | --pruefen | <documentKey> --freigabe=JJJJ-MM-TT (genau ein Dokument je Aufruf)'
// ---------------------------------------------------------------------------------------------------------- Dokumente
const NAMEN = {
news: 'News-Pinnwand',
termine: 'Terminplaner',
shop: 'Webshop',
unterkunft: 'Buchungskalender',
zeiten: 'Öffnungszeiten live',
analyse: 'Website-Analyse',
}
const ABO = Object.freeze({
documentKey: 'abo-bedingungen',
dateien: Object.freeze(['public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf', 'public/neu/rechtliches/abo-bedingungen.html']),
titel: 'Abo-Bedingungen',
quelle: 'public/neu/rechtliches/abo-bedingungen.html',
})
/**
* Die elf Dokumente der Fassung 6 in der Reihenfolge der Tabelle: die Abo-Bedingungen, dann je Funktion Leistungsbeschreibung und AVV wie in
* FUNKTIONEN_UNTERLAGEN_V6 (documentKey = Slug der Datei ohne Fassung). dateien = [PDF, HTML]; quelle = der HTML-Pfad, den source beim Hochladen
* trägt (verschiebt P10 die Abo-Seite, ändert sich dateien, quelle bleibt).
*/
export const DOKUMENTE_V6 = Object.freeze([
ABO,
...Object.entries(FUNKTIONEN_UNTERLAGEN_V6).flatMap(([key, unterlagen]) =>
[['lb', unterlagen.lb, `Leistungsbeschreibung ${NAMEN[key]}`], ['avv', unterlagen.avv, `AVV ${NAMEN[key]}`]]
.filter(([, dok]) => dok)
.map(([, dok, titel]) => {
const html = `public${dok.url}`
return Object.freeze({
documentKey: path.basename(dok.url, `-${dok.version}.html`),
dateien: Object.freeze([`public/${dok.pdfFile}`, html]),
titel,
quelle: html,
})
}),
),
])
/** Eintrag der Tabelle zu einem documentKey; sonst Error('Unbekanntes Dokument: …'). */
export function dokumentFuer(documentKey) {
const d = DOKUMENTE_V6.find((x) => x.documentKey === documentKey)
if (!d) throw new Error(`Unbekanntes Dokument: ${String(documentKey).slice(0, 80)}`)
return d
}
// ---------------------------------------------------------------------------------------------------------- reine Funktionen
const MODI = { '--probe': 'probe', '--karten-anlegen': 'karten', '--pruefen': 'pruefen' }
const istKalendertag = (iso) => {
if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return false
const d = new Date(`${iso}T12:00:00Z`)
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === iso
}
/**
* Kommandozeile prüfen: ein Modus allein (--probe, --karten-anlegen, --pruefen) oder genau ein documentKey mit --freigabe=JJJJ-MM-TT.
* @returns {{ modus: 'probe'|'karten'|'pruefen'|'hochladen', documentKey: string|null, freigabe: string|null }}
*/
export function parseArgs(argv) {
const optionen = argv.filter((a) => a.startsWith('--'))
const dokumente = argv.filter((a) => !a.startsWith('--'))
const modi = optionen.filter((o) => Object.hasOwn(MODI, o))
const freigaben = optionen.filter((o) => o.startsWith('--freigabe='))
const fremde = optionen.filter((o) => !Object.hasOwn(MODI, o) && !o.startsWith('--freigabe='))
if (fremde.length > 0) throw new Error(`${USAGE}\nUnbekannte Option: ${fremde[0].slice(0, 40)}`)
if (modi.length > 0) {
if (modi.length > 1 || dokumente.length > 0 || freigaben.length > 0) throw new Error(`${USAGE}\n${modi[0]} steht allein.`)
return { modus: MODI[modi[0]], documentKey: null, freigabe: null }
}
if (dokumente.length !== 1) throw new Error(`${USAGE}\nGenau ein Dokument je Aufruf.`)
if (freigaben.length !== 1) throw new Error('Freigabe fehlt: --freigabe=JJJJ-MM-TT ist beim Hochladen Pflicht.')
const freigabe = freigaben[0].slice('--freigabe='.length)
if (!istKalendertag(freigabe)) throw new Error(`Freigabe ungültig (erwartet JJJJ-MM-TT, ein echter Kalendertag): ${freigabe.slice(0, 20)}`)
dokumentFuer(dokumente[0])
return { modus: 'hochladen', documentKey: dokumente[0], freigabe }
}
/** Der Tag vor einem Datum (JJJJ-MM-TT). */
export function tagVorher(iso) {
const d = new Date(`${iso}T12:00:00Z`)
d.setUTCDate(d.getUTCDate() - 1)
return d.toISOString().slice(0, 10)
}
/**
* Plan für eine neue Version eines Dokuments: schon = gleiche source ist hochgeladen; nummer = vN (Zahl der Versionen mit ^v\d+$ plus eins);
* aktuellId = die bisherige gültige Version (jüngstes validFrom), die abgelöst wird; validToAltes = Tag vor validFrom, bei gleichem oder
* früherem validFrom = validFrom (wie die Vorlage des Webshop-Livegangs).
*/
export function planVersion({ alle, source, validFrom }) {
const aktuell = alle.filter((v) => v.status === 'current').sort((a, b) => b.validFrom.localeCompare(a.validFrom))[0]
return {
schon: alle.some((v) => v.source === source),
nummer: `v${alle.filter((v) => /^v\d+$/.test(v.version)).length + 1}`,
aktuellId: aktuell ? aktuell.$id : null,
validToAltes: aktuell ? (validFrom > aktuell.validFrom ? tagVorher(validFrom) : validFrom) : null,
}
}
/** Dokument-ID einer Karte: der Schlüssel, wenn er höchstens 36 Zeichen hat, sonst 27 Zeichen davon, "-" und 8 Hexzeichen von sha256(key). */
export function kartenId(key) {
if (key.length <= 36) return key
return `${key.slice(0, 27)}-${createHash('sha256').update(key).digest('hex').slice(0, 8)}`
}
/** Die fehlenden Karten in Tabellenreihenfolge: [{ id, daten: { key, title, liveUrl, sort } }], sort ab hoechsteSort + 1. */
export function kartenPlan({ vorhandeneKeys, hoechsteSort }) {
const vorhanden = new Set(vorhandeneKeys)
let sort = hoechsteSort
return DOKUMENTE_V6.filter((d) => !vorhanden.has(d.documentKey)).map((d) => {
sort += 1
return { id: kartenId(d.documentKey), daten: { key: d.documentKey, title: d.titel, liveUrl: `${LIVE}${d.dateien[1].replace(/^public\//, '')}`, sort } }
})
}
const maskiere = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
/** Nachkontrolle: { [documentKey]: Version[] } -> { fehlt: documentKey[] }; ein Dokument zählt mit einer Version status current und source kundenbereich@<Hash>:<quelle>. */
export function pruefeHochgeladen(versionenJe) {
const fehlt = DOKUMENTE_V6.filter((d) => {
const muster = new RegExp(`^kundenbereich@[0-9a-f]{7,40}:${maskiere(d.quelle)}$`)
return !(versionenJe[d.documentKey] ?? []).some((v) => v.status === 'current' && muster.test(v.source ?? ''))
}).map((d) => d.documentKey)
return { fehlt }
}
// ---------------------------------------------------------------------------------------------------------- Zugriff auf WOMS
const q = (o) => encodeURIComponent(JSON.stringify(o))
const neueId = () => Array.from(crypto.getRandomValues(new Uint8Array(10)), (b) => b.toString(16).padStart(2, '0')).join('').slice(0, 20)
/** Kleiner Appwrite-Client über deps.fetch; Fehler werden zu Error mit Methode, Pfad und Status. */
function client(fetchFn, schluessel) {
const kopf = { 'X-Appwrite-Project': PROJECT, 'X-Appwrite-Key': schluessel }
return async function api(method, pfad, body) {
const formular = typeof FormData !== 'undefined' && body instanceof FormData
const res = await fetchFn(`${ENDPOINT}${pfad}`, {
method,
headers: formular ? kopf : { ...kopf, 'Content-Type': 'application/json' },
body: body === undefined ? undefined : formular ? body : JSON.stringify(body),
})
const text = await res.text()
if (!res.ok) throw Object.assign(new Error(`${method} ${pfad}: ${res.status} ${text.slice(0, 200)}`), { status: res.status })
return text ? JSON.parse(text) : null
}
}
async function alleDokumente(api, sammlung) {
const aus = []
for (let offset = 0; ; offset += 100) {
const r = await api('GET', `/databases/${DB}/collections/${sammlung}/documents?queries[]=${q({ method: 'limit', values: [100] })}&queries[]=${q({ method: 'offset', values: [offset] })}`)
aus.push(...r.documents)
if (r.documents.length < 100) return aus
}
}
async function versionen(api, documentKey) {
const r = await api('GET', `/databases/${DB}/collections/legalVersions/documents?queries[]=${q({ method: 'equal', attribute: 'documentKey', values: [documentKey] })}&queries[]=${q({ method: 'limit', values: [100] })}`)
return r.documents
}
const ddmmjjjj = (iso) => iso.split('-').reverse().join('.')
// ---------------------------------------------------------------------------------------------------------- Aufruf
/**
* Einstieg für alle Aufrufe. deps = { fetch, schluessel, log, git, dateien } mit den Standards globales fetch, Datei /root/.appwrite-rechtliches.key,
* console.log, git im Projekt und das Dateisystem; der einzige Weg zu Schlüssel und Netz.
* @returns {Promise<number>} Exit-Code (0 oder 1)
*/
export async function main(argv, deps = {}) {
const log = deps.log ?? console.log
let args
try {
args = parseArgs(argv)
} catch (err) {
log(err.message)
return 1
}
try {
const fetchFn = deps.fetch ?? globalThis.fetch
const api = client(fetchFn, (deps.schluessel ?? (() => readFileSync(KEY_DATEI, 'utf8').trim()))())
if (args.modus === 'probe') return await probe(api, log)
if (args.modus === 'karten') return await kartenAnlegen(api, log)
if (args.modus === 'pruefen') return await pruefen(api, log)
return await hochladen(api, log, args, deps)
} catch (err) {
log(`Fehler: ${err.message}`)
return 1
}
}
/** --probe: nur lesen. Jede fehlende Karte ist ein Stopp. */
async function probe(api, log) {
await api('GET', `/storage/buckets/${BUCKET}/files?queries[]=${q({ method: 'limit', values: [1] })}`)
const karten = new Set((await alleDokumente(api, 'legalDocuments')).map((k) => k.key))
let stopp = 0
for (const d of DOKUMENTE_V6) {
log(`${d.documentKey}: ${(await versionen(api, d.documentKey)).map((v) => `${v.version}:${v.status}`).join(' ') || '—'}`)
if (!karten.has(d.documentKey)) {
log(`STOPP: Karte fehlt: ${d.documentKey}`)
stopp += 1
}
}
if (stopp > 0) return 1
log('Schlüssel ok (files.read + documents.read), alle Karten vorhanden.')
return 0
}
/** --karten-anlegen: legt nur die fehlenden Karten an, vorhandene nie. */
async function kartenAnlegen(api, log) {
const vorhanden = await alleDokumente(api, 'legalDocuments')
const plan = kartenPlan({ vorhandeneKeys: vorhanden.map((k) => k.key), hoechsteSort: Math.max(0, ...vorhanden.map((k) => Number(k.sort) || 0)) })
for (const karte of plan) {
await api('POST', `/databases/${DB}/collections/legalDocuments/documents`, { documentId: karte.id, data: karte.daten })
log(`+ Karte ${karte.daten.key} (ID ${karte.id})`)
}
if (plan.length === 0) log('Alle Karten sind vorhanden.')
return 0
}
/** --pruefen: nur lesen, Nachkontrolle nach den Uploads. */
async function pruefen(api, log) {
const je = {}
for (const d of DOKUMENTE_V6) je[d.documentKey] = await versionen(api, d.documentKey)
const { fehlt } = pruefeHochgeladen(je)
for (const d of DOKUMENTE_V6) log(`${fehlt.includes(d.documentKey) ? 'FEHLT' : 'ok'} ${d.documentKey}`)
return fehlt.length > 0 ? 1 : 0
}
/** <documentKey> --freigabe=…: genau ein Dokument hochladen. Als Erstes die Kartenprüfung, dann Git- und Dateiprüfung, erst dann schreiben. */
async function hochladen(api, log, { documentKey, freigabe }, deps) {
const d = dokumentFuer(documentKey)
const karten = await alleDokumente(api, 'legalDocuments')
if (!karten.some((k) => k.key === documentKey)) {
log(`Karte fehlt: ${documentKey} (zuerst --karten-anlegen)`)
return 1
}
const git = deps.git ?? ((args) => execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' }))
const dateisystem = deps.dateien ?? { existsSync: (rel) => existsSync(path.join(ROOT, rel)), readFileSync: (rel) => readFileSync(path.join(ROOT, rel)) }
for (const rel of d.dateien) {
if (!dateisystem.existsSync(rel)) {
log(`Datei fehlt: ${rel}`)
return 1
}
}
if (git(['status', '--porcelain', '--', ...d.dateien]).trim() !== '') {
log(`Abbruch: nicht committeter Stand in ${d.dateien.join(' und ')} (git status --porcelain ist nicht leer)`)
return 1
}
const commit = git(['rev-parse', '--short', 'HEAD']).trim()
const source = `kundenbereich@${commit}:${d.quelle}`
const alle = await versionen(api, documentKey)
const plan = planVersion({ alle, source, validFrom: freigabe })
if (plan.schon) {
log(`= ${documentKey}: schon hochgeladen`)
return 0
}
const fileIds = []
for (const rel of d.dateien) {
const form = new FormData()
form.append('fileId', neueId())
form.append('file', new Blob([dateisystem.readFileSync(rel)], { type: rel.endsWith('.pdf') ? 'application/pdf' : 'text/html' }), path.basename(rel))
fileIds.push((await api('POST', `/storage/buckets/${BUCKET}/files`, form)).$id)
}
await api('POST', `/databases/${DB}/collections/legalVersions/documents`, {
documentId: neueId(),
data: {
documentKey,
version: plan.nummer,
validFrom: freigabe,
validTo: null,
status: 'current',
changeNote: `Fassung vom ${ddmmjjjj(freigabe)}: Kundenansicht neu (Fassung 6)`,
fileIds,
source,
},
})
if (plan.aktuellId) {
await api('PATCH', `/databases/${DB}/collections/legalVersions/documents/${plan.aktuellId}`, { data: { status: 'superseded', validTo: plan.validToAltes } })
}
log(`+ ${documentKey} ${plan.nummer} (${fileIds.length} Dateien)${plan.aktuellId ? ', bisherige Version abgelöst' : ''}`)
return 0
}
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
main(process.argv.slice(2)).then((code) => {
process.exitCode = code
})
}
+69
View File
@@ -435,6 +435,13 @@ function unterkunftConsent() {
const features = {
news: {
key: 'news',
// Katalog der Kundenansicht neu (P5, V5): Beschreibung (hoechstens zwei Saetze, du-Form, kein Preis), Bild, Ablauf
// (direkt | fragebogen | anfrage), Dauer der Einrichtung und ob die Funktion nur Betriebe bekommen
beschreibung: 'Zeig Neuigkeiten und Aktionen direkt auf deiner Website. Du schreibst sie im Portal, sie erscheinen sofort.',
bild: '/neu/bilder/news.webp',
ablauf: 'direkt',
einrichtung: 'meist in 1–2 Wochen',
nurBetrieb: false,
product: 'newsSetup',
label: { name: 'News-Pinnwand', kurz: 'News', keine: 'keine News-Pinnwand' },
priceLabel: '30 €',
@@ -500,6 +507,13 @@ const features = {
},
termine: {
key: 'termine',
// Katalog der Kundenansicht neu (P5, V5): Beschreibung (hoechstens zwei Saetze, du-Form, kein Preis), Bild, Ablauf
// (direkt | fragebogen | anfrage), Dauer der Einrichtung und ob die Funktion nur Betriebe bekommen
beschreibung: 'Deine Kunden buchen Termine online, rund um die Uhr. Zeiten, Team und Buchungen verwaltest du im Portal.',
bild: '/neu/bilder/termine.webp',
ablauf: 'fragebogen',
einrichtung: 'meist in 1–2 Wochen',
nurBetrieb: false,
product: 'termineSetup',
label: { name: 'Terminplaner', kurz: 'Termine', keine: 'keinen Terminplaner' },
priceLabel: TERMINE_PREIS,
@@ -553,6 +567,13 @@ const features = {
},
shop: {
key: 'shop',
// Katalog der Kundenansicht neu (P5, V5): Beschreibung (hoechstens zwei Saetze, du-Form, kein Preis), Bild, Ablauf
// (direkt | fragebogen | anfrage), Dauer der Einrichtung und ob die Funktion nur Betriebe bekommen
beschreibung: 'Verkaufe Artikel direkt über deine Website. Die Zahlung läuft über dein eigenes Stripe-Konto.',
bild: '/neu/bilder/shop.webp',
ablauf: 'anfrage',
einrichtung: 'meist in 2–3 Wochen',
nurBetrieb: true,
product: 'shopSetup',
label: { name: 'Webshop', kurz: 'Shop', keine: 'keinen Webshop' },
priceLabel: SHOP_PREIS,
@@ -605,6 +626,13 @@ const features = {
},
unterkunft: {
key: 'unterkunft',
// Katalog der Kundenansicht neu (P5, V5): Beschreibung (hoechstens zwei Saetze, du-Form, kein Preis), Bild, Ablauf
// (direkt | fragebogen | anfrage), Dauer der Einrichtung und ob die Funktion nur Betriebe bekommen
beschreibung: 'Gäste fragen deine Unterkunft online an, du gibst frei und behältst Belegung und Preise im Blick. Den Kalender baut WEBklar in deine Website ein.',
bild: '/neu/bilder/unterkunft.webp',
ablauf: 'anfrage',
einrichtung: 'meist in 1–2 Wochen',
nurBetrieb: false,
product: 'unterkunftSetup',
label: { name: 'Buchungskalender', kurz: 'Buchungskalender', keine: 'keinen Buchungskalender' },
priceLabel: UNTERKUNFT_PREIS,
@@ -656,6 +684,13 @@ const features = {
// Oeffnungszeiten live (Spec 2026-10-04): 30 EUR einmalig, im Wartungs-Abo ohne Aufpreis (aboInklusive, consentFrei)
zeiten: {
key: 'zeiten',
// Katalog der Kundenansicht neu (P5, V5): Beschreibung (hoechstens zwei Saetze, du-Form, kein Preis), Bild, Ablauf
// (direkt | fragebogen | anfrage), Dauer der Einrichtung und ob die Funktion nur Betriebe bekommen
beschreibung: 'Deine Öffnungszeiten stehen live auf deiner Website und sind immer aktuell. Du änderst sie im Portal, auch für Feiertage und Urlaub.',
bild: '/neu/bilder/zeiten.webp',
ablauf: 'direkt',
einrichtung: 'meist in wenigen Tagen',
nurBetrieb: false,
product: 'zeitenSetup',
label: { name: 'Öffnungszeiten live', kurz: 'Zeiten', keine: 'keine Öffnungszeiten live' },
priceLabel: '30 €',
@@ -809,6 +844,37 @@ export const config = {
ustHinweis,
// Register der kaufbaren Funktionen (siehe oben)
features,
// Abo-Modell der Funktionen (Kundenansicht neu, P5): Fassung der Einschalt-Erklaerung, Reihenfolge des Katalogs, Entwurfstexte
// (Task 15 / P7 Task 0 stellt fassung und texte.beginn auf Fassung 6; config.js importiert keine Dienste) und der Katalogeintrag der
// Analyse (sie steht nicht im Register der kaufbaren Funktionen). Die Items baut featureRegistry.consentAbo bei jedem Aufruf.
aboFunktionen: {
fassung: '2026-10-16',
reihenfolge: ['news', 'termine', 'shop', 'unterkunft', 'zeiten', 'analyse'],
texte: {
// Haken "Beginn" (nur Privatkunden innerhalb der Widerrufsfrist); Entwurf, wortgleich zu WORTLAUT_V6.beginn.einrichtung (P8)
beginn:
'Bitte beginnt sofort mit der Einrichtung. Mir ist klar: Mit der vollständigen Einrichtung erlischt mein Widerrufsrecht, und wenn ich vorher widerrufe, zahle ich den bis dahin erbrachten Teil.',
// Hinweis auf der Einschalt-Seite (ohne Haken): die Pflichten der alten Haken verantwortung, stripe und gesundheit;
// Entwurf, wortgleich zu WORTLAUT_V6.hinweis (P8)
hinweis: {
shop: 'Du verkaufst in eigenem Namen und bist für deine Produkte, Preise und Rechtstexte verantwortlich. Die Zahlungen deiner Käufer laufen über dein eigenes Stripe-Konto.',
unterkunft:
'Du vermietest in eigenem Namen und bist für Preise, Stornobedingungen, Rechnungen und Meldepflichten verantwortlich. Online-Zahlungen deiner Gäste laufen über dein eigenes Stripe-Konto.',
termine: 'Der Terminplaner ist nicht für den Gesundheitsbereich gedacht. Das fragen wir im Fragebogen ab.',
},
// Haken der Analyse (Kinder unter 16 Jahren, DSGVO Art. 8)
zielgruppe: 'Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.',
analyseKnopf: 'Analyse einschalten',
},
analyse: {
name: 'Website-Analyse',
beschreibung: 'Sieh, wie viele Besucher auf deine Website kommen und welche Seiten sie ansehen. Die Auswertung findest du direkt im Portal.',
bild: '/neu/bilder/analyse.webp',
ablauf: 'direkt',
einrichtung: 'meist in wenigen Tagen',
nurBetrieb: false,
},
},
// Funktionen-Tab (News-Pinnwand): Alias auf den News-Eintrag des Registers, Zeiteinschaetzung auf der
// Buchungsseite und Wortlaut der Zustimmung (die Getter folgen config.features.news zur Laufzeit)
funktionen: {
@@ -854,6 +920,9 @@ export const config = {
abo: {
// Bei jeder Textaenderung an Bedingungen oder Bestellseite hochziehen (wird mit der Zustimmung gespeichert)
termsVersion: '2026-09-30-v5',
// Fassung 6 der Abo-Bedingungen: gilt nur fuer Sitzungen mit neuer Ansicht (aktuelleAboFassung in products.js).
// Entwurfstag; bei der Freigabe durch den Inhaber das Datum auf den Freigabetag setzen (P8 Task 11, Ablauf)
termsVersionNeu: '2026-10-16-v6',
// Mindestlaufzeit ab Leistungsbeginn; Kuendigung zum Ende bis 1 Monat vorher, danach monatlich
minTermMonths: products.wartung.minTermMonths,
minutesPerMonth: products.wartung.minutesPerMonth,
+18
View File
@@ -16,6 +16,7 @@ import customerDataRoutes from './routes/customerData.js'
import chatRoutes from './routes/chat.js'
import aboRoutes from './routes/abo.js'
import funktionenRoutes from './routes/funktionen.js'
import fristenRoutes from './routes/fristen.js'
import termineRoutes from './routes/termine.js'
import shopRoutes from './routes/shop.js'
import shopWebhookRoutes from './routes/shopWebhook.js'
@@ -32,6 +33,7 @@ import { purgeViewLog } from './services/customerViewLog.js'
import { purgeUnclaimedNewsImages } from './services/newsImages.js'
import { runActivation } from './services/siteFeatures.js'
import { runAboCheck, runAnalyseReconcile } from './services/analyse.js'
import { aboFunktionenTagesJob } from './services/aboFunktionenAbgleich.js'
import { fuenfMinutenJob, minutenJob, tagesbericht, taeglicherJob } from './services/termine/jobs.js'
import { minutenJob as unterkunftMinutenJob, viertelstundenJob as unterkunftViertelstundenJob, taeglicherJob as unterkunftTaeglicherJob } from './services/unterkunft/jobs.js'
import { minutenJob as shopMinutenJob, stuendlicherJob as shopStuendlicherJob, taeglicherJob as shopTaeglicherJob } from './services/shop/jobs.js'
@@ -100,6 +102,7 @@ app.use('/api/chat', chatRoutes)
app.use('/api/abo/takeover-credits', takeoverCreditRoutes)
app.use('/api/abo', aboRoutes)
app.use('/api/funktionen', funktionenRoutes)
app.use('/api/fristen', fristenRoutes)
app.use('/api/termine', termineRoutes)
// Stripe-Connect-Webhook des Webshops (ohne Sitzung): vor dem Router, braucht rawBody des globalen Parsers
app.use('/api/shop/stripe-webhook', shopWebhookRoutes)
@@ -241,6 +244,21 @@ const server = app.listen(config.port, '0.0.0.0', () => {
setTimeout(analyseAboCheck, 2 * 60 * 1000).unref()
setInterval(analyseAboCheck, 24 * 60 * 60 * 1000).unref()
// Funktionen des Abo-Modells (Kundenansicht neu, P6): taeglich der Abo-Ende-Abgleich fuer News, Terminplaner, Webshop und
// Buchungskalender (150 Sekunden nach dem Start beginnend, VOR dem Terminplaner-Tagesjob bei 180 s und den Loeschjobs von Shop
// und Buchungskalender, damit ein zurueckgekehrtes Abo nicht mehr geloescht wird). Laeuft unabhaengig von den Funktionsschaltern: ohne
// Dokumente des Abo-Modells kostet er eine Abfrage. Ein noch laufender Lauf wird nicht ueberlappt; im Log stehen keine Kundendaten.
const aboFunktionenAbgleich = () =>
aboFunktionenTagesJob(new Date())
.then((r) => {
if (r.abgeschaltet || r.wiederEin || r.geloescht || r.mails || r.fehler) {
console.log(`[funktionen-abo] ${r.abgeschaltet ?? 0} abgeschaltet, ${r.wiederEin ?? 0} wieder eingeschaltet, ${r.geloescht ?? 0} gelöscht, ${r.mails ?? 0} Mail(s), ${r.fehler ?? 0} Fehler`)
}
})
.catch((err) => console.warn('[funktionen-abo] Abgleich fehlgeschlagen:', err.message))
setTimeout(aboFunktionenAbgleich, 150 * 1000).unref()
setInterval(aboFunktionenAbgleich, 24 * 60 * 60 * 1000).unref()
// Terminplaner (Modus off: jeder Lauf kehrt sofort zurueck, ohne Abfrage; ein noch laufender Job wird nicht ueberlappt):
// jede Minute abgelaufene Haltungen verfallen lassen und die Mail-Warteschlange senden, alle 5 Minuten Erinnerungen, erledigte
// Termine und vergangene Anfragen, taeglich (3 Minuten nach dem Start beginnend) Loeschfristen und Aufraeumen. Die Jobs melden
@@ -8,8 +8,8 @@
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail am Abo) · BCC intern
Platzhalter: {{name}}, {{produkt}}, {{eingang_zeitpunkt}}, {{vertragsende}}, {{vertragsende_paragraph}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): wartung, funktionen
produkt = Katalogname ohne „WEBklar “; vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11); wartung = Hinweis zum Stundenkontingent; funktionen = Hinweis, dass Website-Analyse und Terminplaner mit dem Wartungs-Abo enden (nur Wartungs-Abo; Sie-Form wie die Vorlage).
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): wartung, funktionen, funktionen_abo
produkt = Katalogname ohne „WEBklar “; vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11); wartung = Hinweis zum Stundenkontingent; funktionen = Hinweis, dass Website-Analyse und Terminplaner mit dem Wartungs-Abo enden (nur Wartungs-Abo; Sie-Form wie die Vorlage); funktionen_abo = Hinweis, dass die Funktionen des Abo-Modells (Kundenansicht neu: Terminplaner, Webshop, Buchungskalender, News-Pinnwand, Öffnungszeiten live) mit dem Abo enden und die Eingaben 30 bzw. 90 Tage gespeichert bleiben (nur Wartungs-Abo; statt funktionen, nie beide).
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
@@ -42,6 +42,9 @@ a{color:#111111;}
<!-- WENN funktionen -->
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren.</td></tr>
<!-- ENDE funktionen -->
<!-- WENN funktionen_abo -->
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Die Funktionen Ihrer Website, die in Ihrem Abo enthalten sind (zum Beispiel Terminplaner, Webshop, Buchungskalender, News-Pinnwand oder Öffnungszeiten live), enden mit dem Abo. Ihre Eingaben bleiben danach noch 30 Tage gespeichert (beim Webshop und beim Buchungskalender 90 Tage); mit einem neuen Abo in dieser Zeit schalten wir die Funktionen wieder ein.</td></tr>
<!-- ENDE funktionen_abo -->
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fdf6e7;border:1px solid #f0dcb0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Sie haben das nicht veranlasst?</div><div class="mut" style="color:#4a4843;">Dann melden Sie sich bitte umgehend bei uns: <a class="mut" href="mailto:support@webklar.com" style="color:#4a4843;">support@webklar.com</a> oder 0176 23726355.</div></td></tr></table></td></tr>
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Ihr WEBklar-Team</strong></td></tr>
</table>
@@ -8,8 +8,8 @@
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: E-Mail aus dem Formular · BCC intern
Platzhalter: {{name}}, {{produkt}}, {{kuendigungsart}}, {{eingang_zeitpunkt}}, {{email}}, {{kundennummer}}, {{vertrag}}, {{wunschtermin}}, {{grund}}, {{vertragsende}}, {{vertragsende_paragraph}}, {{pakete}}, {{pakete_neu}}, {{pakete_angabe}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, grund, automatisch_gekuendigt, sonderkuendigung, ausserordentlich, kein_abo_gefunden, zusatzspeicher, pruefen, abo, wartung, speicher_angaben, speicher_kind_pruefung, funktionen
produkt = Katalogname ohne „WEBklar “ („Abo“ ohne gefundenes Abo); vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11); wartung (innerhalb von abo) = Hinweis auf nicht genutzte Arbeitszeit aus dem Stundenkontingent (kein Geldguthaben, v4 § 4 Abs. 1); funktionen (innerhalb von abo, nur Wartungs-Abo) = Hinweis, dass Website-Analyse und Terminplaner mit dem Wartungs-Abo enden (Sie-Form wie die Vorlage).
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): kundennummer, grund, automatisch_gekuendigt, sonderkuendigung, ausserordentlich, kein_abo_gefunden, zusatzspeicher, pruefen, abo, wartung, speicher_angaben, speicher_kind_pruefung, funktionen, funktionen_abo
produkt = Katalogname ohne „WEBklar “ („Abo“ ohne gefundenes Abo); vertragsende_paragraph = Paragraph zum Vertragsende der Fassung des Kunden (v3 § 9, v4 § 11); wartung (innerhalb von abo) = Hinweis auf nicht genutzte Arbeitszeit aus dem Stundenkontingent (kein Geldguthaben, v4 § 4 Abs. 1); funktionen (innerhalb von abo, nur Wartungs-Abo) = Hinweis, dass Website-Analyse und Terminplaner mit dem Wartungs-Abo enden (Sie-Form wie die Vorlage); funktionen_abo (innerhalb von abo, nur Wartungs-Abo) = Hinweis, dass die Funktionen des Abo-Modells (Kundenansicht neu) mit dem Abo enden und die Eingaben 30 bzw. 90 Tage gespeichert bleiben (statt funktionen, nie beide).
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
@@ -82,6 +82,9 @@ a{color:#111111;}
<!-- WENN funktionen -->
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Die Website-Analyse und der Terminplaner gehören zum Wartungs-Abo und enden mit ihm. Die Termindaten löschen wir 30 Tage nach dem Abschalten; vorher können Sie Ihre Termine im Kundenportal ansehen und exportieren.</td></tr>
<!-- ENDE funktionen -->
<!-- WENN funktionen_abo -->
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Die Funktionen Ihrer Website, die in Ihrem Abo enthalten sind (zum Beispiel Terminplaner, Webshop, Buchungskalender, News-Pinnwand oder Öffnungszeiten live), enden mit dem Abo. Ihre Eingaben bleiben danach noch 30 Tage gespeichert (beim Webshop und beim Buchungskalender 90 Tage); mit einem neuen Abo in dieser Zeit schalten wir die Funktionen wieder ein.</td></tr>
<!-- ENDE funktionen_abo -->
<!-- ENDE abo -->
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Ihr WEBklar-Team</strong></td></tr>
</table>
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<!--
K38 · Funktion wird eingerichtet (Einschalten einer Funktion im Abo-Modell)
Betreff: Deine Funktion wird eingerichtet: {{funktion}}
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
Platzhalter: {{kunde_name}}, {{projekt}}, {{funktion}}, {{funktion_akk}}, {{einrichtung}}, {{ticket}}, {{fassung}}, {{portal_url}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): stundenkonto, projekt_kosten, anhang
Geht an Kunden der neuen Ansicht, wenn sie eine Funktion aus ihrem Abo einschalten (News, Terminplaner, Webshop, Buchungskalender) oder das Team sie einschaltet; für Öffnungszeiten live bleibt K27 die Mail. Ohne Preis und ohne Wort „bestellen“.
funktion = Name der Funktion aus dem Register, funktion_akk = ihre Form im Satz („den Terminplaner“), einrichtung = Dauer der Einrichtung („meist in 1–2 Wochen“),
ticket = „ (Ticket #Nr)“ direkt hinter dem Projektnamen oder leer, fassung = Fassung der Leistungsbeschreibung, portal_url = Adresse der Funktion im Kundenportal.
stundenkonto = die Einrichtung geht vom Stundenkonto ab (Einschalten aus dem Abo); projekt_kosten = die Einrichtung ist Teil der Projektkosten (Team schaltet im Projekt ein). Genau einer der beiden ist an.
anhang = Leistungsbeschreibung und Auftragsverarbeitung hängen als PDF an.
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter, Blöcke und die festen Sätze der Blöcke.
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>Deine Funktion wird eingerichtet: {{funktion}}</title>
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
<style>
a{color:#111111;}
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
</style>
</head>
<body style="margin:0;padding:0;background:#f1f0ec;">
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">{{funktion}} wird für dein Projekt eingerichtet.&#8199;&#847;&#8199;&#847;&#8199;&#847;</span>
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
<tr><td align="center" style="padding:32px 12px;">
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Einrichtung</td>
</tr></table></td></tr>
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#ecebe6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#4a4843;">{{funktion}}</td></tr></table></td></tr>
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Deine Funktion wird eingerichtet</td></tr>
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>du hast {{funktion_akk}} eingeschaltet. Wir richten {{funktion_akk}} jetzt für dein Projekt {{projekt}}{{ticket}} ein, {{einrichtung}}. Sobald alles bereit ist, melden wir uns bei dir.</td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;"><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{funktion}}</strong></td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Einrichtung</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{einrichtung}}</td></tr></table></td></tr>
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Was jetzt gilt</div><div class="mut" style="color:#4a4843;">Bis es so weit ist, musst du nichts tun.</div><!-- WENN stundenkonto --><div class="mut" style="color:#4a4843;padding-top:8px;">Die Einrichtung geht von deinem Stundenkonto ab, auch wenn das Guthaben noch nicht reicht. Extrakosten entstehen keine.</div><!-- ENDE stundenkonto --><!-- WENN projekt_kosten --><div class="mut" style="color:#4a4843;padding-top:8px;">Die Einrichtung ist Teil deines Projekts und wird über die Projektkosten abgerechnet. Von deinem Stundenkonto geht nichts ab. Die Funktion läuft, solange dein Wartungs-Abo läuft.</div><!-- ENDE projekt_kosten --><!-- WENN anhang --><div class="mut" style="color:#4a4843;padding-top:8px;">Leistungsbeschreibung und Auftragsverarbeitung (Fassung {{fassung}}) hängen als PDF an.</div><!-- ENDE anhang --></td></tr></table></td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Zum Kundenportal &rarr;</a></td></tr></table></td></tr>
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
</table>
</td></tr>
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<!--
K39 · Funktion beendet (Abo-Ende, Abo-Modell)
Betreff: Deine Funktion wurde beendet: {{funktion}}
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
Platzhalter: {{kunde_name}}, {{projekt}}, {{funktion}}, {{loeschtag}}, {{portal_url}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): abo_ende, hosting, termine, offene
Geht an Kunden der neuen Ansicht, wenn der tägliche Abgleich eine Funktion des Abo-Modells (News, Terminplaner, Webshop, Buchungskalender) mit dem Abo beendet hat. Ohne Preis und ohne Behalten-Angebot.
Genau einer der Gründe abo_ende (Abo beendet) oder hosting (das Abo ist ein Hosting-Abo) ist an. termine = nur beim Terminplaner (Termine ansehen, CSV, Gäste, Erinnerungen); offene = nur bei Webshop und Buchungskalender (offene Bestellungen und Buchungen).
funktion = Name der Funktion aus dem Register; loeschtag = Tag, ab dem die Eingaben gelöscht werden, z. B. „16.11.2026“. Läuft bis dahin wieder ein passendes Abo, kehrt die Funktion still zurück.
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter, Blöcke und die festen Sätze.
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>Deine Funktion wurde beendet: {{funktion}}</title>
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
<style>
a{color:#111111;}
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
</style>
</head>
<body style="margin:0;padding:0;background:#f1f0ec;">
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">{{funktion}} wurde beendet.&#8199;&#847;&#8199;&#847;&#8199;&#847;</span>
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
<tr><td align="center" style="padding:32px 12px;">
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Beendet</td>
</tr></table></td></tr>
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#ecebe6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#4a4843;">{{funktion}}</td></tr></table></td></tr>
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Deine Funktion wurde beendet</td></tr>
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br><!-- WENN abo_ende -->Dein Abo ist beendet. {{funktion}} gehörte dazu und endet deshalb mit ihm.<!-- ENDE abo_ende --><!-- WENN hosting -->Dein Abo ist ein Hosting-Abo. {{funktion}} ist im Wartungs-Abo enthalten; ein Hosting-Abo reicht dafür nicht.<!-- ENDE hosting --></td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;"><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{funktion}}</strong></td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Eingaben gespeichert bis</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{loeschtag}}</td></tr></table></td></tr>
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Was jetzt gilt</div><div class="mut" style="color:#4a4843;">Deine Eingaben bleiben bis zum {{loeschtag}} gespeichert. Danach löschen wir sie. Läuft bis dahin wieder ein passendes Abo, schalten wir {{funktion}} automatisch und ohne Gebühr wieder ein.</div><!-- WENN termine --><div class="mut" style="color:#4a4843;padding-top:8px;">Bis dahin kannst du deine Termine im Kundenportal ansehen und als CSV exportieren. Gäste können ihre Termine weiter absagen, und die Erinnerungen laufen bis zur Löschung weiter. Bitte informiere deine Gäste selbst.</div><!-- ENDE termine --><!-- WENN offene --><div class="mut" style="color:#4a4843;padding-top:8px;">Offene Bestellungen und Buchungen kannst du bis dahin weiter im Kundenportal bearbeiten. Neue gibt es nicht mehr.</div><!-- ENDE offene --></td></tr></table></td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Zum Kundenportal &rarr;</a></td></tr></table></td></tr>
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
</table>
</td></tr>
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<!--
K40 · Zeiten Abo-Ende (Öffnungszeiten live, Abo-Modell)
Betreff: Dein Abo ist beendet – Öffnungszeiten live läuft noch bis {{frist}}
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
Platzhalter: {{kunde_name}}, {{projekt}}, {{frist}}, {{portal_url}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): abo_ende, hosting, wechsel_widerrufen
Geht an Kunden der neuen Ansicht, die Öffnungszeiten live im Abo-Modell eingeschaltet hatten, wenn das Abo endet: Die Funktion läuft noch bis zur Frist unverändert weiter, danach schalten wir sie ab. Ohne Behalten gegen Zahlung und ohne Preis (Ersatz für K29 im Abo-Modell).
Genau einer der Gründe abo_ende (Wartungs-Abo beendet), hosting (das Abo ist ein Hosting-Abo) oder wechsel_widerrufen (Wechsel ins Wartungs-Abo widerrufen) ist an.
frist = Tag, an dem wir abschalten (30 Tage nach dem erkannten Abo-Ende), z. B. „04.11.2026“.
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter und Blöcke.
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>Dein Abo ist beendet – Öffnungszeiten live läuft noch bis {{frist}}</title>
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
<style>
a{color:#111111;}
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
</style>
</head>
<body style="margin:0;padding:0;background:#f1f0ec;">
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">Öffnungszeiten live läuft noch bis {{frist}}.&#8199;&#847;&#8199;&#847;&#8199;&#847;</span>
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
<tr><td align="center" style="padding:32px 12px;">
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Abo beendet</td>
</tr></table></td></tr>
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#ecebe6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#4a4843;">Öffnungszeiten live</td></tr></table></td></tr>
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Öffnungszeiten live läuft noch bis {{frist}}</td></tr>
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br><!-- WENN abo_ende -->Dein Wartungs-Abo ist beendet. Öffnungszeiten live war darin enthalten und endet deshalb mit dem Abo.<!-- ENDE abo_ende --><!-- WENN hosting -->Dein Abo ist ein Hosting-Abo. Öffnungszeiten live ist im Wartungs-Abo enthalten; ein Hosting-Abo reicht dafür nicht.<!-- ENDE hosting --><!-- WENN wechsel_widerrufen -->Du hast den Wechsel in das Wartungs-Abo widerrufen, dein Abo bleibt ein Hosting-Abo. Öffnungszeiten live ist im Wartungs-Abo enthalten; ein Hosting-Abo reicht dafür nicht.<!-- ENDE wechsel_widerrufen --> Die Funktion läuft noch bis zum {{frist}} unverändert weiter.</td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;"><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>Öffnungszeiten live</strong></td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Läuft weiter bis</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{frist}}</td></tr></table></td></tr>
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Was jetzt gilt</div><div class="mut" style="color:#4a4843;">Bis zum {{frist}} bleibt die Funktion eingeschaltet, deine Website zeigt Zeiten, Status und Leisten wie bisher.</div><div class="mut" style="color:#4a4843;padding-top:8px;">Danach schalten wir die Funktion ab. Dann stehen auf deiner Website wieder feste Zeiten, und deine Eingaben bleiben noch 30 Tage gespeichert.</div><div class="mut" style="color:#4a4843;padding-top:8px;">Läuft wieder ein Wartungs-Abo, schalten wir die Funktion ohne Gebühr wieder ein.</div></td></tr></table></td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Zum Kundenportal &rarr;</a></td></tr></table></td></tr>
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
</table>
</td></tr>
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<!--
K41 · Zeiten abgeschaltet (Öffnungszeiten live, Abo-Modell)
Betreff: Öffnungszeiten live ist abgeschaltet
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
Platzhalter: {{kunde_name}}, {{projekt}}, {{loeschdatum}}, {{portal_url}}
Geht an Kunden der neuen Ansicht, deren Abo geendet hat und bei denen Öffnungszeiten live nach der Frist abgeschaltet wurde: Die Leisten sind weg, auf der Seite stehen wieder feste Zeiten; die Eingaben bleiben noch 30 Tage gespeichert. Ohne Preis und ohne Behalten (Ersatz für K30 im Abo-Modell).
loeschdatum = Tag, ab dem wir die Eingaben löschen (30 Tage nach dem Abschalten).
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter und Blöcke.
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>Öffnungszeiten live ist abgeschaltet</title>
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
<style>
a{color:#111111;}
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
</style>
</head>
<body style="margin:0;padding:0;background:#f1f0ec;">
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">Öffnungszeiten live ist abgeschaltet.&#8199;&#847;&#8199;&#847;&#8199;&#847;</span>
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
<tr><td align="center" style="padding:32px 12px;">
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Abgeschaltet</td>
</tr></table></td></tr>
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#ecebe6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#4a4843;">Öffnungszeiten live</td></tr></table></td></tr>
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Öffnungszeiten live ist abgeschaltet</td></tr>
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br>Öffnungszeiten live ist für dein Projekt {{projekt}} abgeschaltet, weil dein Abo beendet ist und die Frist abgelaufen ist. Die Hinweis-Leiste, die Kontakt-Leiste und der Live-Status sind von deiner Website verschwunden; dort stehen ab jetzt wieder feste Zeiten.</td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;"><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>Öffnungszeiten live</strong></td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Eingaben gespeichert bis</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{loeschdatum}}</td></tr></table></td></tr>
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f7f6f3;border:1px solid #e8e6e0;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Was jetzt gilt</div><div class="mut" style="color:#4a4843;">Deine Eingaben (Zeiten, Hinweise und Einstellungen) bleiben bis zum {{loeschdatum}} gespeichert. Danach löschen wir sie.</div><div class="mut" style="color:#4a4843;padding-top:8px;">Läuft wieder ein Wartungs-Abo, schalten wir die Funktion ohne Gebühr wieder ein.</div><div class="mut" style="color:#4a4843;padding-top:8px;">Nach der Löschung musst du die Funktion neu einschalten und deine Zeiten noch einmal eintragen.</div></td></tr></table></td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Zum Kundenportal &rarr;</a></td></tr></table></td></tr>
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
</table>
</td></tr>
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<!--
K42 · Webshop-Anfrage entschieden, Abo-Modell (Annahme oder Ablehnung)
Betreff: Deine Webshop-Anfrage für {{projekt}}
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
Platzhalter: {{kunde_name}}, {{projekt}}, {{begruendung}}, {{portal_url}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): angenommen, abgelehnt
Genau einer der Blöcke ist an. angenommen = Anfrage angenommen, der Kunde kann den Webshop jetzt im Kundenportal einschalten; abgelehnt = Anfrage abgelehnt, mit Begründung des Teams.
Fassung von K24 für Kunden der neuen Ansicht (Abo-Modell, Kundenansicht P6): ohne Preis, ohne Umsatzsteuerhinweis und ohne das Wort „bestellen“. K24 bleibt für die alte Ansicht unverändert.
portal_url = Webshop-Seite des Projekts im Kundenportal.
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter und Blöcke.
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>Deine Webshop-Anfrage für {{projekt}}</title>
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
<style>
a{color:#111111;}
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
</style>
</head>
<body style="margin:0;padding:0;background:#f1f0ec;">
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">Deine Webshop-Anfrage wurde bearbeitet.&#8199;&#847;&#8199;&#847;&#8199;&#847;</span>
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
<tr><td align="center" style="padding:32px 12px;">
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Webshop</td>
</tr></table></td></tr>
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#e4f2e6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#1d5b2c;">Webshop</td></tr></table></td></tr>
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;"><!-- WENN angenommen -->Deine Webshop-Anfrage wurde angenommen<!-- ENDE angenommen --><!-- WENN abgelehnt -->Zu Deiner Webshop-Anfrage<!-- ENDE abgelehnt --></td></tr>
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br><!-- WENN angenommen -->wir haben Deine Webshop-Anfrage für {{projekt}} angenommen. Du kannst den Webshop jetzt im Kundenportal einschalten; danach richten wir ihn ein.<!-- ENDE angenommen --><!-- WENN abgelehnt -->leider können wir Deine Webshop-Anfrage für {{projekt}} nicht annehmen. Den Grund findest Du unten. Wenn Du Fragen hast, antworte einfach auf diese E-Mail.<!-- ENDE abgelehnt --></td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;"><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;"><strong>{{projekt}}</strong></td></tr><!-- WENN abgelehnt --><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-top:1px solid #ecebe6;color:#6b6964;">Begründung</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-top:1px solid #ecebe6;color:#111111;">{{begruendung}}</td></tr><!-- ENDE abgelehnt --></table></td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;"><!-- WENN angenommen -->Webshop einschalten<!-- ENDE angenommen --><!-- WENN abgelehnt -->Zum Kundenportal<!-- ENDE abgelehnt --> &rarr;</a></td></tr></table></td></tr>
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
</table>
</td></tr>
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<!--
K43 · Anfrage Buchungskalender entschieden, Abo-Modell (Annahme oder Ablehnung)
Betreff: Deine Anfrage zum Buchungskalender für {{projekt}}
Absender: „WEBklar“ <Portal-Postfach>
Empfänger: Kunde (E-Mail des Kundenkontos) · BCC intern
Platzhalter: {{kunde_name}}, {{projekt}}, {{begruendung}}, {{portal_url}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): angenommen, abgelehnt
Genau einer der Blöcke ist an. angenommen = Anfrage angenommen, der Kunde kann den Buchungskalender jetzt im Kundenportal einschalten; abgelehnt = mit Begründung des Teams.
Fassung von K34 für Kunden der neuen Ansicht (Abo-Modell, Kundenansicht P6): ohne Preis, ohne Umsatzsteuerhinweis und ohne das Wort „bestellen“. K34 bleibt für die alte Ansicht unverändert.
portal_url = Seite des Projekts im Kundenportal.
Texte sind Entwurf für die Mail-Überarbeitung des Inhabers; verbindlich sind nur Platzhalter und Blöcke.
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>Deine Anfrage zum Buchungskalender für {{projekt}}</title>
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
<style>
a{color:#111111;}
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
</style>
</head>
<body style="margin:0;padding:0;background:#f1f0ec;">
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
<tr><td align="center" style="padding:32px 12px;">
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Buchungskalender</td>
</tr></table></td></tr>
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#e4f2e6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#1d5b2c;">Buchungskalender</td></tr></table></td></tr>
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;"><!-- WENN angenommen -->Deine Anfrage wurde angenommen<!-- ENDE angenommen --><!-- WENN abgelehnt -->Zu Deiner Anfrage<!-- ENDE abgelehnt --></td></tr>
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Hallo {{kunde_name}},<br><!-- WENN angenommen -->wir haben Deine Anfrage zum Buchungskalender für {{projekt}} angenommen. Du kannst ihn jetzt im Kundenportal einschalten; danach richten wir ihn ein.<!-- ENDE angenommen --><!-- WENN abgelehnt -->leider können wir Deine Anfrage zum Buchungskalender für {{projekt}} nicht annehmen. Den Grund findest Du unten. Wenn Du Fragen hast, antworte einfach auf diese E-Mail.<!-- ENDE abgelehnt --></td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;">
<tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;"><strong>{{projekt}}</strong></td></tr>
<!-- WENN abgelehnt --><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Begründung</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{begruendung}}</td></tr>
<!-- ENDE abgelehnt --></table></td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{portal_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Zum Kundenportal &rarr;</a></td></tr></table></td></tr>
<tr><td class="px ink" style="padding:28px 40px 44px 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:24px;color:#111111;">Viele Grüße<br><strong>Dein WEBklar-Team</strong></td></tr>
</table>
</td></tr>
<tr><td class="px" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;"><a class="mut" href="https://project.webklar.com/dashboard.html" style="color:#4a4843;text-decoration:underline;">Kundenportal</a></td></tr>
<tr><td class="px mut" align="center" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">WEBklar GbR · Am Schwimmbad 10 · 67722 Winnweiler<br>Telefon <a class="mut" href="tel:+4917623726355" style="color:#6b6964;text-decoration:none;">0176 23726355</a> · <a class="mut" href="mailto:support@webklar.com" style="color:#6b6964;text-decoration:none;">support@webklar.com</a><br>Vertreten durch die Gesellschafter Kenso Grimm und Justin Klein</td></tr>
</table>
</td></tr>
</table>
</body>
</html>
@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="de" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<!--
T24 · Funktion eingeschaltet (Abo-Modell)
Betreff: [Abo] Funktion eingeschaltet: {{funktion}} – {{kunde_name}}
Absender: „WEBklar Kundenportal“ <Portal-Postfach>
Empfänger: kenso@webklar.com, andrej@webklar.com
Platzhalter: {{kunde_name}}, {{email}}, {{projekt}}, {{funktion}}, {{woid}}, {{kontostand}}, {{woms_url}}
Optionale Blöcke (zwischen WENN-/ENDE-Kommentaren; bei Nichtzutreffen samt Inhalt entfernen): vorschuss, anhang_fehlt
Der Kunde hat eine Funktion aus seinem Abo eingeschaltet (oder das Team hat sie eingeschaltet); die Einrichtung geht aufs Stundenkonto, das Ticket woid ist angelegt.
vorschuss = beim Einschalten ist das Guthaben aufgebraucht (balanceMinutes <= 0) oder es besteht schon ein Vorschuss; kontostand nennt den Stand, z. B. „Guthaben 0 min, Vorschuss 90 min“.
anhang_fehlt = die Vertragsunterlagen (PDF) sind nicht beim Kunden angekommen; das Team sendet sie von Hand nach.
-->
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>[Abo] Funktion eingeschaltet: {{funktion}} – {{kunde_name}}</title>
<!--[if mso]><style>table,td{font-family:Arial,Helvetica,sans-serif!important;}</style><![endif]-->
<style>
a{color:#111111;}
@media (max-width:620px){.w{width:100%!important;}.px{padding-left:24px!important;padding-right:24px!important;}.lbl{display:block!important;width:100%!important;padding-bottom:2px!important;border-bottom:0!important;}.val{display:block!important;width:100%!important;padding-top:0!important;}}
@media (prefers-color-scheme:dark){.bg{background:#161615!important;}.card{background:#1f1f1e!important;}.ink{color:#f2f1ee!important;}.mut{color:#a8a6a0!important;}.box{background:#2a2a28!important;border-color:#3a3a37!important;}.btn{background:#f2f1ee!important;}.btn a{color:#161615!important;}.rowline{border-color:#3a3a37!important;}}
</style>
</head>
<body style="margin:0;padding:0;background:#f1f0ec;">
<span style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all;font-size:1px;line-height:1px;color:#f1f0ec;">{{kunde_name}}: {{funktion}}, Projekt {{projekt}}.&#8199;&#847;&#8199;&#847;&#8199;&#847;</span>
<table role="presentation" class="bg" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#f1f0ec;">
<tr><td align="center" style="padding:32px 12px;">
<table role="presentation" class="w" width="600" cellpadding="0" cellspacing="0" border="0" style="width:600px;max-width:600px;">
<tr><td class="px" style="padding:0 40px 20px 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0"><tr>
<td><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td valign="middle" style="padding-right:10px;"><img src="https://project.webklar.com/webklar-logo.png" width="40" height="40" alt="WEBklar" style="display:block;width:40px;height:40px;border:0;outline:none;text-decoration:none;border-radius:10px;"></td><td valign="middle" class="ink" style="font-family:Arial,Helvetica,sans-serif;font-size:26px;font-weight:bold;letter-spacing:-1px;color:#111111;">WEBklar</td></tr></table></td>
<td align="right" class="mut" style="font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#6b6964;">Intern · Kundenportal</td>
</tr></table></td></tr>
<tr><td class="card" style="background:#ffffff;border-radius:16px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr><td class="px" style="padding:44px 40px 8px 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td style="background:#ecebe6;border-radius:100px;padding:6px 12px;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;color:#4a4843;">Funktionen</td></tr></table></td></tr>
<tr><td class="px ink" style="padding:16px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:28px;line-height:34px;mso-line-height-rule:exactly;font-weight:bold;letter-spacing:-0.5px;color:#111111;">Funktion eingeschaltet: {{funktion}}</td></tr>
<tr><td class="px mut" style="padding:14px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:16px;line-height:25px;mso-line-height-rule:exactly;color:#4a4843;">Der Kunde hat die Funktion aus seinem Abo eingeschaltet. Die Einrichtung geht aufs Stundenkonto.</td></tr>
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;mso-line-height-rule:exactly;"><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Kunde</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{kunde_name}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">E-Mail</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{email}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Projekt</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{projekt}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;border-bottom:1px solid #ecebe6;color:#6b6964;">Funktion</td><td class="val ink rowline" valign="top" style="padding:12px 0;border-bottom:1px solid #ecebe6;color:#111111;">{{funktion}}</td></tr><tr><td class="lbl mut rowline" width="170" valign="top" style="width:170px;padding:12px 12px 12px 0;color:#6b6964;">Ticket</td><td class="val ink rowline" valign="top" style="padding:12px 0;color:#111111;">{{woid}}</td></tr></table></td></tr>
<!-- WENN vorschuss -->
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fdf0ef;border:1px solid #f1c9c5;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Vorschuss</div><div class="mut" style="color:#4a4843;">Das Guthaben reicht voraussichtlich nicht ({{kontostand}}). Die Einrichtung läuft trotzdem; das Minus ist ein Vorschuss und wird mit den nächsten Monatsgutschriften verrechnet. Extrakosten entstehen keine.</div></td></tr></table></td></tr>
<!-- ENDE vorschuss -->
<!-- WENN anhang_fehlt -->
<tr><td class="px" style="padding:24px 40px 0 40px;"><table role="presentation" class="box" width="100%" cellpadding="0" cellspacing="0" border="0" style="background:#fdf0ef;border:1px solid #f1c9c5;border-radius:12px;"><tr><td class="ink" style="padding:20px 22px;font-family:Arial,Helvetica,sans-serif;font-size:14px;line-height:21px;color:#111111;"><div class="ink" style="font-weight:bold;color:#111111;padding-bottom:6px;">Unterlagen von Hand nachsenden</div><div class="mut" style="color:#4a4843;">Die Vertragsunterlagen (Leistungsbeschreibung und Auftragsverarbeitungsvertrag, jeweils als PDF) sind nicht mit der Bestätigung beim Kunden angekommen. Bitte dem Kunden die Unterlagen in der eingeschalteten Fassung per E-Mail nachsenden. Der Grund steht im Log des Portals.</div></td></tr></table></td></tr>
<!-- ENDE anhang_fehlt -->
<tr><td class="px" style="padding:28px 40px 0 40px;"><table role="presentation" cellpadding="0" cellspacing="0" border="0"><tr><td class="btn" bgcolor="#111111" style="background:#111111;border-radius:10px;"><a href="{{woms_url}}" style="display:block;padding:14px 26px;font-family:Arial,Helvetica,sans-serif;font-size:15px;font-weight:bold;color:#ffffff;text-decoration:none;">Im Ticketsystem öffnen &rarr;</a></td></tr></table></td></tr>
<tr><td style="padding:0 0 44px 0;font-size:0;line-height:0;">&nbsp;</td></tr>
</table>
</td></tr>
<tr><td class="px mut" align="center" style="padding:24px 40px 0 40px;font-family:Arial,Helvetica,sans-serif;font-size:12px;line-height:19px;color:#6b6964;">Automatische Benachrichtigung aus dem WEBklar Kundenportal · <a class="mut" href="https://ticket.webklar.com/customers" style="color:#4a4843;text-decoration:underline;">Ticketsystem</a></td></tr>
</table>
</td></tr>
</table>
</body>
</html>
+85 -19
View File
@@ -19,9 +19,13 @@ import {
previewSwitch,
reconcilePendingSwitch,
switchToWartung,
wechselFunktionVon,
} from '../services/planSwitch.js'
import { handleFeatureCheckoutEvent, isFeatureCheckoutEvent, listCustomerFeatureDocs, paidFeatureOrders } from '../services/siteFeatures.js'
import { assertOwnProject, handleFeatureCheckoutEvent, isFeatureCheckoutEvent, listCustomerFeatureDocs, paidFeatureOrders } from '../services/siteFeatures.js'
import { featureConfig } from '../services/featureRegistry.js'
import { istAboModell } from '../services/aboModell.js'
import { ENTHALTEN_IN, aboModellFuerKunde } from '../services/aboFunktionen.js'
import { neueAnsicht } from '../services/kundenansicht/schalter.js'
import { PRODUCT_KEYS, contractAttachmentFor, describeItems, isTermsV4OrNewer, productLabel, productView, termsDocFor } from '../services/products.js'
import {
ACTIVE_STATUSES,
@@ -163,6 +167,21 @@ function isPilotAllowed(customer, offer = null) {
// Kunde (eingeloggt)
// ---------------------------------------------------------------------------
/**
* publicSubscription, in der neuen Ansicht mit wechselFunktion (die beim Wechsel gewuenschte Funktion, die noch einzuschalten ist, sonst null;
* planSwitch.wechselFunktionVon). In der alten Ansicht fehlt der Schluessel. Ein Lesefehler ergibt null (die Seite bleibt nutzbar).
*/
async function subscriptionMitWechselFunktion(req, customerId, sub, events) {
const view = publicSubscription(sub, events)
if (!view || !neueAnsicht(req)) return view
try {
return { ...view, wechselFunktion: await wechselFunktionVon(customerId, events.filter((e) => e.subscriptionId === sub.$id)) }
} catch (err) {
console.error('[abo] Gewünschte Funktion des Wechsels nicht lesbar:', err.message)
return { ...view, wechselFunktion: null }
}
}
/** Katalog, Abo-Status mit Speicherstand und Wechsel-Moeglichkeit, Angebote je Produkt, Stundenkonto (nur Wartung). */
router.get('/', requireSession, async (req, res) => {
try {
@@ -210,7 +229,7 @@ router.get('/', requireSession, async (req, res) => {
},
offers: { hosting: hostingOffer ? offerView(hostingOffer) : null, wartung: wartungOffer ? offerView(wartungOffer) : null },
ledger,
subscription: publicSubscription(sub, events),
subscription: await subscriptionMitWechselFunktion(req, customerId, sub, events),
})
} catch (err) {
return res.status(500).json({ error: err.message || 'Abo konnte nicht geladen werden' })
@@ -363,6 +382,26 @@ router.post('/switch/preview', requireSession, async (req, res) => {
}
})
/**
* Die beim Wechsel gewuenschte Funktion (Kundenansicht neu, P6, V11) aus dem Body: { key, projectId } oder null. Gueltig ist ein Schluessel
* aus dem Register, den nur das Wartungs-Abo enthaelt (ENTHALTEN_IN nur 'wartung': news, termine, shop, unterkunft, zeiten; nicht analyse) und
* ein Projekt des Kunden (assertOwnProject). Alles andere wird verworfen, nie abgelehnt: die Bequemlichkeit darf den bezahlten Vertrag nicht
* blockieren. Nur die beiden Schluessel key und projectId werden uebernommen.
*/
async function wechselFunktionAusBody(customerId, funktion) {
if (!funktion || typeof funktion !== 'object' || Array.isArray(funktion)) return null
const { key, projectId } = funktion
if (typeof key !== 'string' || typeof projectId !== 'string' || !projectId) return null
const enthalten = Object.hasOwn(ENTHALTEN_IN, key) ? ENTHALTEN_IN[key] : null
if (!enthalten || enthalten.length !== 1 || enthalten[0] !== 'wartung') return null
try {
await assertOwnProject(customerId, projectId)
} catch {
return null
}
return { key, projectId }
}
/**
* Bestellung { acceptTerms: true, earlyStart: true, termsVersion, prorationDate, amountDueCents } ("Zahlungspflichtig
* bestellen"); prorationDate und amountDueCents kommen aus der Vorschau.
@@ -390,12 +429,14 @@ router.post('/switch', requireSession, async (req, res) => {
}
const customerId = getSessionCustomerId(req)
const now = new Date().toISOString()
// Gewuenschte Funktion (nur neue Ansicht): geprueft oder verworfen, der Wechsel laeuft in jedem Fall
const funktion = neueAnsicht(req) ? await wechselFunktionAusBody(customerId, req.body?.funktion) : null
const { subscription, paymentUrl } = await switchToWartung(
customerId,
{ termsVersion, consentAt: now, earlyStartConsentAt: now, consentIp: req.ip, consentUserAgent: req.get('user-agent') || '' },
prorationDate,
// Fix 1 (8): der in der Bestelluebersicht gezeigte Betrag; weicht die neue Vorschau ab, 409
{ expectedAmountCents: amountDueCents }
{ expectedAmountCents: amountDueCents, funktion }
)
const events = await listContractEvents(customerId).catch((e) => {
console.error('[abo] Vertragsereignisse nicht lesbar:', e.message)
@@ -413,23 +454,35 @@ router.post('/switch', requireSession, async (req, res) => {
// ---------------------------------------------------------------------------
/**
* Hat der Kunde dieses Abos die Website-Analyse oder den Terminplaner (Funktionsdokument in portalFeatures mit featureKey analyse
* oder termine, Zustand ausser off; ein Dokument ohne Zustand gilt wie off, wie in der Ansicht)? Nur dann gilt der Hinweis in K02 und
* K03 (Block funktionen), dass beide mit dem Wartungs-Abo enden (Entscheidung 2026-10-05). Der Kunde ist der des Abos (sub.customerId,
* wie in customerNameOf); gelesen wird wie sonst auch ueber listCustomerFeatureDocs. Wirft nie: ein Lesefehler zaehlt als ja (der Kunde
* wird eher informiert als nicht), eine Logzeile, die Mail geht in jedem Fall raus.
* Welche Funktionen hat der Kunde dieses Abos? `alt`: die Website-Analyse oder der Terminplaner (Funktionsdokument in portalFeatures
* mit featureKey analyse oder termine, Zustand ausser off; ein Dokument ohne Zustand gilt wie off, wie in der Ansicht); dann gilt der
* Hinweis in K02 und K03 (Block funktionen), dass beide mit dem Wartungs-Abo enden (Entscheidung 2026-10-05). `modell`: mindestens
* ein Dokument des Abo-Modells (istAboModell) in einem Zustand ausser off; dann gilt statt dessen der Hinweis funktionen_abo
* (Kundenansicht neu, P6). Der Kunde ist der des Abos (sub.customerId, wie in customerNameOf); gelesen wird wie sonst auch ueber
* listCustomerFeatureDocs. Wirft nie: ein Lesefehler zaehlt als { alt: true, modell: false } (der Kunde wird eher informiert als
* nicht), eine Logzeile, die Mail geht in jedem Fall raus.
*/
async function hatAboFunktionen(sub) {
try {
if (!sub?.customerId) throw new Error('Abo ohne Kunden-ID')
const docs = await listCustomerFeatureDocs(sub.customerId)
return docs.some((doc) => ABO_FUNKTIONEN.includes(doc.featureKey) && (doc.state || 'off') !== 'off')
const laeuft = (doc) => (doc.state || 'off') !== 'off'
return {
alt: docs.some((doc) => ABO_FUNKTIONEN.includes(doc.featureKey) && laeuft(doc)),
modell: docs.some((doc) => istAboModell(doc) && laeuft(doc)),
}
} catch (err) {
console.error(`[abo] Funktionen des Kunden nicht lesbar, der Hinweis auf das Ende von Analyse und Terminplaner bleibt in der Kündigungsbestätigung (Abo ${sub?.$id || '-'}): ${err.message}`)
return true
return { alt: true, modell: false }
}
}
/** Blöcke funktionen (alter Hinweis) und funktionen_abo (Abo-Modell) der Kündigungsmails K02 und K03; nur beim Wartungs-Abo, sonst wird nichts gelesen. */
async function funktionenBloecke(sub) {
const { alt, modell } = isWartung(sub) ? await hatAboFunktionen(sub) : { alt: false, modell: false }
return { funktionen: alt && !modell, funktionen_abo: modell }
}
/** Anrede in Kundenmails: Name aus den Kundendaten des Portals, sonst der übergebene Ersatz. */
async function customerNameOf(sub, fallback = '') {
const customer = sub?.customerId
@@ -497,9 +550,9 @@ async function sendPortalCancellationConfirmation(sub, createdUnix) {
vertragsende: fmtDate(endsAt),
vertragsende_paragraph: endParagraphOf(sub),
},
// Hinweis auf nicht genutzte Arbeitszeit nur beim Wartungs-Abo; auf das Ende von Website-Analyse und Terminplaner nur beim
// Wartungs-Abo eines Kunden, der eine von beiden hat (hatAboFunktionen)
blocks: { wartung: isWartung(sub), funktionen: isWartung(sub) && (await hatAboFunktionen(sub)) },
// Hinweis auf nicht genutzte Arbeitszeit nur beim Wartungs-Abo; auf das Ende von Website-Analyse und Terminplaner (funktionen)
// bzw. der Funktionen des Abo-Modells (funktionen_abo) nur beim Wartungs-Abo eines Kunden, der sie hat (funktionenBloecke)
blocks: { wartung: isWartung(sub), ...(await funktionenBloecke(sub)) },
})
await updateDocument(config.collections.subscriptions, sub.$id, {
cancelRequestedAt: receivedAt.toISOString(),
@@ -976,9 +1029,9 @@ router.post('/public/cancel', async (req, res) => {
abo: !storage,
// Hinweis auf nicht genutzte Arbeitszeit nur im Wartungs-Abo (Block innerhalb von abo)
wartung: !storage && isWartung(sub),
// Website-Analyse und Terminplaner enden mit dem Wartungs-Abo (Block innerhalb von abo, nie bei der Speicher-Kündigung),
// nur fuer einen Kunden, der eine von beiden hat (hatAboFunktionen)
funktionen: !storage && isWartung(sub) && (await hatAboFunktionen(sub)),
// Website-Analyse und Terminplaner (funktionen) bzw. die Funktionen des Abo-Modells (funktionen_abo) enden mit dem
// Wartungs-Abo (Bloecke innerhalb von abo, nie bei der Speicher-Kündigung), nur fuer einen Kunden, der sie hat (funktionenBloecke)
...(storage ? { funktionen: false, funktionen_abo: false } : await funktionenBloecke(sub)),
automatisch_gekuendigt: !storage && Boolean(effectiveAt) && d.kind !== 'geschaeftsaufgabe',
sonderkuendigung: !storage && Boolean(effectiveAt) && d.kind === 'geschaeftsaufgabe',
ausserordentlich: !storage && !effectiveAt && d.kind === 'ausserordentlich',
@@ -1086,10 +1139,15 @@ async function withdrawOrder(sub, d, receivedAt) {
}
}
if (inTime && type === 'wechsel') {
// Ohne Wartungs-Abo gibt es keine Website-Analyse: Messdienst abschalten (der Widerruf scheitert nie daran)
// Ohne Wartungs-Abo gibt es keine Website-Analyse: Messdienst abschalten (der Widerruf scheitert nie daran). Nicht in der neuen Ansicht
// (Kundenansicht neu, P6): dort enthaelt auch das Hosting-Abo die Analyse, sie bleibt. Ist das nicht lesbar, bleibt sie ebenfalls (der
// taegliche Abo-Check der Analyse raeumt bei Bedarf auf).
try {
const { endAnalyseForCustomer } = await import('../services/analyse.js')
await endAnalyseForCustomer(sub.customerId, { reason: 'widerruf_wechsel', mail: true, now: receivedAt })
const neueAnsichtKunde = await aboModellFuerKunde(sub.customerId)
if (!neueAnsichtKunde) {
const { endAnalyseForCustomer } = await import('../services/analyse.js')
await endAnalyseForCustomer(sub.customerId, { reason: 'widerruf_wechsel', mail: true, now: receivedAt })
}
} catch (err) {
console.error(`[abo] Analyse nach Widerruf des Wechsels (Abo ${sub.$id}) nicht beendet:`, err.message)
}
@@ -1102,6 +1160,14 @@ async function withdrawOrder(sub, d, receivedAt) {
} catch (err) {
console.error(`[abo] Terminplaner nach Widerruf des Wechsels (Abo ${sub.$id}) nicht beendet:`, err.message)
}
// Funktionen des Abo-Modells (News, Terminplaner, Webshop, Buchungskalender, Oeffnungszeiten live; auch im Projekt eingebaute): ebenso sofort,
// K39 mit dem Hosting-Satz; was misslingt, holt der Tagesabgleich nach
try {
const { beendeWartungsFunktionen } = await import('../services/aboFunktionenAbgleich.js')
await beendeWartungsFunktionen(sub.customerId, { now: receivedAt })
} catch (err) {
console.error(`[abo] Funktionen des Abo-Modells nach Widerruf des Wechsels (Abo ${sub.$id}) nicht beendet:`, err.message)
}
}
const event = await recordDeclarationEvent(sub, 'widerruf', receivedAt, { order: d.order, eventId: ids[0] ?? null, eventIds: ids, inTime }, 'offen')
return {
+15 -4
View File
@@ -3,7 +3,9 @@ import { Router } from 'express'
import { config } from '../../config.js'
import { getDocument } from '../../services/appwriteAdmin.js'
import { requireAppwriteTeam } from '../../middleware/appwriteStaff.js'
import { activateAnalyse, analyseEligibility, controllerView, deactivateAnalyse, ELIGIBILITY_MESSAGES, validateTextform } from '../../services/analyse.js'
import { activateAnalyse, analyseEligibility, controllerView, deactivateAnalyse, eligibilityMessage, validateTextform } from '../../services/analyse.js'
import { aboModellFuerKunde } from '../../services/aboFunktionen.js'
import { pruefeAboDeckung } from '../../services/funktionEinschalten.js'
import { getFeatureDoc } from '../../services/siteFeatures.js'
const router = Router()
@@ -13,7 +15,9 @@ router.use(requireAppwriteTeam)
function sendError(res, err) {
const status = [400, 403, 404, 409, 503].includes(err.status) && !err.type ? err.status : 500
if (status === 500) console.error('[admin-analyse]', err.message)
res.status(status).json({ error: status === 500 ? 'Die Analyse konnte nicht geändert werden' : err.message })
// Abo-Modell: code, grund, wechsel (und ab) der 409 abo_fehlt gehen flach in die Antwort, wie bei der Team-Aktivierung der Funktionen
const zusatz = status === 409 && err.code === 'abo_fehlt' && err.body && typeof err.body === 'object' ? err.body : {}
res.status(status).json({ ...zusatz, error: status === 500 ? 'Die Analyse konnte nicht geändert werden' : err.message })
}
// Lesezugriff fuer das WOMS: Zustand + AVV-Angaben (nie siteKey oder Geheimnisse)
@@ -27,9 +31,11 @@ router.get('/:projectId', async (req, res) => {
throw err
}
const customerId = project.customerId || ''
// Abo-Modell (Kundenansicht neu): Hosting oder Wartung genuegt; fuer alle anderen Kunden gilt die Wartungs-Regel wie bisher
const aboModell = await aboModellFuerKunde(customerId)
const [doc, eligibility, controller] = await Promise.all([
customerId ? getFeatureDoc(customerId, project.$id, 'analyse') : null,
analyseEligibility(customerId, project.$id),
analyseEligibility(customerId, project.$id, { aboModell }),
controllerView(customerId),
])
res.json({
@@ -45,7 +51,7 @@ router.get('/:projectId', async (req, res) => {
activatedAt: doc?.activatedAt || null,
offSince: doc?.offSince || null,
eligible: eligibility.ok,
reasonText: eligibility.reason ? ELIGIBILITY_MESSAGES[eligibility.reason] : null,
reasonText: eligibility.reason ? eligibilityMessage(eligibility.reason, { aboModell }) : null,
})
} catch (err) {
sendError(res, err)
@@ -75,6 +81,10 @@ router.post('/:projectId', async (req, res) => {
const customerId = project.customerId
if (!customerId) return res.status(400).json({ error: 'Projekt hat keinen Kunden' })
const by = String(req.appwriteUser?.name || req.appwriteUser?.email || 'Mitarbeiter').slice(0, 100)
// Abo-Modell (Kundenansicht neu, V10): das Abo muss die Analyse decken (409 abo_fehlt mit grund, bevor etwas geschrieben wird); das
// Dokument bekommt den Abo-Marker. Ausschalten braucht kein Abo. Alle anderen Kunden: alles wie bisher.
const aboModell = on && (await aboModellFuerKunde(customerId))
if (aboModell) await pruefeAboDeckung(customerId, 'analyse')
const { doc } = on
? await activateAnalyse({
customerId,
@@ -83,6 +93,7 @@ router.post('/:projectId', async (req, res) => {
by,
textform,
audienceConfirmed,
...(aboModell ? { aboModell: true, abrechnung: 'abo' } : {}),
})
: await deactivateAnalyse({ customerId, projectId: project.$id, by })
res.json({ ok: true, state: doc?.state || 'off' })
+6 -2
View File
@@ -11,6 +11,10 @@ import {
const router = Router()
// Eigene Fehlercodes (klein, mit Unterstrich, z. B. kundenart_fehlt) gehen mit in die Antwort; Appwrite-Fehler tragen eine
// Zahl und Systemfehler Grossbuchstaben (ECONNREFUSED): die Antworten von heute bleiben dafuer unveraendert
const codeVon = (err) => (typeof err.code === 'string' && /^[a-z][a-z_]*$/.test(err.code) ? { code: err.code } : {})
router.use(requireAppwriteStaff)
router.get('/', async (_req, res) => {
@@ -29,7 +33,7 @@ router.post('/', async (req, res) => {
return res.status(201).json(result)
} catch (err) {
const status = err.status || 500
return res.status(status).json({ error: err.message || 'Kunde konnte nicht angelegt werden' })
return res.status(status).json({ error: err.message || 'Kunde konnte nicht angelegt werden', ...codeVon(err) })
}
})
@@ -39,7 +43,7 @@ router.patch('/:customerId', async (req, res) => {
return res.json(result)
} catch (err) {
const status = err.status || 500
return res.status(status).json({ error: err.message || 'Kunde konnte nicht aktualisiert werden' })
return res.status(status).json({ error: err.message || 'Kunde konnte nicht aktualisiert werden', ...codeVon(err) })
}
})
+30 -5
View File
@@ -1,6 +1,8 @@
import { Router } from 'express'
import { config } from '../../config.js'
import { aboMemo } from '../../services/aboStatus.js'
import { aboModellFuerKunde } from '../../services/aboFunktionen.js'
import { ABRECHNUNGEN } from '../../services/aboModell.js'
import { FUNKTION_CHANNELS, validateTextform } from '../../services/analyse.js'
import { getDocument, updateDocument } from '../../services/appwriteAdmin.js'
import { featureConfig, featureHaken } from '../../services/featureRegistry.js'
@@ -46,7 +48,9 @@ const adminName = (req) => req.session.adminName || req.session.name || 'Admin'
function sendError(res, err) {
const status = err.status >= 400 && err.status < 600 ? err.status : 500
if (status >= 500) console.error('[funktionen-admin]', err.message)
return res.status(status).json({ error: err.message || 'Interner Serverfehler' })
// err.body (z. B. code, grund, wechsel der 409 abo_fehlt) geht flach in die Antwort; error bleibt der Text der Meldung
const zusatz = err.body && typeof err.body === 'object' && !Array.isArray(err.body) ? err.body : {}
return res.status(status).json({ ...zusatz, error: err.message || 'Interner Serverfehler' })
}
/** Neuester Eintrag zuerst; aeltere Eintraege fallen vorn heraus, sobald 300 Zeichen voll sind. */
@@ -353,9 +357,23 @@ funktionenAdminRouter.post('/:customerId/:key', async (req, res) => {
}
await assertOwnProject(customerId, projectId)
const by = adminName(req)
// Abo-Modell (Kundenansicht neu, V10): nur fuer Kunden der neuen Ansicht zaehlt abrechnung (Vorgabe 'abo'); sonst wird sie ignoriert und
// alles laeuft wie heute. Eine ungueltige Abrechnung ist 400, bevor etwas geschrieben wird.
const modell = action === 'activate' && (await aboModellFuerKunde(customerId))
let abrechnung = null
if (modell) {
abrechnung = req.body?.abrechnung === undefined ? 'abo' : req.body.abrechnung
if (!ABRECHNUNGEN.includes(abrechnung)) return res.status(400).json({ error: 'Unbekannte Abrechnung' })
}
const rueckmeldung = {} // adminActivate meldet hier die Warnung der Aufbauphase (abo_fehlt)
let freigeschaltet = null // Terminplaner oder Webshop mit Zustimmung in Textform: K16 mit den Unterlagen nach der Sperre
let ersatzTicket
if (key === 'zeiten' && action === 'activate') {
if (key === 'zeiten' && action === 'activate' && modell) {
// Wie zeitenAktivieren ohne feature-admin: Neubau und Ticket laufen in einschalten, die Sperre wuerde dem Team jede Aktion am Projekt halten
const bisher = await getFeatureDoc(customerId, projectId, key)
const adminNote = composeAdminNote(bisher?.adminNote, { by, action, note })
await adminActivate(customerId, projectId, key, { by, adminNote, abrechnung, rueckmeldung })
} else if (key === 'zeiten' && action === 'activate') {
await zeitenAktivieren({ customerId, projectId, by, note })
} else if (key === 'zeiten' && action === 'disable') {
ersatzTicket = await zeitenAbschalten({ customerId, projectId, by, note })
@@ -365,9 +383,14 @@ funktionenAdminRouter.post('/:customerId/:key', async (req, res) => {
const adminNote = composeAdminNote(doc?.adminNote, { by, action, note })
if (action === 'activate') {
const textform = await textformFuerAktivierung({ body: req.body, customerId, key, doc })
const attachments = textform ? await unterlagenAlsAnhang(key) : []
const aktiviert = await adminActivate(customerId, projectId, key, { by, adminNote, textform })
if (textform) freigeschaltet = { doc: aktiviert, attachments }
if (modell) {
// Die Unterlagen gehen mit K38 (aboFunktionenMeldung.js), nicht mit K16
await adminActivate(customerId, projectId, key, { by, adminNote, textform, abrechnung, rueckmeldung })
} else {
const attachments = textform ? await unterlagenAlsAnhang(key) : []
const aktiviert = await adminActivate(customerId, projectId, key, { by, adminNote, textform })
if (textform) freigeschaltet = { doc: aktiviert, attachments }
}
} else if (action === 'abnahme') {
await freischaltenNachAbnahme({ customerId, projectId, key, by, adminNote })
} else if (action === 'ticket') {
@@ -392,6 +415,8 @@ funktionenAdminRouter.post('/:customerId/:key', async (req, res) => {
// Ergebnis des Ersatz-Tickets beim Abschalten der Zeiten (angelegt | aktuell | fehler), damit ein Fehler im Panel sichtbar wird
...(ersatzTicket ? { ersatzTicket } : {}),
...(warnung ? { warnung } : {}),
// Aufbauphase (E9): im Projekt eingebaut, obwohl noch kein Abo laeuft; der String unterscheidet sich vom alten warnung: true (K16 ging nicht raus)
...(rueckmeldung.warnung ? { warnung: rueckmeldung.warnung.code, grund: rueckmeldung.warnung.grund } : {}),
})
} catch (err) {
sendError(res, err)
+3
View File
@@ -42,6 +42,7 @@ import {
import { logViewAction } from '../services/customerViewLog.js'
import { isStaffAccount } from '../services/staffLabels.js'
import { neueAnsichtFuerSession } from '../services/kundenansicht/schalter.js'
import { kundenartGespeichert, kundenartWirksam } from '../services/kundenart.js'
const router = Router()
@@ -56,6 +57,8 @@ function sanitizeCustomer(customer) {
location: customer.location || '',
customerStatus: customer.customerStatus || '',
portalAccessEnabled: Boolean(customer.portalAccessEnabled),
kundenart: kundenartGespeichert(customer),
kundenartWirksam: kundenartWirksam(customer),
}
}
+22
View File
@@ -0,0 +1,22 @@
import { Router } from 'express'
import { getSessionCustomerId, requireSession } from '../middleware/session.js'
import { sammleFristen } from '../services/fristen.js'
import { einschaltenDeps } from '../services/zeiten/einschalten.js'
/**
* Gesammelte Widerrufsfristen des angemeldeten Kunden (V6). Mount: /api/fristen. Nur lesend, daher auch in der
* Kundenansicht erlaubt (der Wächter sperrt nur Schreibzugriffe).
*/
const router = Router()
router.get('/', requireSession, async (req, res) => {
res.set('Cache-Control', 'no-store')
try {
return res.json(await sammleFristen(getSessionCustomerId(req), { now: einschaltenDeps.jetzt() }))
} catch (err) {
console.error('[fristen] Laden fehlgeschlagen:', err.message)
return res.status(500).json({ error: 'Die Fristen konnten gerade nicht geladen werden.' })
}
})
export default router
+163 -13
View File
@@ -25,12 +25,18 @@ import { zeitenFuerKunde } from '../services/zeiten/schalter.js'
import { einschaltenDeps, einschaltenZeiten, zeitenProjektAnsicht } from '../services/zeiten/einschalten.js'
import { starteBehalten } from '../services/zeiten/behalten.js'
import { aboMemo } from '../services/aboStatus.js'
import { featureConfig, featureHaken, featureKeys, featureSperre, freiweg, oeffentlicheAnsicht } from '../services/featureRegistry.js'
import { aboProdukt } from '../services/aboFunktionen.js'
import { analyseUnterlagen } from '../services/aboUnterlagen.js'
import { anfrageStatusJeProjekt, katalogFuerKunde } from '../services/aboKatalog.js'
import { datumDe, einschalten, EINSCHALTEN_TEXTE, pruefeAboDeckung, pruefeEinschaltZustand } from '../services/funktionEinschalten.js'
import { kundenartWirksam, NUR_BETRIEB_TEXT } from '../services/kundenart.js'
import { neueAnsicht } from '../services/kundenansicht/schalter.js'
import { abowegAnalyse, aboweg, consentAbo, consentAboAktiv, featureConfig, featureHaken, featureKeys, featureSperre, freiweg, katalogFelder, oeffentlicheAnsicht } from '../services/featureRegistry.js'
import { aboMeldung, bestellFehler, istFortsetzen, oeffentlicheConsent, pruefeAbo, pruefeInklusive, pruefeZustimmung } from '../services/featureOrder.js'
import {
ELIGIBILITY_MESSAGES,
activateAnalyse,
eligibilityMessage,
analyseDeps,
analyseHosts,
isDevPreview,
@@ -55,11 +61,11 @@ function fail(res, err, fallback) {
const ANALYSE_OFF = 'Website-Analyse ist noch nicht freigeschaltet'
const ANALYSE_ERROR = 'Die Website-Analyse konnte gerade nicht geändert werden. Bitte versuch es später noch einmal oder schreib uns im Chat.'
async function analyseView(customerId, projectId) {
async function analyseView(customerId, projectId, { aboModell = false } = {}) {
if (!config.analyse.enabled) return { comingSoon: true }
const [doc, eligibility, controller] = await Promise.all([
getFeatureDoc(customerId, projectId, 'analyse'),
analyseEligibility(customerId, projectId),
analyseEligibility(customerId, projectId, { aboModell }),
controllerView(customerId),
])
let devMode = false
@@ -73,9 +79,10 @@ async function analyseView(customerId, projectId) {
accentColor: doc?.accentColor || '#111111',
eligible: eligibility.ok,
reason: eligibility.reason || null,
reasonText: eligibility.reason ? ELIGIBILITY_MESSAGES[eligibility.reason] : null,
avvVersion: config.analyse.avvVersion,
legal: legalPaths(config.analyse.avvVersion),
reasonText: eligibility.reason ? eligibilityMessage(eligibility.reason, { aboModell }) : null,
// Neue Ansicht: Fassung und Adressen aus derselben Quelle wie der Nachweis (die Oberflaeche zeigt und sendet, was der Server prueft)
avvVersion: aboModell ? analyseUnterlagen().avvVersion : config.analyse.avvVersion,
legal: aboModell ? analyseUnterlagen().legal : legalPaths(config.analyse.avvVersion),
controller,
activatedAt: doc?.state === 'active' ? doc.activatedAt || null : null,
}
@@ -210,10 +217,11 @@ router.get('/', requireSession, async (req, res) => {
const projects = await listCustomerFeatures(customerId, { docs })
const zeitenAn = zeitenFuerKunde(req.session.email)
const jetzt = einschaltenDeps.jetzt()
const neu = neueAnsicht(req)
// Ein Fehler bei einem Projekt (z. B. Abo-Pruefung) darf nicht die ganze Liste kosten
await Promise.all(
projects.map(async (p) => {
p.analyse = await analyseView(customerId, p.projectId).catch((err) => {
p.analyse = await analyseView(customerId, p.projectId, { aboModell: neu }).catch((err) => {
console.error('[funktionen] Analyse-Zustand fehlgeschlagen:', err.message)
return { state: 'unknown' }
})
@@ -222,7 +230,7 @@ router.get('/', requireSession, async (req, res) => {
)
// Der Abo-Stand wird fuer Terminplaner und Oeffnungszeiten nur einmal gelesen
const abos = aboMemo()
res.json({
const antwort = {
projects,
// Preise und Bestelltexte je Funktion aus dem Register (nur die oeffentliche Ansicht, auch wenn der Terminplaner
// dem Kunden noch nicht freigegeben ist); setupEstimate und consentText bleiben als News-Alias fuer aeltere Clients
@@ -244,7 +252,23 @@ router.get('/', requireSession, async (req, res) => {
// Oeffnungszeiten live: Schalter ZEITEN_ENABLED (off | pilot | on); nur wenn verfuegbar zeigt das Portal die Kachel, dann mit
// dem Abo-Stand (inklusive, grund), dem Preis und den Texten des Gratisweges "Einschalten"
zeiten: await zeitenAngebot(req.session.email, customerId, abos),
})
}
// Kundenansicht neu (V5): Abo-Modell mit Kundenart, Abo-Stand, Katalog und Anfragestatus von Webshop und Buchungskalender je Projekt.
// Ohne neue Ansicht bleibt die Antwort wie sie war. Ein Lesefehler beim Abo-Stand ist ein Fehler der Liste (nie ein erfundenes "kein Abo").
if (neu) {
const [kunde, abo, katalog, anfrageStatus] = await Promise.all([
kundenDokument(customerId),
aboProdukt(customerId, { now: jetzt }),
katalogFuerKunde({ email: req.session.email }),
anfrageStatusJeProjekt({ docs, projects }),
])
for (const p of projects) {
p.features.shop.anfrageStatus = anfrageStatus.get(p.projectId)?.shop ?? ''
p.features.unterkunft.anfrageStatus = anfrageStatus.get(p.projectId)?.unterkunft ?? ''
}
Object.assign(antwort, { modell: 'abo', kundenart: kundenartWirksam(kunde), abo, katalog })
}
res.json(antwort)
} catch (err) {
fail(res, err, 'Die Funktionen konnten gerade nicht geladen werden. Bitte lade die Seite gleich noch einmal.')
}
@@ -390,7 +414,10 @@ const EINSCHALTEN_ERROR = 'Öffnungszeiten live konnte gerade nicht eingeschalte
// 1 Kundenansicht, 2 Zustimmung (Gratis-Fassung: ein Haken, Knopf "Einschalten"), 3 Projekt (eigen, nicht archiviert),
// 4 Schalter/Pilot, 5 Unterlagen. Das Abo wird erst unter der Sperre frisch geprueft (einschaltenZeiten), dort entstehen auch Nachweis,
// Zustand, Neubau, Ticket und K27; ein zweiter Klick holt Fehlendes nach.
router.post('/zeiten/einschalten', requireSession, async (req, res) => {
// Weiche (Kundenansicht neu): wer die neue Ansicht sieht, geht an POST /:key/einschalten (unten); alle anderen behalten diesen Weg unveraendert
const alteAnsichtNur = (req, _res, next) => (neueAnsicht(req) ? next('route') : next())
router.post('/zeiten/einschalten', requireSession, alteAnsichtNur, async (req, res) => {
const now = new Date()
try {
// 1 Kundenansicht: Mitarbeiter schalten nicht fuer den Kunden ein
@@ -426,6 +453,107 @@ router.post('/zeiten/einschalten', requireSession, async (req, res) => {
}
})
const EINSCHALTEN_NEU_ERROR = 'Die Funktion konnte gerade nicht eingeschaltet werden. Bitte versuch es später noch einmal oder schreib uns im Chat.'
const ZUSTIMMUNG_EINSCHALTEN = 'Bitte bestätige die Erklärung, um einzuschalten.'
/** Kundendokument einmal lesen; ein unbekannter Kunde ergibt {} (Kundenart privat). */
async function kundenDokument(customerId) {
try {
return await getDocument(config.collections.customers, customerId)
} catch (err) {
if (err.status === 404) return {}
throw err
}
}
// Einschalten im Abo-Modell (V5, nur neue Ansicht; Spec 5.4): ohne Zahlung, mit Einrichtungs-Ticket. Vorpruefungen, jede ohne Seiteneffekt:
// 0 neue Ansicht (sonst 404), 1 Kundenansicht (403), 2 Funktionsschluessel, 3 Eingabe (Projekt, Zustimmung), 4 Projekt,
// 5 409 in dieser Reihenfolge: nicht_verfuegbar (dann 503 unterlagen_fehlen), abo_fehlt, nur_betrieb, bestellung_offen, beginn_noetig,
// bereits_eingeschaltet | bestellung_laeuft | abgeschaltet, fassung_veraltet; 6 einschalten() (Sperre, Abo frisch, Nachweis, Zustand, Ticket).
// Die Kundenart kommt nur aus dem Kundendokument, nie aus dem Koerper.
router.post('/:key/einschalten', requireSession, async (req, res) => {
const jetzt = einschaltenDeps.jetzt()
try {
if (!neueAnsicht(req)) return res.status(404).json({ error: 'Nicht gefunden' })
if (req.session.viewAs) return res.status(403).json({ error: 'In der Kundenansicht kann nicht eingeschaltet werden.' })
const key = req.params.key
const feature = featureConfig(key)
if (!feature) return res.status(404).json({ error: 'Unbekannte Funktion' })
const body = req.body || {}
const { projectId, zustimmung } = body
if (typeof projectId !== 'string' || !projectId) return res.status(400).json({ error: 'Bitte wähle ein Projekt.' })
const zustimmungOk =
zustimmung !== null && typeof zustimmung === 'object' && !Array.isArray(zustimmung) && typeof zustimmung.version === 'string' &&
Array.isArray(zustimmung.haken) && zustimmung.haken.every((id) => id === 'beginn')
if (!zustimmungOk) throw bestellFehler(400, ZUSTIMMUNG_EINSCHALTEN, 'zustimmung_fehlt')
const customerId = getSessionCustomerId(req)
await assertOwnProject(customerId, projectId, { forNewOrder: true })
// nicht_verfuegbar (Schalter der Funktion), danach die Vertragsunterlagen
const sperre = featureSperre(key, req.session.email)
if (sperre) throw bestellFehler(409, sperre, 'nicht_verfuegbar')
const haken = featureHaken(key)
await pruefeUnterlagen(haken)
// abo_fehlt: das Abo frisch lesen, Lesefehler sind 503
await pruefeAboDeckung(customerId, key, jetzt)
// nur_betrieb
const kunde = await kundenDokument(customerId)
const privat = kundenartWirksam(kunde) === 'privat'
if (privat && katalogFelder(key).nurBetrieb) throw bestellFehler(409, NUR_BETRIEB_TEXT, 'nur_betrieb')
// bestellung_offen: offene Punkte der Funktion (Fragebogen, Freigabe der Anfrage)
if (haken.bestellpruefung) {
const offen = await haken.bestellpruefung({ customerId, projectId, feature })
if (!Array.isArray(offen)) throw new TypeError('bestellpruefung muss eine Liste liefern')
if (offen.length > 0) throw bestellFehler(409, 'Es fehlen noch Angaben, bevor du einschalten kannst.', 'bestellung_offen', { offen })
}
// beginn_noetig: Privatkunden innerhalb der Widerrufsfrist brauchen das Haekchen fuer den sofortigen Beginn
let fristBis = null
if (privat) {
try {
fristBis = (await aboProdukt(customerId, { now: jetzt })).widerrufsfristBis
} catch (err) {
console.error(`[funktionen] Widerrufsfrist für das Einschalten nicht lesbar: ${err?.name ?? 'Fehler'} ${err?.status ?? ''}`.trim())
throw bestellFehler(503, EINSCHALTEN_TEXTE.nichtPruefbar)
}
}
const beginnNoetig = privat && fristBis !== null && Date.parse(fristBis) >= jetzt.getTime()
const gesetzt = privat && zustimmung.haken.includes('beginn')
if (beginnNoetig && !gesetzt) {
const ab = new Date(Date.parse(fristBis) + 1).toISOString()
throw bestellFehler(409, `Für den sofortigen Beginn brauchst du das Häkchen. Ohne Häkchen kannst du ab dem ${datumDe(ab)} einschalten.`, 'beginn_noetig', { ab })
}
// bereits_eingeschaltet | bestellung_laeuft | abgeschaltet (setup ohne Ticket durch Abo oder Projekt heilt der Dienst)
pruefeEinschaltZustand(await getFeatureDoc(customerId, projectId, key))
// fassung_veraltet: der Kunde hat einen alten Wortlaut gesehen
if (zustimmung.version !== consentAbo(key).version) {
throw bestellFehler(409, 'Die Erklärung wurde inzwischen geändert. Bitte lies sie noch einmal und schalte dann ein.', 'fassung_veraltet', {
consent: consentAboAktiv(key, { beginn: beginnNoetig }),
})
}
const { doc, ticketWoid } = await einschalten({
customerId,
projectId,
key,
by: 'kunde',
abrechnung: 'abo',
now: jetzt,
// Nachweis ohne Stripe-Sitzung und ohne Belehrung; ein mitgeschicktes beginn zaehlt nur fuer Privatkunden
nachweis: () =>
speichereNachweisAusAnfrage(req, {
feature: aboweg(feature, { beginn: gesetzt }),
customerId,
projectId,
klickAt: jetzt.toISOString(),
kundenart: kundenartWirksam(kunde),
}),
})
res.json({ feature: await featureView(doc, key), ticketWoid })
} catch (err) {
fail(res, err, EINSCHALTEN_NEU_ERROR)
}
})
const BEHALTEN_ERROR = 'Das Behalten konnte gerade nicht gestartet werden. Bitte versuch es später noch einmal oder schreib uns im Chat.'
// Behalten (Spec 3.3): nach dem Ende des Wartungs-Abos Öffnungszeiten live fuer einmalig 30 € behalten, ohne neuen Einbau. Vorpruefungen wie im
@@ -476,14 +604,36 @@ router.post('/analyse/activate', analyseGuard, async (req, res) => {
return res.status(403).json({ error: 'In der Kundenansicht kann nichts geändert werden.' })
}
const { projectId, accentColor, consent } = req.body || {}
await assertOwnProject(getSessionCustomerId(req), analyseProjectId(projectId), { forNewOrder: true })
// Neue Ansicht (Abo-Modell): die Zielgruppen-Aussage (keine Website fuer Kinder unter 16) ist serverseitig erzwungen, nicht nur ein
// Haekchen im Browser; vor jedem Schreiben. Die alte Ansicht bleibt unveraendert.
const neu = neueAnsicht(req)
if (neu && req.body?.audienceConfirmed !== true) {
throw bestellFehler(400, 'Bitte bestätige, dass sich deine Website nicht gezielt an Kinder unter 16 Jahren richtet.', 'zielgruppe_fehlt')
}
const customerId = getSessionCustomerId(req)
await assertOwnProject(customerId, analyseProjectId(projectId), { forNewOrder: true })
// Nachweis der Erklaerung (featureConsents) mit der Kundenart des Dokuments, nie aus dem Body; entsteht nur bei einer neuen Aktivierung
const abo = neu
? {
aboModell: true,
nachweis: async () =>
speichereNachweisAusAnfrage(req, {
feature: abowegAnalyse(),
customerId,
projectId,
klickAt: einschaltenDeps.jetzt().toISOString(),
kundenart: kundenartWirksam(await kundenDokument(customerId)),
}),
}
: {}
const { doc } = await activateAnalyse({
customerId: getSessionCustomerId(req),
customerId,
projectId: analyseProjectId(projectId),
accentColor,
consent,
by: 'customer',
actor: actingPerson(req.session),
...abo,
})
res.json({ ok: true, state: doc.state })
} catch (err) {
+16 -1
View File
@@ -8,6 +8,8 @@ import { isKundenansicht, logIfKundenansicht } from '../services/kundenansicht.j
import { createWriteLimiter } from '../services/writeLimiter.js'
import { anfrageAnsicht, ensureShopConfig, leseShopConfig, stelleAnfrage, zieheZurueck } from '../services/shop/anfrage.js'
import { shopFuerKunde } from '../services/shop/schalter.js'
import { kundenartWirksam } from '../services/kundenart.js'
import { neueAnsicht } from '../services/kundenansicht/schalter.js'
import { kontoAnlegen, kontoStatus, onboardingLink, statusAusKonto } from '../services/shop/connect.js'
import { bestellungKunde, bestellungLesen, csv as bestellungenCsv, erstatte, listeBestellungen, setzeStatus } from '../services/shop/bestellungen.js'
import { listeWiderrufe, markiereBearbeitet } from '../services/shop/widerrufe.js'
@@ -210,12 +212,25 @@ router.get('/status', requireSession, route(async (req, res) => {
res.json(antwort)
}))
/** Kundenart des Kundendokuments (leer oder unbekannt = privat) - nur der Server entscheidet, nie der Client; ein unbekannter Kunde ist privat. */
async function kundenartDesKunden(customerId) {
try {
return kundenartWirksam(await getDocument(config.collections.customers, customerId))
} catch (err) {
if (err?.status === 404) return 'privat'
throw err
}
}
router.post('/anfrage', requireSession, nurKunde, limitAnfrage, route(async (req, res) => {
const customerId = getSessionCustomerId(req)
const anfrage = await stelleAnfrage({
customerId: getSessionCustomerId(req),
customerId,
projectId: projectIdOf(req),
eingabe: req.body?.anfrage,
person: person(req),
// Abo-Modell (neue Ansicht): Unternehmer aus der Kundenart, privat nur_betrieb; sonst unveraendert (undefined)
kundenart: neueAnsicht(req) ? await kundenartDesKunden(customerId) : undefined,
})
res.json({ anfrage })
}))
+25
View File
@@ -1,6 +1,7 @@
import { Router } from 'express'
import { config } from '../../config.js'
import { listViewLog } from '../../services/customerViewLog.js'
import { getLedger } from '../../services/hourLedger.js'
import { getPortalAccessStatus } from '../../services/portalAccessStatus.js'
import { PRODUCT_KEYS } from '../../services/products.js'
import { ACTIVE_STATUSES, getCurrentSubscription } from '../../services/stripeBilling.js'
@@ -53,6 +54,30 @@ router.get('/:id/abo', async (req, res) => {
}
})
// Stundenkonto fuers Ticketsystem (Abo-Modell der Funktionen): Produkt wie /:id/abo, dazu Guthaben, Vorschuss
// (Einrichtung ohne Deckung) und Mehraufwand. Nur das Wartungs-Abo hat ein Konto, auch nach dem Ende
// (dann Guthaben 0, der Vorschuss bleibt stehen); Hosting und kein Abo ergeben ledger null
router.get('/:id/stundenkonto', async (req, res) => {
try {
const sub = await getCurrentSubscription(req.params.id)
if (!sub) return res.json({ produkt: null, ledger: null })
const produkt = PRODUCT_KEYS.includes(sub.productKey) ? sub.productKey : 'wartung'
if (produkt !== 'wartung') return res.json({ produkt, ledger: null })
const l = await getLedger(req.params.id, { active: ACTIVE_STATUSES.has(sub.status) })
return res.json({
produkt,
ledger: {
balanceMinutes: l.balanceMinutes,
vorschussMinutes: l.vorschussMinutes,
overageMinutes: l.overageMinutes,
nextExpiry: l.nextExpiry,
},
})
} catch (err) {
return res.status(500).json({ error: err.message || 'Stundenkonto konnte nicht geladen werden' })
}
})
// Kundenansicht oeffnen: Einmal-Link (60 s), den das Ticketsystem in einem neuen Tab aufruft
router.post('/:id/kundenansicht', async (req, res) => {
try {
+22
View File
@@ -0,0 +1,22 @@
import { listSubscriptionsForCustomer } from './stripeBilling.js'
/**
* Aufbauphase (E9, N28; Kundenansicht neu, P6): Hatte der Kunde noch nie ein Abo, weil seine Website beim Projektstart noch im Bau ist?
* Gemeinsame Quelle fuer die Team-Aktivierung "im Projekt" (siteFeatures.adminActivate) und die beiden Tagesabgleiche (Funktionen des
* Abo-Modells, Oeffnungszeiten live): Solange es wahr ist, bleibt eine im Projekt eingebaute Funktion in Ruhe; mit dem ersten Abo
* gilt die Abo-Regel auch fuer 'projekt'.
*/
/** Stati abgebrochener Bestellungen (Checkout nie abgeschlossen): sie zaehlen nicht als Abo. */
const ABGEBROCHEN = new Set(['incomplete', 'incomplete_expired'])
/**
* true = der Kunde hatte noch nie ein Abo: kein Abo-Dokument mit einem Status ausser incomplete und incomplete_expired (jeder andere
* Status zaehlt, auch ein beendetes Abo). Lesefehler werden weitergereicht (fail-closed: der Aufrufer behandelt den Kunden dann nicht
* als Aufbauphase). Eine leere oder keine String-Kunden-ID ist ein TypeError.
*/
export async function ohneAboBisher(customerId) {
if (typeof customerId !== 'string' || customerId === '') throw new TypeError('ohneAboBisher: Kunden-ID erwartet')
const abos = await listSubscriptionsForCustomer(customerId)
return !abos.some((abo) => !ABGEBROCHEN.has(abo.status))
}
+109
View File
@@ -0,0 +1,109 @@
import { config } from '../config.js'
import { getDocument } from './appwriteAdmin.js'
import { ACTIVE_STATUSES, getCurrentSubscription } from './stripeBilling.js'
import { listContractEvents, openWithdrawals } from './contractEvents.js'
import { neueAnsichtFuerKunde } from './kundenansicht/schalter.js'
/**
* Abo-Zuordnung der Funktionen (V5, Spec E3/E8): Welches Abo deckt welche Funktion? Ein Lesepfad (lies) fuer alle
* Fragen, damit Einschalten, Katalog und Abgleich nie verschieden urteilen. Fail-closed: ein Lesefehler wirft, er wird
* nie zu kein_abo. Wer ihn abfaengt, entscheidet selbst (Einschalten: 503, Abgleichsjob: Kunde ueberspringen).
*
* Produkt: ohne productKey gilt Wartung (Altbestand, wie routes/abo.js). Ist das juengste wechsel-Ereignis dieses Abos
* widerrufen, sagt der Spiegel noch Wartung, es gilt aber Hosting (der Widerruf setzt den Spiegel nicht zurueck, wie in
* aboStatus.js). Ein Abo mit unbekanntem productKey deckt nichts (produkt null, laeuft false).
*/
export const ENTHALTEN_IN = Object.freeze({
analyse: ['hosting', 'wartung'],
news: ['wartung'],
termine: ['wartung'],
shop: ['wartung'],
unterkunft: ['wartung'],
zeiten: ['wartung'],
})
// Nur diese Ereignisse haben eine Widerrufsfrist, die den Beginn einer Funktion betrifft (Speicher-Plus zaehlt nicht)
const FRIST_TYPEN = new Set(['abschluss', 'wechsel'])
function pruefeSchluessel(key) {
if (!Object.hasOwn(ENTHALTEN_IN, key)) throw new TypeError(`Unbekannte Funktion: ${String(key)}`)
}
/** Zeitpunkt in ms aus einem Text oder Date; NaN, wenn es keiner ist (null und '' sind keine Zeitpunkte). */
function zeitpunkt(wert) {
if (wert instanceof Date) return wert.getTime()
return typeof wert === 'string' && wert !== '' ? Date.parse(wert) : Number.NaN
}
async function lies(customerId, now) {
const abo = await getCurrentSubscription(customerId)
const ohne = { laeuft: false, produkt: null, gekuendigt: false, widerrufsfristBis: null, status: abo?.status ?? null, trialEnd: null }
if (!abo || !ACTIVE_STATUSES.has(abo.status)) return ohne
const spiegel = abo.productKey || 'wartung'
if (spiegel !== 'wartung' && spiegel !== 'hosting') return ohne
// Ereignisse dieses Abos: Wechsel-Regel und offene Widerrufsfristen
const ereignisse = (await listContractEvents(customerId)).filter((e) => e.subscriptionId === abo.$id)
const wechselWiderrufen = ereignisse.find((e) => e.type === 'wechsel')?.status === 'widerrufen' // neueste zuerst
const produkt = spiegel === 'wartung' && wechselWiderrufen ? 'hosting' : spiegel
const enden = openWithdrawals(ereignisse.filter((e) => FRIST_TYPEN.has(e.type)), now).map((f) => zeitpunkt(f.until))
const spaetestes = enden.length ? Math.max(...enden) : null
return {
laeuft: true,
produkt,
gekuendigt: Boolean(abo.cancelAtPeriodEnd || abo.cancelAt),
widerrufsfristBis: spaetestes === null ? null : new Date(spaetestes).toISOString(),
status: abo.status,
trialEnd: abo.trialEnd,
}
}
/** { produkt: 'hosting' | 'wartung' | null, laeuft, gekuendigt, widerrufsfristBis: ISO | null } (genau diese vier Schluessel) */
export async function aboProdukt(customerId, { now = new Date() } = {}) {
const { produkt, laeuft, gekuendigt, widerrufsfristBis } = await lies(customerId, now)
return { produkt, laeuft, gekuendigt, widerrufsfristBis }
}
/**
* Deckt das Abo des Kunden diese Funktion? { ok: true } | { ok: false, grund } mit grund in der Reihenfolge
* kein_abo, falsches_abo, gekuendigt, beginnt_spaeter (mit ab = Ende der Probezeit als ISO), nicht_aktiv.
* beginnt_spaeter: Status trialing mit gueltigem trialEnd nach now (Privatkunde, der ohne Haekchen bestellt hat).
*/
export async function aboDeckt(customerId, key, { now = new Date() } = {}) {
pruefeSchluessel(key)
const l = await lies(customerId, now)
if (!l.laeuft) return { ok: false, grund: 'kein_abo' }
if (!ENTHALTEN_IN[key].includes(l.produkt)) return { ok: false, grund: 'falsches_abo' }
if (l.gekuendigt) return { ok: false, grund: 'gekuendigt' }
if (l.status === 'trialing') {
const ende = zeitpunkt(l.trialEnd)
if (ende > now.getTime()) return { ok: false, grund: 'beginnt_spaeter', ab: new Date(ende).toISOString() }
}
if (l.status !== 'active') return { ok: false, grund: 'nicht_aktiv' }
return { ok: true }
}
/** Laeuft ein Abo, das diese Funktion enthaelt? Gekuendigt und past_due zaehlen noch (fuer Abgleichsjobs). */
export async function aboLaeuftFuer(customerId, key) {
pruefeSchluessel(key)
const l = await lies(customerId, new Date())
return l.laeuft && ENTHALTEN_IN[key].includes(l.produkt)
}
/**
* Gehoert dieser Kunde zum Abo-Modell (sieht er die neue Ansicht)? Fuer Jobs und Dienste ohne Cookie (Tagespruefung der Analyse,
* Team-Aktivierung): liest das Kundendokument (kein Dokument: false) und fragt neueAnsichtFuerKunde({ customerId, email }). Nie
* neueAnsichtFuerSession: eine Sitzung braucht dort auch appwriteUserId, die ein Job nicht hat. Lesefehler werden weitergereicht.
*/
export async function aboModellFuerKunde(customerId) {
if (typeof customerId !== 'string' || customerId === '') return false
let kunde
try {
kunde = await getDocument(config.collections.customers, customerId)
} catch (err) {
if (err.status === 404) return false
throw err
}
return neueAnsichtFuerKunde({ customerId, email: kunde.email })
}
+373
View File
@@ -0,0 +1,373 @@
import { config } from '../config.js'
import { ENTHALTEN_IN, aboProdukt } from './aboFunktionen.js'
import { ohneAboBisher } from './aboAufbauphase.js'
import { istAboModell } from './aboModell.js'
import { meldeBeendet } from './aboFunktionenMeldung.js'
import { Query, deleteDocument, getDocument, listAllDocuments, updateDocument } from './appwriteAdmin.js'
import { withLocks } from './keyedLock.js'
import { deleteNewsImage } from './newsImages.js'
import { listPosts } from './newsPosts.js'
import { activate, disable, listCustomerFeatureDocs, stateFields } from './siteFeatures.js'
import { pruefeNow } from './termine/util.js'
import { loeschTag } from './termine/frist.js'
import { ladeEinstellung } from './termine/kontext.js'
import { heute } from './termine/zeit.js'
import { leereCache } from './unterkunft/kontext.js'
/**
* Abo-Ende-Abgleich der Funktionen des Abo-Modells (Kundenansicht neu, P6, V11, N31): News-Pinnwand, Terminplaner, Webshop und
* Buchungskalender gehoeren zum Abo und enden mit ihm, spaetestens am Folgetag (Tagesjob, angeschlossen in Task 11). Die Oeffnungszeiten
* live bleiben bei zeiten/aboAbgleich.js (30 Tage Frist, Task 10). Nur Dokumente des Abo-Modells (istAboModell: unlockedBy UND
* einrichtungAbrechnung aus abo oder projekt) fasst dieser Abgleich an; Dokumente ohne Marker (Kauf-Bestand, Team, Altbestand) gehoeren dem
* alten Terminplaner-Abgleich (termine/aboAbgleich.js) und bleiben unberuehrt.
*
* Abschalten setup|active und das Abo deckt die Funktion nicht (aboProdukt/ENTHALTEN_IN; gekuendigt und past_due laufen noch, nichts
* abschalten) => disable (Grund abo_ende), danach K39 (meldeBeendet) und der Marker aboEndeMailAt.
* Aufbauphase (E9, E21): ein Dokument mit unlockedBy 'projekt' bleibt, solange der Kunde noch nie ein Abo hatte
* (ohneAboBisher, abgebrochene Bestellungen zaehlen nicht): die Website ist noch im Bau. Ab dem ersten nicht deckenden Abo
* gilt fuer 'projekt' dieselbe Regel wie fuer 'abo'. ohneAboBisher wird nur gelesen, wenn ein projekt-Dokument vom Abo
* nicht gedeckt ist (je Kunde hoechstens einmal je Lauf) und unter der Sperre noch einmal frisch.
* Nachholen disabled mit Grund abo_ende ohne Marker => K39 erneut (Mailfehler, Kunde ohne Adresse); disabledAt bleibt, die Loeschfrist waechst nie.
* Rueckkehr disabled mit Grund abo_ende und das Abo deckt wieder, solange die Daten da sind (News: das Dokument ist noch disabled;
* Terminplaner: die Einstellung ist gespeichert; Webshop und Buchungskalender: die Notiz nennt keine Loeschung "gelöscht am")
* => wieder active (activate), ohne activatedAt wieder setup. Still, ohne Mail (K39 hat es angekuendigt).
* Loeschen News: disabled mit Grund abo_ende, das Abo deckt nicht und disabledAt plus 30 Tage ist erreicht (Berliner Kalendertag wie
* termine/frist.js) => alle Beitraege des Projekts samt Bild loeschen, dann das Dokument auf off (Felder ticketId, activatedAt,
* aboEndeMailAt, disabledAt, disabledReason geleert); scheitert ein Loeschen, bleibt das Dokument disabled (fehler, der naechste
* Lauf wiederholt). Deckt das Abo wieder, gilt die Rueckkehr vor der Loeschung.
* Zuruecksetzen Webshop und Buchungskalender: haben die bestehenden Jobs (shop/jobs.js, unterkunft/aufbewahrung.js) nach 90 Tagen die
* Daten geloescht und "Shopdaten gelöscht am" bzw. "Buchungsdaten gelöscht am" in adminNote vermerkt, wird das Dokument off
* (wie bei News, der Vermerk endet damit), unabhaengig vom Abo. Ohne Vermerk bleibt alles. Der Terminplaner wird von
* termine/jobs.js selbst auf off gesetzt.
*
* Sofortpfad (V11, N27): beendeWartungsFunktionen(customerId) beendet alle Abo-Modell-Funktionen eines Kunden, die nur im Wartungs-Abo stecken,
* sofort, wenn dessen Wechsel ins Wartungs-Abo in der Frist widerrufen wird (routes/abo.js, withdrawOrder). Ohne Abo-Pruefung (der Spiegel
* steht bis zur Handarbeit des Teams noch auf Wartung); die Analyse gehoert zu Hosting und bleibt. Die Mail K39 geht wie im Tagesabgleich
* (meldeBeendet), ausser fuer zeiten: dort sendet der Zeiten-Abgleich K41 samt Ersatz-Ticket.
*
* Regeln wie termine/aboAbgleich.js: eine Listenabfrage fuer alle Kunden; das Abo je Kunde einmal fuer die Vorauswahl lesen. Geschrieben
* wird nur unter der Sperre feature-admin:<projectId> nach frischem Lesen von Dokument UND Abo (nie aus der Vorauswahl); die Mail und der
* Marker folgen nach der Sperre. Fail-closed: ist etwas nicht lesbar (Liste, Abo, Dokument, Aufbauphase), wird nichts abgeschaltet; Kunde
* bzw. Dokument werden uebersprungen, `fehler` zaehlt, der naechste Lauf versucht es wieder. Das Log nennt weder Namen noch Adressen.
*/
/** Funktionen dieses Abgleichs; zeiten gehoert zeiten/aboAbgleich.js (Task 10). */
export const ABGLEICH_KEYS = Object.freeze(['news', 'termine', 'shop', 'unterkunft'])
const GRUND = 'abo_ende'
const NOTIZ = 'System: Abgeschaltet: Abo beendet'
const NOTIZ_WIEDER = 'System: Wieder eingeschaltet: Abo läuft wieder'
const NOTIZ_WECHSEL = 'Abgeschaltet: Wechsel widerrufen'
const ADRESSE = /[^\s<>"',;()]+@[^\s<>"',;()]+/g
/** Fehlertext fuers Log: Name und gekuerzte Meldung, E-Mail-Adressen (SMTP-Antworten nennen sie) unkenntlich. */
const fehlerText = (err) => `${err?.name ?? 'Fehler'} ${String(err?.message ?? '').replace(ADRESSE, '[Adresse]').slice(0, 200)}`
const portalFeatures = () => config.collections.portalFeatures
/** Deckt das Abo (Ergebnis von aboProdukt) diese Funktion? Gekuendigt und past_due laufen noch und zaehlen. */
const deckt = (abo, key) => abo.laeuft === true && ENTHALTEN_IN[key].includes(abo.produkt)
/** Grund fuer K39: das Abo ist ein Hosting-Abo, sonst Abo beendet. */
const artVon = (abo) => (abo.produkt === 'hosting' ? 'hosting' : GRUND)
/**
* Sind die Daten der abgeschalteten Funktion noch da? News: das Dokument ist noch disabled (der Loeschschritt setzt es auf off);
* Terminplaner: die Einstellung ist gespeichert (ladeEinstellung wirft bei Lesefehlern, dann bleibt alles); Webshop und Buchungskalender:
* ein Vermerk der Loeschjobs in der Notiz wird vor der Rueckkehr behandelt (setzeNachLoeschungZurueck), ohne Vermerk sind die Daten da.
*/
async function datenDa(doc) {
if (doc.featureKey === 'termine') return (await ladeEinstellung(doc.$id)) !== null
return !hatLoeschVermerk(doc)
}
const TAG_FELDER_LEER = Object.freeze({ ticketId: null, activatedAt: null, aboEndeMailAt: null, disabledAt: null, disabledReason: null })
/** Vermerk der bestehenden Loeschjobs von Webshop und Buchungskalender in der Notiz. */
const LOESCH_VERMERK = /\s*(?:[-–|]\s*)?(?:Shopdaten|Buchungsdaten) gelöscht am \d{4}-\d{2}-\d{2}/g
const hatLoeschVermerk = (doc) => (doc.featureKey === 'shop' || doc.featureKey === 'unterkunft') && /gelöscht am/.test(String(doc.adminNote ?? ''))
/** News: ist disabledAt plus die Loeschfrist (30 Tage) am Berliner Kalendertag erreicht? Ohne gueltiges disabledAt nie. */
function loeschReif(doc, now) {
const tag = loeschTag(doc.disabledAt)
return tag !== null && tag <= heute(now)
}
/** K39 nach der Sperre; ein Misserfolg zaehlt als Fehler, der naechste Lauf wiederholt (Marker leer). */
async function melde(doc, art, ctx) {
const fertig = await meldeBeendet(doc, { art, now: ctx.now, onGesendet: () => (ctx.erg.mails += 1) })
if (!fertig) ctx.erg.fehler += 1
}
/**
* Abschalten, ein Dokument: unter der Sperre frisch lesen (das Team kann inzwischen abgeschaltet haben), frisch das Abo lesen (es kann
* zurueckgekommen sein) und fuer ein projekt-Dokument frisch die Aufbauphase pruefen, erst dann disable. K39 folgt nach der Sperre.
*/
async function schalteAb(doc, ctx) {
const { erg } = ctx
const gefunden = await withLocks([`feature-admin:${doc.projectId}`], async () => {
const frisch = await getDocument(portalFeatures(), doc.$id)
if (!istAboModell(frisch) || (frisch.state !== 'setup' && frisch.state !== 'active')) return null
const abo = await aboProdukt(frisch.customerId, { now: ctx.now }) // wirft bei Lesefehler: dann bleibt alles, wie es ist
if (deckt(abo, frisch.featureKey)) return null // das Abo ist zurueck
if (frisch.unlockedBy === 'projekt' && (await ohneAboBisher(frisch.customerId))) return null // Aufbauphase
const zuMelden = await disable(frisch.$id, { reason: GRUND, adminNote: NOTIZ })
if (zuMelden.state !== 'disabled' || zuMelden.disabledReason !== GRUND) return null // jemand war zwischen Lesen und Schreiben schneller
if (zuMelden.featureKey === 'unterkunft' && zuMelden.siteKey) leereCache(zuMelden.siteKey)
erg.abgeschaltet += 1
return { zuMelden, art: artVon(abo) }
})
if (gefunden) await melde(gefunden.zuMelden, gefunden.art, ctx)
}
/** Nachholen, ein Dokument: unter der Sperre frisch lesen (noch abo_ende-abgeschaltet und ohne Marker, das Abo deckt weiter nicht), K39 danach. */
async function holeMeldungNach(doc, ctx) {
const gefunden = await withLocks([`feature-admin:${doc.projectId}`], async () => {
const frisch = await getDocument(portalFeatures(), doc.$id)
if (!istAboModell(frisch) || frisch.state !== 'disabled' || frisch.disabledReason !== GRUND || frisch.aboEndeMailAt) return null
const abo = await aboProdukt(frisch.customerId, { now: ctx.now })
if (deckt(abo, frisch.featureKey)) return null // das Abo ist zurueck: Rueckkehr, nicht melden
return { zuMelden: frisch, art: artVon(abo) }
})
if (gefunden) await melde(gefunden.zuMelden, gefunden.art, ctx)
}
/**
* Rueckkehr, ein Dokument: unter der Sperre frisch lesen (noch disabled/abo_ende), frisch das Abo lesen, die Daten pruefen, dann activate
* (mit activatedAt) oder zurueck auf setup (nie aktiv gewesen: die Abnahme laeuft wie sonst ueber das Ticket). Keine Mail.
*/
async function schalteWiederEin(doc, ctx) {
const { erg } = ctx
await withLocks([`feature-admin:${doc.projectId}`], async () => {
const frisch = await getDocument(portalFeatures(), doc.$id)
if (!istAboModell(frisch) || frisch.state !== 'disabled' || frisch.disabledReason !== GRUND) return
const abo = await aboProdukt(frisch.customerId, { now: ctx.now })
if (!deckt(abo, frisch.featureKey)) return // das Abo endete zwischen Vorauswahl und Sperre wieder
if (!(await datenDa(frisch))) return // Daten geloescht: Rueckkehr nur, solange sie existieren
if (frisch.activatedAt) {
await activate(frisch.$id, { adminNote: NOTIZ_WIEDER })
} else {
await updateDocument(portalFeatures(), frisch.$id, {
...stateFields('setup'),
disabledAt: null,
disabledReason: null,
aboEndeMailAt: null,
adminNote: NOTIZ_WIEDER,
})
}
erg.wiederEin += 1
})
}
/**
* News, ein Dokument: unter der Sperre frisch lesen (noch disabled/abo_ende, Frist erreicht, das Abo deckt nicht), zuerst alle Beitraege des
* Projekts samt Bild loeschen (erst das Bild, dann der Beitrag, wie retention.js), dann das Dokument auf off. Ein Fehler laesst das
* Dokument disabled (fehler, der naechste Lauf wiederholt); die uebrigen Beitraege werden trotzdem versucht.
*/
async function loescheNews(doc, ctx) {
const { erg } = ctx
await withLocks([`feature-admin:${doc.projectId}`], async () => {
const frisch = await getDocument(portalFeatures(), doc.$id)
if (!istAboModell(frisch) || frisch.featureKey !== 'news' || frisch.state !== 'disabled' || frisch.disabledReason !== GRUND) return
if (!loeschReif(frisch, ctx.now)) return
const abo = await aboProdukt(frisch.customerId, { now: ctx.now })
if (deckt(abo, frisch.featureKey)) return // das Abo ist zurueck: Rueckkehr, keine Loeschung
const { hanging, expired } = await listPosts(frisch.projectId, ctx.now)
let geloeschtOk = true
for (const post of [...hanging, ...expired]) {
try {
if (post.imageId) await deleteNewsImage(post.projectId || frisch.projectId, post.imageId) // erst das Bild, bei Fehler bleibt der Beitrag
await deleteDocument(config.collections.newsPosts, post.$id).catch((err) => {
if (err?.status !== 404) throw err
})
} catch (err) {
geloeschtOk = false
console.error(`[abo-funktionen] News-Beitrag ${post.$id} nicht gelöscht, das Dokument bleibt abgeschaltet: ${fehlerText(err)}`)
}
}
if (!geloeschtOk) {
erg.fehler += 1
return
}
await updateDocument(portalFeatures(), frisch.$id, { ...stateFields('off'), ...TAG_FELDER_LEER })
erg.geloescht += 1
})
}
/**
* Webshop und Buchungskalender, ein Dokument: die bestehenden Jobs haben die Daten geloescht (Vermerk in adminNote), unabhaengig vom Abo
* zurueck auf off. Der Vermerk endet damit, damit er nicht in eine spaetere Abschaltung desselben Dokuments hineinreicht.
*/
async function setzeNachLoeschungZurueck(doc, ctx) {
const { erg } = ctx
await withLocks([`feature-admin:${doc.projectId}`], async () => {
const frisch = await getDocument(portalFeatures(), doc.$id)
if (!istAboModell(frisch) || frisch.state !== 'disabled' || frisch.disabledReason !== GRUND || !hatLoeschVermerk(frisch)) return
const adminNote = String(frisch.adminNote ?? '').replace(LOESCH_VERMERK, '').trim()
await updateDocument(portalFeatures(), frisch.$id, { ...stateFields('off'), ...TAG_FELDER_LEER, adminNote })
erg.geloescht += 1
})
}
async function bearbeiteKunde(customerId, dokumente, ctx) {
const { erg } = ctx
let abo
try {
abo = await aboProdukt(customerId, { now: ctx.now })
} catch (err) {
erg.fehler += 1
console.error(`[abo-funktionen] Abo eines Kunden nicht lesbar, ${dokumente.length} Funktion(en) übersprungen: ${fehlerText(err)}`)
return
}
erg.geprueft += dokumente.length
let aufbauphase = null // Promise-frei: einmal je Kunde und Lauf, nur gelesen, wenn ein projekt-Dokument nicht gedeckt ist
let aufbauphaseFehler = false
const inAufbauphase = async () => {
if (aufbauphaseFehler) return true // nicht lesbar: nichts abschalten (der Fehler ist gezaehlt)
if (aufbauphase === null) {
try {
aufbauphase = await ohneAboBisher(customerId)
} catch (err) {
aufbauphaseFehler = true
erg.fehler += 1
console.error(`[abo-funktionen] Abo-Verlauf eines Kunden nicht lesbar, Projekt-Funktionen übersprungen: ${fehlerText(err)}`)
return true
}
}
return aufbauphase
}
for (const doc of dokumente) {
try {
const gedeckt = deckt(abo, doc.featureKey)
if (doc.state === 'setup' || doc.state === 'active') {
if (gedeckt) continue
if (doc.unlockedBy === 'projekt' && (await inAufbauphase())) continue
await schalteAb(doc, ctx)
} else if (doc.state === 'disabled' && doc.disabledReason === GRUND) {
if (hatLoeschVermerk(doc)) await setzeNachLoeschungZurueck(doc, ctx) // Daten weg: nie Rueckkehr, off
else if (gedeckt) await schalteWiederEin(doc, ctx)
else if (doc.featureKey === 'news' && loeschReif(doc, ctx.now)) await loescheNews(doc, ctx)
else if (!doc.aboEndeMailAt) await holeMeldungNach(doc, ctx)
}
} catch (err) {
erg.fehler += 1
console.error(`[abo-funktionen] Funktion ${doc.$id} nicht bearbeitet: ${fehlerText(err)}`)
}
}
}
/**
* Taeglicher Abgleich der Funktionen des Abo-Modells mit dem Abo (siehe Kopf der Datei). Wirft nie; jedes Problem steht im Log und in `fehler`.
* @param {Date} [now]
* @returns {Promise<{ geprueft: number, abgeschaltet: number, wiederEin: number, geloescht: number, mails: number, fehler: number }>}
* geprueft = bewertete Dokumente des Abo-Modells, deren Kunde lesbar war; abgeschaltet und wiederEin = in diesem Lauf; geloescht = Dokumente,
* die der Loeschschritt in diesem Lauf auf off gesetzt hat (News nach 30 Tagen, Webshop und Buchungskalender nach der Loeschung); mails = versendete K39; fehler = Kunden und Dokumente, die nicht bearbeitet werden
* konnten, sowie ein gescheiterter K39-Versand
*/
export async function gleicheAboFunktionenAb(now = new Date()) {
const erg = { geprueft: 0, abgeschaltet: 0, wiederEin: 0, geloescht: 0, mails: 0, fehler: 0 }
try {
pruefeNow(now, 'gleicheAboFunktionenAb')
let dokumente
try {
dokumente = await listAllDocuments(portalFeatures(), [Query.equal('featureKey', [...ABGLEICH_KEYS]), Query.notEqual('state', 'off')])
} catch (err) {
erg.fehler += 1
console.error(`[abo-funktionen] Funktionen nicht lesbar, der Abgleich entfällt: ${fehlerText(err)}`)
return erg
}
const nachKunde = new Map()
for (const doc of dokumente.filter(istAboModell)) {
if (!doc.customerId || !doc.projectId) {
// ohne Kunde gaelte "kein Abo": nie abschalten, was sich nicht zuordnen laesst
erg.fehler += 1
console.error(`[abo-funktionen] Funktion ${doc.$id} ohne Kunde oder Projekt übersprungen`)
continue
}
if (!nachKunde.has(doc.customerId)) nachKunde.set(doc.customerId, [])
nachKunde.get(doc.customerId).push(doc)
}
const ctx = { erg, now }
for (const [customerId, liste] of nachKunde) {
try {
await bearbeiteKunde(customerId, liste, ctx)
} catch (err) {
erg.fehler += 1
console.error(`[abo-funktionen] Kunde nicht bearbeitet: ${fehlerText(err)}`)
}
}
} catch (err) {
erg.fehler += 1
console.error(`[abo-funktionen] Abgleich abgebrochen: ${fehlerText(err)}`)
}
return erg
}
/** Steckt die Funktion nur im Wartungs-Abo (ENTHALTEN_IN genau ['wartung'])? Die Analyse (auch Hosting) nicht. */
const nurImWartungsAbo = (key) => Object.hasOwn(ENTHALTEN_IN, key) && ENTHALTEN_IN[key].length === 1 && ENTHALTEN_IN[key][0] === 'wartung'
/**
* Sofortpfad beim Widerruf des Wechsels ins Wartungs-Abo (V11, N27): alle Dokumente des Abo-Modells (auch projekt) des Kunden im Zustand
* setup oder active, deren Funktion nur im Wartungs-Abo steckt (news, termine, shop, unterkunft, zeiten; nicht analyse), werden sofort
* abgeschaltet (Grund abo_ende, Notiz "Abgeschaltet: Wechsel widerrufen"), je Dokument unter feature-admin:<projectId> nach frischem Lesen
* und ohne Abo-Pruefung. Danach, ausserhalb der Sperre, K39 mit dem Hosting-Satz (meldeBeendet, art hosting) fuer alle ausser zeiten (dort
* sendet der Zeiten-Abgleich K41 samt Ersatz-Ticket). Ein Fehler bei einem Dokument stoppt die uebrigen nicht; was misslingt, holt der
* Tagesabgleich nach (aboProdukt kennt den widerrufenen Wechsel). Wirft nie; das Log nennt weder Namen noch Adressen.
* @param {string} customerId
* @param {{ now?: Date }} [options] Zeitpunkt des Widerrufs (Marker aboEndeMailAt)
* @returns {Promise<{ beendet: string[] }>} die IDs der abgeschalteten Funktionsdokumente
*/
export async function beendeWartungsFunktionen(customerId, { now = new Date() } = {}) {
const beendet = []
if (!customerId) return { beendet }
try {
const kandidaten = (await listCustomerFeatureDocs(customerId)).filter(
(doc) => istAboModell(doc) && doc.projectId && (doc.state === 'setup' || doc.state === 'active') && nurImWartungsAbo(doc.featureKey)
)
for (const doc of kandidaten) {
try {
const abgeschaltet = await withLocks([`feature-admin:${doc.projectId}`], async () => {
const frisch = await getDocument(portalFeatures(), doc.$id)
if (!istAboModell(frisch) || (frisch.state !== 'setup' && frisch.state !== 'active')) return null // das Team war schneller
const aus = await disable(frisch.$id, { reason: GRUND, adminNote: NOTIZ_WECHSEL })
if (aus.state !== 'disabled' || aus.disabledReason !== GRUND) return null // jemand war zwischen Lesen und Schreiben schneller
if (aus.featureKey === 'unterkunft' && aus.siteKey) leereCache(aus.siteKey)
return aus
})
if (!abgeschaltet) continue
beendet.push(abgeschaltet.$id)
if (abgeschaltet.featureKey !== 'zeiten') await meldeBeendet(abgeschaltet, { art: 'hosting', now }) // nach der Sperre; bei Misserfolg wiederholt der Tagesabgleich
} catch (err) {
console.error(`[abo-funktionen] Sofortpfad: Funktion ${doc.$id} nicht vollständig beendet, der Tagesabgleich holt nach: ${fehlerText(err)}`)
}
}
} catch (err) {
console.error(`[abo-funktionen] Sofortpfad: Funktionen eines Kunden nicht lesbar, nichts beendet: ${fehlerText(err)}`)
}
return { beendet }
}
let tagesJobLaeuft = false
/**
* Tagesjob (server/index.js): der Abo-Ende-Abgleich ohne Ueberlappung. Laeuft noch ein Lauf (Merker im Speicher des Prozesses, im finally
* geloescht), kehrt der Aufruf sofort mit { uebersprungen: true } zurueck. Wirft nie: ein unerwarteter Fehler steht im Log, das Ergebnis
* ist { fehler: 1 }. Das Log nennt weder Namen noch Adressen.
* @param {Date} [now]
* @returns {Promise<ReturnType<typeof gleicheAboFunktionenAb> | { uebersprungen: true } | { fehler: 1 }>}
*/
export async function aboFunktionenTagesJob(now = new Date()) {
if (tagesJobLaeuft) return { uebersprungen: true }
tagesJobLaeuft = true
try {
return await gleicheAboFunktionenAb(now)
} catch (err) {
console.error(`[abo-funktionen] Tagesjob fehlgeschlagen: ${fehlerText(err)}`)
return { fehler: 1 }
} finally {
tagesJobLaeuft = false
}
}
+145
View File
@@ -0,0 +1,145 @@
import { config } from '../config.js'
import { anleitungsLink, featureConfig, katalogFelder } from './featureRegistry.js'
import { unterlagenFuer } from './aboUnterlagen.js'
import { LOESCHFRIST_TAGE } from './termine/frist.js'
import { datumKurz } from './termine/zeit.js'
/**
* Mails der Funktionen im Abo-Modell (Kundenansicht neu, P6; Vorlagen server/mail-templates/K38, K39, T24):
* K38 Deine Funktion wird eingerichtet (Einschalten aus dem Abo oder durch das Team; nicht für Öffnungszeiten live, dort bleibt K27)
* K39 Deine Funktion wurde beendet (der Tagesabgleich beendet sie mit dem Abo)
* T24 Team-Hinweis: Funktion eingeschaltet (Einrichtung geht aufs Stundenkonto, Block vorschuss)
* K42, K43 Webshop- und Buchungskalender-Anfrage entschieden (Zusage oder Absage), Ersatz fuer K24 und K34 im Abo-Modell: ohne Preis und ohne
* das Wort "bestellen" (anfrageEntschiedenMail)
* Die Builder sind rein (kein Zugriff auf Appwrite, kein Versand) und liefern { template, values, blocks }, so wie
* zeiten/mails.js: die Werte decken genau die Platzhalter der Vorlage ab. Keine Preise, kein Wort „bestellen“.
*
* Anrede: `kunde` ist der Name oder das Kundendokument; ohne Namen bleibt kunde_name leer (renderMail macht aus „Hallo ,“ „Hallo,“).
*/
/** Anrede-Wert: Name aus einem String oder einem Kundendokument, getrimmt; sonst ''. */
function kundenName(kunde) {
const name = typeof kunde === 'string' ? kunde : kunde?.name
return typeof name === 'string' ? name.trim() : ''
}
/** Date -> 'TT.MM.JJJJ' (Berliner Tag). Wirft bei etwas anderem als einem gültigen Date: ein leeres Datum in der Mail wäre falsch. */
function datum(wert, feld) {
if (!(wert instanceof Date) || Number.isNaN(wert.getTime())) throw new TypeError(`${feld}: gültiges Date erwartet`)
return datumKurz(wert)
}
/** Registereintrag einer Funktion; ein unbekannter Schlüssel ist ein Programmfehler (TypeError). */
function funktion(key) {
const eintrag = featureConfig(key)
if (!eintrag) throw new TypeError(`Unbekannte Funktion: ${String(key)}`)
return eintrag
}
/** Adresse der Funktion im Kundenportal (returnHash des Registers). */
const portalUrl = (eintrag) => `${config.publicBaseUrl}/dashboard.html#${eintrag.returnHash}`
/**
* K38: Die Funktion wird eingerichtet.
* @param {{ kunde: string | { name?: string }, projekt: string, key: string, abrechnung: 'abo' | 'projekt', ticketNr?: string | number | null, anhang?: boolean }} p
* abrechnung: 'abo' (Einrichtung geht vom Stundenkonto ab, Block stundenkonto) oder 'projekt' (Teil der Projektkosten, Block projekt_kosten);
* ticketNr: Nummer des Einrichtungs-Tickets (mit oder ohne #), ohne sie steht kein Ticket in der Mail;
* anhang: Leistungsbeschreibung und Auftragsverarbeitung hängen als PDF an (Block anhang).
*/
export function k38Mail({ kunde, projekt, key, abrechnung, ticketNr = null, anhang = false }) {
const eintrag = funktion(key)
const nr = String(ticketNr ?? '').trim().replace(/^#/, '')
return {
template: 'K38',
values: {
kunde_name: kundenName(kunde),
projekt,
funktion: eintrag.mails.funktion,
funktion_akk: eintrag.mails.formen.akk,
einrichtung: katalogFelder(key).einrichtung,
// führendes Leerzeichen: steht direkt hinter dem Projektnamen, ohne Ticket bleibt nichts übrig
ticket: nr ? ` (Ticket #${nr})` : '',
fassung: unterlagenFuer(key).leistungsbeschreibung?.version ?? '',
portal_url: portalUrl(eintrag),
},
blocks: { stundenkonto: abrechnung === 'abo', projekt_kosten: abrechnung === 'projekt', anhang: anhang === true },
}
}
/**
* K39: Die Funktion wurde mit dem Abo beendet.
* @param {{ kunde: string | { name?: string }, projekt: string, key: string, loeschdatum: Date, art: string }} p
* loeschdatum: Tag, ab dem die Eingaben gelöscht werden; art: 'hosting' (das Abo ist ein Hosting-Abo), sonst (auch 'beendet') Abo beendet.
* Zusatzblöcke: termine nur beim Terminplaner, offene nur bei Webshop und Buchungskalender (unterkunft).
*/
export function k39Mail({ kunde, projekt, key, loeschdatum, art }) {
const eintrag = funktion(key)
return {
template: 'K39',
values: {
kunde_name: kundenName(kunde),
projekt,
funktion: eintrag.mails.funktion,
loeschtag: datum(loeschdatum, 'loeschdatum'),
portal_url: portalUrl(eintrag),
},
blocks: { abo_ende: art !== 'hosting', hosting: art === 'hosting', termine: key === 'termine', offene: key === 'shop' || key === 'unterkunft' },
}
}
/**
* T24: Team-Hinweis, dass eine Funktion eingeschaltet wurde.
* @param {{ kunde: string | { name?: string }, email: string, projekt: string, key: string, customerId: string, projectId: string, woid: string,
* kontostand?: string, anhangFehlt?: boolean }} p
* kontostand: Text aus kontostandText(), nur gesetzt, wenn das Guthaben beim Einschalten nicht reicht (Block vorschuss);
* anhangFehlt: die Vertragsunterlagen sind nicht beim Kunden angekommen (Block anhang_fehlt).
*/
export function t24Mail({ kunde, email, projekt, key, customerId, projectId, woid, kontostand = '', anhangFehlt = false }) {
const eintrag = funktion(key)
return {
template: 'T24',
values: {
kunde_name: kundenName(kunde),
email: String(email ?? ''),
projekt,
funktion: eintrag.mails.funktion,
woid: String(woid ?? ''),
kontostand,
woms_url: anleitungsLink(customerId, projectId),
},
blocks: { vorschuss: kontostand !== '', anhang_fehlt: anhangFehlt === true },
}
}
/**
* K42 (Webshop) oder K43 (Buchungskalender): die Anfrage wurde vom Team entschieden. Im Abo-Modell ohne Preiszeile und ohne "bestellen":
* bei Zusage kann der Kunde die Funktion im Kundenportal einschalten. Werte und Adresse wie die alten Mails K24 und K34 (sendeEntscheidung).
* @param {{ kunde: string | { name?: string }, projekt: string, key: 'shop' | 'unterkunft', projectId: string, angenommen: boolean, begruendung?: string }} p
* kunde_name ist der getrimmte Name, leer 'Kunde'; ein anderer key ist ein TypeError.
*/
export function anfrageEntschiedenMail({ kunde, projekt, key, projectId, angenommen, begruendung = '' }) {
if (key !== 'shop' && key !== 'unterkunft') throw new TypeError(`Keine Anfrage-Mail fuer die Funktion: ${String(key)}`)
const eintrag = funktion(key)
return {
template: key === 'shop' ? 'K42' : 'K43',
values: {
kunde_name: kundenName(kunde) || 'Kunde',
projekt,
begruendung,
portal_url: `${config.publicBaseUrl}/dashboard.html#${eintrag.returnHash}?projekt=${encodeURIComponent(projectId)}`,
},
blocks: { angenommen: Boolean(angenommen), abgelehnt: !angenommen },
}
}
/** Tage zwischen dem Abschalten und der Löschung der Eingaben: news 30, termine 30, shop und unterkunft aus der Konfiguration; sonst TypeError. */
export function loeschFristTage(key) {
if (key === 'news') return 30
if (key === 'termine') return LOESCHFRIST_TAGE
if (key === 'shop') return config.shop.loeschTageNachAbschalten
if (key === 'unterkunft') return config.unterkunft.loeschTageNachAbschalten
throw new TypeError(`Keine Löschfrist für die Funktion: ${String(key)}`)
}
/** Kontostand für T24, z. B. 'Guthaben 0 min, Vorschuss 90 min'. */
export const kontostandText = ({ balanceMinutes = 0, vorschussMinutes = 0 }) => `Guthaben ${balanceMinutes} min, Vorschuss ${vorschussMinutes} min`
+152
View File
@@ -0,0 +1,152 @@
import { updateDocument } from './appwriteAdmin.js'
import { config } from '../config.js'
import { aboProdukt } from './aboFunktionen.js'
import { unterlagenFuer } from './aboUnterlagen.js'
import { k38Mail, k39Mail, kontostandText, loeschFristTage, t24Mail } from './aboFunktionenMails.js'
import { featureConfig } from './featureRegistry.js'
import { getLedger } from './hourLedger.js'
import { featureLegalAttachments } from './legalDocs.js'
import { sendAdminNotice, sendCustomerMail } from './mailer.js'
import { customerOf, projectName } from './siteFeatures.js'
/**
* Mails zum Einschalten einer Funktion im Abo-Modell (Kundenansicht neu, P6; Builder in aboFunktionenMails.js):
* K38 an den Kunden (BCC Team wie sendCustomerMail): Die Funktion wird eingerichtet, fuer news, termine, shop und unterkunft;
* nicht fuer zeiten, dort sendet holeZeitenNach die K27. Mit den Vertragsunterlagen als PDF (ohne Widerrufsbelehrung), wenn
* die Funktion welche hat und sie lesbar sind.
* T24 ans Team, nur wenn der Kunde selbst eingeschaltet hat (by 'kunde'), fuer alle fuenf Funktionen.
* K39 an den Kunden, wenn der Tagesabgleich (aboFunktionenAbgleich.js) eine Funktion mit dem Abo beendet hat (meldeBeendet).
* meldeEingeschaltet wirft nie: jeder Schritt steht in einem eigenen try, ein Fehler ist eine Logzeile mit Art und Status (weder
* Name noch Adresse des Kunden) und der Rest laeuft weiter. Aufrufer ist einschalten() (funktionEinschalten.js).
*/
/** Austauschpunkte fuer Tests: die Anhaenge der Vertragsunterlagen (Standard: Leistungsbeschreibung und AVV, ohne Widerrufsbelehrung). */
export const meldungDeps = {
anhaenge: (key) => featureLegalAttachments(key, { ohne: ['widerruf'] }),
}
/** Fehlerart fuers Log: Name und Status, nie die Meldung (SMTP-Antworten nennen gern die Adresse). */
const fehlerArt = (err) => `${err?.name ?? 'Fehler'} ${err?.status ?? err?.responseCode ?? ''}`.trim()
/**
* Meldung nach dem Einschalten: K38 an den Kunden, danach (nur by 'kunde') T24 ans Team.
* @param {{ doc: object, ticketWoid?: string|null, by: 'kunde'|'team', abrechnung: 'abo'|'projekt' }} p
* doc: das eingeschaltete Funktionsdokument (setup, mit Ticket); ticketWoid: Nummer des Einrichtungs-Tickets.
* T24 traegt den Block vorschuss, wenn abrechnung 'abo' ist, das Abo ein Wartungs-Abo ist und das Stundenkonto balanceMinutes <= 0
* zeigt oder schon einen Vorschuss hat; anhang_fehlt, wenn die Funktion Rechtsunterlagen hat und der Abruf der PDFs scheiterte.
* @returns {Promise<{ k38: boolean, t24: boolean }>} was tatsaechlich versandt wurde
*/
export async function meldeEingeschaltet({ doc, ticketWoid = null, by, abrechnung }) {
const ergebnis = { k38: false, t24: false }
try {
const key = doc?.featureKey
if (!featureConfig(key)) {
console.error(`[funktionen-mail] Meldung entfällt: unbekannte Funktion (${String(key)})`)
return ergebnis
}
const customerId = doc.customerId
// Kunde und Projektname; fehlt etwas, bleibt es leer (Anrede ohne Namen), die Meldung geht trotzdem raus
let customer = null
let projekt = ''
try {
;[customer, projekt] = await Promise.all([customerOf(customerId), projectName(doc.projectId)])
} catch (err) {
console.error(`[funktionen-mail] Kunde oder Projekt nicht lesbar (${key}): ${fehlerArt(err)}`)
}
const hatUnterlagen = unterlagenFuer(key).avv !== null
let anhangFehler = false
// K38 an den Kunden (nicht fuer zeiten: dort bleibt K27)
if (key !== 'zeiten') {
try {
const to = String(customer?.email ?? '').trim()
if (!to) {
console.warn(`[funktionen-mail] K38 (${key}) entfällt: Kunde nicht mehr vorhanden oder ohne E-Mail-Adresse`)
} else {
let anhaenge = []
try {
const geholt = await meldungDeps.anhaenge(key)
anhaenge = Array.isArray(geholt) ? geholt : []
} catch (err) {
anhangFehler = true
console.error(`[funktionen-mail] Vertragsunterlagen (${key}) nicht lesbar, K38 geht ohne Anhang raus: ${fehlerArt(err)}`)
}
const nachricht = k38Mail({ kunde: customer, projekt, key, abrechnung, ticketNr: ticketWoid, anhang: anhaenge.length > 0 })
await sendCustomerMail(to, { ...nachricht, attachments: anhaenge })
ergebnis.k38 = true
}
} catch (err) {
console.error(`[funktionen-mail] K38 (${key}) nicht gesendet: ${fehlerArt(err)}`)
}
}
// T24 ans Team, nur wenn der Kunde selbst eingeschaltet hat
if (by === 'kunde') {
let kontostand = ''
if (abrechnung === 'abo') {
try {
if ((await aboProdukt(customerId)).produkt === 'wartung') {
const ledger = await getLedger(customerId)
if (ledger.balanceMinutes <= 0 || ledger.vorschussMinutes > 0) kontostand = kontostandText(ledger)
}
} catch (err) {
console.error(`[funktionen-mail] Stundenkonto für T24 (${key}) nicht lesbar: ${fehlerArt(err)}`)
}
}
try {
const nachricht = t24Mail({
kunde: customer, email: customer?.email, projekt, key, customerId, projectId: doc.projectId, woid: ticketWoid,
kontostand, anhangFehlt: anhangFehler && hatUnterlagen,
})
ergebnis.t24 = (await sendAdminNotice(nachricht))?.sent === true
} catch (err) {
console.error(`[funktionen-mail] T24 (${key}) nicht gesendet: ${fehlerArt(err)}`)
}
}
} catch (err) {
console.error(`[funktionen-mail] Meldung nach dem Einschalten fehlgeschlagen: ${fehlerArt(err)}`)
}
return ergebnis
}
const TAG_MS = 24 * 60 * 60 * 1000
/**
* K39 (Deine Funktion wurde beendet) an den Kunden, danach der Marker aboEndeMailAt (erst nach erfolgreichem Versand): die Funktion
* wurde mit dem Abo beendet (disabled, Grund abo_ende). loeschdatum ist disabledAt plus die Loeschfrist der Funktion (loeschFristTage;
* disabledAt aendert dieser Pfad nie, eine Wiederholung nach einem Mailfehler nennt dasselbe Datum). Kunde nicht mehr vorhanden oder ohne
* Adresse: nichts zu senden, der Marker wird gesetzt, damit es keine Wiederholung gibt. Ein Dokument, das nicht abo_ende-abgeschaltet
* ist oder den Marker schon traegt, ist erledigt (true, nichts geschrieben). Wirft nie; das Log nennt weder Namen noch Adressen.
* @param {object} doc portalFeatures-Dokument (frisch gelesen, disabled)
* @param {{ art: string, now?: Date, onGesendet?: (() => void) | null }} options art 'hosting' (das Abo ist ein Hosting-Abo), sonst Abo beendet;
* onGesendet wird nach dem versendeten K39 gerufen (Zaehler des Abgleichs)
* @returns {Promise<boolean>} true = nichts mehr offen (gesendet, nichts zu senden oder Marker gesetzt), false = Versand oder Marker gescheitert
* (Marker leer: der naechste Lauf wiederholt)
*/
export async function meldeBeendet(doc, { art, now = new Date(), onGesendet = null } = {}) {
let gesendet = false
try {
if (doc?.state !== 'disabled' || doc.disabledReason !== 'abo_ende' || doc.aboEndeMailAt) return true
const zeitpunkt = now instanceof Date ? now : new Date(now)
const setzeMarker = () => updateDocument(config.collections.portalFeatures, doc.$id, { aboEndeMailAt: zeitpunkt.toISOString() })
const customer = await customerOf(doc.customerId)
const to = String(customer?.email ?? '').trim()
if (!to) {
console.warn(`[funktionen-mail] K39 (${doc.featureKey}) entfällt: Kunde nicht mehr vorhanden oder ohne E-Mail-Adresse, Marker wird gesetzt`)
await setzeMarker()
return true
}
const projekt = (await projectName(doc.projectId)) || 'Dein Projekt'
const beginn = Number.isFinite(Date.parse(doc.disabledAt)) ? Date.parse(doc.disabledAt) : zeitpunkt.getTime() // disabledAt fehlt nie, ausser bei Altdaten
const loeschdatum = new Date(beginn + loeschFristTage(doc.featureKey) * TAG_MS)
await sendCustomerMail(to, k39Mail({ kunde: customer, projekt, key: doc.featureKey, loeschdatum, art }))
gesendet = true
onGesendet?.()
await setzeMarker()
return true
} catch (err) {
console.error(`[funktionen-mail] K39 (${doc?.featureKey}) ${gesendet ? 'Marker nicht gesetzt' : 'nicht gesendet'}, der nächste Lauf wiederholt: ${fehlerArt(err)}`)
return false
}
}
+85
View File
@@ -0,0 +1,85 @@
import { config } from '../config.js'
import { ENTHALTEN_IN } from './aboFunktionen.js'
import { consentAbo, featureConfig, featureSperre, katalogFelder, katalogReihenfolge } from './featureRegistry.js'
import { unterlagenFuer } from './aboUnterlagen.js'
import { leseShopConfig } from './shop/anfrage.js'
import { ladeEinstellungen } from './unterkunft/speicher.js'
/**
* Katalog der Funktionen im Abo-Modell (V5) fuer GET /api/funktionen mit neuer Ansicht. Reine Lesefunktionen; der Katalog enthaelt
* keinen Preis. Alle Texte kommen vom Server (Register, config.aboFunktionen), nie vom Client.
*/
/** Oeffentliche Sicht der Erklaerung: version, knopf und die Items in Anzeigereihenfolge (nurPrivatInFrist nur, wo es gesetzt ist). */
function oeffentlicheErklaerung(consent) {
if (!consent) return null
return {
version: consent.version,
knopf: consent.knopf,
items: consent.items.map((item) => ({
id: item.id,
text: item.text,
haken: Boolean(item.haken),
...(item.nurPrivatInFrist ? { nurPrivatInFrist: true } : {}),
})),
}
}
/**
* Katalogeintraege in der Reihenfolge von katalogReihenfolge(); die Analyse nur bei config.analyse.enabled.
* @param {{ email?: string }} p email: Konto des Kunden (Schalter und Pilotlisten der Funktionen)
* @returns {Promise<Array<{ key: string, name: string, beschreibung: string, bild: string, enthaltenIn: 'hosting'|'wartung', verfuegbar: boolean,
* ablauf: string, einrichtung: string, unterlagen: object, nurBetrieb: boolean, consent: object|null, hinweis: string|null }>>}
*/
export async function katalogFuerKunde({ email } = {}) {
const eintraege = []
for (const key of katalogReihenfolge()) {
if (key === 'analyse' && !config.analyse.enabled) continue
const felder = katalogFelder(key)
const consent = oeffentlicheErklaerung(consentAbo(key)) // null bei analyse
eintraege.push({
key,
name: key === 'analyse' ? config.aboFunktionen.analyse.name : featureConfig(key).label.name,
beschreibung: felder.beschreibung,
bild: felder.bild,
enthaltenIn: ENTHALTEN_IN[key][0], // das kleinste Abo, das die Funktion enthaelt
verfuegbar: key === 'analyse' ? true : featureSperre(key, email) === null,
ablauf: felder.ablauf,
einrichtung: felder.einrichtung,
unterlagen: unterlagenFuer(key),
nurBetrieb: felder.nurBetrieb,
consent,
hinweis: consent?.items.find((item) => item.id === 'hinweis')?.text ?? null,
})
}
return eintraege
}
/** Anfragestatus einer Funktion zum Dokument: shopConfig (shop) bzw. ukEinstellungen (unterkunft). Lesefehler ergeben '' und einen Logeintrag. */
async function leseAnfrageStatus(key, doc) {
if (!doc) return ''
try {
const gelesen = key === 'shop' ? await leseShopConfig(doc.$id) : await ladeEinstellungen(doc.$id)
return typeof gelesen?.anfrageStatus === 'string' ? gelesen.anfrageStatus : ''
} catch (err) {
console.error(`[funktionen] Anfragestatus (${key}) nicht lesbar: ${err?.name ?? 'Fehler'} ${err?.status ?? ''}`.trim())
return ''
}
}
/**
* Anfragestatus von Webshop und Buchungskalender je Projekt: Map<projectId, { shop, unterkunft }> mit '' | 'offen' | 'angenommen' |
* 'abgelehnt' | 'zurueckgezogen'. Je Projekt mit Funktionsdokument wird genau eine Konfiguration gelesen, ohne Dokument gilt ''.
* @param {{ docs: object[], projects: Array<{ projectId: string }> }} p docs: portalFeatures-Dokumente des Kunden; projects: Projekte der Liste
*/
export async function anfrageStatusJeProjekt({ docs, projects }) {
const ergebnis = new Map()
await Promise.all(
projects.map(async (projekt) => {
const dokument = (key) => docs.find((d) => d.projectId === projekt.projectId && d.featureKey === key)
const [shop, unterkunft] = await Promise.all([leseAnfrageStatus('shop', dokument('shop')), leseAnfrageStatus('unterkunft', dokument('unterkunft'))])
ergebnis.set(projekt.projectId, { shop, unterkunft })
})
)
return ergebnis
}
+12
View File
@@ -0,0 +1,12 @@
/**
* Abo-Modell der Funktionen (Kundenansicht neu, P5/P6). Blatt-Modul ohne Importe: Routen, Abgleiche und Mails nutzen es gemeinsam.
*
* Ein portalFeatures-Dokument gehört zum Abo-Modell, wenn `unlockedBy` UND `einrichtungAbrechnung` beide 'abo' oder 'projekt' sind
* (Marker, den das Einschalten aus P5 setzt). Dokumente ohne Marker (Kauf-Bestand, Team, die Gratis-Zeiten der alten Ansicht mit
* unlockedBy 'abo') berührt keine neue Logik.
*/
export const ABRECHNUNGEN = Object.freeze(['abo', 'projekt'])
/** true, wenn das Dokument zum Abo-Modell gehört; null und undefined sind false. */
export const istAboModell = (doc) => ABRECHNUNGEN.includes(doc?.unlockedBy) && ABRECHNUNGEN.includes(doc?.einrichtungAbrechnung)
+32
View File
@@ -0,0 +1,32 @@
import { config } from '../config.js'
import { legalPaths } from './analyseLegal.js'
/**
* Einzige Quelle der verlinkten Unterlagen im Abo-Modell (Katalog, Einschalt-Nachweis, Analyse). Blattmodul: importiert
* nur config und analyseLegal.js. Liest bei jedem Aufruf (Tests aendern config zur Laufzeit). Task 15 (Fassung 6) stellt
* nur diese Quelle um; die Aufrufer bleiben unveraendert.
*/
const fassung = (unterlage) => (unterlage ? { version: unterlage.version, url: unterlage.url ?? '' } : null)
/**
* Leistungsbeschreibung und AVV einer Funktion: { leistungsbeschreibung: { version, url } | null, avv: { version, url } | null }.
* Funktionen aus dem Register: deren Eintraege; 'analyse': die versionierten Seiten aus legalPaths(config.analyse.avvVersion).
* Ein unbekannter Schluessel ist ein Programmfehler (TypeError).
*/
export function unterlagenFuer(key) {
if (key === 'analyse') {
const version = config.analyse.avvVersion
const pfade = legalPaths(version)
return { leistungsbeschreibung: { version, url: pfade.lb }, avv: { version, url: pfade.avv } }
}
if (typeof key !== 'string' || !Object.hasOwn(config.features, key)) throw new TypeError(`Unbekannte Funktion: ${String(key)}`)
const eintrag = config.features[key]
return { leistungsbeschreibung: fassung(eintrag.leistungsbeschreibung), avv: fassung(eintrag.avv) }
}
/** Unterlagen der Analyse fuer Nachweis und Mail: { avvVersion, legal: { lb, avv } } (Adressen der versionierten Seiten). */
export function analyseUnterlagen() {
const { leistungsbeschreibung, avv } = unterlagenFuer('analyse')
return { avvVersion: avv.version, legal: { lb: leistungsbeschreibung.url, avv: avv.url } }
}
+42 -11
View File
@@ -9,6 +9,9 @@ import { sendCustomerMail } from './mailer.js'
import { triggerRebuild } from './previewRebuild.js'
import { ensureFeatureDoc, getFeatureDoc, newSiteKey, stateFields, assertOwnProject } from './siteFeatures.js'
import { laeuft } from './aboStatus.js'
import { aboDeckt, aboLaeuftFuer, aboModellFuerKunde } from './aboFunktionen.js'
import { ABRECHNUNGEN } from './aboModell.js'
import { consentAboAnalyse } from './featureRegistry.js'
import { createFeatureTicket } from './ticketAdmin.js'
import { berlinDateTime } from './berlinTime.js'
import { legalPaths, legalPdfAttachments } from './analyseLegal.js'
@@ -40,6 +43,10 @@ export const ELIGIBILITY_MESSAGES = {
keine_subdomain: 'Für dieses Projekt ist noch keine Subdomain eingerichtet.',
}
const ELIGIBILITY_STATUS = { nicht_dein_projekt: 403, kein_abo: 403, nicht_oeffentlich: 409, keine_subdomain: 409 }
/** Im Abo-Modell (neue Ansicht) reicht auch ein Hosting-Abo: nur dieser Text weicht ab. */
export const ELIGIBILITY_MESSAGES_ABO = { kein_abo: 'Die Website-Analyse gibt es nur mit einem laufenden Hosting- oder Wartungs-Abo.' }
export const eligibilityMessage = (reason, { aboModell = false } = {}) =>
(aboModell && ELIGIBILITY_MESSAGES_ABO[reason]) || ELIGIBILITY_MESSAGES[reason]
/** Nur Wartungs-Abo (kein Hosting) mit laufendem Status; strenge Regel des Abo-Bausteins: ein Spiegel ohne productKey zaehlt nicht. */
const hasWartungsAbo = (customerId) => laeuft(customerId, { altbestand: false })
@@ -53,11 +60,16 @@ async function loadOwnProject(customerId, projectId) {
}
}
/** @returns {Promise<{ ok: boolean, reason?: string, project?: object }>} */
export async function analyseEligibility(customerId, projectId) {
/**
* @param {{ aboModell?: boolean }} [optionen] aboModell (neue Ansicht): ein laufendes Hosting- oder Wartungs-Abo genuegt (aboDeckt:
* nicht gekuendigt, aktiv; in der Probezeit gilt weiter kein_abo); sonst die strenge Regel "Wartungs-Abo laeuft".
* @returns {Promise<{ ok: boolean, reason?: string, project?: object }>}
*/
export async function analyseEligibility(customerId, projectId, { aboModell = false } = {}) {
const project = await loadOwnProject(customerId, projectId)
if (!project) return { ok: false, reason: 'nicht_dein_projekt' }
if (!(await hasWartungsAbo(customerId))) return { ok: false, reason: 'kein_abo' }
const aboOk = aboModell ? (await aboDeckt(customerId, FEATURE_KEY)).ok === true : await hasWartungsAbo(customerId)
if (!aboOk) return { ok: false, reason: 'kein_abo' }
if (project.isPublic !== true) return { ok: false, reason: 'nicht_oeffentlich' }
if (!project.subdomain) return { ok: false, reason: 'keine_subdomain' }
return { ok: true, project }
@@ -278,12 +290,22 @@ async function sendK18(doc, project) {
return doc
}
export async function activateAnalyse({ customerId, projectId, accentColor, consent, by = 'customer', actor, textform, audienceConfirmed, now = new Date() }) {
/**
* Analyse einschalten. aboModell (neue Ansicht, Kunde oder Team): ein Hosting- oder Wartungs-Abo genuegt, die Fassung ist die der
* Abo-Erklaerung (consentAboAnalyse().avvVersion; beim Kunden auch die erwartete). nachweis: async () => void, laeuft im selben try wie
* recordConsent, nach dem Dokument und davor; ein Fehler nimmt alles zurueck (kein "on" ohne Nachweis). Beides nur bei einer neuen Aktivierung.
* abrechnung (Team im Abo-Modell, Nachzug P6b): 'abo' | 'projekt' setzt den Marker des Abo-Modells (unlockedBy und einrichtungAbrechnung,
* istAboModell) am Dokument, ebenfalls nur bei einer neuen Aktivierung und im selben Schritt (die Rücknahme stellt ihn wieder her); null
* (Vorgabe) schreibt keinen Marker.
*/
export async function activateAnalyse({ customerId, projectId, accentColor, consent, by = 'customer', actor, textform, audienceConfirmed, now = new Date(), aboModell = false, nachweis = null, abrechnung = null }) {
if (abrechnung !== null && !ABRECHNUNGEN.includes(abrechnung)) throw httpError(400, 'Unbekannte Abrechnung')
const viaCustomer = by === 'customer'
const avvVersion = aboModell ? consentAboAnalyse().avvVersion : config.analyse.avvVersion
let accepted
let proof = { date: '', channel: '' }
if (viaCustomer) {
if (!consent || consent.accepted !== true || consent.avvVersion !== config.analyse.avvVersion) {
if (!consent || consent.accepted !== true || consent.avvVersion !== avvVersion) {
throw httpError(400, 'Bitte den Auftragsverarbeitungsvertrag bestätigen')
}
// Name und E-Mail der handelnden Person (bei Admin-Sitzung der Admin, siehe Route)
@@ -299,8 +321,8 @@ export async function activateAnalyse({ customerId, projectId, accentColor, cons
if (!COLOR_RE.test(color)) throw httpError(400, 'Ungültige Akzentfarbe')
return withLocks([`analyse:${projectId}`], async () => {
const eligibility = await analyseEligibility(customerId, projectId)
if (!eligibility.ok) throw httpError(ELIGIBILITY_STATUS[eligibility.reason], ELIGIBILITY_MESSAGES[eligibility.reason])
const eligibility = await analyseEligibility(customerId, projectId, { aboModell })
if (!eligibility.ok) throw httpError(ELIGIBILITY_STATUS[eligibility.reason], eligibilityMessage(eligibility.reason, { aboModell }))
const { project } = eligibility
if (!analyseDomains(project).length) throw httpError(409, 'Keine gültige Domain für die Analyse')
@@ -323,14 +345,14 @@ export async function activateAnalyse({ customerId, projectId, accentColor, cons
projectId,
siteKey,
event: 'on',
avvVersion: config.analyse.avvVersion,
avvVersion,
via: viaCustomer ? 'portal' : 'woms',
acceptedAt: at,
acceptedBy: accepted,
textformDate: proof.date,
textformChannel: proof.channel,
audienceConfirmed: true, // Kunden-Haken enthaelt die Zielgruppen-Aussage; Mitarbeiter bestaetigen sie getrennt
audienceTextVersion: config.analyse.avvVersion,
audienceTextVersion: avvVersion,
}
let updated = false
try {
@@ -345,6 +367,7 @@ export async function activateAnalyse({ customerId, projectId, accentColor, cons
avvConsentChannel: proof.channel,
audienceConfirmed: true,
audienceTextVersion: entry.audienceTextVersion,
...(abrechnung ? { unlockedBy: abrechnung, einrichtungAbrechnung: abrechnung } : {}),
activatedAt: at,
offSince: '',
activatedMailAt: '', // jede neue Aktivierung bekommt ihre K18
@@ -353,14 +376,20 @@ export async function activateAnalyse({ customerId, projectId, accentColor, cons
publishTicketId: '',
})
updated = true
// Nachweis erst nach dem Dokument: scheitert er, wird alles zurueckgenommen (kein "on" ohne aktives Dokument)
// Nachweis der Abo-Erklaerung (featureConsents) und Historie erst nach dem Dokument: scheitert einer, wird alles
// zurueckgenommen (kein "on" ohne Nachweis und ohne aktives Dokument)
await nachweis?.()
await analyseDeps.recordConsent(entry)
} catch (err) {
await analyseDeps.client
.putSite(siteKey, { projectId, domains: analyseDomains(project), active: false })
.catch((e) => console.warn(`[analyse] Rücknahme für Projekt ${projectId} fehlgeschlagen:`, e.message))
if (updated) {
await updateDocument(config.collections.portalFeatures, existing.$id, { ...stateFields(existing.state || 'off'), offSince: existing.offSince || '' })
await updateDocument(config.collections.portalFeatures, existing.$id, {
...stateFields(existing.state || 'off'),
offSince: existing.offSince || '',
...(abrechnung ? { unlockedBy: existing.unlockedBy ?? null, einrichtungAbrechnung: existing.einrichtungAbrechnung ?? null } : {}),
})
.catch((e) => console.warn(`[analyse] Zurücksetzen für Projekt ${projectId} fehlgeschlagen:`, e.message))
}
throw err
@@ -480,6 +509,8 @@ export async function runAboCheck({ now = new Date() } = {}) {
try {
// Ein Lesefehler (Stripe/Appwrite) beendet nie eine Analyse
if (await hasWartungsAbo(doc.customerId)) continue
// Neue Ansicht (Abo-Modell): auch ein laufendes Hosting-Abo haelt die Analyse (gekuendigt zaehlt bis zum Abo-Ende); alle anderen wie bisher
if ((await aboModellFuerKunde(doc.customerId)) && (await aboLaeuftFuer(doc.customerId, FEATURE_KEY))) continue
const { changed } = await deactivateAnalyse({ customerId: doc.customerId, projectId: doc.projectId, now, by: 'system' })
if (!changed) continue
deactivated.push(doc.$id)
+23
View File
@@ -14,6 +14,8 @@ import {
Query,
} from './appwriteAdmin.js'
import { forgetPasswordVersion } from './passwordVersion.js'
import { KUNDENARTEN, kundenartGespeichert } from './kundenart.js'
import { neueAnsichtModus } from './kundenansicht/schalter.js'
function readPortalPassword(customer) {
if (customer?.portalPassword) return customer.portalPassword
@@ -27,6 +29,17 @@ function notesWithoutPortalPassword(notes) {
return (notes || '').replace(/^__portalPassword__:[^\n]*\n?/, '').trim()
}
/** Fehler fuer eine fehlende oder ungueltige Kundenart (400 kundenart_fehlt); die Route reicht `code` durch. */
function kundenartFehler() {
const error = new Error('Kundenart ist erforderlich (unternehmen oder privat).')
error.status = 400
error.code = 'kundenart_fehlt'
return error
}
/** Als "nicht mitgeschickt" gelten undefined, null und ''. */
const kundenartFehlt = (wert) => wert === undefined || wert === null || wert === ''
function sanitizeCustomer(customer, { portalPassword } = {}) {
const storedPassword = portalPassword || readPortalPassword(customer)
return {
@@ -41,6 +54,7 @@ function sanitizeCustomer(customer, { portalPassword } = {}) {
appwriteUserId: customer.appwriteUserId || '',
customerStatus: customer.customerStatus || '',
archived: Boolean(customer.archived),
kundenart: kundenartGespeichert(customer),
portalPassword: storedPassword || '',
}
}
@@ -73,6 +87,7 @@ export async function createCustomerWithPortalAccess(payload) {
email,
phone = '',
password,
kundenart,
} = payload || {}
if (!name?.trim()) {
@@ -90,6 +105,9 @@ export async function createCustomerWithPortalAccess(payload) {
error.status = 400
throw error
}
// Kundenart (V4): Pflicht nur ab Schalter pilot (bei off legt das WOMS Kunden wie heute ohne Kundenart an);
// ein mitgeschickter, ungueltiger Wert ist in jedem Modus ein Fehler. Vor jedem Schreibzugriff.
if (kundenartFehlt(kundenart) ? neueAnsichtModus() !== 'off' : !KUNDENARTEN.includes(kundenart)) throw kundenartFehler()
const existing = await getCustomerByEmail(email.trim())
if (existing) {
@@ -117,6 +135,7 @@ export async function createCustomerWithPortalAccess(payload) {
portalAccessEnabled: true,
appwriteUserId: appwriteUser.$id,
customerStatus: 'active',
...(kundenartFehlt(kundenart) ? {} : { kundenart }),
createdAt: now,
updatedAt: now,
})
@@ -150,6 +169,7 @@ export async function updateCustomerWithPortalAccess(customerId, payload) {
email,
phone,
password,
kundenart,
} = payload || {}
const nextEmail = (email ?? customer.email ?? '').trim()
@@ -163,6 +183,8 @@ export async function updateCustomerWithPortalAccess(customerId, payload) {
error.status = 400
throw error
}
// Kundenart optional: nur wenn mitgeschickt, dann gueltig (V4)
if (kundenart !== undefined && !KUNDENARTEN.includes(kundenart)) throw kundenartFehler()
const normalizedPassword =
typeof password === 'string' && password.trim().length > 0 ? password : undefined
@@ -216,6 +238,7 @@ export async function updateCustomerWithPortalAccess(customerId, payload) {
...(location !== undefined ? { location: String(location).trim() } : {}),
email: nextEmail,
...(phone !== undefined ? { phone: String(phone).trim() } : {}),
...(kundenart !== undefined ? { kundenart } : {}),
...(nextPortalPassword ? { portalPassword: nextPortalPassword } : {}),
...(notesChanged ? { notes: cleanedNotes } : {}),
// CRM-Stufe (lead/customer/lost) nicht ueberschreiben - nur setzen, wenn leer
+10 -3
View File
@@ -5,6 +5,7 @@
import { createHash } from 'node:crypto'
import { config } from '../config.js'
import { createDocument, listDocuments, Query } from './appwriteAdmin.js'
import { KUNDENARTEN } from './kundenart.js'
// Grenzen der Collection (scripts/feature-setup.mjs, FEATURE_CONSENT_ATTRIBUTES)
const MAX_ID = 36
@@ -74,9 +75,11 @@ export function texteHash(texte) {
* Nachweis anfuegen. fassungen: Name => Fassung (z. B. zustimmung, leistungsbeschreibung, avv); texte: [{ id, text, haken }]
* genau in Anzeigereihenfolge. Erlaubt leer: stripeSessionId, ip, userAgent (Nachweis der Aktivierung durch das Team) und
* haken je Text. Wirft 400 (und schreibt nichts), wenn etwas fehlt, ungueltig oder zu lang ist.
* kundenart (optional, 'unternehmen' | 'privat'): Kundenart zum Klickzeitpunkt (V4); das Feld wird nur geschrieben, wenn es
* uebergeben wurde (ohne Setup-Skript kennt Appwrite es nicht), jeder andere uebergebene Wert ist ein 400.
* @returns {Promise<object>} das angelegte Dokument
*/
export async function speichereZustimmung({ customerId, projectId, featureKey, stripeSessionId, fassungen, texte, ip, userAgent, konto, at } = {}) {
export async function speichereZustimmung({ customerId, projectId, featureKey, stripeSessionId, fassungen, texte, ip, userAgent, konto, at, kundenart } = {}) {
const daten = {
customerId: kennung(customerId, MAX_ID, 'Kunde'),
projectId: kennung(projectId, MAX_ID, 'Projekt'),
@@ -90,8 +93,10 @@ export async function speichereZustimmung({ customerId, projectId, featureKey, s
const texteJson = JSON.stringify(texte)
if (fassungenJson.length > MAX_FASSUNGEN) throw httpError(400, 'Zustimmungsnachweis: Fassungen zu lang')
if (texteJson.length > MAX_TEXTE) throw httpError(400, 'Zustimmungsnachweis: Texte zu lang')
if (kundenart !== undefined && !KUNDENARTEN.includes(kundenart)) throw httpError(400, 'Zustimmungsnachweis: Kundenart ungültig')
return createDocument(config.collections.featureConsents, {
...daten,
...(kundenart !== undefined ? { kundenart } : {}),
fassungen: fassungenJson,
texte: texteJson,
textHash: sha256(texteJson),
@@ -180,9 +185,10 @@ const kontoLabel = (person) => (person?.name && person?.email ? `${person.name}
* steht nur im Log. Ohne sessionId (Gratisweg) traegt der Nachweis keine Stripe-Sitzung.
* @param {object} req Express-Anfrage (ip, User-Agent, Sitzung)
* @param {{ feature: object, haken?: object, customerId: string, projectId: string, sessionId?: string, klickAt: string }} p
* feature: Registereintrag, beim Gratisweg freiweg(feature) (Zustimmung consentFrei)
* feature: Registereintrag, beim Gratisweg freiweg(feature) (Zustimmung consentFrei), im Abo-Modell aboweg(feature)
* kundenart (optional): Kundenart zum Klickzeitpunkt, wird nur durchgereicht, wenn sie uebergeben wurde
*/
export async function speichereNachweisAusAnfrage(req, { feature, haken = {}, customerId, projectId, sessionId, klickAt }) {
export async function speichereNachweisAusAnfrage(req, { feature, haken = {}, customerId, projectId, sessionId, klickAt, kundenart }) {
const belehrung = (await haken.belehrung?.()) ?? null
const texte = nachweisTexte(feature, { belehrung })
try {
@@ -197,6 +203,7 @@ export async function speichereNachweisAusAnfrage(req, { feature, haken = {}, cu
userAgent: req.get('user-agent'),
konto: kontoLabel(actingPerson(req.session)),
at: klickAt,
...(kundenart !== undefined ? { kundenart } : {}),
})
} catch (err) {
throw Object.assign(new Error(`Zustimmungsnachweis nicht gespeichert: ${err?.message}`), { status: 500, cause: err })
+74 -1
View File
@@ -3,10 +3,12 @@ import { termineFuerKunde } from './termine/schalter.js'
import { shopFuerKunde } from './shop/schalter.js'
import { unterkunftFuerKunde } from './unterkunft/schalter.js'
import { zeitenFuerKunde } from './zeiten/schalter.js'
import { analyseUnterlagen, unterlagenFuer } from './aboUnterlagen.js'
// Helfer fuer das Funktionen-Register config.features (News-Pinnwand, Terminplaner, Webshop, Oeffnungszeiten live; Daten in server/config.js).
// Diese Datei wird von siteFeatures.js importiert und importiert selbst keine Dienste statisch (einzige Ausnahmen:
// termine/schalter.js, shop/schalter.js und zeiten/schalter.js, die nur config lesen): Standard-Haken laden ihre Dienste per dynamischem import()
// termine/schalter.js, shop/schalter.js und zeiten/schalter.js, die nur config lesen, und aboUnterlagen.js, ein Blattmodul,
// das nur config und analyseLegal.js importiert): Standard-Haken laden ihre Dienste per dynamischem import()
// innerhalb der Funktion (sonst entstuenden Importzyklen).
/** Namen der Haken, die ein Registereintrag hat (optional): Bestell-, Freischalt- und Unterlagenpruefungen, dazu nachFreischaltung. */
@@ -216,6 +218,77 @@ export function freiweg(feature) {
return { ...feature, consent: feature.consentFrei }
}
// ---------------------------------------------------------------------------------------------------------------
// Abo-Modell der Funktionen (Kundenansicht neu, P5): Katalogfelder, Einschalt-Erklaerung ohne Preis und Unterlagen.
// Reine Lesefunktionen auf config.features und config.aboFunktionen; die Unterlagen kommen aus aboUnterlagen.js.
// ---------------------------------------------------------------------------------------------------------------
/** Schluessel des Katalogs in Anzeigereihenfolge (Kopie), mit 'analyse' am Ende. */
export function katalogReihenfolge() {
return [...config.aboFunktionen.reihenfolge]
}
/** { beschreibung, bild, ablauf, einrichtung, nurBetrieb } einer Funktion (auch 'analyse'); unbekannter Schluessel => null. */
export function katalogFelder(key) {
const quelle = key === 'analyse' ? config.aboFunktionen.analyse : featureConfig(key)
if (!quelle) return null
const { beschreibung, bild, ablauf, einrichtung, nurBetrieb } = quelle
return { beschreibung, bild, ablauf, einrichtung, nurBetrieb }
}
/**
* Einschalt-Erklaerung einer Funktion im Abo-Modell: { version, knopf: 'Einschalten', items } in Anzeigereihenfolge
* einschalten-zeile (Linkzeile mit Fassung, kein Haken), hinweis (nur shop, unterkunft, termine; kein Haken) und beginn
* (Haken, nur fuer Privatkunden innerhalb der Widerrufsfrist). Baut die Items bei jedem Aufruf aus aboUnterlagen.js und
* config.aboFunktionen.texte. Analyse und unbekannte Schluessel => null.
*/
export function consentAbo(key) {
if (!featureConfig(key)) return null
const { leistungsbeschreibung, avv } = unterlagenFuer(key)
const { fassung: version, texte } = config.aboFunktionen
const zeile = avv
? `Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung ${leistungsbeschreibung.version}).`
: `Es gilt die Leistungsbeschreibung (Fassung ${leistungsbeschreibung.version}).`
const items = [{ id: 'einschalten-zeile', text: zeile, haken: false }]
if (Object.hasOwn(texte.hinweis, key)) items.push({ id: 'hinweis', text: texte.hinweis[key], haken: false })
items.push({ id: 'beginn', text: texte.beginn, haken: true, nurPrivatInFrist: true })
return { version, knopf: 'Einschalten', items }
}
/** Die Erklaerung, wie sie angezeigt wird: ohne beginn, solange das Haekchen nicht verlangt ist. */
export function consentAboAktiv(key, { beginn = false } = {}) {
const consent = consentAbo(key)
if (!consent) return null
return { ...consent, items: consent.items.filter((item) => item.id !== 'beginn' || beginn) }
}
/** Abo-Weg einer Funktion fuer den Nachweis: derselbe Eintrag mit der Abo-Erklaerung, den Unterlagen aus aboUnterlagen.js und ohne Widerrufsbelehrung. */
export function aboweg(feature, { beginn = false } = {}) {
const { leistungsbeschreibung, avv } = unterlagenFuer(feature.key)
return { ...feature, consent: consentAboAktiv(feature.key, { beginn }), leistungsbeschreibung, avv, widerruf: null }
}
/** Einschalt-Erklaerung der Analyse: Linkzeile (Fassung der AVV), Zielgruppen-Haken (Kinder unter 16) und Knopf. */
export function consentAboAnalyse() {
const { avvVersion } = analyseUnterlagen()
const { fassung: version, texte } = config.aboFunktionen
return {
version,
avvVersion,
knopf: texte.analyseKnopf,
items: [
{ id: 'einschalten-zeile', text: `Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung ${avvVersion}).`, haken: false },
{ id: 'zielgruppe', text: texte.zielgruppe, haken: true },
],
}
}
/** Abo-Weg der Analyse in der Form, die speichereNachweisAusAnfrage erwartet. */
export function abowegAnalyse() {
const { avvVersion } = analyseUnterlagen()
return { key: 'analyse', consent: consentAboAnalyse(), leistungsbeschreibung: { version: avvVersion }, avv: { version: avvVersion }, widerruf: null }
}
const fassung = (f) => (f ? { version: f.version, url: f.url ?? '' } : null)
/**
+80
View File
@@ -0,0 +1,80 @@
import { config } from '../config.js'
import { getDocument } from './appwriteAdmin.js'
import { listContractEvents, openWithdrawals } from './contractEvents.js'
import { withdrawalDeadline } from './deadlines.js'
import { FEATURE_LABELS, paidFeatureOrders } from './siteFeatures.js'
import { listAuftraege } from './auftraege/auftragStore.js'
import { auftraegeAktiv } from './auftraege/schalter.js'
import { kundenartWirksam } from './kundenart.js'
/**
* Gesammelte Widerrufsfristen eines Kunden (V6, Spec 5.6): jede noch offene Frist aus Abo-Abschluss, Wechsel ins
* Wartungs-Abo, Zusatzspeicher (nur Plus), bezahlten Funktionen (Altbestand mit Kauf) und Auftragsbestätigungen
* (nur mit Schalter und Snapshot-Kundenart privat). Offen ist eine Frist bis einschließlich ihres Endes
* (23:59:59.999 Berliner Zeit am letzten Tag). `anzeigen` ist nur für Privatkunden wahr; Betriebe haben kein
* Widerrufsrecht und sehen keine Fristen (die Liste bleibt trotzdem gefüllt). Lesefehler werden weitergereicht.
*/
const BEZEICHNUNG = {
abschluss: 'Abo-Abschluss',
wechsel: 'Wechsel ins Wartungs-Abo',
speicher: 'Zusatzspeicher',
}
const zeitpunkt = (wert) => (wert instanceof Date ? wert.getTime() : Date.parse(wert))
/** Kundendokument einmal lesen; ein unbekannter Kunde ergibt {}. */
async function leseKunde(customerId) {
try {
return await getDocument(config.collections.customers, customerId)
} catch (err) {
if (err.status === 404) return {}
throw err
}
}
/** Ende der Frist zu einem Vertragsschluss; kein lesbarer Zeitpunkt ergibt null (keine Frist, nie ein Fehler). */
function fristEnde(vertragAt) {
const ms = zeitpunkt(vertragAt || '')
return Number.isNaN(ms) ? null : withdrawalDeadline(new Date(ms)).endsAt
}
/**
* @returns {Promise<{ fristen: Array<{ quelle: 'abschluss'|'wechsel'|'speicher'|'funktion'|'projekt', bezeichnung: string, bis: string, featureKey?: string, projektName?: string }>, fruehestesEnde: string|null, anzeigen: boolean }>}
*/
export async function sammleFristen(customerId, { now = new Date() } = {}) {
const kunde = await leseKunde(customerId)
const fristen = []
for (const f of openWithdrawals(await listContractEvents(customerId), now)) {
fristen.push({ quelle: f.type, bezeichnung: BEZEICHNUNG[f.type], bis: new Date(zeitpunkt(f.until)).toISOString() })
}
for (const order of await paidFeatureOrders(customerId)) {
const ende = fristEnde(order.vertragAt)
if (!ende || now.getTime() > ende.getTime()) continue
fristen.push({
quelle: 'funktion',
bezeichnung: FEATURE_LABELS[order.featureKey]?.name || order.featureKey,
bis: ende.toISOString(),
featureKey: order.featureKey,
projektName: order.projekt,
})
}
if (auftraegeAktiv(kunde)) {
for (const auftrag of await listAuftraege({ kundenId: customerId })) {
if (auftrag.kunde?.kundenart !== 'privat' || !['versendet', 'bestaetigt'].includes(auftrag.status)) continue
const ende = fristEnde(auftrag.gesendetAm)
if (!ende || now.getTime() > ende.getTime()) continue
fristen.push({ quelle: 'projekt', bezeichnung: `Auftrag ${auftrag.nummer}`, bis: ende.toISOString() })
}
}
fristen.sort((a, b) => Date.parse(a.bis) - Date.parse(b.bis))
return {
fristen,
fruehestesEnde: fristen[0]?.bis ?? null,
anzeigen: kundenartWirksam(kunde) === 'privat' && fristen.length > 0,
}
}
+183
View File
@@ -0,0 +1,183 @@
import { config } from '../config.js'
import { updateDocument } from './appwriteAdmin.js'
import { aboDeckt } from './aboFunktionen.js'
import { meldeEingeschaltet } from './aboFunktionenMeldung.js'
import { berlinDay } from './deadlines.js'
import { bestellFehler } from './featureOrder.js'
import { featureConfig, ticketTexte } from './featureRegistry.js'
import { withLocks } from './keyedLock.js'
import { assertOwnProject, customerOf, ensureFeatureDoc, getFeatureDoc, notizVorn, stateFields, ticketWoid, unlockedByVon } from './siteFeatures.js'
import { ABRECHNUNGS_ZEILEN, createFeatureTicket } from './ticketAdmin.js'
import { einschaltenZeiten } from './zeiten/einschalten.js'
/**
* Generisches Einschalten einer Funktion im Abo-Modell (V7, Spec 5.4): ohne Zahlung, mit Einrichtungs-Ticket. Gleiches Muster wie
* einschaltenZeiten (Sperre feature-order, Nachweis vor dem Zustand, Ticket danach, Heilung statt Doppelticket). Am Ende meldet
* meldeEingeschaltet (aboFunktionenMeldung.js, P6) das Einschalten mit K38 an den Kunden und T24 ans Team; die Team-Aktivierung
* (adminActivate) steht in siteFeatures.js. Fuer news, termine, shop und unterkunft laeuft alles hier; zeiten geht ueber
* einschaltenZeiten (Neubau, Ticket und K27 stehen dort) mit einer Vorpruefung, die dieselben Codes liefert.
*/
/** Wie die Einrichtung abgerechnet wird: im Abo-Stundenkonto (abo) oder im Projekt enthalten (projekt). */
export const ABRECHNUNGEN = Object.freeze(['abo', 'projekt'])
export const EINSCHALTEN_TEXTE = Object.freeze({
bereits: 'Diese Funktion ist für dieses Projekt schon eingeschaltet.',
laeuft: 'Für diese Funktion läuft schon eine Bestellung.',
abgeschaltet: 'Diese Funktion wurde für dieses Projekt abgeschaltet. Schreib uns im Chat, wenn du sie wieder möchtest.',
ticket: 'Die Einrichtung konnte gerade nicht angelegt werden. Bitte versuch es gleich noch einmal.',
abo: Object.freeze({
kein_abo: 'Für diese Funktion brauchst du ein laufendes Abo.',
falsches_abo: 'Diese Funktion ist im Wartungs-Abo enthalten.',
gekuendigt: 'Dein Abo ist gekündigt. Zum Einschalten brauchst du ein laufendes Abo.',
nicht_aktiv: 'Dein Abo ist gerade nicht aktiv, zum Beispiel wegen einer offenen Zahlung. Sobald es wieder aktiv ist, kannst du die Funktion einschalten.',
beginnt_spaeter: 'Dein Abo beginnt am {datum}. Ab dann kannst du einschalten.',
}),
nichtPruefbar: 'Gerade nicht prüfbar – bitte versuch es gleich noch einmal.',
})
/** Berliner Kalendertag eines Zeitpunkts (ISO) als TT.MM.JJJJ; kein Zeitpunkt ist ein TypeError. */
export function datumDe(zeitpunkt) {
const ms = Date.parse(zeitpunkt)
if (Number.isNaN(ms)) throw new TypeError(`datumDe: kein Zeitpunkt: ${String(zeitpunkt)}`)
const [jahr, monat, tag] = berlinDay(new Date(ms)).split('-')
return `${tag}.${monat}.${jahr}`
}
/**
* Kundentext zu einem Abo-Grund. Bei beginnt_spaeter steht der Berliner Tag von `ab` (ISO) als TT.MM.JJJJ im Text,
* z. B. "Dein Abo beginnt am 03.11.2026. Ab dann kannst du einschalten."
*/
export function aboFehlerText(grund, ab = null) {
if (!Object.hasOwn(EINSCHALTEN_TEXTE.abo, grund)) throw new TypeError(`aboFehlerText: unbekannter Grund ${String(grund)}`)
const text = EINSCHALTEN_TEXTE.abo[grund]
return grund === 'beginnt_spaeter' ? text.replace('{datum}', datumDe(ab)) : text
}
/** 409 abo_fehlt: body.grund, body.wechsel (Hosting-Kunde: Wechsel ins Wartungs-Abo anbieten) und bei beginnt_spaeter body.ab. */
const aboFehlt = (grund, ab) =>
bestellFehler(409, aboFehlerText(grund, ab), 'abo_fehlt', { grund, wechsel: grund === 'falsches_abo', ...(grund === 'beginnt_spaeter' ? { ab } : {}) })
const konflikt = (code, text) => bestellFehler(409, text, code)
/**
* Abo frisch lesen und gegen die Funktion pruefen (immer ohne Zwischenspeicher): 409 abo_fehlt oder nichts. Lesefehler sind 503,
* nie stillschweigend erlaubt. Auch fuer die Vorpruefungen der Route.
*/
export async function pruefeAboDeckung(customerId, key, now = new Date()) {
let ergebnis
try {
ergebnis = await aboDeckt(customerId, key, { now })
} catch (err) {
console.error(`[funktionen] Abo für das Einschalten nicht lesbar: ${err?.name ?? 'Fehler'} ${err?.status ?? ''}`.trim())
throw bestellFehler(503, EINSCHALTEN_TEXTE.nichtPruefbar)
}
if (ergebnis.ok !== true) throw aboFehlt(ergebnis.grund, ergebnis.ab)
}
/** setup ohne Ticket und durch Abo oder Projekt eingeschaltet: der Dienst heilt (nur das Ticket), es entsteht nichts Neues. */
const heilbar = (doc) => doc?.state === 'setup' && !doc.ticketId && ['abo', 'projekt'].includes(unlockedByVon(doc))
/**
* Zustand des Dokuments pruefen: wirft den 409 mit Code (bestellung_laeuft, abgeschaltet, bereits_eingeschaltet); ein fehlendes oder
* ausgeschaltetes (off) Dokument und die Heilung (setup ohne Ticket, durch Abo oder Projekt) sind frei. Auch fuer die Vorpruefungen der Route.
*/
export function pruefeEinschaltZustand(doc) {
if (!doc || doc.state === 'off') return
if (doc.state === 'ordered') throw konflikt('bestellung_laeuft', EINSCHALTEN_TEXTE.laeuft)
if (doc.state === 'disabled') throw konflikt('abgeschaltet', EINSCHALTEN_TEXTE.abgeschaltet)
if (!heilbar(doc)) throw konflikt('bereits_eingeschaltet', EINSCHALTEN_TEXTE.bereits)
}
/** Notiz des Teams: staff.adminNote oder "<ISO> <Name>: Eingeschaltet (<abrechnung>)" vor der bisherigen Notiz; ohne beides keine. */
function teamNotiz(staff, bisherige, abrechnung, now) {
if (typeof staff?.adminNote === 'string' && staff.adminNote) return staff.adminNote.slice(0, 300)
if (!staff?.name) return null
return notizVorn(bisherige, `${now.toISOString()} ${staff.name}: Eingeschaltet (${abrechnung})`)
}
/** Einrichtungs-Ticket anlegen und an das Dokument haengen; jeder Fehler hier ist 503 (der Zustand bleibt setup, der naechste Aufruf heilt). */
async function legeTicketAn(doc, { key, feature, project, customerId, abrechnung }) {
try {
const customer = await customerOf(customerId).catch(() => null)
const texte = await ticketTexte({ feature, doc, project, customer })
const { ticketId } = await createFeatureTicket({ customerId, project, featureKey: key, siteKey: doc.siteKey, ...texte, abrechnung })
return await updateDocument(config.collections.portalFeatures, doc.$id, { ticketId })
} catch (err) {
console.error(`[funktionen] Einrichtung ${key} (${doc.$id}) nicht angelegt: ${err?.name ?? 'Fehler'} ${err?.status ?? ''} ${err?.message ?? ''}`.trim())
throw Object.assign(new Error(EINSCHALTEN_TEXTE.ticket), { status: 503, cause: err })
}
}
/**
* Funktion fuer ein Projekt einschalten (Zustand setup, Einrichtungs-Ticket mit Abrechnung).
* @param {{ customerId: string, projectId: string, key: string, by: 'kunde'|'team', abrechnung: 'abo'|'projekt',
* nachweis?: (() => Promise<unknown>)|null, staff?: { name?: string, adminNote?: string }|null, now?: Date }} p
* by 'kunde': das Abo wird unter der Sperre frisch gegen die Funktion geprueft (aboDeckt); by 'team': keine Abo-Pruefung.
* nachweis: speichert den Zustimmungsnachweis, bevor der Zustand geaendert wird; wirft er, bleibt alles wie es war.
* @returns {Promise<{ doc: object, ticketWoid: string|null }>}
* @throws {TypeError} key ausserhalb des Registers (auch analyse), by oder abrechnung ungueltig
* @throws {Error} 403/409 (Projekt), 409 bereits_eingeschaltet | bestellung_laeuft | abgeschaltet | abo_fehlt, 503 (Abo nicht lesbar, Ticket gescheitert)
*/
export async function einschalten({ customerId, projectId, key, by, abrechnung, nachweis = null, staff = null, now = new Date() } = {}) {
const feature = featureConfig(key)
if (!feature) throw new TypeError(`einschalten: unbekannte Funktion ${String(key)}`)
if (by !== 'kunde' && by !== 'team') throw new TypeError(`einschalten: by muss kunde oder team sein, nicht ${String(by)}`)
if (!ABRECHNUNGEN.includes(abrechnung)) throw new TypeError(`einschalten: abrechnung muss abo oder projekt sein, nicht ${String(abrechnung)}`)
const project = await assertOwnProject(customerId, projectId, { forNewOrder: true })
// Oeffnungszeiten: Sperre, Nachweis, Zustand, Neubau, Ticket und K27 stehen in einschaltenZeiten; die Vorpruefung unter der Sperre
// liefert dieselben Codes wie unten (nur fuer den Kunden, das Team schaltet wie bisher)
if (key === 'zeiten') {
const vorpruefung = async () => {
await pruefeAboDeckung(customerId, key, now)
pruefeEinschaltZustand(await getFeatureDoc(customerId, projectId, key))
}
const { doc, changed } = await einschaltenZeiten({ customerId, projectId, by, staff, nachweis, now, abrechnung, vorpruefung })
const woid = await ticketWoid(doc.ticketId)
// nur wenn dieser Aufruf den Zustand gesetzt hat (K27 sendet holeZeitenNach); ein idempotenter Aufruf meldet nichts
if (changed === true) await meldeEingeschaltet({ doc, ticketWoid: woid, by, abrechnung })
return { doc, ticketWoid: woid }
}
// Abrechnung, mit der dieser Aufruf endet: bei der Heilung die am Dokument gespeicherte
let abgerechnet = abrechnung
const doc = await withLocks([`feature-order:${customerId}:${projectId}`], async () => {
// 1 Abo frisch (nie aus einem Zwischenspeicher); fuer das Team entfaellt es
if (by === 'kunde') await pruefeAboDeckung(customerId, key, now)
// 2 Zustand: Heilung (setup ohne Ticket, durch Abo oder Projekt) legt nur das Ticket an
const bisher = await getFeatureDoc(customerId, projectId, key)
pruefeEinschaltZustand(bisher)
if (heilbar(bisher)) {
const gespeichert = Object.hasOwn(ABRECHNUNGS_ZEILEN, bisher.einrichtungAbrechnung) ? bisher.einrichtungAbrechnung : abrechnung
abgerechnet = gespeichert
return legeTicketAn(bisher, { key, feature, project, customerId, abrechnung: gespeichert })
}
// 3 Zustimmung speichern, vor dem Zustand: scheitert sie, bleibt alles wie es war
if (nachweis) await nachweis()
// 4 Dokument auf setup. Reste einer frueheren Abschaltung gehoeren zum alten Lauf und enden hier.
const vorhanden = bisher ?? (await ensureFeatureDoc(customerId, projectId, key))
const notiz = by === 'team' ? teamNotiz(staff, vorhanden.adminNote, abrechnung, now) : null
const gespeichert = await updateDocument(config.collections.portalFeatures, vorhanden.$id, {
...stateFields('setup'),
featureKey: key,
unlockedBy: abrechnung,
unlockedByPurchase: false,
einrichtungAbrechnung: abrechnung,
disabledAt: null,
disabledReason: null,
aboEndeMailAt: null,
...(notiz ? { adminNote: notiz } : {}),
})
// 5 Ticket; scheitert es, bleibt setup ohne Ticket und der naechste Aufruf heilt
return legeTicketAn(gespeichert, { key, feature, project, customerId, abrechnung })
})
// Der Aufruf hat den Zustand gesetzt oder das Ticket angelegt (Heilung zaehlt); die Meldung wirft nie
const woid = await ticketWoid(doc.ticketId)
await meldeEingeschaltet({ doc, ticketWoid: woid, by, abrechnung: abgerechnet })
return { doc, ticketWoid: woid }
}
+56 -13
View File
@@ -11,6 +11,13 @@ import { worksheetMinutes } from './worksheetMinutes.js'
* Verrechnung: FIFO - die aelteste noch gueltige Gutschrift wird zuerst verbraucht.
* Was kein Guthaben mehr findet, ist Mehraufwand (separat abzurechnen).
* Vertragsende: Restguthaben verfaellt (siehe Abo-Bedingungen Ziff. 4).
*
* Vorschuss (Abo-Modell der Funktionen): Arbeitsblaetter auf Tickets mit einrichtungAbrechnung
* 'abo' sind Einrichtung. Was davon kein Guthaben findet, ist Vorschuss
* (vorschussMinutes), nie Mehraufwand; normale Arbeit bleibt Mehraufwand. Eine spaetere
* Gutschrift gleicht zuerst den Vorschuss aus (Durchgang 2: Einrichtung vor den
* uebrigen Eintraegen). Nach Vertragsende bleibt der Vorschuss stehen und wird nicht
* berechnet (active false setzt nur Guthaben und credits[].remaining auf 0).
*/
function parseSheetDate(ws) {
@@ -68,6 +75,7 @@ export async function listPlanUsage(customerId) {
supportMinutes: total - (Number(ws.totalTime) || 0),
details: String(ws.details || '').slice(0, 300),
employee: ws.employeeName || ws.employeeShort || '',
einrichtung: wo.einrichtungAbrechnung === 'abo',
}
})
.sort((a, b) => a.date.localeCompare(b.date))
@@ -86,20 +94,54 @@ export function computeLedger(credits, usage, { now = new Date(), active = true
.sort((a, b) => String(a.grantedAt).localeCompare(String(b.grantedAt)))
let overageMinutes = 0
const entries = usage.map((u) => {
let need = u.minutes
const at = new Date(u.date)
for (const c of pool) {
if (!need) break
// Nur Gutschriften, die zum Arbeitszeitpunkt noch gueltig waren
if (c.remaining <= 0 || new Date(c.expiresAt) <= at) continue
const take = Math.min(need, c.remaining)
c.remaining -= take
need -= take
let vorschussMinutes = 0
let entries
if (!usage.some((u) => u.einrichtung === true)) {
entries = usage.map((u) => {
let need = u.minutes
const at = new Date(u.date)
for (const c of pool) {
if (!need) break
// Nur Gutschriften, die zum Arbeitszeitpunkt noch gueltig waren
if (c.remaining <= 0 || new Date(c.expiresAt) <= at) continue
const take = Math.min(need, c.remaining)
c.remaining -= take
need -= take
}
overageMinutes += need
return { ...u, coveredMinutes: u.minutes - need, overageMinutes: need, einrichtung: false, vorschussMinutes: 0 }
})
} else {
// Zwei Durchgaenge in Datumsreihenfolge (bei gleichem Datum bleibt die Eingabereihenfolge)
const ordered = usage
.map((u, index) => ({ u, index, at: new Date(u.date), need: u.minutes }))
.sort((a, b) => String(a.u.date).localeCompare(String(b.u.date)) || a.index - b.index)
const decke = (e, nurGebucht) => {
for (const c of pool) {
if (!e.need) break
if (c.remaining <= 0 || new Date(c.expiresAt) <= e.at) continue
if (nurGebucht && new Date(c.grantedAt) > e.at) continue
const take = Math.min(e.need, c.remaining)
c.remaining -= take
e.need -= take
}
}
overageMinutes += need
return { ...u, coveredMinutes: u.minutes - need, overageMinutes: need }
})
// Durchgang 1: nur Gutschriften, die zur Arbeitszeit schon gebucht und noch gueltig waren
for (const e of ordered) decke(e, true)
// Durchgang 2: den Rest rueckwirkend, zuerst alle Einrichtungseintraege, dann die uebrigen
for (const e of ordered.filter((x) => x.u.einrichtung === true)) decke(e, false)
for (const e of ordered.filter((x) => x.u.einrichtung !== true)) decke(e, false)
const byIndex = new Map(ordered.map((e) => [e.index, e]))
entries = usage.map((u, index) => {
const e = byIndex.get(index)
const einrichtung = u.einrichtung === true
const vorschuss = einrichtung ? e.need : 0
const mehr = einrichtung ? 0 : e.need
overageMinutes += mehr
vorschussMinutes += vorschuss
return { ...u, coveredMinutes: u.minutes - e.need, overageMinutes: mehr, einrichtung, vorschussMinutes: vorschuss }
})
}
const valid = pool.filter((c) => c.remaining > 0 && new Date(c.expiresAt) > now)
const expiredMinutes = pool
@@ -115,6 +157,7 @@ export function computeLedger(credits, usage, { now = new Date(), active = true
grantedMinutes: pool.reduce((s, c) => s + c.minutes, 0),
usedMinutes: entries.reduce((s, e) => s + e.coveredMinutes, 0),
overageMinutes,
vorschussMinutes,
expiredMinutes,
nextExpiry: nextExpiry ? { minutes: nextExpiry.remaining, at: nextExpiry.expiresAt } : null,
credits: pool.map((c) => ({
+21
View File
@@ -0,0 +1,21 @@
import { KUNDENARTEN } from './auftraege/konstanten.js'
/**
* Kundenart (V4, Spec E4): Der Server ist die einzige Quelle; er nimmt die Kundenart nie vom Client.
* Gespeichert wird sie am Kundendokument (customers.kundenart). Vergleich exakt: kein Trimmen, kein Kleinschreiben.
* Eine leere oder unbekannte Kundenart gilt ueberall als privat (kundenartWirksam).
*/
export { KUNDENARTEN }
/** 'unternehmen' | 'privat' | '' (leer, unbekannt, kein Dokument) */
export function kundenartGespeichert(customerDoc) {
const wert = customerDoc?.kundenart
return KUNDENARTEN.includes(wert) ? wert : ''
}
/** 'unternehmen' | 'privat'; leer oder unbekannt gilt als 'privat' */
export function kundenartWirksam(customerDoc) {
return kundenartGespeichert(customerDoc) || 'privat'
}
export const NUR_BETRIEB_TEXT = 'Diese Funktion gibt es nur für Betriebe. Schreib uns im Chat, wenn du Fragen dazu hast.'
+55 -23
View File
@@ -4,6 +4,7 @@ import { getDocument } from './appwriteAdmin.js'
import { eventIdFor, getContractEvent, listContractEvents, recordContractEvent, updateContractEvent } from './contractEvents.js'
import { sendAdminNotice, sendCustomerMail } from './mailer.js'
import { contractAttachmentFor, describeItems } from './products.js'
import { getFeatureDoc } from './siteFeatures.js'
import {
addMonths,
ensurePrice,
@@ -335,6 +336,9 @@ const monthlyCentsOf = (facts) => facts.monthlyCents ?? config.products.wartung.
// Ein Wechsel je Abo zur selben Zeit (Doppelklick): der zweite wartet nicht, sondern bekommt 409
const switchesInFlight = new Set()
// Obergrenze fuer details des Ereignisses `wechsel` (Schutz des Nachweises): die gewuenschte Funktion (details.funktion) kommt nur hinein, wenn es passt
const DETAILS_MAX = 2000
/**
* Wechsel ins Wartungs-Abo (Button "Zahlungspflichtig bestellen"). consent: { termsVersion, consentAt,
* consentIp, consentUserAgent, earlyStartConsentAt } - ohne Zustimmung zum Sofortbeginn kein Wechsel (400, F16).
@@ -343,8 +347,11 @@ const switchesInFlight = new Set()
* ab, 409 ohne Aenderung bei Stripe.
* Liefert { subscription, paymentUrl, event }: paymentUrl nur, wenn Stripe die Aenderung bis zur Zahlung
* zurueckhaelt (pending_update, Karte), sonst null - auch bei SEPA, dort zieht Stripe die Lastschrift ein.
* options.funktion ({ key, projectId }, von der Route geprueft; Kundenansicht neu, P6): die Funktion, die der Kunde mit dem Wechsel einschalten
* will, landet in event.details.funktion (GET /api/abo nennt sie als wechselFunktion). Sie ist nur eine Bequemlichkeit: passt sie nicht mehr
* in den Nachweis (details ueber 2000 Zeichen), bleibt sie weg, der Wechsel laeuft in jedem Fall.
*/
export async function switchToWartung(customerId, consent, prorationDate, { expectedAmountCents } = {}) {
export async function switchToWartung(customerId, consent, prorationDate, { expectedAmountCents, funktion = null } = {}) {
if (!consent?.termsVersion || !consent?.consentAt) {
throw httpError(400, 'Bitte stimmen Sie den Vertragsbedingungen zu, um zu wechseln.')
}
@@ -357,13 +364,13 @@ export async function switchToWartung(customerId, consent, prorationDate, { expe
if (switchesInFlight.has(doc.$id)) throw httpError(409, 'Ihr Wechsel wird gerade verarbeitet.')
switchesInFlight.add(doc.$id)
try {
return await performSwitch(customerId, doc, consent, prorationDate, expectedAmountCents)
return await performSwitch(customerId, doc, consent, prorationDate, expectedAmountCents, funktion)
} finally {
switchesInFlight.delete(doc.$id)
}
}
async function performSwitch(customerId, current, consent, prorationDate, expectedAmountCents) {
async function performSwitch(customerId, current, consent, prorationDate, expectedAmountCents, funktion = null) {
const ctx = await switchContext(customerId, current)
const { doc, customer, stripeSub } = ctx
// Seit der Vorschau hat ein neuer Abrechnungsmonat begonnen: Gutschrift und Betrag stimmen nicht mehr
@@ -422,31 +429,34 @@ async function performSwitch(customerId, current, consent, prorationDate, expect
})) || doc
}
const details = {
from: 'hosting',
amountDueCents,
monthlyCents,
prorationDate,
switchedAt,
paymentMethod: ctx.paymentMethod,
invoiceId: idOf(updated.latest_invoice),
// F12/F27: Link zur Stripe-Zahlungsseite, solange die Zahlung offen ist (GET /api/abo switchPending)
paymentUrl,
// Fix 1 (2): Ablauf der zurueckgehaltenen Aenderung, fuer den Abgleich ohne Webhook
pendingExpiresAt: pending && updated.pending_update.expires_at ? new Date(updated.pending_update.expires_at * 1000).toISOString() : null,
effectiveAt: effective ? switchedAt : null,
paidAt: effective && paid ? switchedAt : null,
// F27: Stand vor dem Wechsel, damit eine gescheiterte Lastschrift ihn wiederherstellen kann
previousTermStartAt: termStartOf(stripeSub).toISOString(),
previousMetadata: Object.fromEntries(
RESTORABLE_METADATA.map((key) => [key, stripeSub.metadata?.[key] == null ? null : String(stripeSub.metadata[key]).slice(0, 300)])
),
}
// Gewuenschte Funktion (nur Bequemlichkeit): der Nachweis darf dadurch nie zu gross werden
if (funktion && JSON.stringify({ ...details, funktion }).length <= DETAILS_MAX) details.funktion = funktion
const event = await recordSwitchEvent({
doc,
customerId,
consent,
status: effective && paid ? 'erledigt' : 'offen',
details: {
from: 'hosting',
amountDueCents,
monthlyCents,
prorationDate,
switchedAt,
paymentMethod: ctx.paymentMethod,
invoiceId: idOf(updated.latest_invoice),
// F12/F27: Link zur Stripe-Zahlungsseite, solange die Zahlung offen ist (GET /api/abo switchPending)
paymentUrl,
// Fix 1 (2): Ablauf der zurueckgehaltenen Aenderung, fuer den Abgleich ohne Webhook
pendingExpiresAt: pending && updated.pending_update.expires_at ? new Date(updated.pending_update.expires_at * 1000).toISOString() : null,
effectiveAt: effective ? switchedAt : null,
paidAt: effective && paid ? switchedAt : null,
// F27: Stand vor dem Wechsel, damit eine gescheiterte Lastschrift ihn wiederherstellen kann
previousTermStartAt: termStartOf(stripeSub).toISOString(),
previousMetadata: Object.fromEntries(
RESTORABLE_METADATA.map((key) => [key, stripeSub.metadata?.[key] == null ? null : String(stripeSub.metadata[key]).slice(0, 300)])
),
},
details,
})
const contact = { name: customer?.name || '', email: customer?.email || doc.email || '' }
@@ -461,6 +471,28 @@ async function performSwitch(customerId, current, consent, prorationDate, expect
return { subscription, paymentUrl, event }
}
/** Frist, in der die beim Wechsel gewuenschte Funktion noch angeboten wird (Tage nach der Erledigung des Wechsels). */
const WECHSEL_FUNKTION_TAGE = 14
/**
* Die beim Wechsel gewuenschte Funktion (Kundenansicht neu, P6, V11), die der Kunde jetzt noch einschalten kann: { key, projectId } oder null.
* events: Vertragsereignisse des aktuellen Abos (listContractEvents, neueste zuerst, details geparst). Es zaehlt das juengste Ereignis
* `wechsel`: es ist erledigt, traegt details.funktion, wurde vor hoechstens 14 Tagen erledigt (doneAt, sonst details.paidAt, sonst
* details.effectiveAt) und das Funktionsdokument des Projekts fehlt oder ist off (die Funktion ist noch nicht eingeschaltet). Gescheitert,
* widerrufen und offen ergeben null, ebenso ein juengerer Wechsel ohne Funktion. Liest hoechstens ein Funktionsdokument; ein Lesefehler wird weitergereicht.
*/
export async function wechselFunktionVon(customerId, events, { now = new Date() } = {}) {
const event = (events || []).find((e) => e.type === 'wechsel')
if (!event || event.status !== 'erledigt') return null
const funktion = event.details?.funktion
if (!funktion || typeof funktion.key !== 'string' || typeof funktion.projectId !== 'string' || !funktion.key || !funktion.projectId) return null
const erledigt = Date.parse(event.doneAt || event.details?.paidAt || event.details?.effectiveAt || '')
if (!Number.isFinite(erledigt) || now.getTime() - erledigt > WECHSEL_FUNKTION_TAGE * 24 * 60 * 60 * 1000) return null
const doc = await getFeatureDoc(customerId, funktion.projectId, funktion.key)
if (doc && doc.state !== 'off') return null
return { key: funktion.key, projectId: funktion.projectId }
}
/**
* Ereignis `wechsel` (Nachweis der Bestellung und Stand der Zahlung). Feste ID je Abo und Wechselzeitpunkt
* (eventIdFor, 36 Zeichen). Ein Fehler beim Speichern bricht den schon bei Stripe ausgefuehrten Wechsel nicht
+128 -22
View File
@@ -1,6 +1,7 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { config } from '../config.js'
import { neueAnsicht } from './kundenansicht/schalter.js'
/**
* Abo-Produkte (Hosting-Abo, Wartungs-Abo) und E-Mail-Zusatzspeicher.
@@ -28,43 +29,86 @@ export function getProduct(key) {
/** Die archivierte Fassung 3 (public/vertragsunterlagen/v3): gilt fuer jede Zustimmung ohne Fassung "-v4"/"-v5". */
export const TERMS_VERSION_V3 = '2026-09-25-v3'
/**
* Fassungstabelle der Abo-Bedingungen (V12). Reihenfolge = Suche: der erste Eintrag, dessen muster auf die
* gespeicherte Fassung passt (nur als Endung, nie "-v60" oder "v6-..."), sonst der Eintrag sonst (Fassung 3).
* Fassung 6 liegt bis zum Umschalten (P10) im Vorschaupfad /neu/rechtliches/; Fassung 5 bleibt an den heutigen
* Adressen, archiv nennt die Adressen der Kopie unter public/vertragsunterlagen/v5/ (erst P10 schaltet sie ein).
* endParagraph: der Paragraph zum Vertragsende (Daten, AuthCode, Loeschung) fuer die Mails K02/K03/T05/T07 (F08).
*/
export const FASSUNGEN = Object.freeze([
Object.freeze({
muster: /-v6$/,
nummer: 6,
url: '/neu/rechtliches/abo-bedingungen.html',
pdfFile: 'neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 11',
}),
Object.freeze({
muster: /-v5$/,
nummer: 5,
url: '/abo-bedingungen.html',
pdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 11',
archiv: Object.freeze({
url: '/vertragsunterlagen/v5/abo-bedingungen.html',
pdfFile: 'vertragsunterlagen/v5/WEBklar-Abo-Vertragsunterlagen.pdf',
}),
}),
Object.freeze({
muster: /-v4$/,
nummer: 4,
url: '/vertragsunterlagen/v4/abo-bedingungen.html',
pdfFile: 'vertragsunterlagen/v4/WEBklar-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 11',
}),
Object.freeze({
sonst: true,
nummer: 3,
url: '/vertragsunterlagen/v3/abo-bedingungen.html',
pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 9',
}),
])
/** Erster Tabelleneintrag mit passendem muster; ohne Treffer (auch bei Nicht-Text) der Eintrag sonst. */
function fassungFuer(termsVersion) {
const treffer = typeof termsVersion === 'string' ? FASSUNGEN.find((f) => f.muster && f.muster.test(termsVersion)) : null
return treffer || FASSUNGEN.find((f) => f.sonst)
}
/**
* Abo-Fassung der Sitzung: Fassung 6 nur bei neuer Ansicht (Pilot-Schalter), sonst die bisherige Fassung 5.
* req wie bei neueAnsicht(req) (req.session oder signiertes Cookie).
*/
export function aktuelleAboFassung(req) {
return neueAnsicht(req) ? config.abo.termsVersionNeu : config.abo.termsVersion
}
/** Aktuelle Fassung 5 (Unterstuetzung in § 4 Abs. 4)? */
export function isCurrentTerms(termsVersion) {
return typeof termsVersion === 'string' && termsVersion.endsWith('-v5')
}
/** Fassung 4 oder neuer (Hosting- und Wartungs-Abo, Zusatzspeicher, Wechsel)? Aeltere und fehlende Fassungen: nein. */
/** Fassung 4 oder neuer (Hosting- und Wartungs-Abo, Zusatzspeicher, Wechsel; v4, v5, v6)? Aeltere und fehlende Fassungen: nein. */
export function isTermsV4OrNewer(termsVersion) {
return typeof termsVersion === 'string' && /-v[45]$/.test(termsVersion)
return typeof termsVersion === 'string' && FASSUNGEN.some((f) => f.muster && f.muster.test(termsVersion))
}
/**
* Vertragsunterlagen zur zugestimmten Fassung: Fassung 5 (Suffix "-v5") zeigt die aktuellen Bedingungen samt PDF,
* Fassung 4 das Archiv unter public/vertragsunterlagen/v4, jede aeltere Version (auch unbekannt/keine) das Archiv
* der Version 3.
* Vertragsunterlagen zur zugestimmten Fassung (Tabelle FASSUNGEN): Fassung 6 (Suffix "-v6") zeigt die neuen Bedingungen
* im Vorschaupfad, Fassung 5 die bisherigen Bedingungen samt PDF, Fassung 4 das Archiv unter public/vertragsunterlagen/v4,
* jede aeltere Version (auch unbekannt/keine) das Archiv der Version 3.
* version: die Fassung, die eine Mail nennt - die gespeicherte, ohne gespeicherte die archivierte Fassung 3,
* damit Versionsangabe und PDF-Anhang immer zusammenpassen (Task 12 Fix 1).
* endParagraph: der Paragraph zum Vertragsende (Daten, AuthCode, Loeschung) in dieser Fassung, fuer die
* Mails K02/K03/T05/T07 (F08) - ab Fassung 4 ist § 9 der Wechsel zwischen den Abos, § 11 das Vertragsende.
* Die Rueckgabe hat immer nur diese vier Felder (kein archiv, muster, nummer).
*/
export function termsDocFor(termsVersion) {
if (isCurrentTerms(termsVersion)) {
return { version: termsVersion, url: '/abo-bedingungen.html', pdfFile: 'WEBklar-Abo-Vertragsunterlagen.pdf', endParagraph: '§ 11' }
}
if (isTermsV4OrNewer(termsVersion)) {
return {
version: termsVersion,
url: '/vertragsunterlagen/v4/abo-bedingungen.html',
pdfFile: 'vertragsunterlagen/v4/WEBklar-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 11',
}
}
return {
version: typeof termsVersion === 'string' && termsVersion ? termsVersion : TERMS_VERSION_V3,
url: '/vertragsunterlagen/v3/abo-bedingungen.html',
pdfFile: 'WEBklar-Wartungs-Abo-Vertragsunterlagen.pdf',
endParagraph: '§ 9',
}
const f = fassungFuer(termsVersion)
const version = typeof termsVersion === 'string' && termsVersion ? termsVersion : TERMS_VERSION_V3
return { version, url: f.url, pdfFile: f.pdfFile, endParagraph: f.endParagraph }
}
/** Katalogname ohne "WEBklar " (z. B. "Hosting-Abo") fuer {{produkt}} in den Mails (F33); ohne Schluessel "Abo". */
@@ -170,3 +214,65 @@ export function creditMinutesForInvoice(invoice, priceKeys) {
}
return 0
}
/* ---------------------------------------------------------------------------------------------
* Fassung 6 (Entwurf bis zur Freigabe, E20): Wortlaute fuer die Bestellseiten und Einschalt-Seiten
* sowie die Unterlagen-Tabelle der Funktionen. Dieses Paket liefert die Konstanten; verdrahtet werden
* sie erst mit P5/P7 (Nachtrag N33). Texte in "du"-Form, ohne Preise.
* ------------------------------------------------------------------------------------------- */
/** Fassung der Funktionsunterlagen (Leistungsbeschreibungen, AVV). Entwurfstag; bei der Freigabe das Datum setzen. */
export const FUNKTIONEN_FASSUNG_V6 = '2026-10-16'
/**
* Wortlaute der Oberflaeche. linkzeile: genau ein Platzhalter {fassung} (mitFassung fuellt ihn);
* bestellen gilt fuer Abo, Wechsel und Zusatzspeicher. beginn: das Haekchen nur fuer Privatkunden in der Frist.
* hinweis: die Pflichten der alten Haekchen verantwortung, stripe, gesundheit als ein Satz auf der Einschalt-Seite.
*/
export const WORTLAUT_V6 = Object.freeze({
linkzeile: Object.freeze({
bestellen: 'Es gelten die Vertragsbedingungen mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung {fassung}). Alles kommt danach auch als PDF per Mail.',
einschaltenMitAvv: 'Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung {fassung}).',
einschaltenOhneAvv: 'Es gilt die Leistungsbeschreibung (Fassung {fassung}).',
}),
beginn: Object.freeze({
abo: 'Bitte beginnt sofort mit dem Abo. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.',
wechsel: 'Bitte beginnt sofort mit dem Wechsel. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.',
einrichtung: 'Bitte beginnt sofort mit der Einrichtung. Mir ist klar: Mit der vollständigen Einrichtung erlischt mein Widerrufsrecht, und wenn ich vorher widerrufe, zahle ich den bis dahin erbrachten Teil.',
}),
hinweis: Object.freeze({
shop: 'Du verkaufst in eigenem Namen und bist für deine Produkte, Preise und Rechtstexte verantwortlich. Die Zahlungen deiner Käufer laufen über dein eigenes Stripe-Konto.',
unterkunft: 'Du vermietest in eigenem Namen und bist für Preise, Stornobedingungen, Rechnungen und Meldepflichten verantwortlich. Online-Zahlungen deiner Gäste laufen über dein eigenes Stripe-Konto.',
termine: 'Der Terminplaner ist nicht für den Gesundheitsbereich gedacht. Das fragen wir im Fragebogen ab.',
}),
})
/** Ersetzt {fassung} im Wortlaut; wirft bei leerer Fassung oder wenn der Text keinen Platzhalter enthaelt. */
export function mitFassung(text, fassung) {
if (typeof fassung !== 'string' || fassung.trim() === '') throw new Error('Fassung fehlt oder ist leer')
if (typeof text !== 'string' || !text.includes('{fassung}')) throw new Error('Platzhalter {fassung} fehlt im Text')
return text.split('{fassung}').join(fassung)
}
/** Ein Dokument der Funktionsunterlagen im Vorschaupfad: Seite, PDF und Dateiname des Mail-Anhangs. */
function dok(slug, anhang) {
return Object.freeze({
version: FUNKTIONEN_FASSUNG_V6,
url: `/neu/rechtliches/${slug}-${FUNKTIONEN_FASSUNG_V6}.html`,
pdfFile: `neu/rechtliches/${slug}-${FUNKTIONEN_FASSUNG_V6}.pdf`,
anhangName: `${anhang}-${FUNKTIONEN_FASSUNG_V6}.pdf`,
})
}
/**
* Unterlagen je Funktion in Fassung 6: { [key]: { lb, avv } }. avv ist null, wo keine personenbezogenen Daten im
* Auftrag verarbeitet werden (News, Oeffnungszeiten live). Reihenfolge: news, termine, shop, unterkunft, zeiten, analyse.
*/
export const FUNKTIONEN_UNTERLAGEN_V6 = Object.freeze({
news: Object.freeze({ lb: dok('leistungsbeschreibung-news', 'Leistungsbeschreibung-News-Pinnwand'), avv: null }),
termine: Object.freeze({ lb: dok('leistungsbeschreibung-terminplaner', 'Leistungsbeschreibung-Terminplaner'), avv: dok('avv-terminplaner', 'AVV-Terminplaner') }),
shop: Object.freeze({ lb: dok('leistungsbeschreibung-webshop', 'Leistungsbeschreibung-Webshop'), avv: dok('avv-webshop', 'AVV-Webshop') }),
unterkunft: Object.freeze({ lb: dok('leistungsbeschreibung-unterkunft', 'Leistungsbeschreibung-Buchungskalender'), avv: dok('avv-unterkunft', 'AVV-Buchungskalender') }),
zeiten: Object.freeze({ lb: dok('leistungsbeschreibung-zeiten', 'Leistungsbeschreibung-Oeffnungszeiten-live'), avv: null }),
analyse: Object.freeze({ lb: dok('leistungsbeschreibung-website-analyse', 'Leistungsbeschreibung-Website-Analyse'), avv: dok('avv-website-analyse', 'AVV-Website-Analyse') }),
})
+23 -6
View File
@@ -3,7 +3,10 @@ import { createDocument, getDocument, updateDocument } from '../appwriteAdmin.js
import { withLocks } from '../keyedLock.js'
import { sendAdminNotice, sendCustomerMail } from '../mailer.js'
import { assertOwnProject, customerOf, ensureFeatureDoc, getFeatureDoc } from '../siteFeatures.js'
import { aboModellFuerKunde } from '../aboFunktionen.js'
import { anfrageEntschiedenMail } from '../aboFunktionenMails.js'
import { shopFuerKunde } from './schalter.js'
import { NUR_BETRIEB_TEXT } from '../kundenart.js'
/**
* Webshop-Anfrage (Spezifikation 2026-10-05-webshop-design.md, Abschnitt 4 "Auf Anfrage"). Der Kunde stellt eine Anfrage, das Team
@@ -163,15 +166,21 @@ async function pruefeZugang({ customerId, projectId, person, neu }) {
}
/**
* Anfrage stellen. Reihenfolge: Projekt (403/409 archiviert), Schalter (409), Eingabe (400 mit `fehler`), dann unter Sperre
* je Projekt: Funktion muss `off` sein (409), Anfrage-Status '' | abgelehnt | zurueckgezogen (409). Speichert JSON, Status `offen`
* und anfrageAt, loescht eine fruehere Entscheidung und schickt T21 ans Team (ein Fehler dabei wird nur geloggt).
* Anfrage stellen. Reihenfolge: Projekt (403/409 archiviert), Schalter (409), Kundenart (409 nur_betrieb, nur im Abo-Modell), Eingabe
* (400 mit `fehler`), dann unter Sperre je Projekt: Funktion muss `off` sein (409), Anfrage-Status '' | abgelehnt | zurueckgezogen (409).
* Speichert JSON, Status `offen` und anfrageAt, loescht eine fruehere Entscheidung und schickt T21 ans Team (ein Fehler dabei wird nur geloggt).
* kundenart (nur mit neuer Ansicht, kundenartWirksam des Kundendokuments; ohne Angabe wie bisher): nur Betriebe ('unternehmen') duerfen
* anfragen, jeder andere Wert ist 409 nur_betrieb. Fuer Betriebe setzt der Server `unternehmer` auf true (ein Wert des Clients zaehlt
* nicht); die gespeicherte Anfrage traegt zusaetzlich kundenart.
* @returns {Promise<object>} anfrageAnsicht des neuen Standes
*/
export async function stelleAnfrage({ customerId, projectId, eingabe, person = {}, now = new Date() }) {
export async function stelleAnfrage({ customerId, projectId, eingabe, person = {}, now = new Date(), kundenart }) {
const project = await pruefeZugang({ customerId, projectId, person, neu: true })
const { anfrage, fehler } = normalisiereAnfrage(eingabe)
const abo = kundenart !== undefined
if (abo && kundenart !== 'unternehmen') throw stop(409, NUR_BETRIEB_TEXT, 'nur_betrieb')
const { anfrage: geprueft, fehler } = normalisiereAnfrage(abo && istObjekt(eingabe) ? { ...eingabe, unternehmer: true } : eingabe)
if (fehler) throw Object.assign(stop(400, Object.values(fehler)[0]), { fehler })
const anfrage = abo ? { ...geprueft, kundenart } : geprueft
const gespeichert = await withLocks([`shop-anfrage:${projectId}`], async () => {
const feature = await ensureFeatureDoc(customerId, projectId, 'shop')
@@ -279,7 +288,11 @@ export async function entscheideAnfrage({ customerId, projectId, entscheidung, b
return { anfrage: anfrageAnsicht(gespeichert), mailFehler }
}
/** K24 an den Kunden; true = gesendet. Jeder Fehler wird nur geloggt (die Entscheidung ist gespeichert). */
/**
* K24 an den Kunden; true = gesendet. Jeder Fehler wird nur geloggt (die Entscheidung ist gespeichert). Fuer Kunden der neuen Ansicht
* (aboModellFuerKunde, Kundenansicht P6) geht stattdessen K42 ohne Preis und ohne "bestellen"; ist das nicht lesbar, geht keine Mail raus
* (lieber keine als eine mit Preis, die Antwort traegt mailFehler).
*/
async function sendeEntscheidung({ customerId, projectId, project, angenommen, begruendung }) {
try {
const kunde = await customerOf(customerId)
@@ -287,6 +300,10 @@ async function sendeEntscheidung({ customerId, projectId, project, angenommen, b
console.warn(`[shop] K24 nicht gesendet: Kunde ${customerId} ohne E-Mail-Adresse`)
return false
}
if (await aboModellFuerKunde(customerId)) {
await sendCustomerMail(kunde.email, anfrageEntschiedenMail({ kunde, projekt: project?.projectName || projectId, key: 'shop', projectId, angenommen, begruendung }))
return true
}
await sendCustomerMail(kunde.email, {
template: 'K24',
values: {
+2 -1
View File
@@ -54,7 +54,8 @@ export async function shopTicketDetails(ctx) {
`5. Vorschau prüfen: https://project.webklar.com/shop/${siteKey}`,
'6. Ticket schließen: das schaltet die Funktion frei.',
'',
'Zeit im Arbeitsblatt erfassen, nicht auf Abo-Stunden anrechnen.',
// Abo-Modell (einrichtungAbrechnung abo oder projekt): die Abrechnung steht allein in der Schlusszeile des Tickets (createFeatureTicket)
...(doc?.einrichtungAbrechnung === 'abo' || doc?.einrichtungAbrechnung === 'projekt' ? [] : ['Zeit im Arbeitsblatt erfassen, nicht auf Abo-Stunden anrechnen.']),
`Verweis: ${anleitungsLink(doc?.customerId, doc?.projectId)}`,
]
return kuerze(zeilen.join('\n').toWellFormed(), TICKET_DETAILS_MAX, { anZeile: true })
+90 -21
View File
@@ -9,7 +9,9 @@ import { speichereZustimmung, zustimmungFuerSitzung } from './featureConsents.js
import { aboLage, laeuft } from './aboStatus.js'
import { berlinDateTime } from './berlinTime.js'
import { loeschTag } from './termine/frist.js'
import { createFeatureTicket } from './ticketAdmin.js'
import { ABRECHNUNGEN, istAboModell } from './aboModell.js'
import { ohneAboBisher } from './aboAufbauphase.js'
import { ABRECHNUNGS_ZEILEN, createFeatureTicket } from './ticketAdmin.js'
export const FEATURE_KEYS = featureKeys()
/** Bezeichnung (name) und Verneinung im Satz "Für dieses Projekt gibt es noch {keine}" (keine) je Funktion; aus dem Register. */
@@ -144,8 +146,8 @@ export const unlockedByVon = (doc) => doc?.unlockedBy || (doc?.unlockedByPurchas
* bei News verspraeche das Datum eine Loeschung, die nie kommt) und fuer Oeffnungszeiten live, dort nur beim Abschaltgrund
* abo_ende (bei einer Abschaltung durch das Team gibt es keine Loeschfrist); es ist null, wenn disabledAt fehlt oder nicht lesbar ist.
* disabledReason und disabledAt gelten fuer jede Funktion.
* Funktionen mit aboInklusive (Oeffnungszeiten live) tragen zusaetzlich unlockedBy (kauf | abo | team, null solange die Funktion
* aus ist), aboEndeAt (Ende des Wartungs-Abos erkannt) und einbau (angestossen | manuell, Ergebnis des Neubaus).
* Jede Funktion traegt unlockedBy (kauf | abo | projekt | team, null solange die Funktion aus ist). Funktionen mit aboInklusive
* (Oeffnungszeiten live) tragen zusaetzlich aboEndeAt (Ende des Wartungs-Abos erkannt) und einbau (angestossen | manuell, Ergebnis des Neubaus).
* key: Funktionsschluessel (auch ohne Dokument); ohne Angabe der des Dokuments.
*/
export async function featureView(doc, key = doc?.featureKey) {
@@ -162,9 +164,10 @@ export async function featureView(doc, key = doc?.featureKey) {
disabledReason: abgeschaltet ? doc.disabledReason || null : null,
disabledAt: abgeschaltet ? doc.disabledAt || null : null,
loeschungAm: loeschtag ? loeschTag(doc.disabledAt) : null,
// unlockedBy (kauf | abo | projekt | team, null solange die Funktion aus ist) gilt fuer jede Funktion (V7); aboEndeAt und einbau nur mit aboInklusive
unlockedBy: doc && doc.state !== 'off' ? unlockedByVon(doc) : null,
...(mitAbo
? {
unlockedBy: doc && doc.state !== 'off' ? unlockedByVon(doc) : null,
aboEndeAt: doc?.aboEndeAt || null,
einbau: doc?.einbau || null,
}
@@ -454,6 +457,8 @@ async function legeTicketAn(doc, project, customer) {
featureKey: doc.featureKey,
siteKey: doc.siteKey,
...texte,
// Abo-Modell: die Abrechnung am Funktionsdokument (abo | projekt) kommt auch bei einem ersetzten Ticket wieder ans Ticket
...(Object.hasOwn(ABRECHNUNGS_ZEILEN, doc.einrichtungAbrechnung) ? { abrechnung: doc.einrichtungAbrechnung } : {}),
})
return ticketId
}
@@ -647,6 +652,23 @@ function textformNachweisTexte(textform, fassung, key) {
return [satz, ...items.map(({ id, text }) => ({ id, text }))]
}
/** Zustimmung des Kunden in Textform als Nachweis speichern (ohne Stripe-Sitzung, IP und Browser; Konto = Mitarbeiter; Fassung = textformFassung(key)). */
async function speichereTextformNachweis({ customerId, projectId, key, textform, by, now }) {
const fassung = textformFassung(key)
await speichereZustimmung({
customerId,
projectId,
featureKey: key,
stripeSessionId: '',
fassungen: { leistungsbeschreibung: fassung, avv: fassung },
texte: textformNachweisTexte(textform, fassung, key),
ip: '',
userAgent: '',
konto: `Mitarbeiter: ${by || 'Mitarbeiter'}`,
at: now,
})
}
/**
* Freischaltung ohne Zahlung und ohne Ticket (unlockedByPurchase bleibt false). Ausnahme: ein Terminplaner, den ein Kauf
* freigeschaltet hat (unlockedByPurchase true), wird nur bei laufendem Wartungs-Abo aktiviert (409 sonst, Lesefehler => Fehler).
@@ -654,28 +676,51 @@ function textformNachweisTexte(textform, fassung, key) {
* textform (nur Terminplaner, Webshop und Buchungskalender, schon geprueft: { date, channel }): Zustimmung des Kunden in Textform. Der Nachweis
* (featureConsents, ohne Stripe-Sitzung, IP und Browser; Konto = Mitarbeiter; Fassung = textformFassung(key)) wird VOR der Freischaltung
* gespeichert: scheitert er, bleibt die Funktion aus. Der Aufrufer sendet danach K16 mit den Unterlagen (meldeFreigeschaltet).
*
* abrechnung (Kundenansicht neu, P6, V10): null = das heutige Verhalten, direkt active, kein Ticket. 'abo' | 'projekt' (nur fuer Kunden des
* Abo-Modells, das entscheidet der Aufrufer): das Team schaltet im Abo-Modell ein. Ein Dokument aus Kauf (Bestand) bleibt wie bei null.
* Sonst muss ein Abo die Funktion decken (aboDeckt): 409 abo_fehlt mit grund und wechsel, bevor etwas geschrieben wird; auch fuer 'projekt'
* (E21), mit einer Ausnahme (E9, Aufbauphase): 'projekt' bei einem Kunden, der noch nie ein Abo hatte (ohneAboBisher), laeuft durch, und
* rueckmeldung.warnung = { code: 'abo_fehlt', grund: 'kein_abo' } sagt es dem Aufrufer (rueckmeldung ist ein Objekt des Aufrufers). Ein vom
* Team abgeschaltetes Dokument (disabledReason team) wird wie heute direkt active; sonst legt einschalten (funktionEinschalten.js) Zustand
* setup und das Einrichtungs-Ticket an (unlockedBy und einrichtungAbrechnung setzt allein einschalten). Der Textform-Nachweis laeuft dann
* als nachweis vor dem Zustand; K38 (Meldung) ersetzt die K16 des Aufrufers.
*/
export async function adminActivate(customerId, projectId, key, { by, adminNote, textform = null, now = new Date() } = {}) {
export async function adminActivate(customerId, projectId, key, { by, adminNote, textform = null, abrechnung = null, rueckmeldung = null, now = new Date() } = {}) {
assertKey(key)
if (textform && !TEXTFORM_KEYS.includes(key)) throw httpError(400, 'Zustimmung in Textform gibt es nur für den Terminplaner, den Webshop und den Buchungskalender')
if (abrechnung !== null && !ABRECHNUNGEN.includes(abrechnung)) throw httpError(400, 'Unbekannte Abrechnung')
await assertOwnProject(customerId, projectId)
let direkt = abrechnung === null
if (!direkt) {
// Das Dokument wird nur gelesen: ensureFeatureDoc laeuft im Abo-Modell erst innerhalb von einschalten, damit eine Ablehnung kein leeres Dokument hinterlaesst
const vorab = await getFeatureDoc(customerId, projectId, key)
if (vorab && unlockedByVon(vorab) === 'kauf') {
direkt = true // Bestand: wie ohne Abrechnung, einschliesslich der Abo-Pruefung des Terminplaners unten
} else {
// Dynamischer Import: funktionEinschalten.js importiert dieses Modul
const { einschalten, pruefeAboDeckung } = await import('./funktionEinschalten.js')
try {
await pruefeAboDeckung(customerId, key, now)
} catch (err) {
const aufbauphase = abrechnung === 'projekt' && err?.code === 'abo_fehlt' && err.body?.grund === 'kein_abo' && (await ohneAboBisher(customerId))
if (!aufbauphase) throw err
if (rueckmeldung && typeof rueckmeldung === 'object') rueckmeldung.warnung = { code: 'abo_fehlt', grund: 'kein_abo' }
}
if (vorab?.state === 'disabled' && vorab.disabledReason === 'team') {
direkt = true // vom Team abgeschaltet: das Team schaltet wie heute direkt wieder ein
} else {
const nachweis = textform ? () => speichereTextformNachweis({ customerId, projectId, key, textform, by, now }) : null
const { doc } = await einschalten({ customerId, projectId, key, by: 'team', abrechnung, staff: { name: by, adminNote }, nachweis, now })
return doc
}
}
}
const doc = await ensureFeatureDoc(customerId, projectId, key)
if (key === 'termine' && doc.unlockedByPurchase === true) await assertAboLaeuftFuerAktivierung(customerId)
if (textform) {
const fassung = textformFassung(key)
await speichereZustimmung({
customerId,
projectId,
featureKey: key,
stripeSessionId: '',
fassungen: { leistungsbeschreibung: fassung, avv: fassung },
texte: textformNachweisTexte(textform, fassung, key),
ip: '',
userAgent: '',
konto: `Mitarbeiter: ${by || 'Mitarbeiter'}`,
at: now,
})
}
if (textform) await speichereTextformNachweis({ customerId, projectId, key, textform, by, now })
return activate(doc.$id, { by, adminNote, ...(key === 'zeiten' ? { extra: ZEITEN_AKTIVIERUNG_FELDER } : {}) })
}
@@ -945,6 +990,26 @@ async function ersetzeGeloeschtesTicket(doc, note) {
})
}
/**
* Ticket nachlegen (a) fuer ein Dokument des Abo-Modells (istAboModell, setup ohne ticketId; Oeffnungszeiten live hat ticketNachHolen):
* es gibt keine Zahlung bei Stripe, die Heilung von einschalten (funktionEinschalten.js, by team) legt nur das Ticket an, mit der am
* Dokument gespeicherten Abrechnung. Kein Stripe-Aufruf. Die Notiz des Teams wird danach am Dokument vermerkt.
*/
async function ticketNachAbo(doc, note) {
// Dynamischer Import: funktionEinschalten.js importiert dieses Modul
const { einschalten } = await import('./funktionEinschalten.js')
const { doc: geheilt } = await einschalten({
customerId: doc.customerId,
projectId: doc.projectId,
key: doc.featureKey,
by: 'team',
abrechnung: doc.einrichtungAbrechnung,
staff: { name: 'Mitarbeiter' },
})
const updated = note ? await updateDocument(config.collections.portalFeatures, geheilt.$id, { adminNote: note }) : geheilt
return { doc: updated, created: true }
}
/**
* "Ticket nachlegen": nur fuer eine Funktion in Einrichtung ('setup'; sonst 409, ohne Dokument 409).
* (a) ticketId leer: Zahlung bei Stripe pruefen (nicht bezahlt 409, Stripe nicht erreichbar 503) und den Weg des Webhooks gehen,
@@ -963,7 +1028,11 @@ export async function ticketNachlegen({ customerId, projectId, key, by, adminNot
return withLocks([`feature-ticket:${vorab.$id}`], async () => {
const doc = await frischesDokument(vorab, key)
if (doc.state !== 'setup') throw httpError(409, TICKET_NUR_SETUP)
if (!doc.ticketId) return key === 'zeiten' && unlockedByVon(doc) !== 'kauf' ? ticketNachHolen(doc, note) : ticketNachZahlung(doc, note)
if (!doc.ticketId) {
if (key === 'zeiten' && unlockedByVon(doc) !== 'kauf') return ticketNachHolen(doc, note)
if (key !== 'zeiten' && istAboModell(doc)) return ticketNachAbo(doc, note)
return ticketNachZahlung(doc, note)
}
if (await ticketBesteht(doc.ticketId)) return { doc, created: false }
return ersetzeGeloeschtesTicket(doc, note)
})
+7 -2
View File
@@ -1,5 +1,6 @@
import { config } from '../../config.js'
import { aboLage, aboMemo } from '../aboStatus.js'
import { istAboModell } from '../aboModell.js'
import { Query, getDocument, listAllDocuments, updateDocument } from '../appwriteAdmin.js'
import { withLocks } from '../keyedLock.js'
import { sendCustomerMail } from '../mailer.js'
@@ -33,6 +34,10 @@ import { datumKurz } from './zeit.js'
* das Dokument `off` und wird nie automatisch aktiv. Nicht automatisch: 'abo_ende_vor_einrichtung' (die Erstattung laeuft,
* das Team entscheidet per WOMS), 'team', 'kunde_geloescht' und Altbestand ohne Grund.
*
* Abo-Modell der Kundenansicht neu (P6): Dokumente mit Marker (istAboModell: unlockedBy UND einrichtungAbrechnung aus abo oder projekt) gehoeren
* dem Abgleich der Funktionen des Abo-Modells (aboFunktionenAbgleich.js). Dieser Abgleich und der Sofortpfad ueberspringen sie in jedem Zustand;
* sie zaehlen weder in `geprueft` noch in `ausgenommen` und kosten keine Abo-Abfrage.
*
* Sofortpfad (E9): endeTermineFuerKunde(customerId) beendet die Terminplaner eines Kunden sofort, wenn dessen Wechsel ins Wartungs-
* Abo in der Frist widerrufen wird (routes/abo.js, withdrawOrder). Dieselben Aktionen wie Schritt 1 und 1b, aber OHNE Abo-Pruefung
* (der Spiegel steht noch auf Wartung, das Team setzt ihn von Hand zurueck); die Meldung (meldeAboEnde) ist dieselbe wie im
@@ -274,7 +279,7 @@ export async function endeTermineFuerKunde(customerId, { now = new Date() } = {}
if (!customerId) return { beendet }
try {
const kandidaten = (await listCustomerFeatureDocs(customerId)).filter(
(doc) => doc.featureKey === 'termine' && doc.projectId && ['abschalten', 'einrichtung'].includes(aktionFuer(doc))
(doc) => doc.featureKey === 'termine' && doc.projectId && !istAboModell(doc) && ['abschalten', 'einrichtung'].includes(aktionFuer(doc))
)
for (const doc of kandidaten) {
try {
@@ -407,7 +412,7 @@ export async function gleicheAboAb(now) {
return erg
}
const nachKunde = new Map()
for (const doc of dokumente) {
for (const doc of dokumente.filter((d) => !istAboModell(d))) {
if (!doc.customerId || !doc.projectId) {
// ohne Kunde gaelte "kein Abo": nie abschalten, was sich nicht zuordnen laesst
erg.fehler += 1
+27 -3
View File
@@ -488,6 +488,19 @@ const DATENSCHUTZ_REGEL =
'liegt die Datenschutzerklärung in der von WEBklar betreuten Website, fügen wir den Absatz beim Einbau ein, das gehört zur Einrichtung und wird nicht auf das Stundenkontingent angerechnet; '
+ 'sonst den Absatz als Textbaustein per Chat an den Kunden senden, der ihn selbst einfügt'
/**
* Abrechnung am Dokument (einrichtungAbrechnung, Abo-Modell der Kundenansicht neu): 'abo' (aufs Abo-Stundenkonto) oder 'projekt' (im Projekt
* enthalten); alles andere (Kauf-Bestand, Pilot, Kulanz) ist '' und behaelt die bisherigen Texte Zeichen fuer Zeichen.
*/
const abrechnungVon = (feature) => (feature?.einrichtungAbrechnung === 'abo' || feature?.einrichtungAbrechnung === 'projekt' ? feature.einrichtungAbrechnung : '')
/** DATENSCHUTZ_REGEL je Abrechnung: ohne Marker die Konstante wörtlich, im Abo-Modell ohne "nicht angerechnet". */
function datenschutzRegel(feature) {
const abrechnung = abrechnungVon(feature)
if (!abrechnung) return DATENSCHUTZ_REGEL
return DATENSCHUTZ_REGEL.replace('wird nicht auf das Stundenkontingent angerechnet', abrechnung === 'abo' ? 'zählt mit zum Stundenkonto' : 'ist im Projekt enthalten')
}
function abschnittCheckliste(d) {
const { e } = d
const widerruf = zeigtWiderruf(e)
@@ -501,7 +514,7 @@ function abschnittCheckliste(d) {
'Einbau-Code eingefügt (Abschnitt „Einbau-Code (Snippet)“). Das Widget bleibt unsichtbar bis zur Freischaltung: Bis dahin steht nur der Ausweichtext auf der Seite.',
...(knopf ? ['Modus knopf: --wkt-akzent immer setzen (Abschnitt „Aussehen (CSS)“). Der Knopf kennt die Betriebsfarbe erst nach dem Klick und nimmt sonst #1d4ed8.'] : []),
'Ausweichtext geprüft: JavaScript aus oder falscher siteKey: Der Text steht, nichts ist kaputt.',
`Datenschutz-Absatz „Online-Terminbuchung“ steht in der Datenschutzerklärung der Website (Fassung ${e.anrede === 'du' ? 'Du' : 'Sie'}; ${DATENSCHUTZ_REGEL}).`,
`Datenschutz-Absatz „Online-Terminbuchung“ steht in der Datenschutzerklärung der Website (Fassung ${e.anrede === 'du' ? 'Du' : 'Sie'}; ${datenschutzRegel(d.feature)}).`,
`URLs von Impressum (${impressum}) und Datenschutzerklärung (${datenschutz}) stimmen und beginnen mit https://, sonst fehlen die Links im Fuß des Widgets.`,
`„Freischalten (Abnahme)“ im Ticketsystem ausgelöst${wo}. Erst danach antwortet die öffentliche Schnittstelle (nur im Zustand active), und das Widget erscheint.`,
// am sichtbaren Widget
@@ -531,6 +544,16 @@ function einrichtungsPreis() {
}
function abschnittAbrechnung(d) {
const abrechnung = abrechnungVon(d.feature)
if (abrechnung) {
// Abo-Modell: Einrichtung ohne Zahlung, die Zeit gehoert aufs Abo-Stundenkonto bzw. ins Projekt
return neuerAbschnitt('abrechnung', 'Abrechnung', [
abrechnung === 'abo'
? 'Abrechnung: Einrichtung aus dem Abo, Zeit im Arbeitsblatt aufs Abo-Stundenkonto anrechnen (Vorschuss möglich).'
: 'Abrechnung: im Projekt enthalten, Zeit im Arbeitsblatt, nicht aufs Abo anrechnen.',
'Leistungsumfang: ein Widget an einer Stelle, Gestaltung nach Fragebogen, Einbau; Mehrwünsche (zweites Widget, Knöpfe je Leistung, Sonderdesign) nach Stunden, vorher im Chat abstimmen.',
].join('\n'))
}
if (d.feature?.unlockedByPurchase !== true) {
return neuerAbschnitt('abrechnung', 'Abrechnung', 'Abrechnung: ohne Zahlung freigeschaltet (Pilot/Kulanz), keine Abrechnung.')
}
@@ -663,14 +686,15 @@ function kurzUmfang(k) {
function kurzSchritte(k) {
const form = !k.hatEinstellung ? 'Sie- oder Du-Form nach der Anrede der Gäste' : k.anrede === 'du' ? 'Du-Form' : 'Sie-Form'
const bezahlt = k.feature?.unlockedByPurchase === true ? ` (Einrichtung bezahlt, ${einrichtungsPreis()})` : ''
const abrechnung = abrechnungVon(k.feature)
return [
'Schritte:',
'1. WOMS-Panel öffnen (Kunde → Reiter Funktionen → Anleitung des Projekts): dort stehen alle Einstellungen, die Kopierknöpfe und die Abnahme-Checkliste.',
'2. Einbau-Code einfügen: den Code oben an der Stelle einsetzen, die der Kunde nennt (siehe Kundenwunsch), das Skript einmal pro Seite.',
'3. Gestaltung und Kontrast: das CSS aus der Anleitung (Abschnitt „Aussehen (CSS)“) in die Website übernehmen und den Kontrast prüfen.',
`4. Datenschutz-Absatz (${form}) aus der Anleitung in die Datenschutzerklärung der Website aufnehmen, bevor freigeschaltet wird (${DATENSCHUTZ_REGEL}).`,
`4. Datenschutz-Absatz (${form}) aus der Anleitung in die Datenschutzerklärung der Website aufnehmen, bevor freigeschaltet wird (${datenschutzRegel(k.feature)}).`,
'5. „Freischalten (Abnahme)“ im WOMS auslösen, danach eine Probebuchung durchspielen und den Testtermin wieder stornieren (Checkliste in der Anleitung).',
`6. Ticket erledigen und die Zeit im Arbeitsblatt buchen. Nicht auf das Stundenkontingent anrechnen${bezahlt}.`,
abrechnung ? '6. Ticket erledigen und die Zeit im Arbeitsblatt buchen.' : `6. Ticket erledigen und die Zeit im Arbeitsblatt buchen. Nicht auf das Stundenkontingent anrechnen${bezahlt}.`,
].join('\n')
}
+4 -2
View File
@@ -36,13 +36,15 @@ export async function termineTicketDetails(ctx) {
const ANLEITUNG_FEHLT = 'Anleitung konnte nicht erzeugt werden, im WOMS öffnen (Verweis oben).'
const KONTINGENT = 'Nicht auf das Stundenkontingent anrechnen.'
// Abo-Modell (Kundenansicht neu, P6): die Abrechnung steht allein in der Schlusszeile des Tickets (createFeatureTicket, ABRECHNUNGS_ZEILEN)
const imAboModell = (doc) => doc?.einrichtungAbrechnung === 'abo' || doc?.einrichtungAbrechnung === 'projekt'
/** Kopfzeilen und Verweis (minimaleTicketDetails, Funktion wie im Kopf der Kurzfassung) plus Hinweis; wirft nie, auch nicht bei einem unbrauchbaren ctx. */
function notfallDetails(ctx) {
try {
const kopf = minimaleTicketDetails({ ...ctx, feature: { key: 'Terminplaner' } })
return kuerze(`${kopf}\n${ANLEITUNG_FEHLT}\n${KONTINGENT}`.toWellFormed(), TICKET_DETAILS_MAX)
return kuerze(`${kopf}\n${ANLEITUNG_FEHLT}${imAboModell(ctx?.doc) ? '' : `\n${KONTINGENT}`}`.toWellFormed(), TICKET_DETAILS_MAX)
} catch {
return `Funktion: Terminplaner\nAnleitung konnte nicht erzeugt werden, im WOMS öffnen (Kunden → Funktionen).\n${KONTINGENT}`
return `Funktion: Terminplaner\nAnleitung konnte nicht erzeugt werden, im WOMS öffnen (Kunden → Funktionen).${imAboModell(ctx?.doc) ? '' : `\n${KONTINGENT}`}`
}
}
+28 -6
View File
@@ -69,6 +69,15 @@ export async function ensureProjectTicket(project, { repoFullName } = {}) {
return ticket.$id
}
/**
* Schlusszeile der Ticket-Details im Abo-Modell (V7): sagt dem Team, wie die Einrichtung abgerechnet wird. Steht immer als letzte
* Zeile, auch nach dem Kuerzen und im Notfallweg.
*/
export const ABRECHNUNGS_ZEILEN = {
abo: 'Zeit im Arbeitsblatt erfassen und aufs Abo-Stundenkonto anrechnen (Einrichtung aus dem Abo, Vorschuss möglich).',
projekt: 'Zeit im Arbeitsblatt erfassen, nicht aufs Abo anrechnen (im Projekt enthalten).',
}
/**
* Einrichtungs-Ticket fuer eine gekaufte Funktion (z. B. News-Pinnwand, Terminplaner) einer Kundenwebsite.
* Wird geschlossen, sobald die Funktion eingebaut ist; danach schaltet das Portal sie frei.
@@ -78,6 +87,10 @@ export async function ensureProjectTicket(project, { repoFullName } = {}) {
* (LANGE_DETAILS): dann einmal mit fallbackDetails und derselben woid. Jeder andere Fehler (5xx, Netz: unklar, ob das Ticket
* entstand) wird ohne Wiederholung weitergereicht, sonst gaebe es Doppel-Tickets; scheitert auch der Rueckfall, wird der erste
* Fehler geworfen.
* abrechnung ('' | 'abo' | 'projekt', Abo-Modell): setzt am Ticket einrichtungFuer (= featureKey) und einrichtungAbrechnung und haengt
* ABRECHNUNGS_ZEILEN[abrechnung] als letzte Zeile an die Details (der Text wird dafuer auf TICKET_DETAILS_MAX minus Zeile gekuerzt).
* Ohne abrechnung schreibt das Ticket weder die Felder noch die Zeile (alte Ansicht unveraendert). Ein anderer Wert ist ein
* TypeError vor jedem Schreibzugriff.
*/
export async function createFeatureTicket({
customerId,
@@ -87,14 +100,24 @@ export async function createFeatureTicket({
topic = null,
details = null,
fallbackDetails = null,
abrechnung = '',
}) {
if (abrechnung !== '' && !(typeof abrechnung === 'string' && Object.hasOwn(ABRECHNUNGS_ZEILEN, abrechnung))) {
throw new TypeError(`createFeatureTicket: abrechnung muss '', 'abo' oder 'projekt' sein, nicht ${String(abrechnung)}`)
}
const zeile = abrechnung ? ABRECHNUNGS_ZEILEN[abrechnung] : ''
// Details auf TICKET_DETAILS_MAX kuerzen; mit Abrechnung bleibt Platz fuer die Schlusszeile (und ihren Zeilenumbruch)
const kuerzeDetails = (roh) => {
const gekuerzt = kuerze(roh, zeile ? TICKET_DETAILS_MAX - zeile.length - 1 : TICKET_DETAILS_MAX, { anZeile: true, endzeile: TICKET_GEKUERZT })
return { gekuerzt, text: zeile ? `${gekuerzt}\n${zeile}` : gekuerzt }
}
const customer = await getDocument(config.collections.customers, customerId).catch(() => null)
const woid = await generateNextWoid()
const projectName = project?.projectName || project?.subdomain || ''
const domain = project?.liveDomain || project?.previewUrl || project?.subdomain || ''
const embed = `<div data-webklar-news="${siteKey}"></div> + https://project.webklar.com/embed/news.js`
const text = kuerze(
const { gekuerzt, text } = kuerzeDetails(
details ||
[
`Funktion: ${featureKey}`,
@@ -103,9 +126,7 @@ export async function createFeatureTicket({
`siteKey: ${siteKey}`,
`Einbau: ${embed}`,
'Anleitung: docs/funktionen/news-einbau.md (Repo Webklar-Kundenbereich)',
].join('\n'),
TICKET_DETAILS_MAX,
{ anZeile: true, endzeile: TICKET_GEKUERZT }
].join('\n')
)
const fields = {
topic: kuerze(topic || `News-Pinnwand einbauen – ${projectName}`.trim(), TICKET_TOPIC_MAX, { endzeile: '…' }),
@@ -119,6 +140,7 @@ export async function createFeatureTicket({
customerLocation: customer?.location || '',
woid,
createdAt: new Date().toISOString(),
...(abrechnung ? { einrichtungFuer: featureKey, einrichtungAbrechnung: abrechnung } : {}),
}
let ticket
@@ -126,12 +148,12 @@ export async function createFeatureTicket({
ticket = await createDocument(config.collections.workorders, { ...fields, details: text }, ID.unique())
} catch (firstError) {
const hatRueckfall = typeof fallbackDetails === 'string' && fallbackDetails !== ''
if (firstError?.status !== 400 || !hatRueckfall || text.length <= LANGE_DETAILS) throw firstError
if (firstError?.status !== 400 || !hatRueckfall || gekuerzt.length <= LANGE_DETAILS) throw firstError
console.warn(`[ticketAdmin] Funktions-Ticket #${woid} (${featureKey}) abgelehnt (400), zweiter Versuch mit Notfall-Details:`, firstError.message)
try {
ticket = await createDocument(
config.collections.workorders,
{ ...fields, details: kuerze(fallbackDetails, TICKET_DETAILS_MAX, { anZeile: true, endzeile: TICKET_GEKUERZT }) },
{ ...fields, details: kuerzeDetails(fallbackDetails).text },
ID.unique()
)
} catch (secondError) {
+11 -1
View File
@@ -2,6 +2,8 @@ import { config } from '../../config.js'
import { withLocks } from '../keyedLock.js'
import { sendAdminNotice, sendCustomerMail } from '../mailer.js'
import { assertOwnProject, customerOf, ensureFeatureDoc, getFeatureDoc } from '../siteFeatures.js'
import { aboModellFuerKunde } from '../aboFunktionen.js'
import { anfrageEntschiedenMail } from '../aboFunktionenMails.js'
import { unterkunftFuerKunde } from './schalter.js'
import { ladeEinstellungen, speichereEinstellungen } from './speicher.js'
@@ -268,7 +270,11 @@ export async function entscheideAnfrage({ customerId, projectId, entscheidung, b
return { anfrage: anfrageAnsicht(gespeichert), mailFehler }
}
/** K34 an den Kunden; true = gesendet. Jeder Fehler wird nur geloggt (die Entscheidung ist gespeichert). */
/**
* K34 an den Kunden; true = gesendet. Jeder Fehler wird nur geloggt (die Entscheidung ist gespeichert). Fuer Kunden der neuen Ansicht
* (aboModellFuerKunde, Kundenansicht P6) geht stattdessen K43 ohne Preis und ohne "bestellen"; ist das nicht lesbar, geht keine Mail raus
* (lieber keine als eine mit Preis, die Antwort traegt mailFehler).
*/
async function sendeEntscheidung({ customerId, projectId, project, angenommen, begruendung }) {
try {
const kunde = await customerOf(customerId)
@@ -276,6 +282,10 @@ async function sendeEntscheidung({ customerId, projectId, project, angenommen, b
console.warn(`[unterkunft] K34 nicht gesendet: Kunde ${customerId} ohne E-Mail-Adresse`)
return false
}
if (await aboModellFuerKunde(customerId)) {
await _versand.kunde(kunde.email, anfrageEntschiedenMail({ kunde, projekt: project?.projectName || projectId, key: 'unterkunft', projectId, angenommen, begruendung }))
return true
}
await _versand.kunde(kunde.email, {
template: 'K34',
values: {
+2 -1
View File
@@ -46,7 +46,8 @@ export async function unterkunftTicketDetails(ctx) {
'5. Testanfrage über den Kalender stellen und im Portal (Buchungen) prüfen.',
'6. Ticket schließen: das schaltet die Funktion frei (sobald der Kunde Unterkunft, Preise, Stornobedingungen und Firmendaten hinterlegt hat).',
'',
'Zeit im Arbeitsblatt erfassen, nicht auf Abo-Stunden anrechnen.',
// Abo-Modell (einrichtungAbrechnung abo oder projekt): die Abrechnung steht allein in der Schlusszeile des Tickets (createFeatureTicket)
...(doc?.einrichtungAbrechnung === 'abo' || doc?.einrichtungAbrechnung === 'projekt' ? [] : ['Zeit im Arbeitsblatt erfassen, nicht auf Abo-Stunden anrechnen.']),
'Einbauanleitung: docs/funktionen/unterkunft-einbau.md',
`Verweis: ${anleitungsLink(doc?.customerId, doc?.projectId)}`,
]
+30 -8
View File
@@ -1,4 +1,6 @@
import { config } from '../../config.js'
import { ohneAboBisher } from '../aboAufbauphase.js'
import { ABRECHNUNGEN, istAboModell } from '../aboModell.js'
import { aboLage, aboMemo } from '../aboStatus.js'
import { getDocument, listAllDocuments, Query, updateDocument } from '../appwriteAdmin.js'
import { withLocks } from '../keyedLock.js'
@@ -17,8 +19,12 @@ import { ladeZeiten, loescheZeiten } from './speicher.js'
* ist sie abgeschaltet und die Eingaben bleiben noch 30 Tage. gleicheZeitenAboAb(now) ist der Baustein des Tagesjobs (angeschlossen in
* Task 22); er kennt weder Schalter noch Überlappungssperre: der Aufrufer lässt nie zwei Läufe gleichzeitig laufen.
*
* Nur Dokumente mit unlockedBy 'abo' (unlockedByVon) sind ans Abo gekoppelt; 'kauf' und 'team' fasst der Abgleich nie an, und das Abo
* wird dann nicht einmal gelesen. Je Dokument gilt (Abo gelesen mit aboLage, grosszügige Regel inklusive Wechsel-Regel):
* Nur Dokumente mit unlockedBy 'abo' oder 'projekt' (unlockedByVon) sind ans Abo gekoppelt; 'kauf' und 'team' fasst der Abgleich nie an, und
* das Abo wird dann nicht einmal gelesen. 'projekt' (Kundenansicht neu, P6) gilt wie 'abo', ausser in der Aufbauphase (E9): ein projekt-Dokument
* im Zustand setup oder active bleibt unberuehrt, solange das Abo nicht laeuft und der Kunde noch nie ein Abo hatte (ohneAboBisher): die
* Website ist noch im Bau. Mit dem ersten Abo gilt die Abo-Regel auch fuer projekt. Dokumente des Abo-Modells (istAboModell: Marker
* einrichtungAbrechnung) bekommen K40 und K41 statt K29 und K30 (ohne Preis) und kein Behalten (behalten.js); Gratis-Zeiten ohne Marker
* behalten K29, K30 und den Behalten-Weg. Je Dokument gilt (Abo gelesen mit aboLage, grosszügige Regel inklusive Wechsel-Regel):
*
* setup/active, Abo läuft nicht, aboEndeAt leer Schritt 1: aboEndeAt = now, K29 (Variante nach art; Frist = Berliner Tag von
* aboEndeAt + 30 Tage)
@@ -79,8 +85,10 @@ const portalFeatures = () => config.collections.portalFeatures
const istLaufend = (doc) => doc.state === 'setup' || doc.state === 'active'
const istAbgeschaltet = (doc) => doc.state === 'disabled' && doc.disabledReason === GRUND
/** Ans Wartungs-Abo gekoppelt: von ihm freigeschaltet und im laufenden Betrieb oder wegen des Abo-Endes abgeschaltet. */
const istGekoppelt = (doc) => unlockedByVon(doc) === 'abo' && (istLaufend(doc) || istAbgeschaltet(doc))
/** Ans Wartungs-Abo gekoppelt: von ihm freigeschaltet (abo oder, im Abo-Modell, projekt) und im laufenden Betrieb oder wegen des Abo-Endes abgeschaltet. */
const istGekoppelt = (doc) => ABRECHNUNGEN.includes(unlockedByVon(doc)) && (istLaufend(doc) || istAbgeschaltet(doc))
/** Aufbauphasen-Kandidat (E9): im Projekt eingebaut und noch im laufenden Betrieb; ob der Kunde je ein Abo hatte, entscheidet ohneAboBisher. */
const istProjektImBetrieb = (doc) => unlockedByVon(doc) === 'projekt' && istLaufend(doc)
/** Berliner Kalendertag eines Datumsfeldes; unlesbar ist ein Fehler (das Dokument wird übersprungen), nie "heute". */
function tagVon(wert, feld) {
@@ -150,6 +158,8 @@ async function unterSperre(doc, ctx) {
const frisch = await getDocument(portalFeatures(), doc.$id)
if (!istGekoppelt(frisch)) return null // das Team war schneller (anderer Grund, anderer Zustand, nicht mehr im Abo)
const lage = await aboLage(frisch.customerId) // wirft bei Lesefehler: dann bleibt alles, wie es ist
// Aufbauphase (E9): ein im Projekt eingebautes Dokument bleibt, solange es nie ein Abo gab (frisch gelesen; ein Lesefehler wirft: nichts geschrieben)
if (istProjektImBetrieb(frisch) && !lage.laeuft && (await ohneAboBisher(frisch.customerId))) return null
const aktion = entscheide(frisch, lage, now)
const hatteMail = Boolean(frisch.aboEndeMailAt) // vor dem Leeren gelesen: wer nie vom Ende erfuhr, hört nicht von der Rückkehr
@@ -255,7 +265,7 @@ async function sendeWiederMail(doc, einbau, ctx) {
async function danach({ aktion, doc, lage, hatteMail }, ctx) {
switch (aktion) {
case 'k29':
await sendeMitMarker(doc, ({ kunde, projekt }) => k29Mail({ kunde, projekt, art: lage.art, frist: mittags(fristTag(doc)), preis: preis() }), ctx)
await sendeMitMarker(doc, ({ kunde, projekt }) => k29Mail({ kunde, projekt, art: lage.art, frist: mittags(fristTag(doc)), preis: preis(), modell: istAboModell(doc) }), ctx)
return
case 'k30':
// Erst das Ersatz-Ticket, dann K30 mit Marker. Scheitert das Ticket, gibt es weder K30 noch Marker: der nächste Lauf wiederholt beides
@@ -264,7 +274,7 @@ async function danach({ aktion, doc, lage, hatteMail }, ctx) {
ctx.erg.fehler += 1
return
}
await sendeMitMarker(doc, ({ kunde, projekt }) => k30Mail({ kunde, projekt, loeschdatum: mittags(loeschTag(doc)), preis: preis() }), ctx, doc.disabledAt)
await sendeMitMarker(doc, ({ kunde, projekt }) => k30Mail({ kunde, projekt, loeschdatum: mittags(loeschTag(doc)), preis: preis(), modell: istAboModell(doc) }), ctx, doc.disabledAt)
return
case 'zurueck': {
const einbau = await stosseNeubauAn(await ladeProjekt(doc.projectId))
@@ -302,9 +312,21 @@ async function bearbeiteKunde(customerId, dokumente, ctx) {
console.error(`[zeiten-abo] Abo-Lage eines Kunden nicht lesbar, ${gekoppelt.length} Funktion(en) übersprungen: ${fehlerText(err)}`)
return
}
erg.geprueft += gekoppelt.length
// Aufbauphase (E9): im Projekt eingebaute Dokumente bleiben in Ruhe, solange das Abo nicht laeuft und der Kunde noch nie ein Abo hatte;
// gelesen wird das nur, wenn es ein solches Dokument gibt. Ein Lesefehler ueberspringt den Kunden wie beim Lesen der Abo-Lage.
let zuBewerten = gekoppelt
if (!lage.laeuft && gekoppelt.some(istProjektImBetrieb)) {
try {
if (await ohneAboBisher(customerId)) zuBewerten = gekoppelt.filter((doc) => !istProjektImBetrieb(doc))
} catch (err) {
erg.fehler += 1
console.error(`[zeiten-abo] Abo-Verlauf eines Kunden nicht lesbar, ${gekoppelt.length} Funktion(en) übersprungen: ${fehlerText(err)}`)
return
}
}
erg.geprueft += zuBewerten.length
for (const doc of gekoppelt) {
for (const doc of zuBewerten) {
// je Dokument gefangen (auch Entscheidung, Builder und Versand): ein kaputtes Datum stoppt weder den Kunden noch den Lauf
try {
if (!entscheide(doc, lage, now)) continue // Vorauswahl mit dem Memo: nichts zu tun kostet weder Sperre noch Abo-Abfrage
+3 -1
View File
@@ -1,5 +1,6 @@
import { config } from '../../config.js'
import { aboLage } from '../aboStatus.js'
import { istAboModell } from '../aboModell.js'
import { getDocument, updateDocument } from '../appwriteAdmin.js'
import { featureConfig } from '../featureRegistry.js'
import { withLocks } from '../keyedLock.js'
@@ -43,9 +44,10 @@ async function datenDa(featureId) {
/**
* Darf der Kunde die Funktion jetzt behalten? In Einrichtung oder aktiv: sobald das Ende des Abos erkannt ist (aboEndeAt). Abgeschaltet:
* nur nach dem Abo-Ende (disabledReason abo_ende, nicht: Team, vor der Einrichtung, Kunde geloescht) und solange die Eingaben noch da sind.
* Nie fuer ein Dokument des Abo-Modells (istAboModell).
*/
async function behaltenMoeglich(doc) {
if (!doc) return false
if (!doc || istAboModell(doc)) return false // Abo-Modell (P6): kein Behalten gegen Zahlung, die Funktion endet mit dem Abo und kehrt mit einem neuen zurueck
if (doc.state === 'setup' || doc.state === 'active') return Boolean(doc.aboEndeAt)
if (doc.state === 'disabled' && doc.disabledReason === 'abo_ende') return datenDa(doc.$id)
return false
+16 -5
View File
@@ -4,7 +4,7 @@ import { pruefeAboFuerEinschalten } from '../featureOrder.js'
import { featureConfig, ticketTexte } from '../featureRegistry.js'
import { withLocks } from '../keyedLock.js'
import { customerOf, ensureFeatureDoc, getFeatureDoc, stateFields, ticketWoid, unlockedByVon } from '../siteFeatures.js'
import { createFeatureTicket } from '../ticketAdmin.js'
import { ABRECHNUNGS_ZEILEN, createFeatureTicket } from '../ticketAdmin.js'
import { k27Mail, sendeZeitenMail } from './mails.js'
import { stosseNeubauAn } from './neubau.js'
import { ladeZeiten } from './speicher.js'
@@ -50,16 +50,24 @@ function notizDesTeams(staff, bisherige, now) {
* by 'kunde': Abo frisch pruefen (kein_abo/gekuendigt/nicht_aktiv 409, Lesefehler 503), unlockedBy 'abo', K27 geht an den Kunden.
* by 'team': keine Abo-Pruefung, unlockedBy 'team' (ein bisher gekauftes Dokument bleibt kauf), keine K27; staff ergibt die Notiz am Dokument.
* nachweis: speichert den Zustimmungsnachweis (Schritt 3, nur wenn wirklich eingeschaltet wird); wirft er, bleibt der Zustand unveraendert.
* abrechnung ('' | 'abo' | 'projekt', Abo-Modell der Kundenansicht neu, V7): ueberschreibt unlockedBy ('abo' | 'projekt'; ein gekauftes
* Dokument bleibt kauf) und setzt einrichtungAbrechnung am Dokument (das Einrichtungs-Ticket traegt es in holeZeitenNach weiter).
* Ohne abrechnung bleibt alles wie bisher. Ein anderer Wert ist ein TypeError.
* vorpruefung: Funktion, die unter der Sperre statt pruefeAboFuerEinschalten laeuft (nur by 'kunde'); sie wirft, was der Aufrufer
* als Fehler will (das Einschalten im Abo-Modell prueft damit Abo und Zustand mit eigenen Codes).
* @returns {Promise<{ doc: object, changed: boolean }>} doc: das Dokument nach dem Nachholen; changed: false bei idempotentem Aufruf
* @throws {Error} 409 (Abo, anderer Zustand), 503 (Abo nicht lesbar; oder Zustand gespeichert, aber Neubau/Ticket nicht fertig: erneut aufrufen)
*/
export async function einschaltenZeiten({ customerId, projectId, by = 'kunde', staff = null, nachweis = null, now = new Date() } = {}) {
export async function einschaltenZeiten({ customerId, projectId, by = 'kunde', staff = null, nachweis = null, now = new Date(), abrechnung = '', vorpruefung = null } = {}) {
if (by !== 'kunde' && by !== 'team') throw new TypeError(`einschaltenZeiten: by muss kunde oder team sein, nicht ${String(by)}`)
if (abrechnung !== '' && abrechnung !== 'abo' && abrechnung !== 'projekt') {
throw new TypeError(`einschaltenZeiten: abrechnung muss '', 'abo' oder 'projekt' sein, nicht ${String(abrechnung)}`)
}
const feature = featureConfig(KEY)
const { doc, changed } = await withLocks([`feature-order:${customerId}:${projectId}`], async () => {
// 1 Abo frisch (nie aus einem Zwischenspeicher); fuer das Team entfaellt es
if (by === 'kunde') await pruefeAboFuerEinschalten(customerId)
if (by === 'kunde') await (vorpruefung ? vorpruefung() : pruefeAboFuerEinschalten(customerId))
// 2 Zustand: schon eingeschaltet (setup/active) ist idempotent; sonst nur aus off (der Kunde) bzw. off/ordered/disabled (das Team)
const bisher = await getFeatureDoc(customerId, projectId, KEY)
@@ -84,8 +92,9 @@ export async function einschaltenZeiten({ customerId, projectId, by = 'kunde', s
const gespeichert = await updateDocument(config.collections.portalFeatures, vorhanden.$id, {
...stateFields('setup'),
featureKey: KEY,
unlockedBy: gekauft ? 'kauf' : by === 'kunde' ? 'abo' : 'team',
unlockedBy: gekauft ? 'kauf' : abrechnung || (by === 'kunde' ? 'abo' : 'team'),
unlockedByPurchase: gekauft,
...(abrechnung ? { einrichtungAbrechnung: abrechnung } : {}),
...(gekauft ? {} : { einbau: '' }),
aboEndeAt: null,
behaltenSessionId: '',
@@ -141,7 +150,9 @@ export async function holeZeitenNach(doc, { now = new Date() } = {}) {
const customer = await customerOf(frisch.customerId)
if (!frisch.ticketId) {
const texte = await ticketTexte({ feature, doc: frisch, project, customer })
const { ticketId } = await createFeatureTicket({ customerId: frisch.customerId, project, featureKey: KEY, siteKey: frisch.siteKey, ...texte })
// Abo-Modell: die Abrechnung am Dokument (abo | projekt) geht ans Ticket; Dokumente ohne Marker (alte Ansicht) bleiben wie sie waren
const abrechnung = Object.hasOwn(ABRECHNUNGS_ZEILEN, frisch.einrichtungAbrechnung) ? { abrechnung: frisch.einrichtungAbrechnung } : {}
const { ticketId } = await createFeatureTicket({ customerId: frisch.customerId, project, featureKey: KEY, siteKey: frisch.siteKey, ...texte, ...abrechnung })
frisch = await updateDocument(config.collections.portalFeatures, frisch.$id, { ticketId })
}
+15 -5
View File
@@ -8,6 +8,7 @@ import { datumKurz, wochentag } from '../termine/zeit.js'
* K28 Feiertags-Erinnerung (eine Mail mit allen neuen Tagen, ohne BCC)
* K29 Wartungs-Abo beendet: Funktion läuft bis zur Frist, „für einmalig 30 € behalten“ (Varianten nach der Abo-Lage)
* K30 abgeschaltet: feste Zeiten auf der Seite, Eingaben noch bis zum Löschdatum
* K40, K41 dieselben Mails im Abo-Modell der Kundenansicht neu (k29Mail und k30Mail mit modell: true): ohne Preis und ohne Behalten
* Die Builder sind rein (kein Zugriff auf Appwrite, kein Versand) und liefern { template, values, blocks }, so wie
* siteFeatures.bestellMail: die Werte decken genau die Platzhalter der Vorlage ab. sendeZeitenMail verschickt sie.
*
@@ -93,24 +94,33 @@ export function k28Mail({ kunde, projekt, tage }) {
/**
* K29: Das Wartungs-Abo ist beendet, die Funktion läuft noch bis zur Frist; für einmalig `preis` behalten.
* @param {{ kunde: string | { name?: string }, projekt: string, art?: string, frist: Date, preis: string }} p
* @param {{ kunde: string | { name?: string }, projekt: string, art?: string, frist: Date, preis: string, modell?: boolean }} p
* art: Abo-Lage aus aboStatus.aboLage(): 'hosting' (das Abo ist ein Hosting-Abo), 'wechsel_widerrufen' (Wechsel ins Wartungs-Abo
* widerrufen), sonst (auch 'beendet' und Unbekanntes) 'abo_ende'. frist: Tag des Abschaltens (aboEndeAt + 30 Tage).
* modell: Abo-Modell der Kundenansicht neu (Task 10): Vorlage K40 ohne Preis und ohne Behalten, die Blöcke bleiben gleich.
*/
export function k29Mail({ kunde, projekt, art, frist, preis }) {
export function k29Mail({ kunde, projekt, art, frist, preis, modell = false }) {
const blocks = { abo_ende: art !== 'hosting' && art !== 'wechsel_widerrufen', hosting: art === 'hosting', wechsel_widerrufen: art === 'wechsel_widerrufen' }
if (modell) {
return { template: 'K40', values: { kunde_name: kundenName(kunde), projekt, frist: datum(frist, 'frist'), portal_url: portalUrl() }, blocks }
}
return {
template: 'K29',
values: { kunde_name: kundenName(kunde), projekt, frist: datum(frist, 'frist'), preis, portal_url: portalUrl() },
blocks: { abo_ende: art !== 'hosting' && art !== 'wechsel_widerrufen', hosting: art === 'hosting', wechsel_widerrufen: art === 'wechsel_widerrufen' },
blocks,
}
}
/**
* K30: Öffnungszeiten live ist abgeschaltet (Wartungs-Abo beendet, Frist zum Behalten abgelaufen).
* @param {{ kunde: string | { name?: string }, projekt: string, loeschdatum: Date, preis: string }} p
* @param {{ kunde: string | { name?: string }, projekt: string, loeschdatum: Date, preis: string, modell?: boolean }} p
* loeschdatum: Tag, ab dem die Eingaben gelöscht werden (30 Tage nach dem Abschalten); preis: Preis zum Wiedereinschalten.
* modell: Abo-Modell der Kundenansicht neu (Task 10): Vorlage K41 ohne Preis.
*/
export function k30Mail({ kunde, projekt, loeschdatum, preis }) {
export function k30Mail({ kunde, projekt, loeschdatum, preis, modell = false }) {
if (modell) {
return { template: 'K41', values: { kunde_name: kundenName(kunde), projekt, loeschdatum: datum(loeschdatum, 'loeschdatum'), portal_url: portalUrl() } }
}
return {
template: 'K30',
values: { kunde_name: kundenName(kunde), projekt, loeschdatum: datum(loeschdatum, 'loeschdatum'), preis, portal_url: portalUrl() },
+115
View File
@@ -0,0 +1,115 @@
import { test, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
// Anfragen im Abo-Modell (P5 Task 12): die Shop-Anfrage nimmt Unternehmer aus der Kundenart (nur Betriebe), der Buchungskalender bleibt offen
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
shopConfig: schemaKeys('shopConfig'),
ukEinstellungen: schemaKeys('ukEinstellungen'),
featureConsents: schemaKeys('featureConsents'),
},
datetimes: { ukEinstellungen: ['rechnungAuftragAt', 'anfrageAt', 'anfrageEntschiedenAt', 'icalFehlerMailAt'] },
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, post } = await import('./support/testApp.js')
const { customerCookie, customers, seedFixtures } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { NUR_BETRIEB_TEXT } = await import('../server/services/kundenart.js')
const { default: shopRoutes, resetShopRoutesForTests } = await import('../server/routes/shop.js')
const { default: ukRoutes, resetUnterkunftRoutesForTests } = await import('../server/routes/unterkunft.js')
const app = await startTestApp({ '/api/shop': shopRoutes, '/api/unterkunft': ukRoutes })
const sink = mailSink()
setMailTransportForTests(sink)
const vorher = {
neu: { mode: config.kundenansichtNeu.mode, pilotEmails: [...config.kundenansichtNeu.pilotEmails] },
shop: { mode: config.shop.mode, pilotEmails: [...config.shop.pilotEmails] },
unterkunft: { mode: config.unterkunft.mode, pilotEmails: [...config.unterkunft.pilotEmails] },
}
after(() => {
Object.assign(config.kundenansichtNeu, vorher.neu)
Object.assign(config.shop, vorher.shop)
Object.assign(config.unterkunft, vorher.unterkunft)
setMailTransportForTests(null)
return Promise.all([app.close(), aw.close(), st.close()])
})
const c1 = customerCookie('c1')
const anfrage = (extra = {}) => ({ warenart: 'waren', beschreibung: 'Backwaren aus eigener Herstellung', artikelzahl: 'bis20', versand: false, abholung: true, keineAusgeschlossenen: true, ...extra })
const senden = (a, cookie = c1) => post('/api/shop/anfrage', { projectId: 'p1', anfrage: a }, { cookie })
// aw.docs() liefert eine Kopie: Dokumente werden ueber die API geloescht
const leere = async (...collections) => {
for (const col of collections) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
}
beforeEach(async () => {
await leere('portalFeatures', 'shopConfig', 'ukEinstellungen', 'featureConsents', 'websiteProjects')
seedFixtures(aw)
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei', archived: false })
config.kundenansichtNeu.mode = 'pilot'
config.kundenansichtNeu.pilotEmails = ['k@example.invalid', 'test@example.invalid']
config.shop.mode = config.unterkunft.mode = 'on'
resetShopRoutesForTests()
resetUnterkunftRoutesForTests()
sink.sent.length = 0
})
test('privat (Kundenart leer): 409 nur_betrieb, auch mit gefälschtem unternehmer: true; nichts geschrieben, keine Mail', async () => {
const r = await senden(anfrage({ unternehmer: true }))
assert.deepEqual([r.status, r.body.code, r.body.error], [409, 'nur_betrieb', NUR_BETRIEB_TEXT])
assert.deepEqual([aw.docs('shopConfig').size, aw.docs('portalFeatures').size, sink.sent.length], [0, 0, 0])
for (const kundenart of ['', 'kaputt', 'Unternehmen', 'privat']) {
aw.seed('customers', { ...customers.c1, kundenart })
const x = await senden(anfrage({ unternehmer: true, kundenart: 'unternehmen' }))
assert.deepEqual([x.status, x.body.code], [409, 'nur_betrieb'], kundenart)
}
})
test('Betrieb: unternehmer kommt vom Server, Anfrage speichert die Kundenart', async () => {
aw.seed('customers', { ...customers.c1, kundenart: 'unternehmen' })
const r = await senden(anfrage())
assert.deepEqual([r.status, r.body.anfrage.status], [200, 'offen'])
assert.deepEqual([r.body.anfrage.anfrage.unternehmer, r.body.anfrage.anfrage.kundenart], [true, 'unternehmen'])
assert.equal(sink.sent.length, 1, 'T21 ans Team')
await leere('shopConfig', 'portalFeatures')
const falsch = await senden(anfrage({ unternehmer: false })) // der Client-Wert zählt nicht
assert.deepEqual([falsch.status, falsch.body.anfrage.anfrage.unternehmer], [200, true])
await leere('shopConfig', 'portalFeatures')
const ohne = await senden(anfrage({ keineAusgeschlossenen: false }))
assert.deepEqual([ohne.status, Object.keys(ohne.body.fehler)], [400, ['keineAusgeschlossenen']])
})
test('alte Ansicht unverändert: unternehmer bleibt Pflicht, privat darf anfragen', async () => {
config.kundenansichtNeu.mode = 'off'
const r = await senden(anfrage())
assert.deepEqual([r.status, Object.keys(r.body.fehler)], [400, ['unternehmer']])
const ok = await senden(anfrage({ unternehmer: true }))
assert.equal(ok.status, 200)
assert.equal(Object.hasOwn(ok.body.anfrage.anfrage, 'kundenart'), false, 'ohne neue Ansicht keine Kundenart in der Anfrage')
})
test('Reihenfolge: Projekt und Schalter vor der Kundenart, die Kundenart vor der Eingabe', async () => {
assert.equal((await post('/api/shop/anfrage', { projectId: 'pfremd', anfrage: anfrage() }, { cookie: c1 })).status, 403)
config.shop.mode = 'off'
assert.equal((await senden(anfrage())).body.code, 'nicht_verfuegbar')
config.shop.mode = 'on'
const leer = await senden({}) // ungültige Eingabe, aber privat: nur_betrieb kommt zuerst
assert.deepEqual([leer.status, leer.body.code], [409, 'nur_betrieb'])
aw.seed('customers', { ...customers.c1, kundenart: 'unternehmen' })
assert.equal((await senden({})).status, 400)
})
test('Buchungskalender: privat darf in der neuen Ansicht anfragen (nurBetrieb false)', async () => {
const r = await post('/api/unterkunft/anfrage-funktion', { projectId: 'p1', art: 'ferienwohnung', einheiten: 2, portale: ['booking'] }, { cookie: c1 })
assert.equal(r.status, 200, JSON.stringify(r.body))
assert.equal(r.body.anfrage.status, 'offen')
})
+33
View File
@@ -0,0 +1,33 @@
import { test, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
// Aufbauphase (E9, N28): ohneAboBisher = der Kunde hatte noch nie ein Abo. Gemeinsame Quelle für die Team-Aktivierung und die Abgleiche.
const aw = await startAppwriteMock({ schema: { subscriptions: schemaKeys('subscriptions') } })
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { subscriptionDoc, wartungDoc, hostingDoc } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { ohneAboBisher } = await import('../server/services/aboAufbauphase.js')
// aw.docs() liefert eine Kopie: geloescht wird ueber die API
const loescheAbos = async () => {
for (const id of aw.docs('subscriptions').keys()) await deleteDocument('subscriptions', id)
}
beforeEach(loescheAbos)
test('ohneAboBisher: wahr ohne Abo-Dokument und bei abgebrochener Bestellung, falsch bei jedem anderen Status (auch beendet), je Kunde', async () => {
assert.equal(await ohneAboBisher('c1'), true)
for (const status of ['incomplete', 'incomplete_expired']) { await loescheAbos(); aw.seed('subscriptions', subscriptionDoc('sub_x', { ...wartungDoc, status })); assert.equal(await ohneAboBisher('c1'), true, status) }
for (const status of ['active', 'trialing', 'past_due', 'canceled', 'unpaid', 'paused']) { await loescheAbos(); aw.seed('subscriptions', subscriptionDoc('sub_x', { ...wartungDoc, status })); assert.equal(await ohneAboBisher('c1'), false, status) }
await loescheAbos(); aw.seed('subscriptions', subscriptionDoc('sub_h', hostingDoc)) // Hosting zählt als Abo; das Dokument gehört c1
assert.deepEqual([await ohneAboBisher('c1'), await ohneAboBisher('c2')], [false, true])
})
test('ohneAboBisher: Lesefehler werden weitergereicht, leere oder falsche Kunden-ID ist ein TypeError', async () => {
aw.failNext({ collection: 'subscriptions', method: 'GET', status: 500 }); await assert.rejects(() => ohneAboBisher('c1'))
for (const id of ['', undefined, null, 5]) await assert.rejects(() => ohneAboBisher(id), TypeError)
})
+128
View File
@@ -0,0 +1,128 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
const { config } = await import('../server/config.js')
const ENTWURF = true
const lies = (rel) => readFileSync(new URL(`../${rel}`, import.meta.url), 'utf8')
const PFLICHT = [ // stelle = Überschrift im Entwurf; von/bis = Anker-Ids der Seite (null = Dokumentanfang bzw. -ende)
{ stelle: 'Das Wichtigste auf einen Blick', von: null, bis: 'p1', texte: ['Das Hosting-Abo enthält die Website-Analyse, das Wartungs-Abo zusätzlich News-Pinnwand, Terminplaner, Webshop, Buchungskalender und Öffnungszeiten live. Das Einschalten kostet nichts extra; die Einrichtung geht vom Stundenkontingent ab (§ 4 Abs. 8).'] },
{ stelle: '§ 1 Abs. 2', von: 'p1', bis: 'p2', texte: ['Die Bestellseiten richten sich nach der Kundenart, die wir im Kundenkonto hinterlegt haben („Betrieb“ oder „privat“). Wir hinterlegen sie nach den Angaben des Kunden; ist keine hinterlegt, gilt der Kunde als privat. Ob der Kunde Verbraucher oder Unternehmer ist, richtet sich nach dem Zweck, zu dem er den Vertrag abschließt, nicht nach dieser Angabe.'] },
{ stelle: '§ 2 Abs. 2 c', von: 'p2', bis: 'p3', texte: ['Direkt über der Schaltfläche steht der Hinweis, dass diese Bedingungen mit den Leistungsbeschreibungen, dem Auftragsverarbeitungsvertrag (Anlage 1) und der Widerrufsbelehrung gelten, jeweils mit Fassung und Link; die Unterlagen erhält der Kunde nach der Bestellung zusätzlich als PDF per E-Mail. Ist die Kundenart „privat“ hinterlegt, kann der Kunde dort durch ein Kästchen verlangen, dass wir sofort mit der Leistung beginnen; ohne das Kästchen beginnen wir erst nach Ablauf der 14-tägigen Widerrufsfrist. Ist die Kundenart „Betrieb“ hinterlegt, beginnen wir mit Vertragsschluss, ohne dass der Kunde etwas markieren muss.'] },
{ stelle: '§ 2 Abs. 6', von: 'p2', bis: 'p3', texte: ['Wir speichern außerdem den Wortlaut der angezeigten Hinweise und Kästchen, die hinterlegte Kundenart sowie Zeit, IP-Adresse und Browserkennung des Klicks.'] },
{ stelle: '§ 3 Abs. 8', von: 'p3', bis: 'p4', texte: [
'Zum Abo gehören Funktionen, die der Kunde im Kundenportal unter „Funktionen“ einschaltet. Das Hosting-Abo enthält die Website-Analyse. Das Wartungs-Abo enthält News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse. Umfang, Voraussetzungen und Pflichten jeder Funktion beschreiben ihre Leistungsbeschreibung und, soweit die Funktion personenbezogene Daten im Auftrag des Kunden verarbeitet, ihr Auftragsverarbeitungsvertrag, jeweils in der Fassung, die das Kundenportal beim Einschalten anzeigt. Eine Funktion kostet keine zusätzliche Gebühr; die Einrichtung richtet sich nach § 4 Abs. 8 und 9.',
'Vorbehaltlich § 11 Abs. 4 läuft eine Funktion nur, solange das Abo läuft, das sie enthält. Das gilt auch für eine Funktion, die wir auf Wunsch des Kunden schon bei der Erstellung seiner Website eingebaut haben. Funktionen, die der Kunde vor Veröffentlichung dieser Fassung einzeln gekauft hat, laufen nach den Bedingungen weiter, unter denen er sie gekauft hat.'] },
{ stelle: '§ 4 Abs. 2', von: 'p4', bis: 'p5', texte: ['Einen Vorschuss nach Absatz 8 gleichen die folgenden Kontingente zuerst aus.'] },
{ stelle: '§ 4 Abs. 8', von: 'p4', bis: 'p5', texte: ['Die Einrichtung einer Funktion rechnen wir als Arbeitszeit auf das Stundenkontingent an. Angerechnet wird die tatsächlich aufgewendete Zeit nach Absatz 4. Reicht das verfügbare Kontingent nicht aus, führen wir die Einrichtung trotzdem ohne weitere Rückfrage aus. Die fehlende Zeit ist ein Vorschuss, den die folgenden Monatskontingente ausgleichen, sobald sie bereitstehen. Dafür berechnen wir nichts, weder als Mehraufwand nach Absatz 6 noch in einer gesonderten Rechnung. Endet das Wartungs-Abo, solange noch ein Vorschuss offen ist, verfällt der Rest; der Kunde schuldet ihn nicht.'] },
{ stelle: '§ 4 Abs. 9', von: 'p4', bis: 'p5', texte: ['Im Hosting-Abo ist die Einrichtung der Website-Analyse für den Kunden kostenlos; ein Stundenkontingent gibt es dort nicht (Absatz 7). Lässt der Kunde eine Funktion auf seinen Wunsch schon bei der Erstellung seiner Website einbauen, rechnen wir die Einrichtung nicht über das Stundenkontingent ab, sondern über die Kosten des Projekts, wie wir es mit ihm vereinbart haben.'] },
{ stelle: '§ 6 Abs. 6', von: 'p6', bis: 'p7', texte: ['Schaltet der Kunde die Website-Analyse ein, ist er für die Messung auf seiner Website Verantwortlicher. Er entfernt oder umgeht das Cookie-Banner nicht, bindet ohne Abstimmung mit uns keine weiteren Mess- oder Tracking-Werkzeuge ein und ergänzt seine Datenschutzerklärung um einen Hinweis auf die von uns erzeugte Zusatzseite „/datenschutz-analyse.html“.'] },
{ stelle: '§ 9 Abs. 6', von: 'wechsel', bis: 'zusatzspeicher', texte: ['Will der Kunde im Hosting-Abo eine Funktion einschalten, die nur das Wartungs-Abo enthält, führt ihn das Kundenportal zum Wechsel. Nach Wirksamkeit des Wechsels schaltet er die Funktion in einem eigenen Vorgang ein. Der Wechsel bleibt eine eigene Bestellung nach Absatz 2.'] },
{ stelle: '§ 11 Abs. 4', von: 'p11', bis: 'p12', texte: ['Mit dem Ende des Abos, das eine Funktion enthält, endet die Funktion; abweichend davon läuft Öffnungszeiten live noch 30 Tage unverändert weiter. Wir schalten sie ab und löschen ihre Daten nach den Fristen in ihrer Leistungsbeschreibung; die Messdaten der Website-Analyse löschen wir 60 Tage nach der Abschaltung. Beginnt innerhalb dieser Fristen wieder ein Abo, das die Funktion enthält, schalten wir sie mit den vorhandenen Daten wieder ein. Wechselt der Kunde vom Wartungs- ins Hosting-Abo, bleibt die Website-Analyse; die übrigen Funktionen enden. Das Gleiche gilt, wenn der Kunde den Wechsel ins Wartungs-Abo widerruft.'] },
{ stelle: '§ 14 Abs. 2', von: 'p14', bis: 'p15', texte: ['Für Funktionen, die personenbezogene Daten im Auftrag des Kunden verarbeiten, gilt zusätzlich der Auftragsverarbeitungsvertrag der Funktion, dem der Kunde beim Einschalten mit dem Klick auf „Einschalten“ zustimmt.'] },
{ stelle: '§ 15', von: 'p15', bis: 'p16', texte: ['Fassung 6 gilt für Abos, die ab ihrer Veröffentlichung abgeschlossen werden. Für Abos, die nach einer früheren Fassung abgeschlossen wurden, gelten diese Fassungen weiter. Schaltet der Kunde mit einem solchen Abo eine Funktion ein, gelten für die Funktion ihre Leistungsbeschreibung und ihr Auftragsverarbeitungsvertrag in der angezeigten Fassung, einschließlich der Regeln zur Einrichtung und zum Stundenkontingent.'] },
{ stelle: 'Leistungsbeschreibung A Nr. 1', von: 'hosting', bis: 'wartung', texte: ['Website-Analyse im Kundenportal (§ 3 Abs. 8)'] },
{ stelle: 'Leistungsbeschreibung B Nr. 1', von: 'wartung', bis: 'widerruf', texte: ['Funktionen im Kundenportal: News-Pinnwand, Terminplaner, Webshop, Buchungskalender, Öffnungszeiten live und Website-Analyse (§ 3 Abs. 8)'] },
{ stelle: 'Widerruf: Hinweise', von: 'widerruf-hinweise', bis: 'avv', texte: ['Das Einschalten einer Funktion ist kein eigener Vertrag, sondern Teil des Abos. Es verlängert die Widerrufsfrist des Abos nicht. Widerruft ein Verbraucher das Abo, endet auch die Funktion (§ 11 Abs. 4).'] },
{ stelle: 'Anlage 3', von: 'sub', bis: null, texte: ['Server für die Website-Analyse (Messdienst, nur bei eingeschalteter Funktion)', 'Deutschland (Rechenzentrum Falkenstein)'] },
]
test('Entwurf Fassung 6: Stellen in Reihenfolge, jeder Pflichtsatz im Wortlaut unter seiner Stelle', () => {
const md = lies('docs/rechtliches/abo-bedingungen-v6.md')
assert.match(md, /^# Abo-Bedingungen Fassung 6 \(Entwurf\)\n/)
assert.deepEqual([...md.matchAll(/^## (.+)$/gm)].map((m) => m[1]), [...PFLICHT.map((p) => p.stelle), 'Was gleich bleibt'])
assert.equal(md.includes('Entwurf zur Freigabe, keine Rechtsberatung'), ENTWURF)
for (const p of PFLICHT) {
const teil = md.slice(md.indexOf(`## ${p.stelle}\n`)).split(/\n## /)[0]
for (const t of p.texte) assert.ok(teil.includes(t), `${p.stelle}: ${t.slice(0, 50)}`)
assert.match(teil, /\*\*Begründung\.\*\*/, p.stelle)
}
assert.doesNotMatch(md.slice(0, md.indexOf('## Was gleich bleibt')), /€/, 'die Pflichtsätze nennen keinen Preis')
})
// Hilfen aus test/legalTexts.test.js (kein Import möglich)
const strip = (html) => html.replace(/<[^>]+>/g, ' ').replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/\s+/g, ' ').trim()
const between = (s, a, b) => {
const i = s.indexOf(a)
assert.ok(i >= 0, `Text fehlt: ${a}`)
const j = s.indexOf(b, i)
assert.ok(j > i, `Text fehlt: ${b}`)
return s.slice(i, j)
}
const V6 = 'public/neu/rechtliches/abo-bedingungen.html', V5A = 'public/vertragsunterlagen/v5/abo-bedingungen.html'
const abschnitt = (html, von, bis) => {
const a = von ? html.indexOf(`id="${von}"`) : 0
assert.ok(a >= 0, `Anker #${von}`)
const b = bis ? html.indexOf(`id="${bis}"`, a + 1) : html.length
assert.ok(b > a, `Anker #${bis}`)
return strip(html.slice(a, b))
}
test('v6-Seite: gleiche Anker wie Fassung 5, Absatzzahlen der geänderten Paragrafen', () => {
const h = lies(V6), ids = (x) => [...x.matchAll(/ id="([^"]+)"/g)].map((m) => m[1])
assert.deepEqual(ids(h), ids(lies(V5A)))
const n = (von, bis) => [...h.slice(h.indexOf(`id="${von}"`), h.indexOf(`id="${bis}"`)).matchAll(/^ <li>([\s\S]*?)<\/li>$/gm)].length
assert.deepEqual([['p1', 'p2'], ['p2', 'p3'], ['p4', 'p5'], ['p6', 'p7'], ['wechsel', 'zusatzspeicher'], ['p11', 'p12'], ['p14', 'p15']].map(([a, b]) => n(a, b)), [4, 6, 9, 6, 6, 4, 2])
assert.equal((h.slice(h.indexOf('id="p3"'), h.indexOf('id="p4"')).match(/<h3>\(\d\)/g) ?? []).length, 8)
})
test('v6-Seite: Version, Stand, Entwurfsmarke und PDF-Link passen zu termsVersionNeu', () => {
const m = /^(\d{4})-(\d{2})-(\d{2})-v6$/.exec(config.abo.termsVersionNeu); assert.ok(m)
const h = lies(V6), [, y, mo, d] = m
assert.ok(strip(h).includes(`Version 6 · Stand ${d}.${mo}.${y}`))
assert.ok(h.includes(`content: "Version 6 · Stand ${d}.${mo}.${y}"`), 'Kopfzeile im Druck')
assert.ok(!strip(h).includes('Version 5'))
assert.equal(strip(h).includes('Entwurf – Prüfung durch Kanzlei offen'), ENTWURF)
assert.ok(h.includes('href="/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf"') && !h.includes('href="/WEBklar-Abo-Vertragsunterlagen.pdf"'))
})
test('v6-Seite: jede Pflichtstelle steht im Wortlaut an ihrem Platz', () => {
const h = lies(V6)
for (const p of PFLICHT) for (const t of p.texte) assert.ok(abschnitt(h, p.von, p.bis).includes(t), `${p.stelle}: ${t.slice(0, 50)}`)
assert.ok(strip(h).includes(`Funktionen ${PFLICHT[0].texte[0]}`), 'Zeile „Funktionen“ in der Übersichtstabelle')
})
test('v6-Seite: Widerrufsbelehrung und Muster-Formular wortgleich mit Fassung 5, Abo-Preise unverändert, Funktionen ohne Preis', () => {
const h = lies(V6), v5 = lies(V5A)
const muster = (x) => between(abschnitt(x, 'widerruf', 'formular'), 'Widerrufsrecht Sie haben das Recht', 'Ende der Widerrufsbelehrung')
assert.equal(muster(h), muster(v5))
const formular = (x) => between(abschnitt(x, 'formular', 'widerruf-hinweise'), 'Muster-Widerrufsformular', '(*) Unzutreffendes streichen.')
assert.equal(formular(h), formular(v5))
for (const s of ['20,00 €', '50,00 €', '1,00 €', '100 € pro Stunde', 'zwölf Monaten']) assert.ok(strip(h).includes(s), s)
assert.doesNotMatch(abschnitt(h, 'p3', 'p4').split('(8) Funktionen')[1], /€/)
})
test('v6-Seite: neues Aussehen, keine alten Farben, Druckkopf', () => {
const style = lies(V6).match(/<style>([\s\S]*?)<\/style>/)[1]
for (const t of ['#1a1f36', '#5e6678', '#ebeef1', '#d8dde3', '#f8f9fb', '#111', '-apple-system', 'color-scheme: light']) assert.ok(style.includes(t), t)
assert.doesNotMatch(style, /#6d1fd1|#9d3dfe|gradient|prefers-color-scheme|backdrop-filter|#f7f6f3/)
})
test('v6-Seite: Querverweise treffen (16 Paragrafen, mindestens 15 Verweise, Leistungsbeschreibung A/B Nr.)', () => {
const html = lies(V6)
const ids = [...html.matchAll(/ id="([^"]+)"/g)].map((m) => m[1])
for (const [, ref] of html.matchAll(/href="#([^"]+)"/g)) assert.equal(ids.filter((i) => i === ref).length, 1, `#${ref}`)
// Absätze je Paragraf: <li> der ol.abs auf oberster Ebene; bei § 3 die Überschriften "(M)"
const absaetze = {}
for (const [, n, body] of html.matchAll(/<h2 id="[^"]+">§ (\d+) [^<]*<\/h2>([\s\S]*?)(?=<h2 |<p class="anlage-kopf)/g)) {
const h3 = [...body.matchAll(/<h3>\((\d+)\)/g)].map((x) => Number(x[1]))
if (h3.length) { absaetze[n] = Math.max(...h3); continue }
const start = body.indexOf('<ol class="abs">')
let depth = 0, count = 0
if (start >= 0) for (const [, close, tag] of body.slice(start).matchAll(/<(\/?)(ol|ul|li)\b[^>]*>/g)) {
if (tag === 'li') { if (!close && depth === 1) count++; continue }
depth += close ? -1 : 1
if (depth === 0) break
}
absaetze[n] = count
}
assert.equal(Object.keys(absaetze).length, 16)
assert.deepEqual([absaetze[3], absaetze[4], absaetze[6], absaetze[9], absaetze[11], absaetze[14], absaetze[15]], [8, 9, 6, 6, 4, 2, 2])
const text = strip(html)
const refs = [...text.matchAll(/§ (\d+) Abs\. (\d+)/g)]
assert.ok(refs.length >= 15, `Querverweise: ${refs.length}`)
for (const [ref, n, a] of refs) assert.ok(Number(a) <= (absaetze[n] ?? 0), `${ref}: § ${n} hat ${absaetze[n] ?? 0} Absätze`)
// Leistungsbeschreibung A/B Nr. N: nummerierte <h3> der jeweiligen Beschreibung
const nummern = (fromId, toId) => [...html.slice(html.indexOf(`id="${fromId}"`), html.indexOf(`id="${toId}"`)).matchAll(/<h3>(\d+)\. /g)].map((x) => Number(x[1]))
const maxA = Math.max(...nummern('hosting', 'wartung')), maxB = Math.max(...nummern('wartung', 'widerruf'))
for (const [ref, ab, nr] of text.matchAll(/Leistungsbeschreibung ([AB]) Nr\. (\d+)/g)) assert.ok(Number(nr) <= (ab === 'A' ? maxA : maxB), ref)
})
test('v6-Seite: Entwurf ist nicht indexierbar (N45)', () => {
assert.equal(lies(V6).includes('<meta name="robots" content="noindex">'), ENTWURF)
})
+121
View File
@@ -0,0 +1,121 @@
import { test, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
// Abo-Zuordnung der Funktionen (V5): aboProdukt, aboDeckt, aboLaeuftFuer. Fail-closed: Lesefehler werden weitergereicht.
const aw = await startAppwriteMock({ schema: { subscriptions: schemaKeys('subscriptions'), contractEvents: schemaKeys('contractEvents') } })
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { subscriptionDoc, wartungDoc, hostingDoc } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { ENTHALTEN_IN, aboProdukt, aboDeckt, aboLaeuftFuer } = await import('../server/services/aboFunktionen.js')
const JETZT = new Date('2026-10-17T08:00:00.000Z')
const abo = (doc) => aw.seed('subscriptions', subscriptionDoc('sub_h', doc))
const ereignis = (id, type, contractAt, over = {}) =>
aw.seed('contractEvents', { $id: id, customerId: 'c1', subscriptionId: 'sub_h', type, productKey: 'wartung', status: 'erledigt', consentAt: contractAt, details: JSON.stringify({ contractAt }), ...over })
// aw.docs() liefert eine Kopie: geloescht wird ueber die API
const loesche = (collection, id) => deleteDocument(collection, id)
beforeEach(async () => {
for (const collection of ['subscriptions', 'contractEvents']) {
for (const id of aw.docs(collection).keys()) await deleteDocument(collection, id)
}
})
const FAELLE = [ // [Name, Abo-Dokument|null, aboProdukt-Teil, aboDeckt(termine), aboDeckt(analyse)]
['kein Abo', null, { produkt: null, laeuft: false, gekuendigt: false }, 'kein_abo', 'kein_abo'],
['Wartung aktiv', wartungDoc, { produkt: 'wartung', laeuft: true, gekuendigt: false }, null, null],
['Altbestand ohne productKey', { ...wartungDoc, productKey: undefined }, { produkt: 'wartung', laeuft: true, gekuendigt: false }, null, null],
['Hosting aktiv', hostingDoc, { produkt: 'hosting', laeuft: true, gekuendigt: false }, 'falsches_abo', null],
['Wartung zum Periodenende gekündigt', { ...wartungDoc, cancelAtPeriodEnd: true }, { produkt: 'wartung', laeuft: true, gekuendigt: true }, 'gekuendigt', 'gekuendigt'],
['Wartung mit cancelAt', { ...wartungDoc, cancelAt: '2026-12-01T00:00:00.000Z' }, { produkt: 'wartung', laeuft: true, gekuendigt: true }, 'gekuendigt', 'gekuendigt'],
['Wartung past_due', { ...wartungDoc, status: 'past_due' }, { produkt: 'wartung', laeuft: true, gekuendigt: false }, 'nicht_aktiv', 'nicht_aktiv'],
['Wartung trialing ohne Probezeit-Ende', { ...wartungDoc, status: 'trialing' }, { produkt: 'wartung', laeuft: true, gekuendigt: false }, 'nicht_aktiv', 'nicht_aktiv'],
['Wartung beendet', { ...wartungDoc, status: 'canceled' }, { produkt: null, laeuft: false, gekuendigt: false }, 'kein_abo', 'kein_abo'],
]
for (const [name, doc, teil, termine, analyse] of FAELLE) {
test(`Abo-Lage: ${name}`, async () => {
if (doc) abo(doc)
assert.deepEqual(await aboProdukt('c1', { now: JETZT }), { ...teil, widerrufsfristBis: null })
assert.deepEqual(await aboDeckt('c1', 'termine'), termine ? { ok: false, grund: termine } : { ok: true })
assert.deepEqual(await aboDeckt('c1', 'analyse'), analyse ? { ok: false, grund: analyse } : { ok: true })
})
}
test('Probezeit (Privatkunde bestellte ohne Häkchen, nach_frist): beginnt_spaeter mit Datum, nicht nicht_aktiv', async () => {
const ENDE = '2026-11-03T05:00:00.000Z' // 06:00 Berlin am Tag nach dem Fristende
const probe = { ...wartungDoc, status: 'trialing', startChoice: 'nach_frist', trialEnd: ENDE }
abo(probe)
assert.deepEqual(await aboProdukt('c1', { now: JETZT }), { produkt: 'wartung', laeuft: true, gekuendigt: false, widerrufsfristBis: null })
for (const key of ['termine', 'analyse']) assert.deepEqual(await aboDeckt('c1', key, { now: JETZT }), { ok: false, grund: 'beginnt_spaeter', ab: ENDE }, key)
for (const trialEnd of [null, '', 'kaputt', '2026-10-17T07:59:59.000Z']) { // kein oder abgelaufenes Ende: wie bisher nicht_aktiv
abo({ ...probe, trialEnd })
assert.deepEqual(await aboDeckt('c1', 'termine', { now: JETZT }), { ok: false, grund: 'nicht_aktiv' }, String(trialEnd))
}
abo({ ...probe, cancelAtPeriodEnd: true })
assert.equal((await aboDeckt('c1', 'termine', { now: JETZT })).grund, 'gekuendigt')
abo({ ...hostingDoc, status: 'trialing', trialEnd: ENDE })
assert.equal((await aboDeckt('c1', 'termine', { now: JETZT })).grund, 'falsches_abo') // Reihenfolge: Abo vor Beginn
assert.deepEqual(await aboDeckt('c1', 'analyse', { now: JETZT }), { ok: false, grund: 'beginnt_spaeter', ab: ENDE })
})
test('aboLaeuftFuer: gekündigt und past_due zählen noch, falsches Abo und beendet nicht', async () => {
abo({ ...wartungDoc, cancelAtPeriodEnd: true })
assert.equal(await aboLaeuftFuer('c1', 'termine'), true)
abo({ ...wartungDoc, status: 'past_due' })
assert.equal(await aboLaeuftFuer('c1', 'termine'), true)
abo({ ...hostingDoc })
assert.deepEqual([await aboLaeuftFuer('c1', 'termine'), await aboLaeuftFuer('c1', 'analyse')], [false, true])
abo({ ...wartungDoc, status: 'canceled' })
assert.equal(await aboLaeuftFuer('c1', 'news'), false)
})
test('widerrufener Wechsel: Spiegel sagt Wartung, es gilt Hosting', async () => {
abo(wartungDoc)
ereignis('ev_w', 'wechsel', '2026-10-10T09:00:00.000Z', { status: 'widerrufen' })
assert.equal((await aboProdukt('c1')).produkt, 'hosting')
assert.deepEqual(await aboDeckt('c1', 'shop'), { ok: false, grund: 'falsches_abo' })
assert.equal(await aboLaeuftFuer('c1', 'shop'), false)
})
test('widerrufsfristBis: spätestes offenes Ende von Abschluss und Wechsel; Speicher, fremde und abgelaufene zählen nicht', async () => {
abo(wartungDoc)
ereignis('ev_a', 'abschluss', '2026-10-17T07:00:00.000Z') // Sa + 14 Tage = Sa 31.10. (Feiertag), So 01.11. (Feiertag) -> Mo 02.11.
ereignis('ev_w', 'wechsel', '2026-10-10T09:00:00.000Z') // -> Mo 26.10., 23:59:59.999 MEZ
ereignis('ev_s', 'speicher', '2026-10-16T09:00:00.000Z', { details: JSON.stringify({ contractAt: '2026-10-16T09:00:00.000Z', fromUnits: 0, toUnits: 1 }) })
ereignis('ev_f', 'wechsel', '2026-10-15T09:00:00.000Z', { subscriptionId: 'sub_alt', status: 'widerrufen' })
assert.equal((await aboProdukt('c1', { now: JETZT })).widerrufsfristBis, '2026-11-02T22:59:59.999Z')
await loesche('contractEvents', 'ev_a')
assert.equal((await aboProdukt('c1', { now: JETZT })).widerrufsfristBis, '2026-10-26T22:59:59.999Z')
ereignis('ev_alt', 'abschluss', '2026-09-20T09:00:00.000Z')
await loesche('contractEvents', 'ev_w') // Frist endete Mo 05.10.
assert.equal((await aboProdukt('c1', { now: JETZT })).widerrufsfristBis, null)
})
test('widerrufsfristBis: offene Frist eines anderen Abos zählt nicht', async () => {
abo(wartungDoc)
ereignis('ev_x', 'abschluss', '2026-10-16T09:00:00.000Z', { subscriptionId: 'sub_alt' })
assert.equal((await aboProdukt('c1', { now: JETZT })).widerrufsfristBis, null)
})
test('Form: ENTHALTEN_IN eingefroren und genau die Schlüssel von V5; unbekannte Funktion ist ein Programmfehler', async () => {
assert.ok(Object.isFrozen(ENTHALTEN_IN))
assert.deepEqual(Object.keys(ENTHALTEN_IN), ['analyse', 'news', 'termine', 'shop', 'unterkunft', 'zeiten'])
assert.deepEqual(ENTHALTEN_IN, { analyse: ['hosting', 'wartung'], news: ['wartung'], termine: ['wartung'], shop: ['wartung'], unterkunft: ['wartung'], zeiten: ['wartung'] })
assert.deepEqual(Object.keys(await aboProdukt('c1')).sort(), ['gekuendigt', 'laeuft', 'produkt', 'widerrufsfristBis'])
await assert.rejects(() => aboDeckt('c1', 'gibtEsNicht'), TypeError)
await assert.rejects(() => aboLaeuftFuer('c1', 'gibtEsNicht'), TypeError)
})
test('fail-closed: ein Lesefehler wird weitergereicht, nie zu kein_abo', async () => {
abo(wartungDoc)
aw.failNext({ collection: 'subscriptions', method: 'GET', status: 500 })
await assert.rejects(() => aboDeckt('c1', 'termine'))
aw.failNext({ collection: 'contractEvents', method: 'GET', status: 500 })
await assert.rejects(() => aboProdukt('c1', { now: JETZT }))
})
+133
View File
@@ -0,0 +1,133 @@
import { test, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
// Abo-Ende-Abgleich der Funktionen des Abo-Modells (P6 Task 8): News, Terminplaner, Webshop und Buchungskalender enden mit dem Abo
// (K39), kehren mit einem neuen Abo zurück, solange die Daten da sind. Aufbauphase (E9, N28): projekt ohne jedes Abo bleibt.
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
termineConfig: schemaKeys('termineConfig'),
newsPosts: schemaKeys('newsPosts'),
},
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { customers, hostingDoc, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { deleteDocument, updateDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { datumKurz } = await import('../server/services/termine/zeit.js')
const { gleicheAboFunktionenAb } = await import('../server/services/aboFunktionenAbgleich.js')
const mails = mailSink()
setMailTransportForTests(mails)
after(() => {
setMailTransportForTests(null)
return Promise.all([aw.close(), st.close()])
})
const JETZT = new Date('2026-10-17T08:00:00.000Z'), TAG = 86400000, KEYS = ['news', 'termine', 'shop', 'unterkunft']
const NAMEN = { news: 'News-Pinnwand', termine: 'Terminplaner', shop: 'Webshop', unterkunft: 'Buchungskalender' }
const seed = (key, over = {}) => aw.seed('portalFeatures', { $id: `f_p1_${key}`, customerId: 'c1', projectId: 'p1', featureKey: key, siteKey: 'abcdefghijklmnop', state: 'active', enabled: true, activatedAt: '2026-10-01T09:00:00.000Z', unlockedBy: 'abo', unlockedByPurchase: false, einrichtungAbrechnung: 'abo', ...over })
const f = (key) => aw.docs('portalFeatures').get(`f_p1_${key}`)
const abo = (doc) => aw.seed('subscriptions', subscriptionDoc('sub_h', doc))
const k39 = (key) => mails.sent.find((m) => m.subject === `Deine Funktion wurde beendet: ${NAMEN[key]}`)
// aw.docs() liefert eine Kopie: Dokumente werden ueber die API geloescht
const leere = async (...collections) => {
for (const col of collections) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
}
seedFixtures(aw)
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei' })
beforeEach(async () => {
await leere('portalFeatures', 'subscriptions', 'contractEvents', 'termineConfig', 'newsPosts')
mails.sent.length = 0
setMailTransportForTests(mails)
aw.seed('customers', customers.c1)
})
for (const [name, doc] of [['aktiv', wartungDoc], ['zum Periodenende gekündigt', { ...wartungDoc, cancelAtPeriodEnd: true }], ['past_due', { ...wartungDoc, status: 'past_due' }]]) test(`Wartung ${name}: nichts wird abgeschaltet`, async () => {
abo(doc); KEYS.forEach((k) => seed(k)); const r = await gleicheAboFunktionenAb(JETZT)
assert.deepEqual([r.geprueft, r.abgeschaltet, r.mails, r.fehler, mails.sent.length], [4, 0, 0, 0, 0]); KEYS.forEach((k) => assert.equal(f(k).state, 'active'))
})
test('Abo beendet: alle vier abgeschaltet (setup zählt wie active), K39 je Funktion mit eigener Frist, Marker; zweiter Lauf tut nichts', async () => {
abo({ ...wartungDoc, status: 'canceled' }); KEYS.forEach((k) => seed(k)); seed('news', { state: 'setup', enabled: false, activatedAt: null })
const r = await gleicheAboFunktionenAb(JETZT); assert.deepEqual([r.abgeschaltet, r.mails, r.fehler], [4, 4, 0])
for (const k of KEYS) {
const d = f(k); assert.deepEqual([d.state, d.enabled, d.disabledReason], ['disabled', false, 'abo_ende']); assert.ok(d.aboEndeMailAt && d.disabledAt)
const tag = datumKurz(new Date(Date.parse(d.disabledAt) + (k === 'shop' || k === 'unterkunft' ? 90 : 30) * TAG)); assert.ok(k39(k).text.includes(tag), k)
assert.equal(/CSV/.test(k39(k).text), k === 'termine'); assert.equal(/Offene Bestellungen/.test(k39(k).text), k === 'shop' || k === 'unterkunft')
}
const vorher = f('shop').disabledAt; const noch = await gleicheAboFunktionenAb(new Date(JETZT.getTime() + TAG)); assert.deepEqual([noch.abgeschaltet, noch.mails, mails.sent.length, f('shop').disabledAt], [0, 0, 4, vorher])
})
test('Hosting statt Wartung: K39 mit Hosting-Satz', async () => { abo(hostingDoc); seed('shop'); await gleicheAboFunktionenAb(JETZT); assert.match(k39('shop').text, /ein Hosting-Abo reicht dafür nicht/) })
test('Abo nicht lesbar: nichts abgeschaltet, fehler gezählt, Log ohne Adresse', async (t) => {
abo({ ...wartungDoc, status: 'canceled' }); seed('news'); const log = t.mock.method(console, 'error', () => {}); aw.failNext({ collection: 'subscriptions', method: 'GET', status: 500, message: 'k@example.invalid kaputt' })
const r = await gleicheAboFunktionenAb(JETZT); assert.ok(r.fehler >= 1); assert.equal(f('news').state, 'active'); assert.equal(mails.sent.length, 0); assert.ok(log.mock.calls.every((c) => !String(c.arguments[0]).includes('@')))
})
test('Rückkehr: nur solange die Daten da sind; ohne activatedAt zurück auf setup; ohne Mail', async () => {
abo(wartungDoc); const aus = { state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: '2026-10-10T09:00:00.000Z', aboEndeMailAt: '2026-10-10T09:05:00.000Z' }
seed('news', aus); seed('termine', { ...aus, activatedAt: null }); seed('shop', aus); seed('unterkunft', aus)
const r = await gleicheAboFunktionenAb(JETZT); assert.deepEqual([r.wiederEin, mails.sent.length], [3, 0]) // termine: keine Einstellung gespeichert → bleibt
assert.deepEqual([f('news').state, f('news').disabledReason, f('news').aboEndeMailAt, f('shop').state, f('unterkunft').state, f('termine').state], ['active', null, null, 'active', 'active', 'disabled'])
aw.seed('termineConfig', { $id: 'f_p1_termine', einstellung: '{}', version: 1 }); await gleicheAboFunktionenAb(JETZT); assert.equal(f('termine').state, 'setup') // ohne activatedAt: setup, nicht active
})
test('Rückkehr des Webshops und des Buchungskalenders nie nach der Löschung der Daten: das Dokument wird off (Task 9)', async () => {
abo(wartungDoc); const aus = { state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: '2026-07-10T09:00:00.000Z', aboEndeMailAt: '2026-07-10T09:05:00.000Z' }
seed('shop', { ...aus, adminNote: '2026-10-10T09:00:00.000Z System: Shopdaten gelöscht am 2026-10-10' }); seed('unterkunft', { ...aus, adminNote: 'Buchungsdaten gelöscht am 2026-10-10' })
const r = await gleicheAboFunktionenAb(JETZT); assert.deepEqual([r.wiederEin, r.geloescht, f('shop').state, f('unterkunft').state], [0, 2, 'off', 'off'])
})
test('Alte Dokumente bleiben unberührt; Kunde ohne Adresse setzt nur den Marker; Mailfehler: Marker leer, nächster Lauf sendet, disabledAt bleibt', async (t) => {
t.mock.method(console, 'error', () => {}); t.mock.method(console, 'warn', () => {})
abo({ ...wartungDoc, status: 'canceled' }); seed('news', { unlockedBy: 'kauf', unlockedByPurchase: true, einrichtungAbrechnung: undefined }); seed('termine', { einrichtungAbrechnung: '' }); seed('shop', { unlockedBy: 'team' })
assert.deepEqual([(await gleicheAboFunktionenAb(JETZT)).geprueft, f('news').state, f('termine').state, f('shop').state], [0, 'active', 'active', 'active'])
seed('unterkunft'); aw.seed('customers', { ...customers.c1, email: '' }); await gleicheAboFunktionenAb(JETZT); assert.deepEqual([f('unterkunft').state, Boolean(f('unterkunft').aboEndeMailAt), mails.sent.length], ['disabled', true, 0])
aw.seed('customers', customers.c1); seed('news'); setMailTransportForTests({ sendMail: async () => { throw new Error('smtp') } })
const bad = await gleicheAboFunktionenAb(JETZT); const am = f('news').disabledAt; assert.deepEqual([bad.fehler, Boolean(f('news').aboEndeMailAt)], [1, false])
setMailTransportForTests(mails); const ok = await gleicheAboFunktionenAb(JETZT); assert.deepEqual([ok.mails, f('news').disabledAt, Boolean(f('news').aboEndeMailAt)], [1, am, true])
})
test('Projektstart-Funktionen (projekt): ohne jedes Abo unberührt (Aufbauphase), mit deckendem Abo auch, mit dem ersten nicht deckenden Abo gilt die Abo-Regel', async () => {
KEYS.forEach((k) => seed(k, { unlockedBy: 'projekt', einrichtungAbrechnung: 'projekt' }))
const leer = await gleicheAboFunktionenAb(JETZT); assert.deepEqual([leer.abgeschaltet, leer.mails, leer.fehler, mails.sent.length], [0, 0, 0, 0]); KEYS.forEach((k) => assert.equal(f(k).state, 'active')) // kein Abo-Dokument
abo({ ...wartungDoc, status: 'incomplete' }); assert.equal((await gleicheAboFunktionenAb(JETZT)).abgeschaltet, 0) // eine abgebrochene Bestellung ist kein Abo
await leere('subscriptions'); abo(wartungDoc); assert.equal((await gleicheAboFunktionenAb(JETZT)).abgeschaltet, 0) // deckendes Abo
await leere('subscriptions'); abo(hostingDoc); const h = await gleicheAboFunktionenAb(JETZT) // das erste Abo deckt nicht: Abo-Regel
assert.deepEqual([h.abgeschaltet, h.mails], [4, 4]); KEYS.forEach((k) => assert.equal(f(k).disabledReason, 'abo_ende')); assert.match(k39('shop').text, /ein Hosting-Abo reicht dafür nicht/)
})
test('abo-Dokumente kennen keine Aufbauphase: ohne jedes Abo-Dokument werden sie wie bisher abgeschaltet', async () => {
seed('news'); const r = await gleicheAboFunktionenAb(JETZT); assert.deepEqual([r.abgeschaltet, f('news').state], [1, 'disabled'])
})
// Task 9: News-Löschschritt und Zurücksetzen nach der Löschung
test('News: nach 30 Tagen werden Beiträge samt Bild gelöscht und das Dokument ist off; vorher und mit laufendem Abo nicht', async () => {
abo({ ...wartungDoc, status: 'canceled' }); const aus = { state: 'disabled', enabled: false, disabledReason: 'abo_ende', aboEndeMailAt: '2026-09-01T00:00:00.000Z' }
seed('news', { ...aus, disabledAt: new Date(JETZT.getTime() - 29 * TAG).toISOString() })
aw.seed('newsPosts', { $id: 'np1', customerId: 'c1', projectId: 'p1', title: 'A', imageId: null, visibleFrom: '2026-09-01T00:00:00.000Z', visibleUntil: null }); aw.seed('newsPosts', { $id: 'np2', customerId: 'c1', projectId: 'p_anders', title: 'B', imageId: null, visibleFrom: '2026-09-01T00:00:00.000Z', visibleUntil: null })
assert.equal((await gleicheAboFunktionenAb(JETZT)).geloescht, 0); assert.equal(aw.docs('newsPosts').size, 2)
await updateDocument('portalFeatures', 'f_p1_news', { disabledAt: new Date(JETZT.getTime() - 31 * TAG).toISOString() })
assert.equal((await gleicheAboFunktionenAb(JETZT)).geloescht, 1); assert.deepEqual([...aw.docs('newsPosts').keys()], ['np2'])
assert.deepEqual([f('news').state, f('news').disabledAt, f('news').aboEndeMailAt], ['off', null, null])
seed('news', { ...aus, disabledAt: new Date(JETZT.getTime() - 40 * TAG).toISOString() }); aw.seed('subscriptions', subscriptionDoc('sub_h', wartungDoc)) // Abo kehrt zurück: Rückkehr statt Löschung
aw.seed('newsPosts', { $id: 'np3', customerId: 'c1', projectId: 'p1', title: 'C', imageId: null, visibleFrom: '2026-09-01T00:00:00.000Z', visibleUntil: null })
await gleicheAboFunktionenAb(JETZT); assert.deepEqual([f('news').state, aw.docs('newsPosts').has('np3')], ['active', true])
})
test('News: ein Fehler beim Löschen lässt das Dokument disabled und zählt fehler', async (t) => {
abo({ ...wartungDoc, status: 'canceled' }); seed('news', { state: 'disabled', enabled: false, disabledReason: 'abo_ende', aboEndeMailAt: 'x', disabledAt: new Date(JETZT.getTime() - 31 * TAG).toISOString() })
aw.seed('newsPosts', { $id: 'np1', customerId: 'c1', projectId: 'p1', title: 'A', imageId: null, visibleFrom: '2026-09-01T00:00:00.000Z', visibleUntil: null }); t.mock.method(console, 'error', () => {}); aw.failNext({ collection: 'newsPosts', method: 'DELETE', status: 500 })
const r = await gleicheAboFunktionenAb(JETZT); assert.deepEqual([r.fehler, f('news').state], [1, 'disabled']); assert.equal((await gleicheAboFunktionenAb(JETZT)).geloescht, 1)
})
test('Shop und Unterkunft: nach der Löschung durch die bestehenden Jobs wird das Dokument off (auch bei laufendem Abo); ohne Marker nichts', async () => {
abo(wartungDoc); const aus = { state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: '2026-06-01T00:00:00.000Z', aboEndeMailAt: '2026-06-01T00:05:00.000Z' }
seed('shop', { ...aus, adminNote: 'Shopdaten gelöscht am 2026-09-01' }); seed('unterkunft', { ...aus, adminNote: 'Buchungsdaten gelöscht am 2026-09-01' }); seed('termine', { ...aus })
const r = await gleicheAboFunktionenAb(JETZT); assert.equal(r.geloescht, 2); assert.deepEqual([f('shop').state, f('unterkunft').state, f('shop').aboEndeMailAt], ['off', 'off', null])
assert.deepEqual([f('termine').state, f('shop').adminNote], ['disabled', '']) // Terminplaner: setzt termine/jobs.js selbst auf off; der Löschvermerk endet mit dem Zurücksetzen
})
test('News: ohne Marker und nach Ablauf der Frist wird gelöscht, ohne noch einmal K39 zu senden', async () => {
abo({ ...wartungDoc, status: 'canceled' }); seed('news', { state: 'disabled', enabled: false, disabledReason: 'abo_ende', disabledAt: new Date(JETZT.getTime() - 35 * TAG).toISOString(), aboEndeMailAt: null })
const r = await gleicheAboFunktionenAb(JETZT); assert.deepEqual([r.geloescht, r.mails, f('news').state, mails.sent.length], [1, 0, 'off', 0])
})
+78
View File
@@ -0,0 +1,78 @@
import { test, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
// Koexistenz (P6 Task 10): Dokumente des Abo-Modells (unlockedBy UND einrichtungAbrechnung) gehören dem neuen Abgleich; der alte
// Terminplaner-Abgleich fasst sie nicht an. Zeiten koppeln auch projekt (außer in der Aufbauphase) und senden K40 und K41 ohne Preis.
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
termineConfig: schemaKeys('termineConfig'),
oeffnungszeiten: schemaKeys('oeffnungszeiten'),
zeitenErinnerungen: schemaKeys('zeitenErinnerungen'),
},
datetimes: {
...ZEITEN_DATETIMES,
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
},
defaultLimit: 25,
})
after(() => aw.close())
useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' })
const { seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { gleicheAboAb, endeTermineFuerKunde } = await import('../server/services/termine/aboAbgleich.js')
const { gleicheZeitenAboAb } = await import('../server/services/zeiten/aboAbgleich.js')
const mails = mailSink()
setMailTransportForTests(mails)
after(() => setMailTransportForTests(null))
seedFixtures(aw)
// aw.docs() liefert eine Kopie: Dokumente werden ueber die API geloescht
const leere = async (...collections) => {
for (const col of collections) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
}
beforeEach(async () => {
await leere('portalFeatures', 'subscriptions', 'oeffnungszeiten', 'zeitenErinnerungen')
mails.sent.length = 0
})
const modell = { unlockedBy: 'abo', unlockedByPurchase: false, einrichtungAbrechnung: 'abo' }
test('Terminplaner-Abgleich überspringt Abo-Modell-Dokumente in jedem Zustand, auch ohne Wartungs-Abo; Kauf-Dokumente werden weiter abgeschaltet', async () => {
// kein Abo; Abo-Modell: active, setup, disabled/abo_ende (ohne Marker); dazu ein gekaufter, aktiver Terminplaner
for (const [id, state, extra] of [['f_a', 'active', {}], ['f_s', 'setup', {}], ['f_d', 'disabled', { disabledReason: 'abo_ende', disabledAt: new Date().toISOString() }]]) aw.seed('portalFeatures', { $id: id, customerId: 'c1', projectId: `p_${id}`, featureKey: 'termine', state, enabled: state === 'active', ...modell, ...extra })
aw.seed('portalFeatures', { $id: 'f_k', customerId: 'c1', projectId: 'p_k', featureKey: 'termine', state: 'active', enabled: true, unlockedBy: 'kauf', unlockedByPurchase: true })
const r = await gleicheAboAb(new Date()); assert.deepEqual([r.abgeschaltet, r.ausgenommen], [1, 0]); assert.deepEqual(['f_a', 'f_s', 'f_d', 'f_k'].map((id) => aw.docs('portalFeatures').get(id).state), ['active', 'setup', 'disabled', 'disabled'])
assert.deepEqual((await endeTermineFuerKunde('c1')).beendet, []); assert.equal(mails.sent.filter((m) => /^Dein Terminplaner wurde beendet/.test(m.subject)).length, 1) // nur der Kauf
})
const T0 = new Date('2026-10-17T08:00:00.000Z'), TAG = 86400000 // Schritt 1 erkennt das Abo-Ende, Schritt 2 schaltet nach der 30-Tage-Frist ab (ohne gespeicherte Zeiten, also ohne Ersatz-Ticket)
const zeit = (id, over) => aw.seed('portalFeatures', { $id: id, customerId: 'c1', projectId: `p_${id}`, featureKey: 'zeiten', siteKey: `zk${id}`.padEnd(16, '2').slice(0, 16), state: 'active', enabled: true, activatedAt: '2026-10-01T09:00:00.000Z', unlockedByPurchase: false, einbau: 'angestossen', ticketId: 't1', einschaltMailAt: '2026-10-01T09:05:00.000Z', ...over })
const zdoc = (id) => aw.docs('portalFeatures').get(id)
const beendetesAbo = () => aw.seed('subscriptions', subscriptionDoc('sub_h', { ...wartungDoc, status: 'canceled' }))
const k40Mails = () => mails.sent.filter((m) => m.subject.startsWith('Dein Abo ist beendet – Öffnungszeiten live läuft noch bis'))
test('Zeiten: Abo-Modell (abo und projekt) bekommt K40 und K41 ohne Preis, Gratis-Zeiten ohne Marker weiter K29 und K30 mit 30 €', async () => {
beendetesAbo() // das Wartungs-Abo des Kunden ist beendet (mit einem Abo-Dokument: sonst gälte für projekt die Aufbauphase, siehe nächster Test)
zeit('z_abo', { unlockedBy: 'abo', einrichtungAbrechnung: 'abo' }); zeit('z_projekt', { unlockedBy: 'projekt', einrichtungAbrechnung: 'projekt' }); zeit('z_alt', { unlockedBy: 'abo' })
await gleicheZeitenAboAb(T0)
const k40 = k40Mails(); const k29 = mails.sent.filter((m) => m.subject.startsWith('Dein Wartungs-Abo ist beendet'))
assert.deepEqual([k40.length, k29.length], [2, 1]); assert.ok(k40.every((m) => !/€|behalten/i.test(m.text))); assert.match(k29[0].text, /30 €/)
await gleicheZeitenAboAb(new Date(T0.getTime() + 31 * TAG))
assert.deepEqual(['z_abo', 'z_projekt', 'z_alt'].map((id) => zdoc(id).disabledReason), ['abo_ende', 'abo_ende', 'abo_ende'])
const k41 = mails.sent.filter((m) => m.subject === 'Öffnungszeiten live ist abgeschaltet'); assert.equal(k41.length, 3); assert.equal(k41.filter((m) => /30 €/.test(m.text)).length, 1) // nur das Gratis-Dokument nennt noch den Preis
})
test('Zeiten, Aufbauphase (E9): projekt ohne jedes Abo bleibt unberührt, abo nicht; mit dem ersten beendeten Abo gilt die Abo-Regel auch für projekt', async () => {
zeit('z_projekt', { unlockedBy: 'projekt', einrichtungAbrechnung: 'projekt' }); zeit('z_abo', { unlockedBy: 'abo', einrichtungAbrechnung: 'abo' }) // kein Abo-Dokument
const r = await gleicheZeitenAboAb(T0); assert.equal(r.fehler, 0)
assert.deepEqual([Boolean(zdoc('z_projekt').aboEndeAt), Boolean(zdoc('z_abo').aboEndeAt), k40Mails().length], [false, true, 1]) // nur z_abo erkennt ein Abo-Ende
await gleicheZeitenAboAb(new Date(T0.getTime() + 31 * TAG)); assert.deepEqual([zdoc('z_projekt').state, zdoc('z_projekt').disabledReason ?? null], ['active', null]) // auch nach 31 Tagen
aw.seed('subscriptions', subscriptionDoc('sub_i', { ...wartungDoc, status: 'incomplete' })); await gleicheZeitenAboAb(T0); assert.equal(Boolean(zdoc('z_projekt').aboEndeAt), false) // abgebrochene Bestellung ist kein Abo
await leere('subscriptions'); beendetesAbo(); await gleicheZeitenAboAb(T0); assert.deepEqual([Boolean(zdoc('z_projekt').aboEndeAt), k40Mails().length], [true, 2])
})
+159
View File
@@ -0,0 +1,159 @@
import { test, after, beforeEach, afterEach, mock } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
import { ZEITEN_DATETIMES } from '../server/services/zeiten/schema.js'
// GET /api/funktionen im Abo-Modell (V5): modell, kundenart, abo, katalog und anfrageStatus je Projekt; ohne neue Ansicht unveraendert
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
featureConsents: schemaKeys('featureConsents'),
shopConfig: schemaKeys('shopConfig'),
ukEinstellungen: schemaKeys('ukEinstellungen'),
oeffnungszeiten: schemaKeys('oeffnungszeiten'),
zeitenErinnerungen: schemaKeys('zeitenErinnerungen'),
},
datetimes: {
...ZEITEN_DATETIMES,
portalFeatures: ['orderedAt', 'activatedAt', 'disabledAt', 'consentAt', 'aboEndeMailAt', 'aboEndeAt', 'einschaltMailAt'],
featureConsents: ['acceptedAt'],
},
defaultLimit: 25,
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, get } = await import('./support/testApp.js')
const { customerCookie, customers, kundenansichtCookie, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { default: funktionenRoutes } = await import('../server/routes/funktionen.js')
const { resetZeitenCacheForTests } = await import('../server/services/zeiten/cache.js')
const { einschaltenDeps } = await import('../server/services/zeiten/einschalten.js')
const app = await startTestApp({ '/api/funktionen': funktionenRoutes })
after(() => Promise.all([app.close(), aw.close(), st.close()]))
after(() => setMailTransportForTests(null))
const JETZT = new Date('2026-10-17T08:00:00.000Z') // ein Samstag
const c1 = customerCookie('c1')
const vorher = {
neu: { mode: config.kundenansichtNeu.mode, pilotEmails: [...config.kundenansichtNeu.pilotEmails] },
termine: config.termine.mode,
shop: { mode: config.shop.mode, pilotEmails: [...config.shop.pilotEmails] },
zeiten: { mode: config.zeiten.mode, pilotEmails: [...config.zeiten.pilotEmails] },
analyse: config.analyse.enabled,
}
after(() => {
Object.assign(config.kundenansichtNeu, vorher.neu)
config.termine.mode = vorher.termine
Object.assign(config.shop, vorher.shop)
Object.assign(config.zeiten, vorher.zeiten)
config.analyse.enabled = vorher.analyse
})
const abschluss = (contractAt = JETZT.toISOString()) =>
aw.seed('contractEvents', { $id: 'ev_a', customerId: 'c1', subscriptionId: 'sub_h', type: 'abschluss', productKey: 'wartung', status: 'erledigt', consentAt: contractAt, details: JSON.stringify({ contractAt }) })
// aw.docs() liefert eine Kopie: Dokumente werden ueber die API geloescht
const leere = async (...collections) => {
for (const col of collections) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
}
const projekt = (id, over = {}) => aw.seed('websiteProjects', { $id: id, customerId: 'c1', projectName: `Projekt ${id}`, subdomain: id, repoFullName: `JUSN/${id}`, previewMode: 'auto', previewDetected: 'static', ...over })
beforeEach(async () => {
await leere('subscriptions', 'portalFeatures', 'featureConsents', 'oeffnungszeiten', 'workorders', 'websiteProjects', 'contractEvents', 'shopConfig', 'ukEinstellungen')
resetZeitenCacheForTests()
seedFixtures(aw)
aw.seed('subscriptions', subscriptionDoc('sub_h', wartungDoc))
projekt('p1')
projekt('p2')
config.kundenansichtNeu.mode = 'pilot'
config.kundenansichtNeu.pilotEmails = ['k@example.invalid', 'test@example.invalid']
config.termine.mode = 'on'
config.shop.mode = 'on'
config.zeiten.mode = 'pilot'
config.zeiten.pilotEmails = ['*']
config.analyse.enabled = false
einschaltenDeps.jetzt = () => JETZT
setMailTransportForTests(mailSink())
mock.method(console, 'error', () => {})
mock.method(console, 'warn', () => {})
mock.method(console, 'log', () => {})
})
afterEach(() => {
mock.restoreAll()
einschaltenDeps.jetzt = () => new Date()
})
const OLD_KEYS = ['consentText', 'features', 'legal', 'projects', 'setupEstimate', 'shop', 'termine', 'unterkunft', 'ustHinweis', 'zeiten']
test('ohne neue Ansicht: Top-Level-Schlüssel unverändert, keine Anfragestatus', async () => {
config.kundenansichtNeu.mode = 'off'
const b = await get('/api/funktionen', { cookie: c1 })
assert.deepEqual(Object.keys(b).sort(), OLD_KEYS)
assert.equal(Object.hasOwn(b.projects[0].features.shop, 'anfrageStatus'), false)
config.kundenansichtNeu.mode = 'pilot'
config.kundenansichtNeu.pilotEmails = ['k2@example.invalid'] // Kunde nicht in der Liste
assert.deepEqual(Object.keys(await get('/api/funktionen', { cookie: c1 })).sort(), OLD_KEYS)
})
test('mit neuer Ansicht: Modell, Kundenart, Abo, Katalog in fester Reihenfolge', async () => {
abschluss() // offener Abschluss vom 17.10.
const b = await get('/api/funktionen', { cookie: c1 })
assert.deepEqual([b.modell, b.kundenart, b.abo], ['abo', 'privat', { produkt: 'wartung', laeuft: true, gekuendigt: false, widerrufsfristBis: '2026-11-02T22:59:59.999Z' }])
aw.seed('customers', { ...customers.c1, kundenart: 'unternehmen' }) // Kundenart aus dem Dokument, auch in der Mitarbeiter-Kundenansicht
assert.equal((await get('/api/funktionen', { cookie: kundenansichtCookie({ customerId: 'c1' }) })).kundenart, 'unternehmen')
aw.seed('customers', customers.c1)
assert.deepEqual(b.katalog.map((e) => e.key), ['news', 'termine', 'shop', 'unterkunft', 'zeiten']) // analyse nur bei ANALYSE_ENABLED
config.analyse.enabled = true
const mit = await get('/api/funktionen', { cookie: c1 })
assert.equal(mit.katalog.at(-1).key, 'analyse')
assert.equal(mit.katalog.at(-1).enthaltenIn, 'hosting')
const t = b.katalog.find((e) => e.key === 'termine')
assert.deepEqual(Object.keys(t).sort(), ['ablauf', 'beschreibung', 'bild', 'consent', 'einrichtung', 'enthaltenIn', 'hinweis', 'key', 'name', 'nurBetrieb', 'unterlagen', 'verfuegbar'])
assert.deepEqual([t.name, t.enthaltenIn, t.ablauf, t.bild, t.nurBetrieb, t.consent.items.map((i) => i.id)], ['Terminplaner', 'wartung', 'fragebogen', '/neu/bilder/termine.webp', false, ['einschalten-zeile', 'hinweis', 'beginn']])
assert.deepEqual(t.consent.items.at(-1), { id: 'beginn', text: config.aboFunktionen.texte.beginn, haken: true, nurPrivatInFrist: true })
assert.deepEqual([t.consent.version, t.consent.knopf], [config.aboFunktionen.fassung, 'Einschalten'])
const hinweise = Object.fromEntries(b.katalog.map((e) => [e.key, e.hinweis])) // Pflichten der alten Häkchen: Server liefert den Satz, aus demselben Item wie der Nachweis
assert.deepEqual(Object.keys(hinweise).filter((k) => hinweise[k] !== null), ['termine', 'shop', 'unterkunft'])
assert.deepEqual([hinweise.shop, hinweise.termine], [b.katalog.find((e) => e.key === 'shop').consent.items[1].text, t.consent.items[1].text])
assert.match(hinweise.shop, /eigenem Namen.*eigenes Stripe-Konto/)
assert.deepEqual([hinweise.news, hinweise.zeiten, mit.katalog.at(-1).hinweis], [null, null, null])
assert.equal(mit.katalog.at(-1).consent, null)
assert.equal(b.katalog.find((e) => e.key === 'shop').nurBetrieb, true)
assert.doesNotMatch(JSON.stringify(b.katalog), /€|priceLabel/)
assert.deepEqual(t.unterlagen.leistungsbeschreibung, { version: config.termine.avvVersion, url: config.features.termine.leistungsbeschreibung.url })
assert.equal(mit.katalog.at(-1).unterlagen.avv.url, `/rechtliches/avv-website-analyse-${config.analyse.avvVersion}.html`)
config.analyse.enabled = false
})
test('verfuegbar folgt dem Schalter je Funktion; nicht verfügbare stehen trotzdem im Katalog', async () => {
config.termine.mode = 'off'
config.shop.mode = 'pilot'
config.shop.pilotEmails = ['k2@example.invalid']
const katalog = (await get('/api/funktionen', { cookie: c1 })).katalog
const v = Object.fromEntries(katalog.map((e) => [e.key, e.verfuegbar]))
assert.deepEqual([v.news, v.termine, v.shop], [true, false, false])
assert.equal(katalog.length, 5)
})
test('anfrageStatus je Projekt für Shop und Buchungskalender', async () => {
aw.seed('portalFeatures', { $id: 'f_p1_shop', customerId: 'c1', projectId: 'p1', featureKey: 'shop', siteKey: 'abcdefghijklmnop', state: 'off', enabled: false })
aw.seed('shopConfig', { $id: 'f_p1_shop', customerId: 'c1', projectId: 'p1', siteKey: 'abcdefghijklmnop', anfrageStatus: 'offen', version: 0 })
aw.seed('portalFeatures', { $id: 'f_p1_unterkunft', customerId: 'c1', projectId: 'p1', featureKey: 'unterkunft', siteKey: 'bbcdefghijklmnop', state: 'off', enabled: false })
aw.seed('ukEinstellungen', { $id: 'f_p1_unterkunft', customerId: 'c1', projectId: 'p1', anfrageStatus: 'angenommen' })
const b = await get('/api/funktionen', { cookie: c1 })
const f = (id) => b.projects.find((p) => p.projectId === id).features
assert.deepEqual([f('p1').shop.anfrageStatus, f('p1').unterkunft.anfrageStatus, f('p2').shop.anfrageStatus, f('p2').unterkunft.anfrageStatus], ['offen', 'angenommen', '', ''])
assert.equal(Object.hasOwn(f('p1').news, 'anfrageStatus'), false)
// ein Lesefehler einer Konfiguration kostet die Liste nichts: '' statt Fehler
aw.failNext({ collection: 'shopConfig', method: 'GET', status: 500 })
const c = await get('/api/funktionen', { cookie: c1 })
assert.equal(c.projects.find((p) => p.projectId === 'p1').features.shop.anfrageStatus, '')
})
+55
View File
@@ -0,0 +1,55 @@
import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
// Builder der Abo-Modell-Mails K38, K39, T24 (server/services/aboFunktionenMails.js) und die Zeiten-Varianten K40, K41
// (k29Mail und k30Mail mit modell: true). Die Vorlagen prüft aboFunktionenVorlagen.test.js, der Versand folgt in Task 6 und 8.
const aw = await startAppwriteMock()
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { renderMail } = await import('../server/services/mailTemplates.js')
const { anleitungsLink } = await import('../server/services/featureRegistry.js')
const { anfrageEntschiedenMail, k38Mail, k39Mail, t24Mail, loeschFristTage, kontostandText } = await import('../server/services/aboFunktionenMails.js')
const { k29Mail, k30Mail } = await import('../server/services/zeiten/mails.js')
test('k38Mail: Werte und Blöcke je Abrechnung', () => {
const m = k38Mail({ kunde: { name: 'Silke Müller' }, projekt: 'Bäckerei', key: 'termine', abrechnung: 'abo', ticketNr: '#100150', anhang: true })
assert.equal(m.template, 'K38'); assert.deepEqual(m.blocks, { stundenkonto: true, projekt_kosten: false, anhang: true })
assert.deepEqual(m.values, { kunde_name: 'Silke Müller', projekt: 'Bäckerei', funktion: 'Terminplaner', funktion_akk: 'den Terminplaner', einrichtung: 'meist in 1–2 Wochen', ticket: ' (Ticket #100150)', fassung: config.features.termine.leistungsbeschreibung.version, portal_url: `${config.publicBaseUrl}/dashboard.html#termine` })
assert.deepEqual(k38Mail({ kunde: 'K', projekt: 'P', key: 'shop', abrechnung: 'projekt' }).blocks, { stundenkonto: false, projekt_kosten: true, anhang: false })
assert.equal(k38Mail({ kunde: 'K', projekt: 'P', key: 'news', abrechnung: 'abo' }).values.ticket, '')
})
test('k39Mail: Frist, Grund und Zusatzblöcke je Funktion', () => {
const ende = new Date('2026-11-16T12:00:00.000Z')
assert.deepEqual(k39Mail({ kunde: 'K', projekt: 'P', key: 'termine', loeschdatum: ende, art: 'beendet' }).blocks, { abo_ende: true, hosting: false, termine: true, offene: false })
assert.deepEqual(k39Mail({ kunde: 'K', projekt: 'P', key: 'shop', loeschdatum: ende, art: 'hosting' }).blocks, { abo_ende: false, hosting: true, termine: false, offene: true })
assert.equal(k39Mail({ kunde: 'K', projekt: 'P', key: 'news', loeschdatum: ende, art: 'x' }).values.loeschtag, '16.11.2026')
assert.throws(() => k39Mail({ kunde: 'K', projekt: 'P', key: 'news', loeschdatum: 'morgen', art: 'x' }), TypeError)
})
test('t24Mail, loeschFristTage, kontostandText', () => {
const t = t24Mail({ kunde: 'Silke', email: 'k@example.invalid', projekt: 'P', key: 'news', customerId: 'c1', projectId: 'p1', woid: '100150', kontostand: kontostandText({ balanceMinutes: 0, vorschussMinutes: 90 }) })
assert.deepEqual([t.template, t.values.kontostand, t.blocks, t.values.woms_url], ['T24', 'Guthaben 0 min, Vorschuss 90 min', { vorschuss: true, anhang_fehlt: false }, anleitungsLink('c1', 'p1')])
assert.equal(t24Mail({ kunde: 'K', email: '', projekt: 'P', key: 'news', customerId: 'c1', projectId: 'p1', woid: '' }).blocks.vorschuss, false)
assert.deepEqual(['news', 'termine', 'shop', 'unterkunft'].map(loeschFristTage), [30, 30, 90, 90]); assert.throws(() => loeschFristTage('zeiten'), TypeError)
})
test('alle Builder-Ergebnisse rendern ohne Probleme und ohne €; Zeiten-Varianten', () => {
const bau = [k38Mail({ kunde: 'K', projekt: 'P', key: 'unterkunft', abrechnung: 'abo' }), k39Mail({ kunde: 'K', projekt: 'P', key: 'termine', loeschdatum: new Date(), art: 'x' }), t24Mail({ kunde: 'K', email: 'e', projekt: 'P', key: 'shop', customerId: 'c', projectId: 'p', woid: '1' })]
for (const m of bau) { const r = renderMail(m.template, m.values, m.blocks); assert.deepEqual(r.problems, []); assert.doesNotMatch(r.text, /€/) }
const frist = new Date('2026-11-16T12:00:00.000Z')
const k40 = k29Mail({ kunde: 'K', projekt: 'P', art: 'hosting', frist, preis: '30 €', modell: true }); assert.equal(k40.template, 'K40'); assert.deepEqual(Object.keys(k40.values).sort(), ['frist', 'kunde_name', 'portal_url', 'projekt']); assert.equal(k40.blocks.hosting, true)
const k41 = k30Mail({ kunde: 'K', projekt: 'P', loeschdatum: frist, preis: '30 €', modell: true }); assert.equal(k41.template, 'K41'); assert.equal(renderMail('K41', k41.values, k41.blocks).problems.length, 0)
assert.equal(k29Mail({ kunde: 'K', projekt: 'P', art: 'beendet', frist, preis: '30 €' }).template, 'K29') // Standard unverändert
})
test('anfrageEntschiedenMail: K42 und K43, Werte, Blöcke, Adresse; ohne Preis', () => {
const m = anfrageEntschiedenMail({ kunde: { name: 'Erika' }, projekt: 'Töpferei', key: 'shop', projectId: 'pt1', angenommen: true })
assert.equal(m.template, 'K42'); assert.deepEqual(m.blocks, { angenommen: true, abgelehnt: false })
assert.deepEqual(m.values, { kunde_name: 'Erika', projekt: 'Töpferei', begruendung: '', portal_url: `${config.publicBaseUrl}/dashboard.html#webshop?projekt=pt1` })
const u = anfrageEntschiedenMail({ kunde: { name: ' ' }, projekt: 'Donau', key: 'unterkunft', projectId: 'a b', angenommen: false, begruendung: 'Passt nicht.' })
assert.deepEqual([u.template, u.blocks, u.values.kunde_name, u.values.begruendung, u.values.portal_url], ['K43', { angenommen: false, abgelehnt: true }, 'Kunde', 'Passt nicht.', `${config.publicBaseUrl}/dashboard.html#buchungen?projekt=a%20b`])
assert.throws(() => anfrageEntschiedenMail({ kunde: 'K', projekt: 'P', key: 'termine', projectId: 'p', angenommen: true }), TypeError)
for (const x of [m, u]) assert.deepEqual(renderMail(x.template, x.values, x.blocks).problems, [])
})
+56
View File
@@ -0,0 +1,56 @@
import { test, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
// Tagesjob des Abo-Ende-Abgleichs (P6 Task 11): Überlappungsschutz, wirft nie, Einbindung in server/index.js vor den Löschjobs.
const aw = await startAppwriteMock({
schema: {
portalFeatures: schemaKeys('portalFeatures'),
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
termineConfig: schemaKeys('termineConfig'),
newsPosts: schemaKeys('newsPosts'),
},
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const { aboFunktionenTagesJob } = await import('../server/services/aboFunktionenAbgleich.js')
const mails = mailSink()
setMailTransportForTests(mails)
after(() => {
setMailTransportForTests(null)
return Promise.all([aw.close(), st.close()])
})
const JETZT = new Date('2026-10-17T08:00:00.000Z')
const seed = (key, over = {}) => aw.seed('portalFeatures', { $id: `f_p1_${key}`, customerId: 'c1', projectId: 'p1', featureKey: key, siteKey: 'abcdefghijklmnop', state: 'active', enabled: true, activatedAt: '2026-10-01T09:00:00.000Z', unlockedBy: 'abo', unlockedByPurchase: false, einrichtungAbrechnung: 'abo', ...over })
const abo = (doc) => aw.seed('subscriptions', subscriptionDoc('sub_h', doc))
seedFixtures(aw)
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei' })
beforeEach(async () => {
for (const col of ['portalFeatures', 'subscriptions', 'contractEvents', 'newsPosts']) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
mails.sent.length = 0
})
test('Überlappungsschutz: der zweite gleichzeitige Lauf wird übersprungen, danach läuft der Job wieder', async () => {
abo({ ...wartungDoc, status: 'canceled' }); seed('news')
const [a, b] = await Promise.all([aboFunktionenTagesJob(JETZT), aboFunktionenTagesJob(JETZT)])
assert.deepEqual([a, b].filter((r) => r.uebersprungen).length, 1); assert.equal([a, b].find((r) => !r.uebersprungen).abgeschaltet, 1)
assert.equal((await aboFunktionenTagesJob(JETZT)).uebersprungen, undefined)
})
test('Der Job wirft nie (Listenfehler → fehler 1) und index.js startet ihn vor den Löschjobs', async (t) => {
t.mock.method(console, 'error', () => {}); aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500 })
assert.equal((await aboFunktionenTagesJob(JETZT)).fehler, 1)
const src = readFileSync(new URL('../server/index.js', import.meta.url), 'utf8')
const start = src.indexOf('setTimeout(aboFunktionenAbgleich, 150 * 1000)'); assert.ok(start > src.indexOf('const analyseAboCheck'))
assert.ok(start < src.indexOf('setTimeout(termineTagesJob, 180 * 1000)')); assert.match(src, /setInterval\(aboFunktionenAbgleich, 24 \* 60 \* 60 \* 1000\)/)
})
+37
View File
@@ -0,0 +1,37 @@
import assert from 'node:assert/strict'
import { test } from 'node:test'
import { loadTemplate, renderMail, templateIds } from '../server/services/mailTemplates.js'
// P6 Task 3: Mailvorlagen K38 bis K41 und T24 des Abo-Modells (Kopien von K16 bzw. T19, du-Form, ohne Euro-Zeichen)
const SOLL = { // [Datei, Betreff, Platzhalter, Blöcke]
K38: ['K38 Funktion wird eingerichtet.html', 'Deine Funktion wird eingerichtet: {{funktion}}', ['kunde_name', 'projekt', 'funktion', 'funktion_akk', 'einrichtung', 'ticket', 'fassung', 'portal_url'], ['stundenkonto', 'projekt_kosten', 'anhang']],
K39: ['K39 Funktion beendet.html', 'Deine Funktion wurde beendet: {{funktion}}', ['kunde_name', 'projekt', 'funktion', 'loeschtag', 'portal_url'], ['abo_ende', 'hosting', 'termine', 'offene']],
K40: ['K40 Zeiten Abo-Ende.html', 'Dein Abo ist beendet – Öffnungszeiten live läuft noch bis {{frist}}', ['kunde_name', 'projekt', 'frist', 'portal_url'], ['abo_ende', 'hosting', 'wechsel_widerrufen']],
K41: ['K41 Zeiten abgeschaltet Abo.html', 'Öffnungszeiten live ist abgeschaltet', ['kunde_name', 'projekt', 'loeschdatum', 'portal_url'], []],
K42: ['K42 Webshop-Anfrage Abo.html', 'Deine Webshop-Anfrage für {{projekt}}', ['kunde_name', 'projekt', 'begruendung', 'portal_url'], ['angenommen', 'abgelehnt']],
K43: ['K43 Anfrage Buchungskalender Abo.html', 'Deine Anfrage zum Buchungskalender für {{projekt}}', ['kunde_name', 'projekt', 'begruendung', 'portal_url'], ['angenommen', 'abgelehnt']],
T24: ['T24 Funktion eingeschaltet.html', '[Abo] Funktion eingeschaltet: {{funktion}} – {{kunde_name}}', ['kunde_name', 'email', 'projekt', 'funktion', 'woid', 'kontostand', 'woms_url'], ['vorschuss', 'anhang_fehlt']],
}
const mail = (id, on) => { const t = loadTemplate(id); return renderMail(id, Object.fromEntries(t.placeholders.map((p) => [p, `W-${p}`])), Object.fromEntries(t.blocks.map((b) => [b, on]))) }
for (const [id, [file, subject, placeholders, blocks]] of Object.entries(SOLL)) test(`${id}: Datei, Betreff, Platzhalter, Blöcke; rendert ohne Reste; ohne € und ohne Sie-Form (K)`, () => {
const t = loadTemplate(id); assert.deepEqual([t.file, t.subject, t.placeholders, t.blocks], [file, subject, placeholders, blocks])
for (const on of [true, false]) { const m = mail(id, on); assert.deepEqual(m.problems, []); assert.doesNotMatch(m.html + m.text, /€/); if (id.startsWith('K')) assert.doesNotMatch(m.text, /\bSie\b|\bIhr/) }
})
test('Festtexte', () => {
assert.match(mail('K38', true).text, /Die Einrichtung geht von deinem Stundenkonto ab, auch wenn das Guthaben noch nicht reicht\. Extrakosten entstehen keine\./)
assert.match(mail('K38', true).text, /Von deinem Stundenkonto geht nichts ab\. Die Funktion läuft, solange dein Wartungs-Abo läuft\./)
assert.doesNotMatch(mail('K38', false).text, /Stundenkonto|Wartungs-Abo läuft/)
assert.match(mail('K39', true).text, /Läuft bis dahin wieder ein passendes Abo, schalten wir W-funktion automatisch und ohne Gebühr wieder ein\./)
assert.match(mail('K39', true).text, /Bitte informiere deine Gäste selbst\./); assert.doesNotMatch(mail('K39', false).text, /CSV|Offene Bestellungen/)
assert.match(mail('T24', true).text, /Das Guthaben reicht voraussichtlich nicht \(W-kontostand\)\./); assert.doesNotMatch(mail('T24', false).text, /Vorschuss/)
})
test('K42, K43: Zusage ohne Preis und ohne „bestellen“, Absage mit Begründung wie K24 und K34', () => {
const werte = { kunde_name: 'Erika', projekt: 'Töpferei', begruendung: 'Wir verkaufen keine <Waffen>.', portal_url: 'https://portal.example/dashboard.html#x?projekt=p1' }
for (const [id, satz, knopf] of [['K42', 'Du kannst den Webshop jetzt im Kundenportal einschalten; danach richten wir ihn ein.', 'Webshop einschalten'], ['K43', 'Du kannst ihn jetzt im Kundenportal einschalten; danach richten wir ihn ein.', 'Zum Kundenportal']]) {
const zu = renderMail(id, werte, { angenommen: true, abgelehnt: false }); assert.deepEqual(zu.problems, [])
assert.doesNotMatch(zu.html + zu.text, /€|UStG|bestell|Preis|Waffen/i); assert.ok(zu.text.includes(satz), id); assert.ok(zu.text.includes(knopf), id)
const ab = renderMail(id, werte, { angenommen: false, abgelehnt: true }); assert.deepEqual(ab.problems, [])
assert.match(ab.text, /leider/); assert.match(ab.text, /Wir verkaufen keine <Waffen>\./); assert.doesNotMatch(ab.text + ab.html, /€|UStG|bestell/i)
}
})
test('templateIds enthält K38 bis K43 und T24', () => { for (const id of Object.keys(SOLL)) assert.ok(templateIds().includes(id)) })
+96
View File
@@ -0,0 +1,96 @@
import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
// Katalogfelder, consentAbo (mit Hinweis) und die Unterlagen-Quelle im Funktionen-Register (P5, V5)
const aw = await startAppwriteMock()
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const reg = await import('../server/services/featureRegistry.js')
const { unterlagenFuer, analyseUnterlagen } = await import('../server/services/aboUnterlagen.js')
const KEYS = ['news', 'termine', 'shop', 'unterkunft', 'zeiten']
test('Reihenfolge und Felder je Funktion', () => {
assert.deepEqual(reg.katalogReihenfolge(), [...KEYS, 'analyse'])
const ablauf = { news: 'direkt', termine: 'fragebogen', shop: 'anfrage', unterkunft: 'anfrage', zeiten: 'direkt', analyse: 'direkt' }
for (const key of [...KEYS, 'analyse']) {
const f = reg.katalogFelder(key)
assert.deepEqual(Object.keys(f).sort(), ['ablauf', 'beschreibung', 'bild', 'einrichtung', 'nurBetrieb'])
assert.deepEqual([f.bild, f.ablauf, f.nurBetrieb], [`/neu/bilder/${key}.webp`, ablauf[key], key === 'shop'], key)
assert.ok(f.beschreibung.split(/(?<=[.!?])\s+/).length <= 2, 'höchstens zwei Sätze')
assert.doesNotMatch(f.beschreibung + f.einrichtung, /€|\bSie\b|\bIhre?[nmrs]?\b/, 'kein Preis, keine Sie-Form')
}
assert.equal(reg.katalogFelder('shop').einrichtung, 'meist in 2–3 Wochen')
assert.equal(reg.katalogFelder('gibtEsNicht'), null)
// die festen Werte der Tabelle
const einrichtung = { news: 'meist in 1–2 Wochen', termine: 'meist in 1–2 Wochen', shop: 'meist in 2–3 Wochen', unterkunft: 'meist in 1–2 Wochen', zeiten: 'meist in wenigen Tagen', analyse: 'meist in wenigen Tagen' }
for (const [key, text] of Object.entries(einrichtung)) assert.equal(reg.katalogFelder(key).einrichtung, text, key)
assert.equal(reg.katalogFelder('news').beschreibung, 'Zeig Neuigkeiten und Aktionen direkt auf deiner Website. Du schreibst sie im Portal, sie erscheinen sofort.')
assert.equal(reg.katalogFelder('analyse').beschreibung, 'Sieh, wie viele Besucher auf deine Website kommen und welche Seiten sie ansehen. Die Auswertung findest du direkt im Portal.')
})
const HINWEIS = { // Pflichten der alten Häkchen verantwortung, stripe, gesundheit; wörtlich wie WORTLAUT_V6.hinweis (P8)
termine: 'Der Terminplaner ist nicht für den Gesundheitsbereich gedacht. Das fragen wir im Fragebogen ab.',
shop: 'Du verkaufst in eigenem Namen und bist für deine Produkte, Preise und Rechtstexte verantwortlich. Die Zahlungen deiner Käufer laufen über dein eigenes Stripe-Konto.',
unterkunft: 'Du vermietest in eigenem Namen und bist für Preise, Stornobedingungen, Rechnungen und Meldepflichten verantwortlich. Online-Zahlungen deiner Gäste laufen über dein eigenes Stripe-Konto.',
}
test('consentAbo: Linkzeile mit Fassung, Hinweis (termine, shop, unterkunft), beginn als Entwurf, kein Häkchen gesundheit', () => {
const V = config.aboFunktionen.fassung
for (const key of KEYS) {
const c = reg.consentAbo(key)
const ids = key in HINWEIS ? ['einschalten-zeile', 'hinweis', 'beginn'] : ['einschalten-zeile', 'beginn']
assert.deepEqual([c.version, c.knopf, c.items.map((i) => i.id)], [V, 'Einschalten', ids], key)
assert.deepEqual(c.items.map((i) => i.haken), ids.map((id) => id === 'beginn'), `${key}: nur beginn ist ein Häkchen`)
assert.deepEqual([c.items.at(-1).text, c.items.at(-1).nurPrivatInFrist], [config.aboFunktionen.texte.beginn, true], key)
if (key in HINWEIS) assert.equal(c.items[1].text, HINWEIS[key], key)
}
assert.equal(config.aboFunktionen.texte.beginn, 'Bitte beginnt sofort mit der Einrichtung. Mir ist klar: Mit der vollständigen Einrichtung erlischt mein Widerrufsrecht, und wenn ich vorher widerrufe, zahle ich den bis dahin erbrachten Teil.')
assert.equal(V, '2026-10-16')
assert.equal(reg.consentAbo('termine').items[0].text, `Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung ${config.termine.avvVersion}).`)
assert.equal(reg.consentAbo('zeiten').items[0].text, 'Es gilt die Leistungsbeschreibung (Fassung 2026-10-04).')
assert.equal(reg.consentAbo('news').items[0].text, `Es gilt die Leistungsbeschreibung (Fassung ${config.termine.avvVersion}).`)
assert.ok(!reg.consentAbo('termine').items.some((i) => i.id === 'gesundheit'), 'das Häkchen gesundheit entfällt, der Satz steht als Hinweis')
assert.doesNotMatch(JSON.stringify(Object.values(HINWEIS)), /€|\bSie\b|\bIhre?[nmrs]?\b/)
assert.equal(reg.consentAbo('analyse'), null)
assert.equal(reg.consentAbo('gibtEsNicht'), null)
})
test('consentAboAktiv und aboweg zeigen nur, was verlangt ist; das alte Register bleibt unverändert', () => {
assert.deepEqual(reg.consentAboAktiv('shop').items.map((i) => i.id), ['einschalten-zeile', 'hinweis'])
assert.deepEqual(reg.consentAboAktiv('news').items.map((i) => i.id), ['einschalten-zeile'])
assert.deepEqual(reg.consentAboAktiv('shop', { beginn: true }).items.map((i) => [i.id, i.haken]), [['einschalten-zeile', false], ['hinweis', false], ['beginn', true]])
const w = reg.aboweg(reg.featureConfig('termine'), { beginn: true })
assert.deepEqual([w.consent.version, w.widerruf, w.avv.version, w.leistungsbeschreibung.version, w.key], [config.aboFunktionen.fassung, null, config.termine.avvVersion, config.termine.avvVersion, 'termine'])
assert.deepEqual(reg.featureConfig('termine').consent.items.map((i) => i.id), ['unterlagen', 'gesundheit', 'beginn', 'information'])
assert.notEqual(reg.featureConfig('termine').widerruf, null, 'aboweg ist eine Kopie, das Register bleibt')
assert.equal(Object.keys(reg.oeffentlicheAnsicht('termine')).length, 11, 'oeffentlicheAnsicht ohne neue Felder')
})
test('unterlagenFuer und analyseUnterlagen: eine Quelle für Katalog, Nachweis und Analyse', () => {
const t = unterlagenFuer('termine')
assert.deepEqual([t.leistungsbeschreibung, t.avv], [{ version: config.termine.avvVersion, url: config.features.termine.leistungsbeschreibung.url }, { version: config.termine.avvVersion, url: config.features.termine.avv.url }])
assert.deepEqual([unterlagenFuer('news').avv, unterlagenFuer('zeiten').avv], [null, null])
assert.ok(unterlagenFuer('news').leistungsbeschreibung.version)
const a = analyseUnterlagen()
assert.deepEqual(a, { avvVersion: config.analyse.avvVersion, legal: { lb: `/rechtliches/leistungsbeschreibung-website-analyse-${config.analyse.avvVersion}.html`, avv: `/rechtliches/avv-website-analyse-${config.analyse.avvVersion}.html` } })
assert.deepEqual(unterlagenFuer('analyse'), {
leistungsbeschreibung: { version: config.analyse.avvVersion, url: a.legal.lb },
avv: { version: config.analyse.avvVersion, url: a.legal.avv },
})
assert.throws(() => unterlagenFuer('gibtEsNicht'), TypeError)
})
test('consentAboAnalyse und abowegAnalyse: Linkzeile, Zielgruppen-Häkchen und Knopf wörtlich wie in P4', () => {
const c = reg.consentAboAnalyse()
assert.deepEqual([c.version, c.avvVersion, c.knopf], [config.aboFunktionen.fassung, config.analyse.avvVersion, 'Analyse einschalten'])
assert.deepEqual(c.items, [
{ id: 'einschalten-zeile', text: `Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung ${config.analyse.avvVersion}).`, haken: false },
{ id: 'zielgruppe', text: 'Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.', haken: true },
])
const w = reg.abowegAnalyse()
assert.deepEqual([w.key, w.leistungsbeschreibung.version, w.avv.version, w.widerruf, w.consent.knopf], ['analyse', config.analyse.avvVersion, config.analyse.avvVersion, null, 'Analyse einschalten'])
})
+50
View File
@@ -0,0 +1,50 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
const { config } = await import('../server/config.js')
const { WORTLAUT_V6, FUNKTIONEN_FASSUNG_V6, FUNKTIONEN_UNTERLAGEN_V6, mitFassung } = await import('../server/services/products.js')
test('Linkzeilen: Wortlaut, Platzhalter wird gefüllt', () => {
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.bestellen, '2026-10-16-v6'), 'Es gelten die Vertragsbedingungen mit Leistungsbeschreibung, Auftragsverarbeitung und Widerrufsbelehrung (Fassung 2026-10-16-v6). Alles kommt danach auch als PDF per Mail.')
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.einschaltenMitAvv, '2026-10-16'), 'Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung 2026-10-16).')
assert.equal(mitFassung(WORTLAUT_V6.linkzeile.einschaltenOhneAvv, '2026-10-16'), 'Es gilt die Leistungsbeschreibung (Fassung 2026-10-16).')
assert.throws(() => mitFassung(WORTLAUT_V6.linkzeile.bestellen, ''), /Fassung/)
assert.throws(() => mitFassung('ohne Platzhalter', '2026-10-16'), /Platzhalter/)
})
test('Häkchen beginn: Wortlaut je Fall (nur privat in der Frist)', () => {
assert.deepEqual(WORTLAUT_V6.beginn, {
abo: 'Bitte beginnt sofort mit dem Abo. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.',
wechsel: 'Bitte beginnt sofort mit dem Wechsel. Mir ist klar: Widerrufe ich innerhalb von 14 Tagen, zahle ich den bis dahin erbrachten Teil.',
einrichtung: 'Bitte beginnt sofort mit der Einrichtung. Mir ist klar: Mit der vollständigen Einrichtung erlischt mein Widerrufsrecht, und wenn ich vorher widerrufe, zahle ich den bis dahin erbrachten Teil.',
})
})
test('Pflichten aus den alten Häkchen verantwortung, stripe, gesundheit', () => {
assert.deepEqual(WORTLAUT_V6.hinweis, {
shop: 'Du verkaufst in eigenem Namen und bist für deine Produkte, Preise und Rechtstexte verantwortlich. Die Zahlungen deiner Käufer laufen über dein eigenes Stripe-Konto.',
unterkunft: 'Du vermietest in eigenem Namen und bist für Preise, Stornobedingungen, Rechnungen und Meldepflichten verantwortlich. Online-Zahlungen deiner Gäste laufen über dein eigenes Stripe-Konto.',
termine: 'Der Terminplaner ist nicht für den Gesundheitsbereich gedacht. Das fragen wir im Fragebogen ab.',
})
})
test('alle Wortlaute: du-Form, kein Preis, nur {fassung} als Platzhalter, tief eingefroren', () => {
const alle = JSON.stringify(WORTLAUT_V6)
assert.doesNotMatch(alle, /€|\bSie\b|\bIhr(e[nmrs]?)?\b/)
assert.deepEqual([...alle.matchAll(/\{[a-z]+\}/g)].map((m) => m[0]).filter((x) => x !== '{fassung}'), [])
for (const t of Object.values(WORTLAUT_V6.linkzeile)) assert.equal((t.match(/\{fassung\}/g) ?? []).length, 1)
for (const o of [WORTLAUT_V6, WORTLAUT_V6.linkzeile, WORTLAUT_V6.beginn, WORTLAUT_V6.hinweis]) assert.ok(Object.isFrozen(o))
})
test('Funktionsunterlagen Fassung 6: Reihenfolge, Vorschaupfad, AVV nur wo Daten im Auftrag verarbeitet werden', () => {
assert.equal(FUNKTIONEN_FASSUNG_V6, '2026-10-16')
assert.deepEqual(Object.keys(FUNKTIONEN_UNTERLAGEN_V6), ['news', 'termine', 'shop', 'unterkunft', 'zeiten', 'analyse'])
assert.deepEqual(FUNKTIONEN_UNTERLAGEN_V6.termine.lb, { version: '2026-10-16', url: '/neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.html', pdfFile: 'neu/rechtliches/leistungsbeschreibung-terminplaner-2026-10-16.pdf', anhangName: 'Leistungsbeschreibung-Terminplaner-2026-10-16.pdf' })
const slugs = Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((u) => [u.lb, u.avv]).filter(Boolean).map((d) => d.url.replace(/^\/neu\/rechtliches\/|-2026-10-16\.html$/g, ''))
assert.deepEqual(slugs, ['leistungsbeschreibung-news', 'leistungsbeschreibung-terminplaner', 'avv-terminplaner', 'leistungsbeschreibung-webshop', 'avv-webshop', 'leistungsbeschreibung-unterkunft', 'avv-unterkunft', 'leistungsbeschreibung-zeiten', 'leistungsbeschreibung-website-analyse', 'avv-website-analyse'])
assert.deepEqual([FUNKTIONEN_UNTERLAGEN_V6.news.avv, FUNKTIONEN_UNTERLAGEN_V6.zeiten.avv], [null, null])
assert.deepEqual(Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((u) => [u.lb, u.avv]).filter(Boolean).map((d) => d.anhangName.replace('-2026-10-16.pdf', '')), ['Leistungsbeschreibung-News-Pinnwand', 'Leistungsbeschreibung-Terminplaner', 'AVV-Terminplaner', 'Leistungsbeschreibung-Webshop', 'AVV-Webshop', 'Leistungsbeschreibung-Buchungskalender', 'AVV-Buchungskalender', 'Leistungsbeschreibung-Oeffnungszeiten-live', 'Leistungsbeschreibung-Website-Analyse', 'AVV-Website-Analyse'])
})
test('Verdrahtung mit P5 (läuft erst nach dem Merge von kn/server; rot, bis der Controller verdrahtet hat)', { skip: config.aboFunktionen === undefined && 'config.aboFunktionen fehlt: P5 noch nicht gemerged' }, async () => {
const reg = await import('../server/services/featureRegistry.js')
assert.equal(config.aboFunktionen.fassung, FUNKTIONEN_FASSUNG_V6)
assert.equal(config.aboFunktionen.texte.beginn, WORTLAUT_V6.beginn.einrichtung)
for (const key of ['termine', 'shop', 'unterkunft']) assert.equal(reg.consentAbo(key).items[0].text, mitFassung(WORTLAUT_V6.linkzeile.einschaltenMitAvv, FUNKTIONEN_FASSUNG_V6), key)
for (const key of ['news', 'zeiten']) assert.equal(reg.consentAbo(key).items[0].text, mitFassung(WORTLAUT_V6.linkzeile.einschaltenOhneAvv, FUNKTIONEN_FASSUNG_V6), key)
})
+205
View File
@@ -0,0 +1,205 @@
import { test, after, beforeEach, afterEach, mock } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
// Team-Weg der Analyse im Abo-Modell (Nachzug P6b): POST/GET /api/admin/analyse/:projectId. Kunde c1 (k@example.invalid) steht in der
// Pilotliste, c2 nicht. Im Abo-Modell reicht Hosting oder Wartung (aboDeckt), das Dokument traegt den Abo-Marker; sonst bleibt alles wie heute.
const ANALYSE_KEYS = ['accentColor', 'avvVersion', 'avvAcceptedAt', 'avvAcceptedBy', 'activatedAt', 'offSince', 'privacyTicketId', 'activatedMailAt', 'avvConsentDate', 'avvConsentChannel', 'removalTicketId', 'publishTicketId', 'audienceConfirmed', 'audienceTextVersion']
const CONSENT_KEYS = ['customerId', 'projectId', 'siteKey', 'event', 'avvVersion', 'via', 'acceptedAt', 'acceptedBy', 'textformDate', 'textformChannel', 'audienceConfirmed', 'audienceTextVersion']
const aw = await startAppwriteMock({
schema: {
portalFeatures: [...schemaKeys('portalFeatures'), ...ANALYSE_KEYS],
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
featureConsents: schemaKeys('featureConsents'),
analyseConsents: CONSENT_KEYS,
},
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, rawGet, post } = await import('./support/testApp.js')
const { hostingDoc, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { default: adminAnalyseRoutes } = await import('../server/routes/admin/analyse.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { istAboModell } = await import('../server/services/aboModell.js')
const { gleicheAboFunktionenAb } = await import('../server/services/aboFunktionenAbgleich.js')
const { analyseUnterlagen } = await import('../server/services/aboUnterlagen.js')
const an = await import('../server/services/analyse.js')
const sf = await import('../server/services/siteFeatures.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const app = await startTestApp({ '/api/admin/analyse': adminAnalyseRoutes })
setMailTransportForTests(mailSink())
const vorher = { mode: config.kundenansichtNeu.mode, pilotEmails: [...config.kundenansichtNeu.pilotEmails], analyse: config.analyse.enabled }
after(() => {
Object.assign(config.kundenansichtNeu, { mode: vorher.mode, pilotEmails: vorher.pilotEmails })
config.analyse.enabled = vorher.analyse
setMailTransportForTests(null)
return Promise.all([app.close(), aw.close(), st.close()])
})
const putSiteAufrufe = []
an.analyseDeps.client = {
putSite: async (siteKey, body) => putSiteAufrufe.push({ siteKey, ...body }),
stopSite: async () => {},
getStats: async () => ({ pv: 5 }),
heatToken: () => 'token',
}
an.analyseDeps.rebuild = async () => {}
const recordConsentOriginal = an.analyseDeps.recordConsent
const JETZT = new Date()
const todayBerlin = () => new Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Berlin' }).format(new Date())
const hdr = { headers: { Authorization: 'Bearer jwt-admin' } }
const tf = (extra = {}) => ({ textformDate: todayBerlin(), textformChannel: 'chat', audienceConfirmed: true, ...extra })
const einschalten = (id, extra = {}) => post(`/api/admin/analyse/${id}`, { on: true, ...tf(), ...extra }, hdr)
const lesen = (id) => rawGet(`/api/admin/analyse/${id}`, hdr)
const abo = (doc) => aw.seed('subscriptions', subscriptionDoc('sub_h', doc))
const featureDoc = (c, p) => sf.getFeatureDoc(c, p, 'analyse')
const consents = () => [...aw.docs('analyseConsents').values()]
// aw.docs() liefert eine Kopie: Dokumente werden ueber die API geloescht
const leere = async (...collections) => {
for (const col of collections) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
}
const nichtsGeschrieben = async (c, p) => {
assert.equal(await featureDoc(c, p), null, 'kein Dokument')
assert.deepEqual([putSiteAufrufe.length, consents().length, aw.docs('workorders').size], [0, 0, 0])
}
beforeEach(async () => {
putSiteAufrufe.length = 0
await leere('portalFeatures', 'featureConsents', 'analyseConsents', 'subscriptions', 'websiteProjects', 'contractEvents', 'workorders')
seedFixtures(aw) // c1 mit Hosting-Abo sub_h, c2 mit sub_h2
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei', subdomain: 'baeckerei', liveDomain: 'baeckerei.de', isPublic: true })
aw.seed('websiteProjects', { $id: 'p2', customerId: 'c1', projectName: 'Privat', subdomain: 'privat', isPublic: false })
aw.seed('websiteProjects', { $id: 'p3', customerId: 'c2', projectName: 'Café', subdomain: 'cafe', isPublic: true })
config.kundenansichtNeu.mode = 'pilot'
config.kundenansichtNeu.pilotEmails = ['k@example.invalid']
config.analyse.enabled = true
an.analyseDeps.recordConsent = recordConsentOriginal
mock.method(console, 'error', () => {})
mock.method(console, 'warn', () => {})
mock.method(console, 'log', () => {})
})
afterEach(() => mock.restoreAll())
test('Hosting-Kunde im Abo-Modell: 200, Analyse aktiv, Abo-Marker am Dokument, Nachweis der Textform, Fassung der Abo-Unterlagen', async () => {
const r = await einschalten('p1')
assert.deepEqual([r.status, r.body], [200, { ok: true, state: 'active' }])
const d = await featureDoc('c1', 'p1')
assert.deepEqual([d.state, d.unlockedBy, d.einrichtungAbrechnung, istAboModell(d)], ['active', 'abo', 'abo', true])
assert.deepEqual([d.avvConsentDate, d.avvConsentChannel, d.audienceConfirmed, d.avvVersion], [todayBerlin(), 'chat', true, analyseUnterlagen().avvVersion])
assert.deepEqual([putSiteAufrufe.length, putSiteAufrufe[0].active], [1, true])
const [n] = consents()
assert.deepEqual([consents().length, n.via, n.textformDate, n.textformChannel, n.audienceConfirmed, n.avvVersion], [1, 'woms', todayBerlin(), 'chat', true, analyseUnterlagen().avvVersion])
// das Team beendet wieder: ohne Abo-Pruefung
await leere('subscriptions')
const aus = await post('/api/admin/analyse/p1', { on: false }, hdr)
assert.deepEqual([aus.status, aus.body], [200, { ok: true, state: 'off' }])
})
test('Wartungs-Kunde im Abo-Modell zählt auch; die Tagesprüfung und der Abo-Ende-Abgleich lassen das Dokument in Ruhe', async () => {
await leere('subscriptions')
abo(wartungDoc)
assert.equal((await einschalten('p1')).status, 200)
assert.equal(istAboModell(await featureDoc('c1', 'p1')), true)
await leere('subscriptions')
abo(hostingDoc)
const tag = await an.runAboCheck({ now: JETZT })
assert.deepEqual(tag.deactivated, [])
const abgleich = await gleicheAboFunktionenAb(JETZT)
assert.deepEqual([abgleich.abgeschaltet, abgleich.fehler], [0, 0])
assert.equal((await featureDoc('c1', 'p1')).state, 'active')
})
test('Kunde ohne Abo-Modell und nur Hosting: wie heute abgewiesen (403, Wartungs-Text), nichts geschrieben; Modus off ebenso', async () => {
const alt = 'Die Website-Analyse gibt es nur mit einem laufenden Wartungs-Abo.'
const r = await einschalten('p3') // c2 steht nicht in der Pilotliste
assert.deepEqual([r.status, r.body], [403, { error: alt }])
await nichtsGeschrieben('c2', 'p3')
config.kundenansichtNeu.mode = 'off'
const aus = await einschalten('p1')
assert.deepEqual([aus.status, aus.body], [403, { error: alt }])
await nichtsGeschrieben('c1', 'p1')
const g = await lesen('p3')
assert.deepEqual([g.status, g.body.eligible, g.body.reasonText], [200, false, alt])
})
test('Kunde ohne Abo-Modell mit Wartungs-Abo: wie heute, ohne Marker', async () => {
await leere('subscriptions')
aw.seed('subscriptions', subscriptionDoc('sub_w2', { ...wartungDoc, customerId: 'c2', email: 'k2@example.invalid' }))
const r = await einschalten('p3')
assert.deepEqual([r.status, r.body], [200, { ok: true, state: 'active' }])
const d = await featureDoc('c2', 'p3')
assert.deepEqual([d.state, d.unlockedBy ?? null, d.einrichtungAbrechnung ?? null, istAboModell(d)], ['active', null, null, false])
assert.equal(d.avvVersion, config.analyse.avvVersion)
})
test('Abo-Modell ohne deckendes Abo: 409 abo_fehlt mit Grund, nichts geschrieben', async () => {
const faelle = [
[null, 'kein_abo', undefined],
[{ ...hostingDoc, status: 'canceled' }, 'kein_abo', undefined],
[{ ...hostingDoc, cancelAtPeriodEnd: true }, 'gekuendigt', undefined],
[{ ...hostingDoc, status: 'past_due' }, 'nicht_aktiv', undefined],
[{ ...wartungDoc, status: 'trialing', trialEnd: '2099-01-01T05:00:00.000Z' }, 'beginnt_spaeter', '2099-01-01T05:00:00.000Z'],
]
for (const [doc, grund, ab] of faelle) {
await leere('subscriptions')
if (doc) abo(doc)
const r = await einschalten('p1')
assert.deepEqual([r.status, r.body.code, r.body.grund, r.body.wechsel, r.body.ab, typeof r.body.error], [409, 'abo_fehlt', grund, false, ab, 'string'], grund)
await nichtsGeschrieben('c1', 'p1')
}
})
test('Abo nicht lesbar: 503 statt Einschalten (fail-closed), nichts geschrieben; danach klappt es', async () => {
aw.failNext({ collection: 'subscriptions', method: 'GET', status: 500 })
const r = await einschalten('p1')
assert.deepEqual([r.status, r.body.code, typeof r.body.error], [503, undefined, 'string'])
await nichtsGeschrieben('c1', 'p1')
assert.equal((await einschalten('p1')).status, 200)
})
test('Reihenfolge: ungültige Angaben (400) und fehlendes Projekt (404) kommen vor abo_fehlt; nicht öffentliches Projekt ist 409 ohne Code', async () => {
await leere('subscriptions')
assert.equal((await einschalten('p1', { audienceConfirmed: undefined })).status, 400)
assert.equal((await einschalten('p1', { textformChannel: 'telefon' })).status, 400)
assert.equal((await einschalten('nix')).status, 404)
await nichtsGeschrieben('c1', 'p1')
abo(hostingDoc)
const privat = await einschalten('p2')
assert.deepEqual([privat.status, privat.body.code], [409, undefined])
await nichtsGeschrieben('c1', 'p2')
})
test('Misslingt der Nachweis, nimmt der Server alles zurück: Dokument aus, kein Marker', async () => {
an.analyseDeps.recordConsent = async () => {
throw new Error('Nachweis nicht gespeichert')
}
const r = await einschalten('p1')
assert.equal(r.status, 500)
const d = await featureDoc('c1', 'p1')
assert.deepEqual([d.state, d.unlockedBy ?? null, d.einrichtungAbrechnung ?? null], ['off', null, null])
assert.deepEqual(putSiteAufrufe.map((a) => a.active), [true, false])
an.analyseDeps.recordConsent = recordConsentOriginal
assert.equal((await einschalten('p1')).status, 200) // erneut versuchbar
assert.equal(istAboModell(await featureDoc('c1', 'p1')), true)
})
test('GET: Eignung und Text folgen dem Abo-Modell (Hosting ok, ohne Abo der Hosting-oder-Wartungs-Text), Rest unverändert', async () => {
const ok = await lesen('p1')
assert.deepEqual([ok.status, ok.body.eligible, ok.body.reasonText, ok.body.state, ok.body.enabled, ok.body.avvVersion], [200, true, null, null, true, config.analyse.avvVersion])
assert.deepEqual(Object.keys(ok.body).sort(), ['accentColor', 'activatedAt', 'avvAcceptedAt', 'avvAcceptedBy', 'avvConsentChannel', 'avvConsentDate', 'avvVersion', 'eligible', 'enabled', 'offSince', 'privacy', 'reasonText', 'state'].sort())
const privat = await lesen('p2')
assert.deepEqual([privat.body.eligible, privat.body.reasonText], [false, an.ELIGIBILITY_MESSAGES.nicht_oeffentlich])
assert.equal((await einschalten('p1')).status, 200)
assert.equal((await lesen('p1')).body.state, 'active')
await leere('subscriptions')
const ohne = await lesen('p1')
assert.deepEqual([ohne.body.eligible, ohne.body.reasonText], [false, 'Die Website-Analyse gibt es nur mit einem laufenden Hosting- oder Wartungs-Abo.'])
})
+184
View File
@@ -0,0 +1,184 @@
import { test, after, beforeEach, afterEach, mock } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { mailSink } from './support/mailSink.js'
import { schemaKeys } from './support/schema.js'
import { useTestEnv } from './support/testEnv.js'
// Analyse im Abo-Modell (P5 Task 13): Hosting reicht, die Zielgruppen-Aussage ist serverseitig erzwungen, der Nachweis liegt in
// featureConsents, die Tagesprüfung lässt Hosting-Analysen der neuen Ansicht in Ruhe. Die alte Ansicht bleibt unverändert.
const ANALYSE_KEYS = ['accentColor', 'avvVersion', 'avvAcceptedAt', 'avvAcceptedBy', 'activatedAt', 'offSince', 'privacyTicketId', 'activatedMailAt', 'avvConsentDate', 'avvConsentChannel', 'removalTicketId', 'publishTicketId', 'audienceConfirmed', 'audienceTextVersion']
const CONSENT_KEYS = ['customerId', 'projectId', 'siteKey', 'event', 'avvVersion', 'via', 'acceptedAt', 'acceptedBy', 'textformDate', 'textformChannel', 'audienceConfirmed', 'audienceTextVersion']
const aw = await startAppwriteMock({
schema: {
portalFeatures: [...schemaKeys('portalFeatures'), ...ANALYSE_KEYS],
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
featureConsents: schemaKeys('featureConsents'),
analyseConsents: CONSENT_KEYS,
},
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, get, post } = await import('./support/testApp.js')
const { customerCookie, customers, hostingDoc, seedFixtures, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { default: funktionenRoutes } = await import('../server/routes/funktionen.js')
const an = await import('../server/services/analyse.js')
const { analyseUnterlagen } = await import('../server/services/aboUnterlagen.js')
const { aboModellFuerKunde } = await import('../server/services/aboFunktionen.js')
const { setMailTransportForTests } = await import('../server/services/mailer.js')
const app = await startTestApp({ '/api/funktionen': funktionenRoutes })
setMailTransportForTests(mailSink())
const vorher = {
neu: { mode: config.kundenansichtNeu.mode, pilotEmails: [...config.kundenansichtNeu.pilotEmails] },
analyse: config.analyse.enabled,
}
after(() => {
Object.assign(config.kundenansichtNeu, vorher.neu)
config.analyse.enabled = vorher.analyse
setMailTransportForTests(null)
return Promise.all([app.close(), aw.close(), st.close()])
})
const putSiteAufrufe = []
an.analyseDeps.client = {
putSite: async (siteKey, body) => putSiteAufrufe.push({ siteKey, ...body }),
stopSite: async () => {},
getStats: async () => ({ pv: 5 }),
heatToken: () => 'token',
}
an.analyseDeps.rebuild = async () => {}
config.analyse.enabled = true
const JETZT = new Date('2026-10-17T08:00:00.000Z')
const c1 = customerCookie('c1')
const abo = (doc) => aw.seed('subscriptions', subscriptionDoc('sub_h', doc))
const aktiveAnalyse = (c, p) => aw.seed('portalFeatures', { $id: `f_${p}_analyse`, customerId: c, projectId: p, featureKey: 'analyse', state: 'active', enabled: true, siteKey: 'abcdefghijklmnop' })
const consent = () => ({ accepted: true, avvVersion: analyseUnterlagen().avvVersion })
// aw.docs() liefert eine Kopie: Dokumente werden ueber die API geloescht
const leere = async (...collections) => {
for (const col of collections) for (const id of aw.docs(col).keys()) await deleteDocument(col, id)
}
beforeEach(async () => {
putSiteAufrufe.length = 0
await leere('portalFeatures', 'featureConsents', 'analyseConsents', 'subscriptions', 'websiteProjects', 'contractEvents', 'workorders')
seedFixtures(aw)
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei', subdomain: 'baeckerei', liveDomain: 'baeckerei.de', isPublic: true })
config.kundenansichtNeu.mode = 'pilot'
config.kundenansichtNeu.pilotEmails = ['k@example.invalid']
config.analyse.enabled = true
mock.method(console, 'error', () => {})
mock.method(console, 'warn', () => {})
mock.method(console, 'log', () => {})
})
afterEach(() => mock.restoreAll())
test('Eignung: Hosting zählt nur im Abo-Modell; gekündigt und kein Abo nicht', async () => {
abo(hostingDoc)
assert.deepEqual(await an.analyseEligibility('c1', 'p1'), { ok: false, reason: 'kein_abo' }) // alte Regel
const neu = await an.analyseEligibility('c1', 'p1', { aboModell: true })
assert.deepEqual([neu.ok, neu.project.$id], [true, 'p1'])
abo({ ...hostingDoc, cancelAtPeriodEnd: true })
assert.deepEqual(await an.analyseEligibility('c1', 'p1', { aboModell: true }), { ok: false, reason: 'kein_abo' })
await leere('subscriptions')
assert.equal((await an.analyseEligibility('c1', 'p1', { aboModell: true })).reason, 'kein_abo')
abo({ ...wartungDoc, status: 'trialing', trialEnd: '2099-01-01T05:00:00.000Z' }) // Probezeit: die Analyse-Eignung meldet weiter kein_abo
assert.equal((await an.analyseEligibility('c1', 'p1', { aboModell: true })).reason, 'kein_abo')
assert.equal(an.eligibilityMessage('kein_abo', { aboModell: true }), 'Die Website-Analyse gibt es nur mit einem laufenden Hosting- oder Wartungs-Abo.')
assert.equal(an.eligibilityMessage('kein_abo'), 'Die Website-Analyse gibt es nur mit einem laufenden Wartungs-Abo.')
assert.equal(an.eligibilityMessage('nicht_oeffentlich', { aboModell: true }), an.ELIGIBILITY_MESSAGES.nicht_oeffentlich)
assert.equal(an.ELIGIBILITY_MESSAGES_ABO.kein_abo, 'Die Website-Analyse gibt es nur mit einem laufenden Hosting- oder Wartungs-Abo.')
})
const aktivieren = (extra = {}, cookie = c1) => post('/api/funktionen/analyse/activate', { projectId: 'p1', consent: consent(), audienceConfirmed: true, ...extra }, { cookie })
const keinSchreiben = () => assert.deepEqual([aw.docs('featureConsents').size, putSiteAufrufe.length, aw.docs('portalFeatures').get('f_p1_analyse')?.state ?? 'off'], [0, 0, 'off'])
test('Liste und Aktivieren über die Routen: neue Ansicht mit Hosting ja, alte Ansicht nein', async () => {
abo(hostingDoc)
const b = await get('/api/funktionen', { cookie: c1 })
const a = b.projects.find((p) => p.projectId === 'p1').analyse
assert.deepEqual([a.eligible, a.reason, a.avvVersion, a.legal], [true, null, analyseUnterlagen().avvVersion, analyseUnterlagen().legal]) // Ansicht zeigt die Fassung, die der Server prüft
assert.equal((await aktivieren()).status, 200)
config.kundenansichtNeu.mode = 'off'
const alt = (await get('/api/funktionen', { cookie: c1 })).projects.find((p) => p.projectId === 'p1').analyse
assert.deepEqual([alt.eligible, alt.reasonText], [false, 'Die Website-Analyse gibt es nur mit einem laufenden Wartungs-Abo.'])
})
test('Zielgruppe serverseitig erzwungen: ohne audienceConfirmed true 400 zielgruppe_fehlt, nichts geschrieben', async () => {
abo(hostingDoc)
for (const audienceConfirmed of [undefined, false, 'true', 1, null]) {
const r = await aktivieren({ audienceConfirmed })
assert.deepEqual([r.status, r.body], [400, { error: 'Bitte bestätige, dass sich deine Website nicht gezielt an Kinder unter 16 Jahren richtet.', code: 'zielgruppe_fehlt' }], String(audienceConfirmed))
keinSchreiben()
}
assert.equal((await aktivieren({ consent: { accepted: true, avvVersion: '1999-01-01' } })).status, 400) // falsche Fassung: weiter abgelehnt, nichts geschrieben
keinSchreiben()
})
test('Nachweis der Aktivierung: Linkzeile, Zielgruppen-Zeile, Knopf, Fassungen und die Kundenart des Dokuments', async () => {
abo(hostingDoc)
for (const [kundenart, soll] of [['unternehmen', 'unternehmen'], ['privat', 'privat'], [undefined, 'privat'], ['kaputt', 'privat']]) {
await leere('portalFeatures', 'featureConsents')
aw.seed('customers', { ...customers.c1, ...(kundenart === undefined ? {} : { kundenart }) })
const r = await aktivieren({ kundenart: 'unternehmen', unternehmer: true })
assert.equal(r.status, 200, String(kundenart)) // Body-Werte zur Kundenart zählen nicht
const [n] = [...aw.docs('featureConsents').values()]
assert.equal(aw.docs('featureConsents').size, 1)
assert.deepEqual([n.featureKey, n.projectId, n.customerId, n.kundenart, n.stripeSessionId], ['analyse', 'p1', 'c1', soll, ''], String(kundenart))
assert.deepEqual(JSON.parse(n.texte), [
{ id: 'einschalten-zeile', text: `Es gelten Leistungsbeschreibung und Auftragsverarbeitung (Fassung ${analyseUnterlagen().avvVersion}).`, haken: false },
{ id: 'zielgruppe', text: 'Meine Website richtet sich nicht gezielt an Kinder unter 16 Jahren.', haken: true },
{ id: 'knopf', text: 'Analyse einschalten', haken: false },
])
assert.deepEqual(JSON.parse(n.fassungen), { zustimmung: config.aboFunktionen.fassung, leistungsbeschreibung: analyseUnterlagen().avvVersion, avv: analyseUnterlagen().avvVersion })
assert.equal(aw.docs('portalFeatures').get('f_p1_analyse').audienceConfirmed, true)
}
assert.equal((await aktivieren({ accentColor: '#222222' })).status, 200)
assert.equal(aw.docs('featureConsents').size, 1) // nur die Farbe einer aktiven Analyse: kein zweiter Nachweis
})
test('Nachweis scheitert: alles zurückgenommen (kein on ohne Nachweis), der nächste Versuch klappt', async () => {
abo(hostingDoc)
aw.failNext({ collection: 'featureConsents', method: 'POST', status: 500 })
const r = await aktivieren()
assert.equal(r.status, 500)
assert.deepEqual([aw.docs('featureConsents').size, aw.docs('portalFeatures').get('f_p1_analyse').state, putSiteAufrufe.map((a) => a.active)], [0, 'off', [true, false]]) // Messdienst angemeldet und wieder zurückgenommen
assert.equal((await aktivieren()).status, 200)
assert.equal(aw.docs('featureConsents').size, 1)
})
test('alte Ansicht unverändert: ohne audienceConfirmed 200, kein Eintrag in featureConsents', async () => {
config.kundenansichtNeu.mode = 'off'
abo({ ...wartungDoc })
assert.equal((await aktivieren({ audienceConfirmed: undefined })).status, 200)
assert.equal(aw.docs('featureConsents').size, 0)
})
test('Tagesprüfung: Hosting-Analyse bleibt für Kunden der neuen Ansicht, sonst endet sie wie bisher (K19)', async () => {
const stelle = (mode, pilotEmails = []) => Object.assign(config.kundenansichtNeu, { mode, pilotEmails })
const lauf = async () => {
aktiveAnalyse('c1', 'p1')
return (await an.runAboCheck({ now: JETZT })).deactivated.length
} // Kunde c1: k@example.invalid
abo(hostingDoc)
for (const [mode, liste, deaktiviert] of [['on', [], 0], ['pilot', ['k@example.invalid'], 0], ['pilot', ['*'], 0], ['pilot', ['k2@example.invalid'], 1], ['pilot', [], 1], ['off', ['*'], 1]]) {
stelle(mode, liste)
assert.equal(await lauf(), deaktiviert, `${mode} ${liste}`)
}
aw.seed('customers', { ...customers.c1, email: ' K@Example.Invalid ' })
stelle('pilot', ['k@example.invalid'])
assert.equal(await lauf(), 0, 'E-Mail des Kunden mit Großbuchstaben und Leerzeichen trifft die Liste')
aw.seed('customers', customers.c1)
stelle('on')
abo({ ...wartungDoc, cancelAtPeriodEnd: true })
assert.equal(await lauf(), 0, 'gekündigt, aber noch laufend')
abo({ ...wartungDoc, status: 'canceled' })
assert.equal(await lauf(), 1, 'beendet')
assert.equal(await aboModellFuerKunde('gibtEsNicht'), false) // kein Kundendokument: kein Abo-Modell
assert.equal(await aboModellFuerKunde(''), false)
})
+88
View File
@@ -0,0 +1,88 @@
import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
// Einrichtungs-Ticket mit Abrechnung (V7) und unlockedBy in featureView fuer alle Funktionen.
// workorders, websiteProjects und customers legt das Setup-Skript nicht an: ohne Schema im Nachbau.
const aw = await startAppwriteMock({ schema: { portalFeatures: schemaKeys('portalFeatures') } })
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { createFeatureTicket, ABRECHNUNGS_ZEILEN } = await import('../server/services/ticketAdmin.js')
const { featureView, ticketNachlegen } = await import('../server/services/siteFeatures.js')
aw.seed('customers', { $id: 'c1', name: 'Kunde Eins', location: 'Berlin', email: 'kunde1@example.test' })
const ALT = ['createdAt', 'customerId', 'customerLocation', 'customerName', 'details', 'priority', 'serviceType', 'status', 'systemType', 'topic', 'type', 'woid']
const base = { customerId: 'c1', project: { $id: 'p1', projectName: 'Bäckerei' }, featureKey: 'termine', siteKey: 'abcdefghijklmnop', topic: 'T', details: 'Zeile 1' }
const nutzer = (doc) => Object.keys(doc).filter((k) => !k.startsWith('$')).sort()
// aw.docs() liefert eine Kopie des Standes zum Aufruf: das Ticket erst anlegen, dann nachsehen
const ticketVon = async (args) => {
const { ticketId } = await createFeatureTicket(args)
return aw.docs('workorders').get(ticketId)
}
test('abrechnung abo und projekt: Felder und letzte Zeile', async () => {
assert.deepEqual(ABRECHNUNGS_ZEILEN, {
abo: 'Zeit im Arbeitsblatt erfassen und aufs Abo-Stundenkonto anrechnen (Einrichtung aus dem Abo, Vorschuss möglich).',
projekt: 'Zeit im Arbeitsblatt erfassen, nicht aufs Abo anrechnen (im Projekt enthalten).',
})
const t = await ticketVon({ ...base, abrechnung: 'abo' })
assert.deepEqual([t.einrichtungFuer, t.einrichtungAbrechnung], ['termine', 'abo'])
assert.equal(t.details, `Zeile 1\n${ABRECHNUNGS_ZEILEN.abo}`)
assert.ok((await ticketVon({ ...base, abrechnung: 'projekt' })).details.endsWith('\nZeit im Arbeitsblatt erfassen, nicht aufs Abo anrechnen (im Projekt enthalten).'))
})
test('ohne abrechnung: keine neuen Felder, Details unverändert (alte Ansicht)', async () => {
for (const abrechnung of [undefined, '']) {
const t = await ticketVon({ ...base, abrechnung })
assert.deepEqual(nutzer(t), ALT)
assert.equal(t.details, 'Zeile 1')
}
})
test('ungültiger Wert: TypeError, kein Ticket', async () => {
const n = aw.docs('workorders').size
for (const abrechnung of ['frei', 'Abo', null, 5]) await assert.rejects(() => createFeatureTicket({ ...base, abrechnung }), TypeError, String(abrechnung))
assert.equal(aw.docs('workorders').size, n)
})
test('lange Details: gekürzt auf 7000, die Schlusszeile bleibt; auch im Notfallweg nach 400', async () => {
const lang = Array.from({ length: 400 }, (_, i) => `Zeile ${i} ${'x'.repeat(30)}`).join('\n')
const t = await ticketVon({ ...base, details: lang, abrechnung: 'abo' })
assert.ok(t.details.length <= 7000 && t.details.endsWith(ABRECHNUNGS_ZEILEN.abo))
assert.ok(t.details.length > 6800, 'es wird nicht zu stark gekürzt')
aw.failNext({ collection: 'workorders', method: 'POST', status: 400, message: 'zu lang' })
const n = await ticketVon({ ...base, details: lang, fallbackDetails: 'Notfall', abrechnung: 'projekt' })
assert.ok(n.details.startsWith('Notfall') && n.details.endsWith(ABRECHNUNGS_ZEILEN.projekt))
})
test('featureView: unlockedBy für alle Funktionen, aboEndeAt/einbau nur bei Zeiten', async () => {
assert.equal((await featureView({ state: 'setup', unlockedBy: 'abo' }, 'news')).unlockedBy, 'abo')
assert.equal((await featureView({ state: 'setup', unlockedBy: 'projekt' }, 'shop')).unlockedBy, 'projekt')
assert.equal((await featureView({ state: 'active', unlockedByPurchase: true }, 'termine')).unlockedBy, 'kauf')
assert.equal((await featureView(undefined, 'news')).unlockedBy, null)
assert.equal((await featureView({ state: 'off', unlockedBy: 'abo' }, 'news')).unlockedBy, null)
assert.equal(Object.hasOwn(await featureView({ state: 'setup', unlockedBy: 'abo' }, 'news'), 'einbau'), false)
assert.equal(Object.hasOwn(await featureView({ state: 'setup', unlockedBy: 'abo' }, 'news'), 'aboEndeAt'), false)
const zeiten = await featureView({ state: 'setup', unlockedBy: 'abo', einbau: 'angestossen' }, 'zeiten')
assert.deepEqual([zeiten.unlockedBy, zeiten.einbau, Object.hasOwn(zeiten, 'aboEndeAt')], ['abo', 'angestossen', true])
})
test('ersetztes Ticket trägt die Abrechnung des Dokuments', async () => {
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei' })
aw.seed('portalFeatures', { $id: 'f_p1_news', customerId: 'c1', projectId: 'p1', featureKey: 'news', siteKey: 'abcdefghijklmnop', state: 'setup', enabled: false, unlockedBy: 'abo', einrichtungAbrechnung: 'abo', ticketId: 'weg' })
const { doc } = await ticketNachlegen({ customerId: 'c1', projectId: 'p1', key: 'news', by: 'Kenso' })
const ticket = aw.docs('workorders').get(doc.ticketId)
assert.deepEqual([ticket.einrichtungAbrechnung, ticket.einrichtungFuer], ['abo', 'news'])
assert.ok(ticket.details.endsWith(ABRECHNUNGS_ZEILEN.abo))
})
test('ersetztes Ticket ohne Abrechnung am Dokument: wie bisher, keine neuen Felder', async () => {
aw.seed('portalFeatures', { $id: 'f_p1_termine', customerId: 'c1', projectId: 'p1', featureKey: 'termine', siteKey: 'bbcdefghijklmnop', state: 'setup', enabled: false, unlockedByPurchase: true, ticketId: 'weg2' })
const { doc } = await ticketNachlegen({ customerId: 'c1', projectId: 'p1', key: 'termine', by: 'Kenso' })
assert.deepEqual(nutzer(aw.docs('workorders').get(doc.ticketId)), ALT)
})
+45
View File
@@ -0,0 +1,45 @@
import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
// Ticket-Texte zum Stundenkonto je Abrechnung (P6 Task 14): Dokumente des Abo-Modells (einrichtungAbrechnung abo oder projekt) sagen im
// Ticket nirgends "nicht anrechnen"; die Abrechnung steht allein in der Schlusszeile aus createFeatureTicket (ABRECHNUNGS_ZEILEN).
const aw = await startAppwriteMock()
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
after(() => Promise.all([aw.close(), st.close()]))
const { termineTicketDetails } = await import('../server/services/termine/ticketDetails.js')
const { shopTicketDetails } = await import('../server/services/shop/ticketDetails.js')
const { unterkunftTicketDetails } = await import('../server/services/unterkunft/ticketDetails.js')
const { einbauanleitung } = await import('../server/services/termine/einbauanleitung.js')
const { ausVorlage } = await import('../server/services/termine/vorlagen.js')
const NOW = new Date('2026-10-17T08:00:00.000Z')
// Salon-Vorlage als gespeicherte Einstellung: erst damit erzeugt der Generator alle Abschnitte (Checkliste, Abrechnung), ohne Einstellung nur das Snippet
const EINSTELLUNG = () => ausVorlage('salon', { name: 'Salon Muster', email: 'inhaber@salon-muster.example', telefon: '0631 123456', adresse: 'Hauptstraße 1, 67655 Kaiserslautern', kleinunternehmer: false, impressumUrl: 'https://salon-muster.example/impressum', datenschutzUrl: 'https://salon-muster.example/datenschutz', bundesland: 'RP' }, NOW)
const ALT = /nicht auf (das )?Stundenkontingent|nicht auf Abo-Stunden|angerechnet/i
const ctx = (key, doc) => ({ feature: { key }, project: { projectName: 'Bäckerei', subdomain: 'baeckerei' }, customer: { name: 'Kunde' }, doc: { $id: `f_${key}`, siteKey: 'abcdefghijklmnop', customerId: 'c1', projectId: 'p1', featureKey: key, ...doc } })
const BAUER = { termine: termineTicketDetails, shop: shopTicketDetails, unterkunft: unterkunftTicketDetails }
for (const [key, bau] of Object.entries(BAUER)) {
test(`${key}: mit Abrechnung abo und projekt keine „nicht anrechnen“-Aussage im Ticket`, async () => {
for (const einrichtungAbrechnung of ['abo', 'projekt']) assert.doesNotMatch(await bau(ctx(key, { einrichtungAbrechnung, unlockedBy: einrichtungAbrechnung })), ALT, `${key} ${einrichtungAbrechnung}`)
})
test(`${key}: Kauf-Bestand und Altdokumente behalten ihren Wortlaut`, async () => { assert.match(await bau(ctx(key, key === 'termine' ? { unlockedByPurchase: true } : {})), ALT) })
}
test('Terminplaner-Anleitung: Text und Kurzfassung je Abrechnung; Notfalltext ohne Kontingent-Satz', async (t) => {
t.mock.method(console, 'error', () => {})
const gen = (feature) => einbauanleitung({ feature: { $id: 'f1', siteKey: 'abcdefghijklmnop', customerId: 'c1', projectId: 'p1', ...feature }, project: { projectName: 'B' }, einstellung: EINSTELLUNG(), fragebogen: null, version: 1, updatedAt: null, kunde: { name: 'K' }, now: NOW })
for (const a of ['abo', 'projekt']) { const g = gen({ einrichtungAbrechnung: a, unlockedBy: a }); assert.doesNotMatch(g.text, ALT, a); assert.doesNotMatch(g.kurz, ALT, a) }
assert.match(gen({ einrichtungAbrechnung: 'abo', unlockedBy: 'abo' }).text, /Einrichtung aus dem Abo, Zeit im Arbeitsblatt aufs Abo-Stundenkonto anrechnen \(Vorschuss möglich\)\./)
assert.match(gen({ einrichtungAbrechnung: 'projekt', unlockedBy: 'projekt' }).text, /im Projekt enthalten, Zeit im Arbeitsblatt, nicht aufs Abo anrechnen\./)
assert.match(gen({ unlockedByPurchase: true }).kurz, /Nicht auf das Stundenkontingent anrechnen/)
// Ersatztexte der Datenschutz-Regel und Schritt 6
assert.match(gen({ einrichtungAbrechnung: 'abo', unlockedBy: 'abo' }).kurz, /das gehört zur Einrichtung und zählt mit zum Stundenkonto/)
assert.match(gen({ einrichtungAbrechnung: 'projekt', unlockedBy: 'projekt' }).kurz, /das gehört zur Einrichtung und ist im Projekt enthalten/)
assert.match(gen({ einrichtungAbrechnung: 'abo', unlockedBy: 'abo' }).kurz, /6\. Ticket erledigen und die Zeit im Arbeitsblatt buchen\.(\n|$)/)
// Notfalltext (Anleitung nicht erzeugbar, hier: leere $id, ladeStand wirft): im Abo-Modell ohne Kontingent-Satz, ohne Marker wie heute
assert.doesNotMatch(await termineTicketDetails(ctx('termine', { einrichtungAbrechnung: 'abo', unlockedBy: 'abo', $id: '' })), ALT)
assert.match(await termineTicketDetails(ctx('termine', { $id: '' })), /Nicht auf das Stundenkontingent anrechnen\./)
})
+54
View File
@@ -0,0 +1,54 @@
import { test, after } from 'node:test'
import assert from 'node:assert/strict'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
// Kundenart im Zustimmungsnachweis (V4): nur geschrieben, wenn uebergeben; ungueltig ist 400 ohne Schreibzugriff
const schema = { featureConsents: schemaKeys('featureConsents') } // bleibt aenderbar wie in featureSetup.test.js
const aw = await startAppwriteMock({ schema })
const st = await startFakeStripe()
after(() => Promise.all([aw.close(), st.close()]))
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const reg = await import('../server/services/featureRegistry.js')
const { speichereZustimmung, speichereNachweisAusAnfrage } = await import('../server/services/featureConsents.js')
const base = { customerId: 'c1', projectId: 'p1', featureKey: 'termine', stripeSessionId: '', fassungen: { zustimmung: 'x' }, texte: [{ id: 'knopf', text: 'Einschalten', haken: false }], ip: '203.0.113.9', userAgent: 'UA/1', konto: 'k', at: '2026-10-17T08:00:00.000Z' }
test('kundenart wird gespeichert; ohne Angabe fehlt der Schlüssel, auch bei altem Schema', async () => {
assert.equal((await speichereZustimmung({ ...base, kundenart: 'privat' })).kundenart, 'privat')
assert.equal((await speichereZustimmung({ ...base, kundenart: 'unternehmen' })).kundenart, 'unternehmen')
schema.featureConsents = schemaKeys('featureConsents').filter((k) => k !== 'kundenart') // Setup-Skript noch nicht gelaufen
assert.equal(Object.hasOwn(await speichereZustimmung(base), 'kundenart'), false)
await assert.rejects(() => speichereZustimmung({ ...base, kundenart: 'privat' }), /Unknown attribute/)
schema.featureConsents = schemaKeys('featureConsents')
})
test('ungültige Kundenart: 400, nichts geschrieben', async () => {
const vorher = aw.docs('featureConsents').size
for (const kundenart of ['firma', '', 'Privat', 3, null]) {
await assert.rejects(() => speichereZustimmung({ ...base, kundenart }), (e) => e.status === 400 && /Kundenart ungültig/.test(e.message), String(kundenart))
}
assert.equal(aw.docs('featureConsents').size, vorher)
})
test('Nachweis aus der Anfrage im Abo-Modell: Zeile, Hinweis, optional Häkchen, Knopf; Fassungen ohne widerruf', async () => {
const req = { ip: '203.0.113.9', get: (h) => (h === 'user-agent' ? 'UA/1' : ''), session: { name: 'Kunde Eins', email: 'k@example.invalid' } }
const feature = reg.featureConfig('termine')
await speichereNachweisAusAnfrage(req, { feature: reg.aboweg(feature), customerId: 'c1', projectId: 'p1', klickAt: '2026-10-17T08:00:00.000Z', kundenart: 'unternehmen' })
await speichereNachweisAusAnfrage(req, { feature: reg.aboweg(feature, { beginn: true }), customerId: 'c1', projectId: 'p2', klickAt: '2026-10-17T08:00:00.000Z', kundenart: 'privat' })
const [a, b] = [...aw.docs('featureConsents').values()].filter((d) => d.featureKey === 'termine' && d.projectId.startsWith('p')).slice(-2)
assert.deepEqual(JSON.parse(a.texte).map((t) => t.id), ['einschalten-zeile', 'hinweis', 'knopf']) // termine trägt den Hinweis (Pflichten der alten Häkchen)
assert.equal(JSON.parse(a.texte)[1].text, reg.consentAbo('termine').items[1].text)
assert.deepEqual(JSON.parse(b.texte).map((t) => [t.id, t.haken]), [['einschalten-zeile', false], ['hinweis', false], ['beginn', true], ['knopf', false]])
assert.deepEqual(Object.keys(JSON.parse(a.fassungen)).sort(), ['avv', 'leistungsbeschreibung', 'zustimmung'])
assert.deepEqual([a.kundenart, b.kundenart, a.konto, a.stripeSessionId], ['unternehmen', 'privat', 'Kunde Eins <k@example.invalid>', ''])
})
test('Nachweis aus der Anfrage ohne kundenart: Schlüssel fehlt (alter Weg bleibt)', async () => {
const req = { ip: '203.0.113.9', get: () => '', session: { name: 'Kunde Eins', email: 'k@example.invalid' } }
await speichereNachweisAusAnfrage(req, { feature: reg.featureConfig('news'), customerId: 'c1', projectId: 'p9', klickAt: '2026-10-17T08:00:00.000Z' })
const doc = [...aw.docs('featureConsents').values()].find((d) => d.projectId === 'p9')
assert.equal(Object.hasOwn(doc, 'kundenart'), false)
})
+2 -2
View File
@@ -188,8 +188,8 @@ test('Schema featureConsents: drei Indizes mit den Attributen aus der Vorgabe (i
})
test('schemaKeys(featureConsents) liefert dieselben Schluessel, der Nachbau nimmt sie an und lehnt andere ab', async () => {
assert.deepEqual(schemaKeys('featureConsents'), CONSENT_KEYS)
assert.deepEqual(schemaKeys('featureConsents'), FEATURE_CONSENT_ATTRIBUTES.map((a) => a.key))
assert.deepEqual(schemaKeys('featureConsents'), [...CONSENT_KEYS, 'kundenart'])
assert.deepEqual(schemaKeys('featureConsents'), [...FEATURE_CONSENT_ATTRIBUTES.map((a) => a.key), 'kundenart'])
schema.featureConsents = schemaKeys('featureConsents')
const { createDocument } = await import('../server/services/appwriteAdmin.js')
const doc = await createDocument(config.collections.featureConsents, {
+114
View File
@@ -0,0 +1,114 @@
import { test, after, beforeEach } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { startAppwriteMock } from './support/appwriteMock.js'
import { startFakeStripe } from './support/fakeStripe.js'
import { useTestEnv } from './support/testEnv.js'
import { schemaKeys } from './support/schema.js'
import { AUFTRAEGE_DATETIMES } from '../server/services/auftraege/schema.js'
// Gesammelte Widerrufsfristen (V6): Abo-Abschluss, Wechsel, Zusatzspeicher (nur Plus), bezahlte Funktionen, Auftraege
const aw = await startAppwriteMock({
schema: {
subscriptions: schemaKeys('subscriptions'),
contractEvents: schemaKeys('contractEvents'),
portalFeatures: schemaKeys('portalFeatures'),
featureConsents: schemaKeys('featureConsents'),
auftraege: schemaKeys('auftraege'),
},
datetimes: AUFTRAEGE_DATETIMES,
})
const st = await startFakeStripe()
useTestEnv({ appwriteUrl: aw.url, stripeUrl: st.url })
const { config } = await import('../server/config.js')
const { startTestApp, rawGet } = await import('./support/testApp.js')
const { customers, customerCookie, kundenansichtCookie, subscriptionDoc, wartungDoc } = await import('./support/fixtures.js')
const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
const { sammleFristen } = await import('../server/services/fristen.js')
const { default: fristenRoutes } = await import('../server/routes/fristen.js')
const app = await startTestApp({ '/api/fristen': fristenRoutes })
const modusVorher = config.auftraege.mode
after(() => {
config.auftraege.mode = modusVorher
return Promise.all([app.close(), aw.close(), st.close()])
})
const JETZT = new Date('2026-10-17T08:00:00.000Z')
const ereignis = (id, type, contractAt, over = {}) =>
aw.seed('contractEvents', { $id: id, customerId: 'c1', subscriptionId: 'sub_h', type, productKey: 'wartung', status: 'erledigt', consentAt: contractAt, details: JSON.stringify({ contractAt }), ...over })
const aufbau = () => { // Ende je Quelle: wechsel Mo 26.10., funktion Do 29.10., speicher Fr 30.10., abschluss Mo 02.11.
ereignis('ev_a', 'abschluss', '2026-10-17T07:00:00.000Z')
ereignis('ev_w', 'wechsel', '2026-10-10T09:00:00.000Z')
ereignis('ev_s', 'speicher', '2026-10-16T09:00:00.000Z', { details: JSON.stringify({ contractAt: '2026-10-16T09:00:00.000Z', fromUnits: 0, toUnits: 1 }) })
ereignis('ev_m', 'speicher', '2026-10-16T10:00:00.000Z', { details: JSON.stringify({ contractAt: '2026-10-16T10:00:00.000Z', fromUnits: 2, toUnits: 1 }) }) // Minus: keine Frist
aw.seed('portalFeatures', { $id: 'f_p1_news', customerId: 'c1', projectId: 'p1', featureKey: 'news', state: 'setup', enabled: false, unlockedByPurchase: true, orderedAt: '2026-10-15T09:00:00.000Z' })
}
beforeEach(async () => {
for (const collection of ['contractEvents', 'portalFeatures', 'auftraege', 'subscriptions', 'websiteProjects']) {
for (const id of aw.docs(collection).keys()) await deleteDocument(collection, id)
}
aw.seed('customers', { ...customers.c1 })
aw.seed('subscriptions', subscriptionDoc('sub_h', wartungDoc))
aw.seed('websiteProjects', { $id: 'p1', customerId: 'c1', projectName: 'Bäckerei' })
config.auftraege.mode = 'off'
})
test('mehrere Quellen, aufsteigend nach Ende; Speicher-Plus zählt, Minus nicht', async () => {
aufbau()
const r = await sammleFristen('c1', { now: JETZT })
assert.deepEqual(r.fristen, [
{ quelle: 'wechsel', bezeichnung: 'Wechsel ins Wartungs-Abo', bis: '2026-10-26T22:59:59.999Z' },
{ quelle: 'funktion', bezeichnung: 'News-Pinnwand', bis: '2026-10-29T22:59:59.999Z', featureKey: 'news', projektName: 'Bäckerei' },
{ quelle: 'speicher', bezeichnung: 'Zusatzspeicher', bis: '2026-10-30T22:59:59.999Z' },
{ quelle: 'abschluss', bezeichnung: 'Abo-Abschluss', bis: '2026-11-02T22:59:59.999Z' },
])
assert.deepEqual([r.fruehestesEnde, r.anzeigen], ['2026-10-26T22:59:59.999Z', true])
})
test('Kundenart: Betrieb hat Fristen, aber anzeigen false; privat, leer und unbekannt zeigen', async () => {
aufbau()
for (const [kundenart, anzeigen] of [['unternehmen', false], ['privat', true], ['', true], ['kaputt', true]]) {
aw.seed('customers', { ...customers.c1, kundenart })
const r = await sammleFristen('c1', { now: JETZT })
assert.deepEqual([r.fristen.length, r.anzeigen], [4, anzeigen], kundenart)
}
})
test('abgelaufene oder fehlende Fristen: leer, fruehestesEnde null, anzeigen false', async () => {
ereignis('ev_alt', 'abschluss', '2026-09-20T09:00:00.000Z') // Ende Mo 05.10.
assert.deepEqual(await sammleFristen('c1', { now: JETZT }), { fristen: [], fruehestesEnde: null, anzeigen: false })
})
test('Aufträge zählen nur mit Schalter und Snapshot-Kundenart privat', async () => {
const ab = (kundenart, status = 'versendet') =>
aw.seed('auftraege', { $id: 'ab1', nummer: 'AB-2026-0007', kundenId: 'c1', status, gesendetAm: '2026-10-16T09:00:00.000Z', kundeJson: JSON.stringify({ kundenart }) })
ab('privat')
config.auftraege.mode = 'off'
assert.deepEqual((await sammleFristen('c1', { now: JETZT })).fristen, [])
config.auftraege.mode = 'on'
assert.deepEqual((await sammleFristen('c1', { now: JETZT })).fristen, [{ quelle: 'projekt', bezeichnung: 'Auftrag AB-2026-0007', bis: '2026-10-30T22:59:59.999Z' }])
ab('privat', 'bestaetigt')
assert.equal((await sammleFristen('c1', { now: JETZT })).fristen.length, 1)
ab('privat', 'widerrufen')
assert.deepEqual((await sammleFristen('c1', { now: JETZT })).fristen, [])
ab('unternehmen')
assert.deepEqual((await sammleFristen('c1', { now: JETZT })).fristen, [])
})
test('Lesefehler werden weitergereicht', async () => {
aufbau()
aw.failNext({ collection: 'contractEvents', method: 'GET', status: 500 })
await assert.rejects(() => sammleFristen('c1', { now: JETZT }))
})
test('GET /api/fristen: 401 ohne Anmeldung, Form, no-store, Kundenansicht erlaubt; Mount in server/index.js', async () => {
assert.equal((await rawGet('/api/fristen')).status, 401)
const res = await rawGet('/api/fristen', { cookie: customerCookie('c1') })
assert.deepEqual([res.status, Object.keys(res.body).sort(), res.headers.get('cache-control')], [200, ['anzeigen', 'fristen', 'fruehestesEnde'], 'no-store'])
assert.equal((await rawGet('/api/fristen', { cookie: kundenansichtCookie({ customerId: 'c1' }) })).status, 200)
const src = readFileSync(new URL('../server/index.js', import.meta.url), 'utf8')
assert.ok(src.indexOf("app.use('/api/fristen', fristenRoutes)") > src.indexOf('app.use(sessionMiddleware())'))
assert.ok(src.indexOf("app.use('/api/fristen', fristenRoutes)") < src.indexOf('app.use(express.static(publicDir))'))
assert.ok(src.indexOf("app.use('/api/fristen', fristenRoutes)") > src.indexOf("app.use('/api/funktionen', funktionenRoutes)"))
})
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff. Show more