From 275dbcd690bf0979c6995199846456c71cef5075 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Oct 2026 14:20:40 +0000 Subject: [PATCH] Spec: Terminplaner (TP1 Kern, TP2 Verkauf, TP3 Online-Zahlung) Co-Authored-By: Claude Opus 5.5 --- .../specs/2026-10-02-terminplaner-design.md | 544 ++++++++++++++++++ 1 file changed, 544 insertions(+) create mode 100644 docs/superpowers/specs/2026-10-02-terminplaner-design.md diff --git a/docs/superpowers/specs/2026-10-02-terminplaner-design.md b/docs/superpowers/specs/2026-10-02-terminplaner-design.md new file mode 100644 index 0000000..f1f0dde --- /dev/null +++ b/docs/superpowers/specs/2026-10-02-terminplaner-design.md @@ -0,0 +1,544 @@ +# Terminplaner – Design + +Stand: 2026-10-02 · Status: im Chat freigegeben (Entscheidungen E1–E8), Spec zur Durchsicht +Teilprojekte: **TP1 Kern** (diese Spec im Detail, erster Plan) · **TP2 Verkauf** · **TP3 Online-Zahlung** +(TP2 und TP3 sind hier verbindlich umrissen und bekommen je einen eigenen Plan; Regel des Nutzers: +ein Teilprojekt live, vom Nutzer getestet, erst dann das nächste). + +Recherche (nur lesend, Quellen dort): `/root/reports/terminplaner-2026-10-02/` +`code-funktionen.md`, `code-kern.md`, `code-woms.md`, `recherche-branchen.md`, `recherche-recht.md`, +`recherche-technik.md` (+ `spike-code/`), `recherche-stripe-connect.md`, `recherche-recht-zahlung.md`. + +## Ziel + +Betriebe (unsere Kunden) bekommen eine Online-Terminbuchung auf ihrer eigenen Website. Der Teil auf der +Website sieht bei jedem Kunden anders aus (Design der Kundenseite), die Verwaltung im Kundenportal ist +für alle gleich. Ein Fragebogen (TP2) klassifiziert den Betrieb und erzeugt für WEBklar eine fertige +Einbauanleitung: wir bauen ein, schließen das Ticket, der Terminplaner läuft. + +Die Funktion ersetzt die Kachel „Terminplaner · Kommt bald“ (`public/funktionen.js:151`). + +## Entscheidungen (vom Nutzer) + +| # | Entscheidung | +|---|---| +| E1 | Abrechnung: **einmalige Einrichtung + nur solange ein Wartungs-Abo läuft** (wie Website-Analyse). Endet das Abo, endet der Terminplaner. Hosting-Abo reicht nicht. | +| E2 | Einrichtungspreis **149 €** (brutto, einmalig, Stripe Checkout wie News). | +| E3 | Umfang V1: **Personen und Plätze** (Einzelperson; Team mit Wahl der Person oder „egal“; buchbare Plätze wie Kabine, Hebebühne). Pro Leistung: **sofort verbindlich** oder **Anfrage** (Betrieb bestätigt). Gruppen/Kurse/Tische später. | +| E4 | Der Gast **bestätigt seine E-Mail-Adresse per Link**, bevor der Termin gilt; der Slot ist so lange 30 Minuten reserviert. | +| E5 | **Ein gemeinsames Widget**, je Kunde gestaltet (Farben, Schrift, Rundungen, Texte, Position) + offene, versionierte Schnittstelle für Sonderfälle. | +| E6 | Der **Fragebogen kommt vor der Zahlung**; das Ticket enthält sofort die Einbauanleitung (TP2). | +| E7 | **Zahlungsmodus legt der Betrieb fest:** nur vor Ort · nur online · online oder vor Ort (dann wählt der Gast). Online-Geld geht **direkt an den Betrieb** (Stripe Connect, WEBklar berührt kein Geld). TP1 kann nur „vor Ort“, TP3 bringt „online“. | +| E8 | Reihenfolge: TP1 Kern (Pilot mit Kunde „test“) → TP2 Verkauf → TP3 Online-Zahlung. | + +Vorgaben aus dem Entwurf, denen der Nutzer nicht widersprochen hat: + +- Gebaut **im Kundenportal** (kein eigener Container, keine Fremdsoftware), Daten in Appwrite `woms-database`. +- **Ein** Buchungssystem für alle Branchen; Branchen-Vorlagen sind Daten, kein Code je Nische. +- **Gesundheitsbereich ausgeschlossen** (Arzt, Zahnarzt, Physio/Ergo/Logo/Podologie, Psychotherapie, + Heilpraktiker, medizinische Kosmetik/Massage). Freitext beim Gast standardmäßig aus. +- Erinnerung an den Gast **nur per nicht vorangekreuztem Haken**. +- Gästemails: Anzeigename = Betrieb, Antworten an den Betrieb, **keine Team-Kopie** (`bccTeam` aus), kein + Tracking, keine Werbung. +- Mailversand über eine **Warteschlange** (Hetzner-Grenze 500 Mails/h für alles). +- Löschfristen: unbestätigt 48 h, abgesagt 30 Tage, vergangen 90 Tage (einstellbar 14–365 Tage). +- Schalter `TERMINE_ENABLED` = `off` | `pilot` | `on`. + +## Begriffe + +- **Betrieb** = unser Kunde (Portal-Kunde, Inhaber der Website). **Gast** = wer auf der Website bucht. +- **Ressource** = was gebucht wird: eine **Person** oder ein **Platz** (je Kapazität 1; zwei Hebebühnen = + zwei Plätze). **Leistung** = was gebucht wird (Dauer, Puffer, Preis, Buchungsart). +- **Funktion** `termine` = Dokument in `portalFeatures` (`featureKey: 'termine'`), mit eigenem `siteKey`. + +--- + +## TP1 Kern + +### 1. Lebenszyklus in TP1 + +- `TERMINE_ENABLED=off` (Standard): Kachel „Kommt bald“, öffentliche Schnittstelle antwortet für jeden + Schlüssel „nicht aktiv“, Routen des Kunden 409. +- `pilot`: Kachel und Ansicht nur für Kunden, deren E-Mail in `TERMINE_PILOT_EMAILS` steht (Komma-Liste, + Muster `ABO_PILOT_EMAILS`). Bestellen gibt es in TP1 nicht; die Funktion wird im Ticketsystem mit + „Aktivieren (ohne Zahlung)“ eingeschaltet (vorhandenes `adminActivate`, auf Schlüssel verallgemeinert). +- `on` kommt mit TP2 (Bestellung für alle mit Wartungs-Abo). +- Zustände der Funktion wie bei News: `off → active → disabled` (TP1); `ordered/setup` folgen mit TP2. + `disabled` ⇒ Widget zeigt den Ausweichtext, keine neuen Buchungen; bestehende Termine bleiben im Portal + sichtbar, Gäste können weiter absagen und widerrufen. + +### 2. Datenmodell (Appwrite, neues Setup-Skript `scripts/termine-setup.mjs`) + +Muster von `scripts/analyse-setup.mjs` (exportierte Attributlisten, injizierbarer Client, `--check`, +legt nur Fehlendes an, Indizes nur bei neuen Collections). Alle Collections `permissions: []`, +`documentSecurity: false` (nur Server-Schlüssel). `test/support/schema.js` liest das neue Skript mit. + +**`termineConfig`** – eine Einstellung je Funktion; Dokument-ID = ID des `portalFeatures`-Dokuments. +`customerId`, `projectId` (64), `data` (Text, JSON bis 200 000 Zeichen), `version` (Ganzzahl), +`updatedAt`, `updatedBy` (100). Inhalt von `data` (validiert in `services/termine/einstellungen.js`): + +```jsonc +{ + "schema": 1, + "vorlage": "salon", // Branchen-Vorlage, nur Herkunft + "betrieb": { "name": "Salon Müller", "email": "info@salon.de", "telefon": "06301 1234", + "adresse": "Hauptstr. 1, 67722 Winnweiler", + "impressumUrl": "https://…", "datenschutzUrl": "https://…", "agbUrl": null }, + "anrede": "sie", // sie | du (Widget, Gästeseiten, Gästemails) + "farbe": "#7c3aed", // Akzent für Gästeseiten und -mails + "bundesland": "RP", "feiertageGeschlossen": true, + "sonderoeffnungen": { "2026-12-24": [["09:00","13:00"]] }, // Datum → Fenster, schlägt Plan/Feiertag + "abwesenheiten": [ { "id": "a1", "ressourceId": null, "von": "2026-12-27", "bis": "2026-12-31", "titel": "Betriebsurlaub" } ], + "ressourcen": [ { "id": "r1", "typ": "person", "name": "Anna", "aktiv": true, + "plan": { "1": [["09:00","12:30"],["13:30","18:00"]], "6": [["09:00","13:00"]] }, + "feedToken": "…" } ], // ISO-Wochentag 1=Mo … 7=So; Pausen = Lücken + "leistungen": [ { "id": "s1", "name": "Damenschnitt", "beschreibung": "", "dauer": 45, "pufferNach": 5, + "preisArt": "fest", "preisCent": 4500, // fest | ab | keiner + "buchungsart": "sofort", // sofort | anfrage + "ressourcen": ["r1"], "aktiv": true, "reihenfolge": 1 } ], + "regeln": { "raster": 15, "vorlaufMin": 120, "horizontTage": 60, "absageBisStd": 24, + "maxOffeneJeEmail": 3, "personWaehlbar": true }, + "gastFelder": { "telefon": "optional", "nachricht": "aus", "zusatz": [] }, // aus | optional | pflicht + "erinnerung": { "anbieten": true, "stundenVorher": 24 }, + "storno": { "text": "Kostenlose Absage bis 24 Stunden vor dem Termin …", "version": "2026-10-02-1" }, + "recht": { "gaesteSind": "verbraucher", // verbraucher | unternehmer | gemischt + "freizeit": false }, // true ⇒ kein Widerrufsrecht (§ 312g Abs. 2 Nr. 9), nur nach Prüfung + "zahlung": { "modus": "vorort", "vorOrtArten": ["bar","ec","kreditkarte"] }, // online/beides erst TP3 + "aufbewahrungTage": 90 +} +``` + +IDs von Ressourcen/Leistungen/Abwesenheiten sind kurze Zufalls-IDs (keine Appwrite-IDs). Änderungen +sind optimistisch gesperrt: `PATCH` schickt `version`, der Server schreibt unter +`withLocks(['termine-cfg:'])` und antwortet bei abweichender Version 409 „Die Einstellungen +wurden inzwischen geändert. Bitte neu laden.“ + +**`termineBookings`** – eine Buchung je Dokument (ID `randomUUID`). +Felder: `featureId`, `customerId`, `projectId` (64); `leistungId`, `leistungName` (120, Schnappschuss), +`ressourceId`, `ressourceName` (80); `startsAt`, `endsAt`, `blockEndsAt` (datetime UTC; `blockEndsAt` = +Ende + Puffer); `status` (16, siehe 4); `quelle` (16: `online` | `manuell` | `sperre`); +`gastName` (100), `gastEmail` (254), `gastTelefon` (40), `gastNachricht` (500), `gastZusatz` (2000, JSON); +`erinnerung` (bool), `erinnerungGesendetAt`; `heldUntil`; `verifyHash`, `manageHash` (64, SHA-256 der +Tokens); `bestaetigtAt`, `angenommenAt`, `abgesagtAt`, `abgesagtVon` (16: `gast`|`betrieb`|`system`), +`absageGrund` (300), `widerrufenAt`; `nachweis` (4000, JSON: Fassung und Wortlaut-Hash von Storno-, +Datenschutz- und Widerrufstext, Zeitpunkt der Zustimmungen, Knopftext, Anrede); `preisCent`, +`preisArt` (8), `buchungsart` (8), `verbraucher` (bool); `zahlart` (8: `vorort` | `online`), +`zahlStatus` (16: `keine`|`offen`|`bezahlt`|`erstattet`|`teilerstattet`|`fehlgeschlagen`; TP1 immer +`keine`), `stripeSessionId` (128), `stripePaymentIntent` (64) (TP3); `icsSequenz` (Ganzzahl); +`createdAt`, `updatedAt`. +Indizes: `idx_feature_start(featureId, startsAt)`, `idx_status_held(status, heldUntil)`, +`idx_feature_email(featureId, gastEmail)`, `idx_verify(verifyHash)`, `idx_manage(manageHash)`, +`idx_status_start(status, startsAt)`. + +**`termineClaims`** – Belegungszellen gegen Doppelbuchung (siehe 5). Dokument-ID deterministisch +`c_` + erste 34 Hex von `sha1(featureId|ressourceId|zellenStartUtcIso)`. Felder `featureId`, +`ressourceId`, `bookingId`, `zelle` (datetime), `bis` (datetime, Haltezeit oder Terminende). +Index `idx_booking(bookingId)`, `idx_bis(bis)`. + +**`termineOutbox`** – Mail-Warteschlange. Felder `featureId`, `bookingId`, `art` (16), `an` (254), +`fromName` (120), `replyTo` (254), `subject` (300), `html` (Text 100 000), `text` (Text 50 000), +`ics` (Text 20 000), `icsMethod` (8), `status` (`wartet`|`gesendet`|`fehler`), `versuche`, +`naechsterVersuch`, `gesendetAt`, `fehler` (300), `createdAt`. Index `idx_status_next(status, naechsterVersuch)`. +Gesendete und endgültig gescheiterte Einträge werden nach 7 Tagen gelöscht (enthalten personenbezogene Daten). + +`portalFeatures` bekommt **keine** neuen Attribute; `featureKey 'termine'` nutzt die vorhandenen. + +### 3. Verfügbarkeit (reine Funktionen, `server/services/termine/`) + +- **Zeit** (`zeit.js`, ohne Abhängigkeit, `Intl`): Wanduhr Europe/Berlin ↔ UTC; Wochentag; Tage addieren. + Nicht existierende Zeiten (Umstellung März, 02:00–02:59) werden **ausgelassen**, doppelte Zeiten + (Oktober) nehmen das **erste** Vorkommen. Tests für 2026-03-29 und 2026-10-25. `deadlines.berlinTime` + wird dafür **nicht** benutzt (kennt die Lücke nicht). +- **Feiertage** (`feiertage.js`): Paket `feiertagejs` (MIT, ohne Abhängigkeiten) je Bundesland; + `sonderoeffnungen` und `abwesenheiten` schlagen Feiertage/Plan. +- **Slots** (`slots.js`, `now` wird übergeben): je Ressource und Berliner Kalendertag die Öffnungsfenster + aus `plan` (oder `sonderoeffnungen`), minus Feiertag (wenn geschlossen), minus Abwesenheiten, minus + belegte Zeiten (Buchungen mit Status `gehalten|angefragt|bestaetigt` und Sperren, jeweils bis + `blockEndsAt`). Startzeiten im `raster` ab Fensterbeginn; Termin + `pufferNach` muss ganz ins Fenster + passen; nicht vor `now + vorlaufMin`; nicht nach `horizontTage`. „Egal“ = Vereinigung über alle + Ressourcen der Leistung; beim Buchen wird die Ressource mit den wenigsten Buchungen an dem Tag gewählt + (bei Gleichstand Reihenfolge der Liste). +- Ein Abruf liefert höchstens 31 Tage; der Server lädt dafür die Buchungen des Zeitraums per Indexabfrage + (`featureId` + `startsAt` zwischen, mit Seitenweise-Laden über `cursorAfter`), nie „alles laden“. + +### 4. Buchungsablauf und Zustände + +Zustände (`status`): `gehalten` (Formular abgeschickt, wartet auf E-Mail-Bestätigung) → `angefragt` +(Leistung „Anfrage“, wartet auf den Betrieb) oder `bestaetigt` → `erledigt` (automatisch 1 h nach Ende) · +`nichterschienen` (Betrieb) · `abgesagt` (Gast, Betrieb, System) · `abgelehnt` (Betrieb lehnt Anfrage ab) · +`verfallen` (Link nicht innerhalb von 30 Minuten bestätigt). Sperren (`quelle: sperre`) sind immer +`bestaetigt` und haben keinen Gast. + +**Gast im Widget** +1. Leistung wählen (Name, Dauer, Preis „45 €“ / „ab 45 €“ / ohne, Kurzbeschreibung). Zahlungsart steht + schon hier („Bezahlung vor Ort: bar, EC-Karte, Kreditkarte“, § 312j Abs. 1 BGB). +2. Person wählen, wenn mehr als eine Ressource infrage kommt und `personWaehlbar` (mit „Egal“ als erster + Option); bei Plätzen kein Schritt. +3. Tag (Monatsraster, nur Tage mit freien Zeiten aktiv) und Uhrzeit. +4. Daten: Name, E-Mail, Telefon/Nachricht/Zusatzfelder nach `gastFelder`. Über der Nachricht steht + „Bitte keine Gesundheitsangaben.“ +5. Zusammenfassung: Leistung, Person/Platz, Datum, Uhrzeit, Preis inkl. USt (bzw. Hinweis Kleinunternehmer + aus dem Einstellungsfeld des Betriebs), Ort, Zahlungsart, Stornoregel im Klartext + Link, Hinweis zum + Datenschutz (Text 11.2 aus `recherche-recht.md`, Link auf `datenschutzUrl`), Haken: + - Pflicht: „Ich habe die Stornobedingungen gelesen.“ (nur wenn `storno.text` gesetzt) + - Pflicht nur bei verbindlicher Leistung mit Preis, Verbrauchern und Termin innerhalb von 14 Tagen nach + der Buchung: Zustimmung zum vorzeitigen Beginn (§ 356 Abs. 5 Nr. 2 BGB, Wortlaut im Rechtsteil). + - Freiwillig, nicht vorangekreuzt (nur wenn `erinnerung.anbieten`): „Ich möchte {24} Stunden vorher eine + Erinnerung per E-Mail.“ + Knopf: **„Zahlungspflichtig buchen“** (verbindlich + Preis), **„Termin buchen“** (verbindlich, ohne + Preis), **„Termin anfragen“** (Buchungsart Anfrage). +6. Ergebnis: „Fast geschafft: Bitte bestätigen Sie den Termin über den Link, den wir an m***@gmail.com + geschickt haben. Der Termin ist 30 Minuten für Sie reserviert.“ + +**Server beim Abschicken** (`POST …/buchungen`): Prüfungen (Abschnitt 8), Slot neu berechnen, unter +`withLocks(['termine-res::'], {waitMs: 5000})` Zellen belegen (5), Buchung +`gehalten` mit `heldUntil = now + 30 min` anlegen, Mail G01 in die Warteschlange (sofort senden). + +**Bestätigungslink** `https://project.webklar.com/termin.html#v=`: Die Seite liest das Token aus +dem Fragment (geht nicht an Server-Logs), zeigt per `POST …/vorschau` die Zusammenfassung und einen Knopf +mit demselben Wortlaut wie im Widget (bei Preis „Zahlungspflichtig bestätigen“). Erst `POST …/bestaetigen` +ändert den Zustand (Mailscanner öffnen nur Links): `gehalten` → `bestaetigt` (G02 an Gast mit .ics, +Verwaltungslink und ggf. Widerrufsbelehrung; K20 an den Betrieb mit .ics) bzw. → `angefragt` (G03 an Gast, +K20-Block „Anfrage“ an den Betrieb). Abgelaufen ⇒ „Diese Reservierung ist abgelaufen“ + Link zur Website. +Ein Token gilt genau einmal. + +**Verwaltungslink** `termin.html#m=` (gültig bis Terminende + 1 Tag): zeigt den Termin und je nach +Lage +- „Termin absagen“ bis `absageBisStd` vor Beginn (danach Text „Absage nur telefonisch: …“) ⇒ `abgesagt`, + Zellen frei, G06 an Gast (ICS CANCEL), K21 an den Betrieb; +- „Termin verschieben“ bis zur selben Frist ⇒ neue Zeit wählen; neuer Slot wird belegt, alter freigegeben + (eine Sperre über beide Ressourcen), Status bleibt, `icsSequenz + 1`, G07 + K21; +- **„Vertrag widerrufen“**, solange ein Widerrufsrecht besteht (verbindlich, Verbraucher, nicht `freizeit`, + innerhalb von 14 Tagen nach `bestaetigtAt`) – unabhängig von der Stornofrist (§ 361 BGB) ⇒ Formular nach + § 356a BGB (Name, Buchungsnummer, E-Mail für die Eingangsbestätigung vorbefüllt, Knopf „Widerruf + bestätigen“) ⇒ `widerrufenAt`, `abgesagt` (von `gast`), G08 Eingangsbestätigung mit Datum und Uhrzeit, + K21-Block „Widerruf“. + +**Widerruf ohne Link** (§ 356a: auf der Oberfläche, über die der Vertrag geschlossen wurde): Das Widget +zeigt im Fuß dauerhaft „Vertrag widerrufen“, sobald der Betrieb verbindliche Leistungen für Verbraucher +anbietet. Formular: Name, E-Mail, Buchungsnummer (steht in G02), Knopf „Widerruf bestätigen“. Der Server +antwortet immer gleich („Ihre Erklärung ist eingegangen“), speichert die Erklärung an der passenden +Buchung (E-Mail + Nummer), schickt G08 an die angegebene Adresse und leitet die Erklärung per K21 an den +Betrieb weiter – auch wenn keine Buchung passt (dann Block „nicht zugeordnet“, der Betrieb prüft). + +**Betrieb im Portal**: Anfrage annehmen (`angefragt` → `bestaetigt`, G02) oder ablehnen (`abgelehnt`, G04, +Zellen frei); bestätigten Termin absagen (Pflicht-Grund-Auswahl + optionale Nachricht, G06) oder verschieben +(G07); „nicht erschienen“; Termin von Hand eintragen (`quelle: manuell`, Gast-E-Mail optional, G02 nur wenn +der Haken „Bestätigung an den Gast senden“ gesetzt ist; ohne Widerrufsbelehrung durch das System, die ist +bei telefonischen Buchungen Sache des Betriebs); Zeit sperren (`quelle: sperre`). + +**Hintergrund-Jobs** (`server/index.js`, Muster der vorhandenen `setInterval`-Jobs, je Job eigene Fehlerbehandlung): +- jede Minute: abgelaufene `gehalten` ⇒ `verfallen` + Zellen löschen; Warteschlange senden (6). +- alle 5 Minuten: Erinnerungen (`bestaetigt`, `erinnerung`, kein `erinnerungGesendetAt`, + `startsAt` in `[now + stundenVorher − 5 min, now + stundenVorher]`; erst Marker setzen, dann einreihen); + `bestaetigt` mit `endsAt < now − 1 h` ⇒ `erledigt`. +- täglich: Löschfristen (11), verwaiste Zellen (Buchung fehlt oder nicht mehr belegend). + +### 5. Doppelbuchung verhindern + +- Zellen à **5 Minuten** von Beginn bis `blockEndsAt`. Jede Zelle ist ein `termineClaims`-Dokument mit + deterministischer ID; eine zweite Belegung derselben Zelle scheitert in der Datenbank mit 409. +- Alle Zellen einer Buchung werden in **einem** Aufruf angelegt (Appwrite 1.8.1 Bulk-Create + `POST …/documents` mit `documents: [...]`, eine Transaktion, höchstens 100 Dokumente ⇒ Termine bis + 500 Minuten). 409 ⇒ „Dieser Termin wurde gerade vergeben. Bitte wählen Sie eine andere Zeit.“ +- Zusätzlich `withLocks` je Ressource im Prozess (Reihenfolge, keine Fehlalarme bei Doppelklick). +- **Spike vor dem Bau (Pflicht, Plan-Aufgabe 1):** auf einer Wegwerf-Collection in der Produktions-Appwrite + 50 parallele Bulk-Creates mit überlappenden IDs ⇒ genau einer 201, Rest 409, keine Teil-Schreibungen; + danach Collection löschen. Braucht die Freigabe des Nutzers. Fällt der Spike negativ aus: Fallback + Transactions-API (`/v1/databases/transactions`, ebenfalls in 1.8.1), sonst Einzel-Creates mit Rückbau + unter der Prozess-Sperre. +- Der Test-Nachbau (`test/support/appwriteMock.js`) lernt Bulk-Create mit Alles-oder-nichts und 409, + `between`/`greaterThanEqual`/`lessThanEqual`/`cursorAfter`. + +### 6. Mails + +**Versand**: neue Funktion `sendGuestMail` in `mailer.js` (nutzt den vorhandenen Transport): From +`"" ` (Anzeigename = Betrieb, Adresse bleibt `noreply@webklar.com`, also keine +DNS-/Postfach-Änderung), `replyTo = betrieb.email`, **nie BCC**, `icalEvent` für Kalenderdaten. Alle +Termine-Mails laufen über `termineOutbox` (Muster „erst speichern, dann senden“): sofortiger Sendeversuch +nach dem Einreihen, sonst Job; höchstens **200 Mails/Stunde** für den Terminplaner (Rest des +Hetzner-Budgets bleibt Portal/Abo); Wiederholung nach 1, 5, 30 Minuten, dann `fehler` und Hinweis im +Portal an der Buchung („Bestätigung konnte nicht zugestellt werden“). **Sicherung:** bei einem +SMTP-Anmeldefehler pausiert die Warteschlange 15 Minuten (Hetzner sperrt die IP nach mehreren +Fehlversuchen für ~10 Minuten). + +**Vorlagen** (Engine `mailTemplates.js`; Kennungen um `G` erweitern, Test `mailTemplates.test.js` +`EXPECTED` mitpflegen). Gästemails haben ein **neutrales Layout** ohne WEBklar-Logo: Kopf mit Betriebsname +in `farbe`, Fuß mit Betrieb (Name, Adresse, Telefon, E-Mail, Links Impressum/Datenschutz) und dem Satz +„Diese E-Mail wurde im Auftrag von {Betrieb} über den Terminplaner von WEBklar versendet.“ Jede Vorlage hat +Blöcke `sie`/`du`. + +| ID | an | Betreff (Sie-Form) | Inhalt | +|---|---|---|---| +| G01 | Gast | Bitte bestätigen Sie Ihren Termin bei {betrieb} | Datum, Uhrzeit, Leistung, Knopf „Termin bestätigen“, „Das waren Sie nicht? Dann ignorieren Sie diese E-Mail.“ Nichts Werbliches. | +| G02 | Gast | Ihr Termin bei {betrieb} am {datum} | Zusammenfassung, Buchungsnummer, Zahlungsart, Stornoregel (Wortlaut), Verwaltungslink, .ics (REQUEST); bei Widerrufsrecht: Widerrufsbelehrung + Muster-Widerrufsformular + Hinweis auf die Widerrufsfunktion (§ 312f Abs. 2 BGB) | +| G03 | Gast | Ihre Terminanfrage bei {betrieb} | „Der Betrieb meldet sich“, Verwaltungslink (nur Absage) | +| G04 | Gast | Ihre Terminanfrage bei {betrieb} | abgelehnt, optionale Nachricht des Betriebs | +| G05 | Gast | Erinnerung: Ihr Termin morgen um {zeit} | Termin, Verwaltungslink; sonst nichts | +| G06 | Gast | Ihr Termin am {datum} wurde abgesagt | wer abgesagt hat, Grund/Nachricht, .ics (CANCEL) | +| G07 | Gast | Ihr Termin wurde verschoben | alte/neue Zeit, .ics (REQUEST, Sequenz + 1) | +| G08 | Gast | Eingangsbestätigung Ihres Widerrufs | Inhalt der Erklärung, Datum und Uhrzeit des Eingangs | +| K20 | Betrieb | Neue Buchung: {leistung} am {datum} um {zeit} (Block „anfrage“: Neue Terminanfrage …) | Gastdaten, Link ins Portal, .ics (PUBLISH) | +| K21 | Betrieb | Termin abgesagt / verschoben / widerrufen: … | Block je Fall, Link ins Portal | + +K20/K21 im WEBklar-Layout (Du-Form wie K15–K19), Empfänger `betrieb.email`, **ohne** Team-BCC (enthalten +Gästedaten). + +**ICS** (`ics.js`, eigener Schreiber, ~50 Zeilen, Zeilenfaltung/Escaping nach RFC 5545): `UID = +@termine.webklar.com` (stabil), `SEQUENCE = icsSequenz`, `DTSTART/DTEND` in UTC, `SUMMARY` = +„{Leistung} – {Betrieb}“ (an den Betrieb: „{Leistung}: {Gastname}“), `LOCATION` = Adresse, `ORGANIZER` = +Betrieb, `ATTENDEE` = Gast (`RSVP=FALSE`), `METHOD` REQUEST/CANCEL. Keine „In Google eintragen“-Links. + +**Kalender-Abo für den Betrieb**: je Ressource geheimer Link `…/v1/kalender/.ics` (nur +bestätigte Termine und Sperren der nächsten 90 Tage, `X-PUBLISHED-TTL: PT15M`; Google aktualisiert nur +alle paar Stunden – steht so im Portal). Link neu erzeugen macht den alten ungültig. + +### 7. Website-Teil + +**Einbau** (vom Mitarbeiter, je Kunde gestaltet): + +```html +
+

