Spec: Terminplaner (TP1 Kern, TP2 Verkauf, TP3 Online-Zahlung)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d8328722b3
commit
275dbcd690
1 file changed
+544
@@ -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:<featureId>'])` 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:<featureId>:<ressourceId>'], {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=<token>`: 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=<token>` (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
|
||||
`"<betrieb.name>" <SMTP_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 =
|
||||
<bookingId>@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/<feedToken>.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
|
||||
<div data-webklar-termine="SITEKEY" data-api="https://project.webklar.com" data-modus="inline">
|
||||
<p>Termine telefonisch unter 06301 1234.</p> <!-- Ausweichtext, bleibt ohne JS/bei Fehlern stehen -->
|
||||
</div>
|
||||
<script src="https://project.webklar.com/embed/termine.js" defer></script>
|
||||
```
|
||||
|
||||
`data-modus`: `inline` (Buchungsstrecke direkt in der Seite) oder `knopf` (Knopf mit Text aus
|
||||
`data-knopf`, öffnet ein `<dialog>`; 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.
|
||||
Reference in new issue
Block a user