Termine telefonisch unter 06301 1234.

+
+ +``` + +`data-modus`: `inline` (Buchungsstrecke direkt in der Seite) oder `knopf` (Knopf mit Text aus +`data-knopf`, öffnet ein ``; Daten werden erst beim Klick geladen). Optional +`data-leistung="s1"` (vorausgewählt, z. B. Knopf neben einer Leistung). + +**`public/embed/termine.js`** (ohne Abhängigkeiten, ohne Build, IIFE wie `news.js`): +- Light DOM mit Klassenpräfix `wkt-`, Standard-Stile in `@layer wkt`, Gestaltung über CSS-Variablen der + Seite: `--wkt-akzent`, `--wkt-akzent-text`, `--wkt-hintergrund`, `--wkt-text`, `--wkt-rand`, + `--wkt-radius`, `--wkt-schrift`, `--wkt-schrift-titel`; Containerabfragen für schmale Spalten/Handy. +- Kein Cookie, kein Web-Storage, kein Auslesen von Referrer/Bildschirm/Zeitzone, keine Drittanfragen; + Text nur per `textContent`. +- Barrierefrei nach WCAG 2.1 AA: echte Formularelemente, sichtbarer Fokus, Monatsraster nach dem + APG-Muster „Date Picker Dialog“ (Pfeiltasten, `aria-selected`, Tage ohne Zeiten `aria-disabled`), + Fehler über `aria-live`, Zielgröße ≥ 24 px, kein Zeitlimit im Widget (die 30 Minuten gelten erst nach + dem Absenden). +- Deutsche Texte in Sie- oder Du-Form nach `anrede`. Fuß: „Vertrag widerrufen“ (wenn nötig, siehe 4) und + „Datenschutz“ (Link `datenschutzUrl`), klein „Terminbuchung über WEBklar“. +- Fehler/inaktiv/Abo beendet: Ausweichtext bleibt sichtbar; bei Netzfehler mitten in der Strecke „Gerade + nicht erreichbar. Bitte rufen Sie an: {telefon}“. +- Demo `public/embed/termine-demo.html`, Einbauanleitung für Mitarbeiter `docs/funktionen/termine-einbau.md` + (Snippet, Variablen-Tabelle, Beispiele für drei Stile, Datenschutz-Absatz, Abnahme-Checkliste). + +**Gästeseiten** `public/termin.html` + `public/termin.js` + `public/termin.css`: Bestätigen, Verwalten, +Widerrufen (Token im Fragment). Kopf mit Betriebsname in `farbe`, Fuß mit Links auf Impressum und +Datenschutz des Betriebs **und** auf das Impressum von WEBklar. `noindex`, keine Cookies. + +### 8. Öffentliche Schnittstelle `/api/public/termine/v1/:siteKey/…` + +Eigener Router (`server/routes/publicTermine.js`), in `server/index.js` **vor** dem globalen +`express.json` eingehängt (eigener Parser `limit: '16kb'`). Unbekannter oder inaktiver Schlüssel ⇒ +immer dieselbe Antwort `{ aktiv: false }` (200), nie ein Hinweis, ob der Schlüssel existiert. Antworten +enthalten **nie** `customerId`, `projectId`, `featureId`, Appwrite-, Stripe- oder Ticket-IDs; nur +Leistungs-/Ressourcen-IDs aus der Einstellung und die Buchungsnummer (8 Zeichen, aus der Buchungs-ID +abgeleitet, nur zur Anzeige). + +| Methode, Pfad | Zweck | Cache | +|---|---|---| +| `GET /config` | Betriebsname, Anrede, Farbe, Leistungen (ohne interne Felder), wählbare Ressourcen, Regeln für die Anzeige, Zahlungsarten, Stornotext + Version, Rechtsflags (Widerruf ja/nein), Formular-Token (HMAC über siteKey + Zeit) | `public, max-age=60`, ETag | +| `GET /tage?leistung=&ressource=&monat=YYYY-MM` | Tage mit freien Zeiten | `max-age=15` | +| `GET /zeiten?leistung=&ressource=&tag=YYYY-MM-DD` | freie Startzeiten; bei „egal“ ohne Angabe der Person | `no-store` | +| `POST /buchungen` | Formular abschicken ⇒ `gehalten`, G01 | – | +| `POST /vorschau` `{token}` | Zusammenfassung für Bestätigen/Verwalten | – | +| `POST /bestaetigen` `{token}` | E-Mail bestätigt | – | +| `POST /absagen`, `POST /verschieben` `{token, …}` | Verwaltung durch den Gast | – | +| `POST /widerruf` `{name, email, nummer, token?}` | Widerrufsfunktion | – | +| `GET /kalender/:feedToken.ics` | Kalender-Abo des Betriebs (Schlüssel im Pfad statt siteKey) | `max-age=300` | + +Schutz (ohne Captcha-Dienst): +- **CORS**: GETs `Access-Control-Allow-Origin: *`; POSTs nur von erlaubten Origins = Hosts des Projekts + (Subdomain unter `project.webklar.com`, `liveDomain` + www-Gegenstück, wie `analyseHosts`) und + `https://project.webklar.com` (Gästeseiten); Preflight erlaubt `Content-Type`. +- **Honeypot** (`website`, unsichtbar) und **Zeitfalle** (Formular-Token muss ≥ 3 s und ≤ 2 h alt sein) ⇒ + bei Treffer dieselbe Erfolgsantwort, aber nichts gespeichert, keine Mail. +- **Mengen** (im Speicher, mit Obergrenze der Schlüssel wie `pushTime.js`): je IP (IPv6 je /64) 10 + Buchungen/10 min und 120 GET/min; je Funktion 60 gehaltene Buchungen/h; je E-Mail-Adresse und + Funktion höchstens `maxOffeneJeEmail` offene Termine und 5 Bestätigungsmails/Tag; `widerruf` 10/h je IP. +- Eingaben streng prüfen (Längen, E-Mail-Syntax, Telefon nur Ziffern/Leerzeichen/+/-/(), Zusatzfelder + nur bekannte Schlüssel); Fehler als Feldfehler `{ fehler: { feld: text } }`. +- Appwrite nicht erreichbar ⇒ 503 `{ fehler: 'Gerade nicht erreichbar' }`, nie Rohfehler. + +### 9. Verwaltung im Portal (für alle gleich) + +Neue Ansicht `#view-termine` (Hash `#termine`), Kachel „Terminplaner“ im Funktionen-Tab mit Pille *Aus* / +*Aktiv* / *Abgeschaltet* (TP1) und Knopf „Termine öffnen“. In der App-Ansicht Unterseite von Funktionen +(`APP_PARENT.termine = 'funktionen'`). Mehrere Website-Projekte ⇒ Projektauswahl oben (wie News). Eigenes +Modul `public/termine.js` + `public/termine.css` (nicht in das schon 794 Zeilen lange `funktionen.js`). + +Reiter: +1. **Termine** – Liste nach Tagen (heute, morgen, …; „Woche“ / „Monat“ / Datumssprung), Filter + offen/angefragt/bestätigt/abgesagt; Zeile: Uhrzeit, Leistung, Person/Platz, Gast, Status, Hinweise + („Bestätigung nicht zugestellt“). Aktionen siehe 4; „+ Termin eintragen“, „Zeit sperren“. Zähler + „{n} Anfragen warten“ auf der Kachel. +2. **Leistungen** – Liste mit Reihenfolge; Felder wie im Datenmodell; Hinweis, dass Leistungsnamen keine + Gesundheitsangaben enthalten dürfen. +3. **Team & Plätze** – Ressourcen mit Wochenplan-Editor (je Tag mehrere Zeitfenster), Zuordnung der + Leistungen, Abwesenheiten/Urlaub, Bundesland + „an Feiertagen geschlossen“, Sonderöffnungen, + Kalender-Abo-Link je Ressource (Kopieren, Neu erzeugen). +4. **Einstellungen** – Betriebsangaben für Gäste (vorbefüllt aus dem Kundenkonto), Anrede, Farbe, Regeln, + Gastfelder, Erinnerung, Stornotext (Mustertext als Vorschlag, deutlich als Muster gekennzeichnet), + Rechtliches („Ihre Gäste sind: Privatpersonen / Unternehmen / beides“; Hinweis, dass der Widerruf die + Stornofrist schlägt), Zahlung (TP1: nur „vor Ort“ mit Zahlarten; „online“ ausgegraut „kommt bald“), + Aufbewahrung, **Export** (CSV der Buchungen), Status „aktiv seit …“. +- **Erstes Öffnen** ohne Einstellung: Auswahl einer Branchen-Vorlage (`salon`, `kosmetik`, `beratung`, + `werkstatt`, `handwerk`, `tattoo`, `fotograf`, `hundesalon`, `sonstiges`; Daten in + `server/services/termine/vorlagen.js`, die TP2 für den Fragebogen wiederverwendet), danach Bearbeiten. + +Kunden-API `server/routes/termine.js` unter `/api/termine` (`requireSession`, `assertOwnProject`, Funktion +`active` außer für reines Lesen, Schreiblimit 120/h je Kunde): `GET/PATCH /einstellungen`, +`GET /buchungen?von&bis&status`, `POST /buchungen` (manuell/Sperre), `POST /buchungen/:id/aktion` +(`annehmen|ablehnen|absagen|verschieben|nichterschienen`), `POST /ressourcen/:id/feed` (neu erzeugen), +`GET /export.csv`. + +**Kundenansicht** (Mitarbeiter als Kunde): Lesen und Schreiben auf `/api/termine/*` erlaubt +(Einstellungen und Termine kosten nichts, sind keine Erklärungen im Namen des Kunden); Einträge in +`KUNDENANSICHT_ALLOWED` + `test/kundenansichtGuard.test.js`, Protokoll `termin_einstellungen`, +`termin_aktion`, `termin_manuell` (Portal `VIEW_ACTIONS`, WOMS `src/lib/viewLog.js`). + +### 10. Ticketsystem (WOMS) in TP1 + +- Portal: `FEATURE_KEYS = ['news','termine']`; `findActiveBySiteKey(siteKey, featureKey)`; + `activateClosedTickets` je Schlüssel (für TP2 vorbereitet); `funktionenAdmin.js` Route + `POST /api/team/funktionen/:customerId/:key` (`key` ∈ `FEATURE_KEYS`; `/news` bleibt gleichwertig) und + `adminView` mit allen Schlüsseln (Ticket-Lookups gebündelt statt N+1). +- WOMS: `src/lib/funktionenView.js` (Register der Funktionen, Zustandstexte, Bestätigungstexte), + `CustomerFunktionen.jsx` zeigt je Projekt **News** und **Terminplaner** (eigene Zeilen), Aktionen + „Aktivieren (ohne Zahlung)“ / „Abschalten“ je Funktion, Ticketnummer als Link `/tickets?woid=`; + `teamApi.setFeature(customerId, key, …)`. `viewLog.js` um die neuen Aktionen. + +### 11. Recht und Datenschutz in TP1 (Pilot) + +TP1 läuft nur im Pilot mit dem eigenen Testkunden; echte Gäste fremder Betriebe erst nach TP2 +(AVV + Leistungsbeschreibung + Freigabe der Texte durch den Nutzer). TP1 liefert als **Entwürfe** +(Kennzeichnung „Entwurf“, Dateien in `docs/rechtliches/` bzw. `docs/funktionen/`): +- Datenschutz-Absatz „Terminbuchung“ für die Website des Betriebs (Sie/Du), Hinweis unter dem Formular, + Stornotext-Muster, Widerrufsbelehrung + Muster-Widerrufsformular für Dienstleistungen (Anlage 1/2 + EGBGB, Platzhalter des Betriebs) – Grundlage `recherche-recht.md` 11.x und `recherche-recht-zahlung.md` 7.3. +- Abschnitt „6c Terminplaner“ für `public/datenschutz.html` (WEBklar als Auftragsverarbeiter; was das + Portal selbst verarbeitet) – nicht veröffentlicht vor TP2. +- Zitat-Korrektur im Bestand („§ 356 Abs. 4“ ⇒ „§ 356 Abs. 5 Nr. 2 BGB“ in `config.funktionen.consentText`, + `funktionen-leistungsbeschreibung.html`, Test) erfolgt in TP2 mit dem neuen Einwilligungstext je Funktion. + +**Löschfristen** (täglicher Job im Terminplaner selbst, unabhängig von `retention.js`, weil die Fristen je +Buchung laufen und nicht am Vertragsende des Kunden hängen): `verfallen` und unbestätigte nach 48 h; `abgesagt`/`abgelehnt` nach 30 Tagen; +`erledigt`/`nichterschienen` nach `aufbewahrungTage` (Standard 90, erlaubt 14–365) ab Terminende; +Warteschlange nach 7 Tagen; Zellen mit der Buchung. Funktion `disabled` ⇒ neue Buchungen aus, nach 30 +Tagen (Zeit für den Export) Löschung aller Buchungen, Zellen, Einstellung. Kunde gelöscht ⇒ Funktion +`disabled` (Haken in `customerAdmin.deleteCustomerWithPortalAccess`, wie Analyse). + +### 12. Fehlerfälle + +- Slot inzwischen weg (409 beim Belegen) ⇒ Meldung im Widget, Zeiten neu laden. +- Bestätigungslink nach Ablauf ⇒ „abgelaufen“; zweiter Klick auf denselben Link ⇒ „bereits bestätigt“ + + Verwaltungslink-Hinweis. +- Mail scheitert endgültig ⇒ Hinweis an der Buchung im Portal; Buchung bleibt gültig. +- Einstellung ungültig (z. B. Leistung ohne Ressource) ⇒ Speichern mit Feldfehler abgelehnt; Leistung ohne + aktive Ressource wird im Widget nicht angezeigt. +- Leistung oder Ressource gelöscht, während Termine bestehen ⇒ nur deaktivieren erlaubt, solange künftige + Termine daran hängen (Meldung mit Anzahl). +- Funktion abgeschaltet ⇒ öffentliche Schreibrouten `{ aktiv: false }`, Gästeseiten erlauben weiter + Absagen/Widerruf bestehender Termine. +- Uhr-Umstellung ⇒ keine Slots in der fehlenden Stunde; Termine über die Umstellung behalten ihre + echte Dauer. + +### 13. Tests TP1 + +- `npm test` (node --test): Zeit (beide Umstellungstage), Feiertage je Land (Stichproben 2026/2027), + Slots (Pausen, Puffer, Vorlauf, Horizont, Abwesenheit, Feiertag, Sonderöffnung, „egal“), Zellen/Bulk + (Mock: Alles-oder-nichts), Tokens (einmalig, Ablauf), ICS (Faltung, Escaping, CANCEL), Vorlagen + (alle G/K-Vorlagen rendern ohne `problems`, `EXPECTED`), Einstellungs-Validierung, öffentliche Routen + (keine internen IDs, `{aktiv:false}` einheitlich, CORS/Origin, Honeypot, Zeitfalle, Mengen, Knopftexte + je Fall, Widerruf mit/ohne Treffer), Kunden-Routen (Rechte, fremdes Projekt 403, Versionskonflikt 409, + Kundenansicht-Liste), Jobs (Verfall, Erinnerung genau einmal, Erledigt, Löschfristen), Warteschlange + (Rate, Wiederholung, Pause nach Anmeldefehler). +- Playwright (Fixture-Server, wie `test/ui/*.ui.mjs`): Widget inline und Knopf auf einer Testseite + (Desktop, Handy, nur Tastatur, Ausweichtext bei Fehler), Gästeseiten (bestätigen, absagen, verschieben, + widerrufen), Portal-Ansicht (alle Reiter, Vorlage wählen, Wochenplan-Editor) – Screenshots nach + `/root/reports/terminplaner-ui/`. +- WOMS: `npm test` (Register, Zustände), `vite build`. +- Live-Spike Bulk-Create (5) vor dem Bau. + +### 14. Livegang TP1 (je Schritt Freigabe des Nutzers) + +1. L1 Schema: `termine-setup.mjs --check`, dann anlegen (im Container). Abhängigkeit `feiertagejs` + kommt mit dem Deploy (`deploy.sh` führt `npm install` aus). +2. L2 Portal: Push `feature/terminplaner` → `main` (der Nutzer pusht); Compose: `TERMINE_ENABLED=pilot`, + `TERMINE_PILOT_EMAILS=kenso.gri@gmail.com`, Container neu erstellen (`docker compose up -d --no-deps + kundenbereich-server`, nie `preview-portal`). +3. L3 WOMS: Push auf `test`. +4. L4 Pilot: im WOMS beim Kunden „test“ den Terminplaner für „Test-Website“ aktivieren; im Portal (Login + des Testkunden) Vorlage wählen, Leistungen/Zeiten pflegen; Widget in `WEBklar/test-website` einbauen + (Push → Deploy) inkl. Datenschutz-Absatz-Entwurf; Testbuchungen mit kenso.gri@gmail.com: bestätigen, + Erinnerung, absagen, verschieben, widerrufen, Anfrage annehmen/ablehnen, Kalender-Abo in Google. +5. L5 Nutzertest; Aufräumen der Testbuchungen nach Wunsch. + +--- + +## TP2 Verkauf (eigener Plan nach TP1) + +- **Fragebogen** im Portal (Kachel „Terminplaner einrichten · 149 €“, nur mit Wartungs-Abo): 8 Schritte + (Branche/Vorlage mit Gesundheits-Ausschluss · wer wird gebucht + Namen · Leistungen · Zeiten, Bundesland, + Urlaub · Regeln + sofort/Anfrage · Gastfelder · Benachrichtigungen, Anrede, Erinnerung · Website: wo + buchen, Knopf/inline, Gestaltungswunsch, Ausweichtelefon, Links Impressum/Datenschutz), Zwischenspeichern + als Entwurf in `termineConfig`. Ergebnis = dieselbe Einstellung wie TP1 + Einbauwünsche. +- **Bestellung**: Zusammenfassung, Haken Leistungsbeschreibung + AVV Terminplaner (Fassung), „kein + Gesundheitsbereich“, Widerrufs-Zustimmung für Verbraucher (§ 356 Abs. 5 Nr. 2 BGB), Knopf + „Zahlungspflichtig bestellen · 149 €“ ⇒ Stripe Checkout (einmalig, Katalogeintrag `termineSetup` + 14900 Cent). Verallgemeinerung des Funktionen-Checkouts: Register `config.features` (Preis, + Einwilligungstext, Zeiteinschätzung, Ticket-Text, Mails, Rücksprung-Hash) statt fester News-Werte; + K15/K16/T16 mit Platzhalter `{{funktion}}`; Widerrufsformular-Beschriftung. +- **Ticket mit Einbauanleitung** (`services/termine/einbauanleitung.js`, deterministisch aus der + Einstellung): Kurzfassung in `ticket.details` (≤ 8000 Zeichen), vollständig im WOMS-Reiter Funktionen + (Kopieren-Knopf) über `GET /api/team/funktionen/:customerId/termine/anleitung`. Inhalt: Modell, + Leistungen, Ressourcen + Zeiten, Regeln, Gastfelder, Benachrichtigungen, Einbau (Snippet mit siteKey, + Modus, CSS-Variablen-Vorschlag, Stelle auf der Website), Datenschutz-Absatz, Abnahme-Checkliste, + Abrechnung („Einrichtung bezahlt, nicht auf Abo-Stunden“). +- **Abo-Kopplung**: Bestellen nur mit laufendem Wartungs-Abo (gemeinsame Prüfung aus `analyse.js` + herausgelöst); täglicher Abgleich: Abo beendet ⇒ `disabled`, Mail an den Kunden, Löschung nach 30 Tagen. +- **Rechtstexte**: Leistungsbeschreibung Terminplaner, AVV Terminplaner (Geschwister des AVV + Website-Analyse), Datenschutz 6c, Gästetexte final, Karten in WOMS „Rechtliches“; Prüfrunde wie bei der + Analyse; danach `TERMINE_ENABLED=on`. + +## TP3 Online-Zahlung (eigener Plan nach TP2) + +- **Stripe Connect**, verbundene Konten mit Standard-Verhalten (`controller`: volles Dashboard, + `losses.payments=stripe`, `fees.payer=account`, `requirement_collection=stripe`), Fähigkeit + `card_payments`, Rahmenvertrag `full`; Einrichtung per **Account Link** (von Stripe gehostet); Status über + `account.updated` + Rücksprung. Sandbox-Plattformeinrichtung ist erledigt (Testkonto zeigt + Negativsalden/Anforderungen/Gebühren = Stripe); Live-Plattformeinrichtung vor dem Livegang. +- **Direct Charges** über von Stripe gehostetes Checkout auf dem Konto des Betriebs (`Stripe-Account`), + `mode: payment`, nur Festpreise ≥ 0,50 €, keine Plattformgebühr, SEPA ausgeschlossen (verzögert), + PayPal nicht verfügbar (Stripe unterstützt es bei Direct Charges nicht), `expires_at` ≥ 30 Minuten ⇒ + Haltezeit bei Online-Zahlung = Sitzungsdauer + 2 Minuten. +- Zahlungsmodus je Betrieb `vorort | online | beides` (beides ⇒ Frage „Wie möchten Sie bezahlen?“ ohne + Vorauswahl, gleicher Preis); online nur, wenn das verbundene Konto `charges_enabled` hat, sonst + automatisch zurück auf vor Ort + Hinweis. +- Ablauf online (Vorschlag, in TP3 zu bestätigen): Widget „Zahlungspflichtig buchen“ ⇒ Bestätigungslink + ⇒ Weiterleitung zu Stripe ⇒ Zahlung ⇒ `bestaetigt` per Webhook; späte Zahlung bei vergebenem Slot ⇒ + sofortige Erstattung. +- **Eigener Connect-Webhook** (eigenes Secret, `event.account` + `livemode` prüfen, idempotent je Sitzung), + nicht in `routes/abo.js`. +- **Erstattungsregeln** (`recherche-recht-zahlung.md` 3.5): Betrieb sagt ab ⇒ voll; Gast in der Frist ⇒ + voll; Widerruf ⇒ voll binnen 14 Tagen; zu spät/nicht erschienen ⇒ nach Regel des Betriebs; fehlendes + Guthaben ⇒ „Erstattung ausstehend“ im Portal + Mail. +- Rechtliches: Stripe-Absatz für die Datenschutzerklärung des Betriebs, AVV/Leistungsbeschreibung + ergänzen, keine Kassenfunktion, kein Rechnungsdruck. + +## Nicht Teil (V1) + +Gruppen/Kurse/Tischreservierung · Person + Platz gleichzeitig · Mehrere Leistungen in einer Buchung · +Varianten (30/60/90) · Warteliste · SMS · Zwei-Wege-Kalender-Sync (Google/Outlook) · Anzahlungen · +Kundenkonten für Gäste · Bewertungs- oder Werbemails · Mehrsprachigkeit · Gesundheitsbereich · +Wechsel des Portal-Containers auf Node 24 (eigene Aufgabe; Node 20 ist seit 30.04.2026 ohne Pflege). + +## Offene Punkte + +1. Spike Bulk-Create (TP1, Aufgabe 1) – Freigabe für Wegwerf-Collection in der Produktions-Appwrite. +2. Rechtstexte: alle als Entwurf; Prüfung vor TP2-Livegang (wie Website-Analyse), u. a. Einstufung + Massage/Fotograf/Coaching als Widerrufsfälle und ob der Bestätigungslink die Bestellung ist. +3. TP3: Stripe-Live-Plattformeinrichtung durch den Nutzer; Ablauf „Bestätigungslink vor Zahlung“ bestätigen.