Terminplaner TP2 Verkauf: Design mit den Entscheidungen D1–D8 des Inhabers
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3a0b0089dd
commit
6ed0e3bcaf
1 file changed
+301
@@ -0,0 +1,301 @@
|
||||
# Terminplaner – TP2 Verkauf: Design
|
||||
|
||||
Stand 2026-10-03. Basis: Portal `main` 3a0b008, WOMS `test` 65e9931, TP1-Spec
|
||||
`docs/superpowers/specs/2026-10-02-terminplaner-design.md` (E1–E8, Abschnitt „TP2 Verkauf“), TP1-Ledger R1–R55
|
||||
(`/root/reports/terminplaner-tp1/progress.md`). Erkundung: `/root/reports/terminplaner-tp2/erkundung-2026-10-03.json`.
|
||||
Pfade: **P** = Portal-Repo (`kundenbereich.webklar.com`), **W** = WOMS-Repo (`ticket.webklar.com`).
|
||||
Alles unten ist aus dem Code gelesen (nichts gegen Produktion oder Stripe gelaufen), außer Aussagen mit „(Annahme)“.
|
||||
„Festlegung“ = technische Entscheidung. Die geschäftlichen und rechtlichen Entscheidungen D1–D8 hat der Inhaber am
|
||||
2026-10-03 getroffen (Abschnitt 10); sie sind in alle Abschnitte eingearbeitet.
|
||||
|
||||
---
|
||||
|
||||
## 1. Ziel und Abgrenzung
|
||||
|
||||
**TP2 liefert:** Ein Kunde mit laufendem Wartungs-Abo bestellt den Terminplaner selbst: Kachel → Fragebogen (Entwurf) →
|
||||
Bestellung mit Zustimmungen → Stripe (149 € einmalig) → automatisches Ticket mit Einbauanleitung → Einbau durch das Team →
|
||||
Freischaltung → Abo-Kopplung (endet das Abo, endet der Terminplaner, Löschung nach 30 Tagen). Dazu der Rechtstexte-Satz
|
||||
(Leistungsbeschreibung, AVV, Datenschutz 6c, Gästetexte final) und danach `TERMINE_ENABLED=on`.
|
||||
|
||||
**Bleibt für TP3:** Online-Zahlung der Gäste (Stripe Connect, Erstattungen, Connect-Webhook), Abo-Bedingungen v6 bzw.
|
||||
Rahmen-AVV mit Anlage je Funktion, Stripe-Absatz in 6c, Nachweisdatensätze über 90 Tage.
|
||||
|
||||
**Pilotkunde „test“ (kenso.gri@gmail.com):** bleibt per WOMS „Aktivieren (ohne Zahlung)“ aktiv (`unlockedByPurchase=false`).
|
||||
Festlegung: Der tägliche Abo-Abgleich (Abschnitt 6) nimmt solche Dokumente aus, sonst würde der Pilot beim ersten Lauf
|
||||
abgeschaltet. Seine Daten und Einstellungen bleiben unverändert. Er dient als Prüfkunde für den Livegang-Test (Abschnitt 9).
|
||||
|
||||
**Festlegungen zum Umfang:**
|
||||
- Ein Terminplaner gilt **je Website-Projekt** (Feature-Dokument je Projekt, wie News). 149 € fällt je Projekt an und
|
||||
umfasst ein Widget an einer Stelle mit Gestaltung nach Fragebogen und den Einbau; Mehrwünsche (zweites Widget, Knöpfe je
|
||||
Leistung, Sonderdesign) nach Stunden (D4).
|
||||
- Nur der Modus „vor Ort bezahlen“ (E7). Keine Gruppen, kein Gesundheitsbereich (Spec „Nicht Teil“).
|
||||
- News bleibt unverändert nutzbar; es wird nur auf das Register umgestellt (Abschnitt 4), Verhalten und Ticket-Text von News bleiben identisch.
|
||||
|
||||
---
|
||||
|
||||
## 2. Ablauf aus Kundensicht
|
||||
|
||||
| Zustand | Kachel (`P/public/funktionen.js`) | Ansicht `#termine` (`P/public/termine.js`) | Mail |
|
||||
|---|---|---|---|
|
||||
| ohne Wartungs-Abo | „Im Wartungs-Abo enthalten. Zu Verträge“ (wie `analyseTile`), kein Fragebogen | Hinweis | – |
|
||||
| `off` (Entwurf, auch ohne Doc) | „Terminplaner einrichten · 149 €“ bzw. „Fragebogen fortsetzen“ | Fragebogen, 8 Schritte + „Prüfen und bestellen“ | – |
|
||||
| `ordered` | „Zahlung offen“ | Statuskarte, Knopf „Zahlung fortsetzen“, Einstellungen nur lesbar | – |
|
||||
| `setup` | „In Einrichtung“ | Statuskarte (Ticketnummer, Rechnung), Einstellungen nur lesbar | K15, T16 |
|
||||
| `active` | „Termine öffnen“ | wie TP1 | K16 |
|
||||
| `disabled` | „Termine ansehen“ | lesen + CSV-Export, Banner nach Grund (Abo beendet + Löschdatum) | K22 |
|
||||
|
||||
Schritte:
|
||||
1. Kunde mit laufendem Wartungs-Abo (Prüfung Abschnitt 6) sieht die Kachel. Er startet den Fragebogen; jeder Schritt speichert sofort als Entwurf (Abschnitt 3).
|
||||
2. „Prüfen und bestellen“: Zusammenfassung, offene Punkte (Server rechnet `bestellpruefung`), Zustimmungen (Abschnitt 4.3), Knopf „Zahlungspflichtig bestellen“.
|
||||
3. `POST /api/funktionen/termine/checkout` legt `ordered` an, Stripe Checkout (Karte + SEPA wie News). Rücksprung `#termine?bestellt=1&projekt=<id>`.
|
||||
4. Webhook (bestehender Pfad, `abo.js:520-525`) → `markPaid`: `setup`, Ticket, K15 an Kunde, T16 an Team.
|
||||
5. Team baut ein (Anleitung im WOMS), Abnahme, Freischaltung durch Ticket „Erledigt“ oder Staff-Knopf (Abschnitt 5) → `active`, K16.
|
||||
6. Ab dann Betrieb wie TP1. Endet das Abo: täglicher Abgleich → `disabled`, K22, nach 30 Tagen Löschung (Abschnitt 6).
|
||||
|
||||
Festlegung Sperre ab Bestellung: Ab `ordered` ist die Einstellung für den Kunden nur lesbar, weil das Team gegen den bezahlten
|
||||
Stand baut (sonst driftet Ticket-Kurzfassung gegen Einstellung). Änderungen laufen über den Chat; Staff in Kundenansicht darf
|
||||
weiter per PATCH ändern. Scheitert die Zahlung (`markCheckoutFailed`, `ordered` → `off`), ist der Entwurf wieder bearbeitbar.
|
||||
|
||||
Mails (alle Du-Form, Layouts der Inhaber-Vorlagen, nur Platzhalter ergänzen; Annahme: Inhaber akzeptiert das):
|
||||
- **K15** Bestellbestätigung: `{{funktion}}`, `{{preis}}` aus Register, Block „kein Abo“ nur für News. Neu: Anhänge Leistungsbeschreibung + AVV (PDF, Fassung) und Widerrufsbelehrung mit Muster-Formular auf dauerhaftem Datenträger (Muster K18/`legalPdfAttachments`).
|
||||
- **K16** „bereit“: `{{funktion}}`, `{{portal_url}}` = `#${returnHash}`, Knopftext aus Register.
|
||||
- **T16** Team: `{{funktion}}`, Link zur Anleitung, neue Blöcke `kein_abo` und `bereits_aktiv` (Abschnitt 4.5).
|
||||
- **K22** „Terminplaner beendet“ (Varianten Abo beendet, auf Hosting gewechselt, vor Einrichtung beendet mit Erstattung) und **K23** „läuft wieder“; **T19** an das Team „Erstattung fällig“ (4.6). IDs K22, K23, T19 sind frei (`test/mailTemplates.test.js` führt die Liste und den Kopfkommentar). Layouts nach den Vorlagen des Inhabers, Texte gehen in seine Mail-Überarbeitung.
|
||||
- **K04/T09** (Widerruf): `{{produkt}}`, `{{preis}}` statt fester „News-Pinnwand, 30 €“-Texte.
|
||||
|
||||
---
|
||||
|
||||
## 3. Fragebogen
|
||||
|
||||
### 3.1 Speicherung (Festlegung: ein Dokument, kein Kopieren)
|
||||
- Der Entwurf **ist** die spätere Live-Einstellung: `termineConfig.einstellung`, Doc-ID = Feature-ID (`kontext.js:12`). Kein Compile- oder Kopierschritt bei Bestellung oder Freischaltung.
|
||||
- Neues Attribut `termineConfig.fragebogen` (JSON, 16 000 Zeichen, Schema 1): `bestaetigt[]` (Schrittschlüssel), `bundeslandGewaehlt`, `preiseGeprueft`, `einbau{modus inline|knopf, knopfText ≤40, stelle ≤300, gestaltung ≤1000, bemerkung ≤1000}`. Grund: `normalisiereEinstellung` verwirft unbekannte Schlüssel (`einstellungen.js:505-541`), und Einbauwünsche dürfen nie in die öffentliche Konfiguration. Setup über `scripts/termine-setup.mjs` (idempotent), `test/termineSetup.test.js:78` und `test/support/schema.js` mitziehen.
|
||||
- Schritt „erledigt“ lässt sich nicht aus den Daten ableiten (Vorlagen machen jeden Stand gültig), deshalb die gespeicherte Bestätigung je Schritt.
|
||||
- `PUT /api/termine/fragebogen` schreibt nur `fragebogen`, `updatedAt`, `updatedBy` unter `withLocks('termine-cfg:'+id)`, **ohne** `version` zu erhöhen (offene Editoren bleiben gültig). Nicht auf der Kundenansicht-Allow-Liste: Schritte bestätigt nur der Kunde selbst.
|
||||
|
||||
### 3.2 Zugriff in Entwurfszuständen
|
||||
- `withTermine({ entwurf: true })` (`P/server/routes/termine.js:137-157`) nur für `GET /einstellungen`, `POST /einstellungen/vorlage`, `PATCH /einstellungen`, `PUT /fragebogen`. Erlaubt `off`, `active`; `ordered`/`setup` nur lesend (GET), Schreiben dort nur mit `req.session.viewAs`. `disabled` bleibt wie TP1. Alle anderen Routen behalten die Schranke `active|disabled` (Gäste- und Job-Pfade unberührt: `kontextPerFeature` bleibt, neuer Helfer `kontextFuerPortal`).
|
||||
- Ohne Feature-Dokument antwortet GET 200 `{state:'off', einstellung:null, vorlagen, branchen}` und legt nichts an; der erste Schreibzugriff ruft `ensureFeatureDoc` (Doc `off`, dauerhafter siteKey).
|
||||
- Entwürfe sind nie öffentlich: `kontextPerSiteKey` → `findActiveBySiteKey` verlangt `active` (`siteFeatures.js:390-400`).
|
||||
- PATCH ruft `normalisiereEinstellung(..., {entwurf: state!=='active'})` (überspringt nur R13/R13a), Antwort enthält `fehlt: fehlendePflichtangaben(...)`; der Client (`termine-speichern.js:116`) übernimmt es statt `[]`. Tests `termineRoutes.test.js:247-264` ändern sich entsprechend.
|
||||
|
||||
### 3.3 Schritte und Wiederverwendung der TP1-Editoren
|
||||
| # | Schritt | Umsetzung |
|
||||
|---|---|---|
|
||||
| 1 | Branche, Vorlage, Gesundheits-Ausschluss | `BRANCHEN`-Liste in `vorlagen.js`; Eintrag „Gesundheitsbereich“ ohne Vorlage blockt; `POST vorlage` mit `ersetzen:true` im Entwurf, optionalen `betrieb`-Feldern (Name, E-Mail, Telefon, Anschrift, Bundesland, Website) und Vorbelegung der Website aus dem Projekt |
|
||||
| 2 | Wer wird gebucht | neue kleine Form: Einzelperson / Team / Plätze, Namen; setzt neue Ressourcen im selben PATCH allen Leistungen zu (Validator verlangt aktive Ressource) |
|
||||
| 3 | Leistungen | `termine-tab-leistungen.js` `render` unverändert, plus Haken „Preise geprüft“ (Vorlagenpreise sind Platzhalter) |
|
||||
| 4 | Zeiten, Bundesland, Urlaub | Wochenplan-Helfer aus `termine-tab-team.js:69` exportiert, `abwesenheiten/feiertage/sonderoeffnungen` (bereits exportiert); Bundesland **ohne** Vorauswahl (Server-Default `RP` bei fehlendem Wert, `einstellungen.js:512`) |
|
||||
| 5 | Regeln, sofort/Anfrage | `regeln`, `storno`, `rechtliches` (gaesteSind, freizeit), `zahlung` aus `termine-tab-einstellungen.js` exportiert; Sammelknopf „sofort/Anfrage für alle“ |
|
||||
| 6 | Gastfelder | `gastFelder` exportiert |
|
||||
| 7 | Benachrichtigungen, Anrede, Erinnerung | `darstellung`, `erinnerung`, `betrieb.email` (einzige Adresse; sie ist auch Reply-To und Ziel von K20/K21) |
|
||||
| 8 | Website | `betriebLinks` (Impressum, Datenschutz, AGB, Website), `telefon` (= Ausweichtelefon), Einbauwünsche-Form |
|
||||
| 9 | Prüfen und bestellen | siehe Abschnitt 4 |
|
||||
|
||||
Festlegungen UI: neue Datei `P/public/termine-fragebogen.js` (Name in die Liste `test/termineView.test.js:277-280`; Du-Form, kein `innerHTML`, kein `href`); die Bestellseite wegen der Link-Regel (`termineView.test.js` ~290: nur ein `exportUrl`-href) in `P/public/funktion-bestellen.js` (heißt nicht `termine*.js`, wird relativ importiert, R4). Jeder Schritt = ein `<form>` mit Weiter = ein PATCH über `neuerSpeicher` (einziger PATCH-Pfad), danach `PUT /fragebogen`. Pro Betrieb nur ein Ressourcentyp (Annahme: Person+Platz gemischt ist im Validator nicht verboten, das Verhalten ist ungeprüft).
|
||||
|
||||
### 3.4 Bestell-Gate (serverseitig, `server/services/termine/fragebogen.js`, reine Funktion `bestellpruefung`)
|
||||
Offen = Liste `{schritt, text}`; Checkout lehnt bei nicht-leerer Liste ab, GET zeigt sie. Festlegung strikt: bestellbar nur wenn
|
||||
(a) `fehlendePflichtangaben` leer, (b) mind. eine sichtbare Leistung, (c) alle Schritte bestätigt, (d) Bundesland gewählt, (e) `einbau.modus` gesetzt, (f) Branche nicht Gesundheit, (g) `impressumUrl` und `datenschutzUrl` vorhanden (der Gästehinweis verweist auf die Datenschutzerklärung; das Widget lässt den Link sonst still weg, `embed/termine.js:519`), (h) Wartungs-Abo sauber laufend (Abschnitt 6). Offene Punkte klärt der Kunde vor der Bestellung, nicht per Chat danach.
|
||||
|
||||
### 3.5 Gesundheits-Ausschluss (D6)
|
||||
Heute nur Hinweistexte (`rechtstexte.js:169`), keine Sperre. Umsetzung nach D6: Branchenfrage mit hartem Ausschluss (kein Entwurf),
|
||||
Haken „Mein Angebot liegt nicht im Gesundheitsbereich“ im Bestellschritt (mit Zeit im Zustimmungsnachweis), und eine weiche
|
||||
Stichwortprüfung über Leistungsnamen, Beschreibungen und Zusatzfeld-Beschriftungen (Liste in `fragebogen.js`, z. B. Physio,
|
||||
Therapie, Rezept, Beschwerden, Diagnose, Behandlung, Arzt, Heilpraktiker): nur ein Warnblock in T16 und im Ticket, kein Fehler
|
||||
für den Kunden. Das Team kann die Einrichtung ablehnen (Erstattung nach 4.6, Variante Widerruf/Kulanz). „Sonstiges“ bleibt wählbar.
|
||||
|
||||
### 3.6 Einbauwünsche
|
||||
Modus `inline` oder `knopf` (Snippet erweitert um `data-modus`, `data-knopf`; Widget kann das, `embed/termine.js:232-234`), Knopftext, Stelle auf der Website in Worten, Gestaltungswunsch, Bemerkung. Festlegung V1: keine Knöpfe je Leistung (`data-leistung` nur auf Nachfrage über Bemerkung, Mehraufwand nach Stunden). Texte des Kunden werden im Ticket nur als Text und in einem Block „Kundenwunsch (Text des Kunden)“ ausgegeben.
|
||||
|
||||
### 3.7 Entwürfe aufbewahren und löschen
|
||||
Heute löscht niemand einen Entwurf (Job `loescheAbgeschaltete` nur `disabled`; `disableCustomerFeatures` überspringt `off`, `siteFeatures.js:321-326`). Festlegung: Tagesjob-Schritt löscht `termineConfig` + Feature-Doc von Dokumenten in `off` (nie `ordered/setup`), die 90 Tage nicht geändert wurden (Annahme: 90 Tage passt zur Datenschutz-Aussage, Rechtsrunde bestätigt); Kundenlöschung (`customerAdmin.js:289`) entfernt Entwürfe ebenfalls. Die Datenschutzerklärung (6c, §7) nennt Entwurfsdaten und die Frist.
|
||||
|
||||
---
|
||||
|
||||
## 4. Bestellung und Zahlung
|
||||
|
||||
### 4.1 Register `config.features` (ersetzt feste News-Werte)
|
||||
`P/server/config.js`: neben `newsSetup` neuer Katalogeintrag `termineSetup = {key, name:'Terminplaner – Einrichtung', amountCents:14900, lookupKey:'webklar_termine_einrichtung_14900', oneTime:true, priceNickname}`, Stripe-Metadaten-Schlüssel `termine-einrichtung`. Neu `config.features = { news:{…}, termine:{…} }` mit: `product`, `label{name,kurz,keine}`, `priceLabel`, `setupEstimate`, `consent{version, items[]}`, `leistungsbeschreibungVersion`, `returnHash`, `ticket{topic(project), details(ctx)}`, `mails{funktion, knopf, danach, preisHinweis}`, `requiresWartungsAbo`, `disabledMessage`, `orderedConflictMessage`. `config.funktionen.{setupEstimate,consentText}` bleiben als News-Alias, damit `funktionenRoutes.test.js:63-64` nur gezielt geändert wird.
|
||||
|
||||
Zu ersetzende News-Festverdrahtungen (alle verifiziert): `stripeBilling.js:54-61` (`PRODUCT_METADATA`, `catalogFor`), `:649` (`ensurePrice('newsSetup')`), `:661`, `:665-666` (URLs), `routes/funktionen.js:122,137,158-171`, `siteFeatures.js:17-18` (`DISABLED_MESSAGE`), `:360` (`activateClosedTickets` nur `news`), `:406` (`portalUrl` = `#news`), `:537-538`, `:584-587`, `ticketAdmin.js:80,85,95-102`, `public/funktionen.js:9,261-317`.
|
||||
|
||||
### 4.2 Stripe
|
||||
`createFeatureCheckout(customer, {projectId, key, consentAt})` liest Preis, Rechnungsbeschreibung, Rücksprung und Abbruch-URL aus dem Register; `invoice_data.metadata` mit `featureKey`, `projectId`. SEPA bleibt wie bei News (Festlegung; Einbau startet erst bei `setup`, also nach Zahlungseingang). Der Preis `termineSetup` wird vor dem ersten Live-Kauf per `scripts/stripe-products.mjs --pruefen/--anlegen` angelegt (`KEYS` `:20` und `catalogEntry` `:30-34` ergänzen; Test nach Vorbild `stripeBilling.test.js:246-260`), sonst legt ihn der erste Kunden-Checkout lazy an (und `ensurePrice` sucht nur in den ersten 100 aktiven Produkten, `:81-82`).
|
||||
|
||||
### 4.3 Route und Zustimmungen
|
||||
- `POST /api/funktionen/:key/checkout` (Schlüssel gegen Register; `/news/checkout` bleibt Alias). **Nicht** unter `/api/termine/`: dort erlaubt `kundenansichtGuard.js:19` jedes POST für Staff in Kundenansicht. Zusätzlich `req.session.viewAs` → 403 (wie `funktionen.js:139`).
|
||||
- Reihenfolge: viewAs, Zustimmungen vollständig und Fassung = aktuelle Serverfassung (sonst 409 mit neuer Fassung), Projekt gehört dem Kunden, `termineFuerKunde(email)` (Pilot-/Modusschranke), `bestellpruefung` leer, Wartungs-Abo (Abschnitt 6), Lock `feature-order:<customer>:<project>`, Zustandsprüfung (Meldungen aus dem Register), `markOrdered`.
|
||||
- Zustimmungen für Termine (Wortlaut ist Rechtsrunde, hier nur Struktur; nach D1 werden **alle** Besteller wie Verbraucher behandelt): (1) Leistungsbeschreibung + AVV Terminplaner, Fassung X, mit versionierten Links; (2) „kein Gesundheitsbereich“; (3) ausdrückliches Verlangen des Beginns vor Ablauf der Widerrufsfrist mit Hinweis auf Erlöschen (§ 356 Abs. 5 Nr. 2 BGB) **und** Wertersatz (§ 357a Abs. 2 BGB; die News-Zustimmung hat nur die erste Hälfte); (4) Pflichtinformationen (Leistung, Gesamtpreis, Ende mit dem Wartungs-Abo, Einrichtungsdauer) und volle Widerrufsbelehrung + Muster-Formular vor dem Knopf; (5) Knopf „Zahlungspflichtig bestellen“; ob „· 149 €“ am Knopf steht, entscheidet die Rechtsrunde (Recherche rät zu nichts außer den gesetzlichen Worten; die Spec schreibt „· 149 €“).
|
||||
- Nachweis: neue append-only Collection `featureConsents` (Muster `analyseConsents`; Felder customerId, projectId, featureKey, stripeSessionId, Fassungen, vollständige gezeigte Texte, Hash, Zeit, IP, UA, Konto). Die 1000-Zeichen-Spalte `portalFeatures.consentText` reicht für den Satz nicht (`setup-abo-collections.mjs:277`). IP/UA kommen in Datenschutz §7.
|
||||
- Widerrufs-Anker: Zeit des Klicks (Vertragsschluss) aus `featureConsents`, nicht `orderedAt` (der bei Neubestellung überschrieben wird). Annahme: so ist die Frist korrekt gedeutet, Rechtsrunde.
|
||||
|
||||
### 4.4 Rechnung
|
||||
Stripe erzeugt sie nach Zahlung (`invoice_creation`), `backfillInvoiceLinks` ergänzt den Link später (K15 geht deshalb oft ohne Link, Block `rechnung`; unverändert). Preistext „149 € (ohne Umsatzsteuer gemäß § 19 UStG)“ statt „brutto/inklusive Mehrwertsteuer“ (Spec E2 sagt „brutto“; Stripe-Footer und Abo sagen § 19, Annahme: WEBklar bleibt Kleinunternehmer, Rechtsrunde bestätigt; gilt gleichzeitig für News).
|
||||
|
||||
### 4.5 Fehlerfälle
|
||||
- **Verschluckte Zahlung (heute real):** Aktiviert Staff ein `ordered`-Dokument per „Aktivieren (ohne Zahlung)“, sieht der Webhook `created:false` und tut nichts (`siteFeatures.js:249,521`). Festlegung: `onFeaturePaid` schickt in dem Fall T16-Block `bereits_aktiv` (Zahlung ohne Ticket), und WOMS blendet die Aktion in `ordered`/`setup` für Termine aus (Abschnitt 5).
|
||||
- **Zahlung ohne Abo** (Abo endete während der 24-h-Sitzung): Dokument geht trotzdem auf `setup` samt Ticket (Geld ist da), T16 mit Block `kein_abo`; Aktivierung bleibt gesperrt, bis ein Abo läuft. Nach D3 wird voll erstattet, wenn das Abo vor der Fertigstellung endet (siehe 4.6).
|
||||
- **Ticket scheitert** nach Zahlung: bestehende Wiederaufnahme (`setup` ohne `ticketId`, Stripe-Retry) bleibt; neu zusätzlich Fallback auf Minimal-Details (Abschnitt 5).
|
||||
- **Zahlung offen:** Knopf „Zahlung fortsetzen“ (derselbe Checkout liefert die offene Sitzung, `openOrderSession`); SEPA kann Tage in `ordered` bleiben.
|
||||
### 4.6 Erstattung (D3)
|
||||
- Gesetzlicher Widerruf: Erstattung abzüglich Wertersatz für einen schon begonnenen Einbau (nach Stundenaufwand, § 357a Abs. 2 BGB).
|
||||
- Zusätzlich **volle Erstattung**, wenn das Wartungs-Abo endet oder widerrufen wird, bevor der Terminplaner `active` ist (Zustand `ordered`/`setup`).
|
||||
Festlegung: Der tägliche Abgleich (Abschnitt 6) erkennt den Fall, setzt das Dokument auf `disabled` mit `disabledReason='abo_ende_vor_einrichtung'`,
|
||||
schließt keine Tickets selbst, und meldet ihn dem Team per Mail **T19** „Erstattung fällig: Terminplaner-Einrichtung“ (T17/T18 sind vergeben) (Kunde, Projekt, Stripe-Sitzung,
|
||||
Rechnung). Die Erstattung selbst löst der Inhaber in Stripe aus (keine automatische Rückzahlung, keine Live-Stripe-Aufrufe durch Code ohne Freigabe).
|
||||
Der Kunde bekommt K22 in der Variante „vor Einrichtung beendet, Betrag wird erstattet“.
|
||||
- Das Ticket bleibt offen, bis das Team es mit Vermerk schließt; die Freischaltung beim Schließen prüft Abo und Zustand und schaltet dann **nicht** frei.
|
||||
|
||||
### 4.7 Widerrufsformular
|
||||
- Widerruf: `widerrufen.html` bekommt eine eigene Auswahl „Terminplaner (Einrichtung)“; `withdrawFeature`/`paidFeatureOrders` rechnen die Frist **je Funktion** (heute Maximum über alle Bestellungen, `abo.js:1091-1093`), T09 nennt die Funktion. Abwicklung bleibt Handarbeit des Teams (Rückerstattung, Abschalten, Ticket).
|
||||
|
||||
---
|
||||
|
||||
## 5. Ticket, Einbauanleitung, WOMS
|
||||
|
||||
**Ticket:** `markPaid` übergibt `topic`/`details` aus dem Register an `createFeatureTicket` (kann es schon, `analyse.js` nutzt es). Festlegungen: `topic` auf 128 Zeichen kürzen (heute ungekürzt, `ticketAdmin.js:85`), Details-Deckel 7000 Zeichen (Schema nennt 8000, **ungeprüft**, das Setup-Skript ist für `portalFeatures` nachweislich veraltet); scheitert `createDocument` mit langen Details, einmal mit Minimal-Details (Kopfzeilen + Verweis) wiederholen. Zuweisung: keine, wie News; T16 meldet das Ticket. Ticket-Hinweis „Nicht auf das Stundenkontingent anrechnen“ (nach `analyseTexts.js`).
|
||||
|
||||
**Generator:** `P/server/services/termine/einbauanleitung.js`, rein und deterministisch (`now` Parameter), liest `ladeEinstellung(featureId)` (`kontext.js:75`, kein Modus-/Zustandscheck), **nie `feedToken`** (Feldweißliste + Test). Ausgabe `{stand{version,updatedAt}, warnungen[], abschnitte[{id,titel,text,kopierbar}], text}` mit: Modell, Leistungen, Ressourcen + Zeiten, Regeln, Gastfelder, Benachrichtigungen, Einbau (Snippet mit siteKey; `einbauSnippet` aus `termine.js:185-196` hierher verschoben und um knopf erweitert; CSS-Variablen `--wkt-akzent` aus `farbe`, Kontrastfarbe per WCAG-Helfer; Ausweichtext; Impressum/Datenschutz-Links), Datenschutz-Absatz (Sie/Du nach `anrede`), Abnahme-Checkliste (bedingt: Verbraucher → Widerruf, Knopf → dauerhafter Widerrufsknopf), Abrechnung („Einrichtung bezahlt (149 €, Rechnung …), Zeit im Arbeitsblatt, nicht auf Abo-Stunden“). Die Quelle des Datenschutz-Absatzes wandert aus dem Markdown als `datenschutzAbsatz(einstellung, anrede)` nach `rechtstexte.js` (Test pinnt beide, wie bisher).
|
||||
- **Kurzfassung** in `ticket.details` (Klartext, WOMS zeigt `pre-wrap`, `TicketCard.jsx:124-127`): Kopf (Funktion, Projekt, Domain, Kunde, siteKey, „Konfigurationsstand: Version N, Datum“), Modus/Stelle/Gestaltungswunsch, Zählungen + erste Namen mit Kürzung, Snippet, 5–6 Schritte, Verweis `womsBaseUrl/customers/<id>?tab=funktionen`. Ohne Datenschutz-Absatz (4,8 k Zeichen je Variante).
|
||||
- **Route:** `GET /api/team/funktionen/:customerId/termine/anleitung?projectId=` in `funktionenAdmin.js` (`requireStaffJwt`, `assertOwnProject`, `getFeatureDoc`; kein TERMINE-Modus-Gate, kein viewLog, nie aus Kundenseite erreichbar). Vergleich mit dem Ticket-Stand liefert Warnung „Einstellungen seit der Bestellung geändert (Ticket Version N, jetzt M)“. Fehlen Einstellungen: 200 mit Warnung und nur Snippet.
|
||||
- **Datenschutz-Absatz-Sperre:** Solange die Gästetexte als ENTWURF gekennzeichnet sind, druckt die Anleitung das Banner mit; mit der Freigabe (Abschnitt 7) fällt es weg.
|
||||
|
||||
**WOMS** (`W/src/components/customer/CustomerFunktionen.jsx`, `W/src/lib/funktionenView.js`, `teamApi.js`):
|
||||
- Je Termine-Zeile Knopf „Anleitung“, klappt ein Panel unter der Zeile auf (Stand, Ticketlink, Warnungen, „Alles kopieren“, Teilkopien für Snippet/CSS/Datenschutz Sie/Du); Zeilen werden zu Fragmenten mit Key. Neu `teamApi.getTermineAnleitung`.
|
||||
- `aktionen(state, key)`: für Termine kein „Aktivieren (ohne Zahlung)“ in `ordered`/`setup`; in `off`/`disabled` bleibt es. Neu in `setup`: **„Freischalten (Abnahme)“** (aktiviert wie Ticket-Schließen, schickt K16, setzt Ticket nicht zurück) und bei „Ticket fehlt“ **„Ticket nachlegen“** (idempotent). Tests `funktionenView.test.mjs:48-55` anpassen.
|
||||
|
||||
**Freischaltung (Festlegung):** `activateClosedTickets` filtert `Query.equal('featureKey', [Schlüssel mit Ticket-Freischaltung])` (heute fest `news`, `siteFeatures.js:360`). Für Termine verlangt sie vorhandene Einstellungen und ein laufendes Abo, sonst bleibt `setup` (Log + T16-Hinweis). Der Öffentlich-API-Zugriff gibt es nur bei `active` (`kontext.js:106`); eine echte Probebuchung vor dem Livegang ist damit nicht möglich. Deshalb der Ablauf: Einbau auf der Kundenseite (Widget bleibt unsichtbar, da `aktiv:false`) → „Freischalten (Abnahme)“ → Probebuchung nach Checkliste und Stornierung → Ticket „Erledigt“. Reines Schließen des Tickets aktiviert weiterhin automatisch (Fallback, Idempotenz via Lock `feature-activate:<id>`). Ein Kundenfreigabe-Schritt entfällt (Festlegung, wie News).
|
||||
|
||||
---
|
||||
|
||||
## 6. Abo-Kopplung
|
||||
|
||||
**Gemeinsame Prüfung (Festlegung):** `P/server/services/aboStatus.js` zieht `hasWartungsAbo` aus `analyse.js:43-46` heraus; liest nur den Appwrite-Spiegel `subscriptions` (`getCurrentSubscription`, `stripeBilling.js:178`), ruft Stripe nicht auf. Zwei Prädikate:
|
||||
- `laeuft(customerId)` = `productKey||'wartung'` (wie `abo.js:68`; die Analyse vergleicht heute strikt `=== 'wartung'`, Annahme: Altbestand ohne `productKey` existiert, Produktion nicht geprüft) und Status ∈ `ACTIVE_STATUSES` (active, trialing, past_due). Dient dem **Weiterlaufen** und dem Abgleich. Lesefehler werfen (nie abschalten bei Fehler).
|
||||
- `bestellbar(customerId)` = Status `active` und keine Kündigung vorgemerkt (`cancelAtPeriodEnd`/`cancelAt`, Muster Zusatzspeicher `abo.js` ~92-95). Dient Kachel, Checkout und `markPaid`. Verhindert 149 € in der letzten Woche eines gekündigten Abos oder in der Widerrufs-Probezeit. Meldungstexte je Funktion (`kein_abo(funktion)` statt „Website-Analyse“); `analyseEligibility` ändert sein Verhalten nicht. Pro Lauf Memo je `customerId` (Feature-Docs sind je Projekt, das Abo je Kunde).
|
||||
|
||||
**Schema (vor dem Deploy ausführen, sonst `Unknown attribute`):** `portalFeatures.disabledReason` (`abo_ende|team|kunde_geloescht|frist`), `aboEndeMailAt` (datetime), in `scripts/setup-abo-collections.mjs` (`test/support/schema.js` liest sie). `disable(reason)` wird idempotent: ein bereits `disabled` Dokument behält `disabledAt` (heute überschreibt es `siteFeatures.js:304-312` und würde die 30-Tage-Löschung endlos verschieben). `activate()` löscht zusätzlich `disabledReason/aboEndeMailAt` (R36 löscht `disabledAt` schon).
|
||||
|
||||
**Täglicher Abgleich** `gleicheAboAb` in `P/server/services/termine/aboAbgleich.js`, im `taeglicherJob` (`jobs.js:420-429`) **vor** `loescheAbgeschaltete`, Ergebnis nur als Zählung im Tagesbericht (`aboAbgleich{geprueft, abgeschaltet, wiederEin, mails, fehler}`), Listen mit `listAllDocuments`.
|
||||
1. `active`-Termine gruppiert je Kunde; Kunde ohne `laeuft` → unter `withLocks('feature-admin:<projectId>')` neu lesen, noch aktiv und noch kein Abo → `disable('abo_ende', 'Abgeschaltet: Wartungs-Abo beendet')`. Ausgenommen: `unlockedByPurchase !== true` (Pilot/Kulanz, siehe Abschnitt 1). Lesefehler → Kunde überspringen.
|
||||
1b. `ordered`/`setup`-Termine (bezahlt oder in Zahlung) eines Kunden ohne `laeuft` → unter derselben Sperre neu lesen →
|
||||
`disable('abo_ende_vor_einrichtung')`, T19 an das Team (Erstattung nach D3, 4.6), K22 Variante „vor Einrichtung beendet“.
|
||||
Eine offene Stripe-Sitzung (`ordered`) wird nicht storniert; kommt die Zahlung danach noch an, meldet `onFeaturePaid` den Fall
|
||||
ebenfalls mit T19 statt ein Ticket anzulegen.
|
||||
2. Mail-Nachlauf: `disabled` + `abo_ende` + kein `aboEndeMailAt` → K22 (Enddatum, Löschdatum = `disabledAt` + 30 Tage, was bleibt möglich: ansehen, CSV-Export, Gäste dürfen absagen; Betrieb soll seine Gäste informieren) und erst nach erfolgreichem Versand `aboEndeMailAt` setzen (Analyse sendet K19 nur einmal ohne Nachlauf, das wäre hier der Verlust der Löschmitteilung).
|
||||
3. Wiederkehr: `disabled` + `abo_ende` + `laeuft` + `termineConfig` noch vorhanden → `activate()` (Daten und Einstellungen bleiben), K23. Nach der Löschung nie automatisch (Konfig fehlt).
|
||||
|
||||
**Sofortpfade:** Widerruf eines Wechsels in der Frist (`abo.js:1051-1058`, beendet heute schon die Analyse) → Termine ebenfalls sofort, `abo_ende`, K22. Kundenlöschung deaktiviert Termine bereits (`customerAdmin.js:285-292`).
|
||||
|
||||
**Löschung nach 30 Tagen:** unverändert (`loescheAbgeschaltete`, `nochAbgeschaltet`). Neu: vor jedem unumkehrbaren Schritt bei `abo_ende` frische Abo-Prüfung (bricht ab, wenn das Abo wieder läuft). Nach der Löschung setzt der Job das Feature-Doc auf `off` (`disabledAt` leer, `unlockedByPurchase=false`, `ticketId` leer), damit es nicht täglich neu gescannt wird (`final-deferred.md:25`) und neu bestellt werden kann (heute blockt `routes/funktionen.js:160` jedes `disabled`); der siteKey bleibt, das eingebettete Snippet bleibt gültig. Test `termineJobs.test.js` ~771 („Funktionsdokument bleibt“) ändert sich. Die Bestellung nach Löschung kostet 149 € erneut (D2).
|
||||
|
||||
**Modus:** Der Abgleich läuft im `taeglicherJob` und steht bei `TERMINE_ENABLED=off` still (`jobs.js:93`); im Pilot läuft er für alle Termine-Docs, nicht nur Pilot-Adressen. Festlegung: Abgleich und Löschung laufen auch bei `off`, solange es Dokumente mit `unlockedByPurchase=true` gibt (Aufteilung des Guards in `fuehreAus`), denn die Datenschutz-Zusage „30 Tage danach“ darf nicht an einem Schalter hängen. Der Abgleich muss vor `on` live sein (der Satz in 6c ist sonst falsch).
|
||||
|
||||
**Rückkehr (D2):** Innerhalb der 30 Tage schaltet der Abgleich automatisch und ohne Gebühr wieder ein (Daten und Einstellungen
|
||||
bleiben). Nach der Löschung braucht es eine neue Einrichtung: neuer Fragebogen und neue 149 €. Dieser Satz steht in LB, AVV,
|
||||
6c und K22.
|
||||
|
||||
**Mehr:** K02/K03 (Kündigungsbestätigungen) bekommen einen Block „Analyse und Terminplaner enden mit dem Abo“. Banner/Meldung in `public/termine.js:65-67` und `routes/termine.js:59` unterscheiden nach `disabledReason` und zeigen das Löschdatum; `featureView` liefert `disabledReason`/`disabledAt`, WOMS zeigt „Abgeschaltet (Abo beendet), Löschung am …“. Erinnerungen an Gäste (G05) laufen weiter, wie R34 entschieden (unverändert; K22 weist darauf hin).
|
||||
|
||||
---
|
||||
|
||||
## 7. Rechtstexte und Rechtsrunde
|
||||
|
||||
### 7.1 Zu erstellen (Quellen Markdown in `P/docs/rechtliches/`, HTML + PDF in `P/public/rechtliches/`)
|
||||
1. **Leistungsbeschreibung Terminplaner** (Geschwister der LB Website-Analyse): Geltung/Rangfolge (Zusatz zum Wartungs-Abo, Hosting reicht nicht), Leistung, Nicht enthalten (SMS, Online-Zahlung, Kalender-Sync, Gesundheitsbereich), Voraussetzungen und Pflichten des Betriebs (Impressum/Datenschutz/AGB, Telefon-Ausweichlösung, Texte unverändert lassen, Beschäftigte informieren), Verfügbarkeit, Haftung für verpasste Termine, **Einrichtung 149 €** (Leistung, Ablauf per Ticket, Dauer, Preis nach § 19 UStG, nicht auf Abo-Stunden), **Ende mit dem Abo** (Abschalten spätestens am Folgetag, Mail, Export, Löschung nach 30 Tagen, auch bei Wechsel auf Hosting und Widerruf), Einstellungsrecht, keine Rechtsberatung/Mustertexte, BFSG-Satz.
|
||||
2. **AVV Terminplaner** (Geschwister des AVV Analyse, kein Zusatz dazu, damit Analyse-Kunden nicht neu zustimmen müssen): eigene Nr. 1–3 (Gast-Name, E-Mail, optional Telefon/Nachricht/Zusatzfelder, Termin, Hashes, Mailstatus; Betroffene Gäste und Beschäftigte; Art. 9 ausgeschlossen), Nr. 7 Hetzner Falkenstein **und** Nürnberg (Buchungsmails über Webhosting-SMTP), Nr. 8 Betroffenenrechte (Anonymisieren, CSV), Nr. 9 Löschfristen genau wie gebaut (Abgeschaltet 30 Tage, 48 h/30 Tage/`aufbewahrungTage`, Outbox 7 Tage/48 h, Backups bis 7 Tage), TOM-Anlage; Abschluss per Haken + Staff nur mit Textform.
|
||||
3. **Datenschutz 6c** aus `termine-datenschutz-6c-entwurf.md` als `<h2 id="termine">` in `P/public/datenschutz.html`, die 8 offenen Punkte klären; §6 (Kundenansicht: Staff darf Termine lesen und ändern, Protokoll) und §7 (Nachweise bei Funktionsbestellung, Entwurfsaufbewahrung) ergänzen; `docs/WEBklar-Datenschutzerklaerung.pdf` neu bauen; Wächtertest `termineRechtstexte.test.js:267-274` entfernen, `legal_datenschutz_kundenansicht` anpassen.
|
||||
4. **Gästetexte final**: ENTWURF-Köpfe in `termine-hinweise.md`, `-widerrufsbelehrung-muster.md`, `-datenschutz-absatz.md` entfernen, `rechtstexte.js` und Markdown gemeinsam ändern (Tests pinnen wörtlich), `RECHTSTEXTE_VERSION` anheben, **Archiv je Fassung** (Nachweis speichert nur Hashes, `ablauf.js:279-290`).
|
||||
5. **Bestelltexte** (Abschnitt 4.3) und Zustimmungsfassungen im Register; Widerrufsformular-Beschriftung; K15-Anhänge.
|
||||
6. **WOMS „Rechtliches“**: Karten `leistungsbeschreibung-terminplaner`, `avv-terminplaner` (und ggf. Datenschutz) in `W/scripts/setup-legal.mjs` `DOCUMENTS`; Upload der Versionen macht der Inhaber (Löschen nicht möglich).
|
||||
7. **Technik dazu:** `analyseLegal.js` zu kleiner Dokument-Registry verallgemeinern (bzw. paralleles Modul), eigene Fassungskonstante `config.termine.avvVersion` unabhängig von `config.analyse.avvVersion`, `build-legal-pdfs.sh` um Modus `termine`; Mails und Zustimmungstexte verlinken nur **versionierte** Adressen.
|
||||
|
||||
### 7.2 Prüfung durch eine Kanzlei (D5)
|
||||
Das **Kernpaket** prüft eine Kanzlei, bevor der erste fremde Betrieb startet: Widerrufsbelehrung und Muster-Formular der
|
||||
149-€-Bestellung, Zustimmungssatz (vorzeitiger Beginn, Erlöschen, Wertersatz), AVV Terminplaner, Haftungs- und
|
||||
Ende-mit-dem-Abo-Klauseln der Leistungsbeschreibung, sowie die Gästetexte mit Gesetzlichkeitsfiktion (Widerrufsbelehrung
|
||||
Gäste, Hinweise im Widget). Alles andere (6c, Portal-Hinweise, Mailtexte) prüft die Hausrunde wie bei der Analyse.
|
||||
Lieferung von uns: **Kanzlei-Paket** `P/docs/rechtliches/termine-kanzlei-paket.md` mit den Texten in Endfassung, dem
|
||||
Produktablauf in Kurzform, den konkreten Fragen (Liste 7.3) und den Fundstellen im Code; der Inhaber beauftragt die Kanzlei.
|
||||
Befund und Änderungen fließen vor Livegang-Schritt 9 ein.
|
||||
|
||||
### 7.3 Rechtliche Punkte, **vor `on`** zu entscheiden oder zu erledigen (Fragen ans Kanzlei-Paket markiert mit ⚖)
|
||||
- ⚖ Stand der Widerrufsbelehrung für Funktionen: LB News §6 hat keine gesetzliche Musterbelehrung, K15 keine Belehrung/Anhänge. Meine Lesart (kein Rechtsurteil): die Frist läuft für Verbraucher womöglich nicht an (§ 356 Abs. 3 BGB). Für News-Altkunden entscheidet der Befund (D7): Nur wenn die Frist danach nicht lief, wird die Belehrung per Mail nachgeholt; sonst gelten die neuen Texte nur künftig.
|
||||
- Zitat „§ 356 Abs. 4“ → „Abs. 5 Nr. 2“ in `config.js:192,196`, `funktionen-leistungsbeschreibung.html:51,73`, `funktionen-news-design.md:73` und Test `legalTexts.test.js:485`; fehlende Wertersatz-Hälfte in der News-Zustimmung; „inklusive Mehrwertsteuer“ (`funktionen.js:272`, LB `:49,:60`) widerspricht § 19 UStG. Gleiche Runde (die Spec schiebt die Korrektur ausdrücklich nach TP2).
|
||||
- Wer fügt den Datenschutz-Absatz auf der Kundenseite ein und ist das im Preis (Analyse LB Nr. 4 Abs. 4 sagt: WEBklar, nicht auf Stunden). Eine Regel für beide LB.
|
||||
- Portalseitige Rechtsaussagen: `WIDERRUF_HINWEIS`, `FREIZEIT_HINWEIS` (Massage/Weiterbildung „in der Regel keine Freizeitleistung“ trägt die Recherche nicht, `termine-tab-einstellungen.js:16-23`), `stornoMuster`, Gesundheitshinweise; **Storno-Vorgabe 50 %/24 h gilt auch für kostenlose Leistungen** (Festlegung: bei Leistungen ohne Preis keine Storno-Vorgabe und kein Pflichthaken, Text als Muster kennzeichnen).
|
||||
- Anfrage mit Festpreis und der Bestätigungslink als „Bestellung“ (G01-Knopf, Seitenknopf „Zahlungspflichtig bestätigen“), Einstufung Massage/Fotograf/Coaching (Spec Offene Punkte 2).
|
||||
- Pflicht von Datenschutz-/Impressum-Link (hier als Bestell-Gate gesetzt) und der „jederzeit widerrufen“-Satz unter dem Erinnerungshaken.
|
||||
- Abo-Ende-Texte (LB, AVV, 6c, K22) und Aussagen „Drittland“ (Kalender-Abos bei Google/Apple), „Falkenstein und Nürnberg“, Anker für „30 Tage“; BFSG-Satz nur so weit getestet (kein axe-/Screenreader-Lauf in TP1).
|
||||
- Staff-Aktivierung (`adminActivate`, `siteFeatures.js:351-356`) prüft heute weder Einwilligung noch AVV. Festlegung: ab Modus `on` verlangt `adminActivate` für Termine einen Textform-Nachweis wie die Analyse (`analyse.js` ~262-270, WOMS-Chatvorlage nach `analyseView.js:55-90`), ausgenommen Pilot-Adressen.
|
||||
- Owner-Prüfung bei Hetzner (nur im Konto prüfbar): AV-Vertrag für Cloud **und** Webhosting, Anlage 1 mit „Terminbucher/Beschäftigte/Buchungsdaten“, Kontoadresse = Impressum, Backups an.
|
||||
|
||||
### 7.4 **Später**
|
||||
Abo-Bedingungen v6 bzw. Rahmen-AVV mit Anlage je Funktion; TP3-Themen (Stripe-Absatz, ZAG/Connect, Vorauszahlung in AGB, PAngV); Geschäftsbrief-Fußzeilen § 37a HGB; Rechtsmissbrauch bei spätem Widerruf; Minderjährige; Mitgliedschaften; wiederholtes Versenden von G08/K21 nach Fristende; Widerruf nur vor Terminbeginn im Verwaltungslink.
|
||||
|
||||
---
|
||||
|
||||
## 8. Technische Voraussetzungen aus TP1
|
||||
|
||||
Skalen-Schätzungen (Annahmen: 6 Buchungen je Betrieb und Tag, 12 Zellen je Buchung, 20 Tage Vorlauf, Spitzenfaktor 6; echte Werte können um Faktor 2–3 abweichen). Heute reicht alles für 1 Betrieb; die Listen laufen bei 40–60 Betrieben (bei Aufbewahrung 365 Tage schon bei ~10) in `list_limit_exceeded`, der nur geloggt wird.
|
||||
|
||||
**MUSS vor `TERMINE_ENABLED=on` (ein fremder Betrieb im Pilot braucht #1, #3, #5, #6):**
|
||||
| # | Maßnahme | Ort |
|
||||
|---|---|---|
|
||||
| 1 | Aufbewahrungs-Löschung begrenzt und seitenweise: je Feature `Query.limit(100)` + Index `(status, endsAt)`, Deckel je Lauf (z. B. 2000 je Feature), Rest läuft an Folgetagen; heute listet `loeschKandidaten` alle (Deckel 20 000, `buchungen.js:191`), Überlauf = **nie** Löschung von Gastdaten, nur Fehlerlog. Gleiches Muster für `abgesagt/abgelehnt/verfallen`, `loescheAbgeschaltete` und `export.csv` | `jobs.js:296-313,350-353`, `routes/termine.js:450-453` |
|
||||
| 2 | Zellen-Sweep seitenweise über `idx_bis` (`lessThan('bis', jetzt-1d)`, Limit 100, Deckel 5000/Lauf) statt `alleZellen()` (Deckel 100 000); `Map.get().push()` statt quadratischem `[...old, zelle]` (`zellen.js:247`); Zeitfenster-Prüfung in `ungueltigeZellen` | `zellen.js:46,224-270,92-99` |
|
||||
| 3 | Kundenansicht-Wildcard `POST /api/termine/.+` (`kundenansichtGuard.js:19`) durch die vier heutigen Routen ersetzen (vorlage, feed, buchungen, aktion) + Test „unbekanntes POST = 403“; neue TP2-Routen bleiben unter `/api/funktionen` | `kundenansichtGuard.js` |
|
||||
| 4 | Mail-Fairness: Kontingent je Feature (z. B. 40/h unter globalen 200/h), Vorrang G01/G02/G08 vor G05/K20/K21, veraltete G01 (älter als Haltezeit + 1 min) verwerfen statt toter Link, Empfänger-Kappe über Betriebe (z. B. 3/h, 10/Tag je Adresse für G01 und Freitext-G04/G06), Frühausstieg in `verarbeite` bei Pause/leerem Budget und begrenzte Liste (`orderAsc('naechsterVersuch')`, `limit`, `select`; Query-Helfer und `appwriteMock.js:145` um `select/isNull/notEqual` erweitern); `HALTUNGEN_JE_SITE` 60 → ~25/h | `outbox.js`, `limits.js`, `appwriteAdmin.js` |
|
||||
| 5 | Sichtbarkeit: Warnlog stündlich ab 70 % Budget, bei ältestem `wartet` > 10 min, bei SMTP-Pause (`pausiertBis()` ist ungenutzt), bei Tagesjob-Fehlern; `GET /api/team/termine/status` (Budget, Queue, Alter, Pause, letzter Tagesjob) und ein Staff-Mail je Vorfall | `jobs.js`, neuer Team-Route |
|
||||
| 6 | `disable()` idempotent (Abschnitt 6) und `activateClosedTickets` verallgemeinert | `siteFeatures.js` |
|
||||
| 7 | Messprüfungen mit Freigabe: Seiten-Listing > 500 Dokumente auf der Live-Appwrite 1.8.1 (bisher nur Mock/Spike), tatsächliche Hetzner-Antwort bei > 500 Mails/h, tatsächliche Größe `workorders.details` | siehe Abschnitt 9 |
|
||||
|
||||
**SOLLTE (TP2 oder kurz danach):** `minutenJob` in zwei unabhängig geschützte Jobs (Verfall, Mailversand) teilen; Transport `pool:true`, Parallelität der `einreihen`-Sendungen begrenzen (`mailer.js:25`); Budget nach Neustart aus der Outbox rekonstruieren; `/tage` und `/zeiten` außerhalb des Horizonts vor `imZeitraum` kurzschließen (`publicTermine.js:450,471`); Index `termineOutbox(bookingId)`; Hetzner-Überlimit-Antworten als Pause statt dauerhaft `fehler` einstufen (`outbox.js:82-88`); `mailSicher` schreibt bei Fehlschlag einen `fehler`-Marker, damit das Portal „nicht zugestellt“ zeigt; Tagesjob zu fester Nachtzeit (heute 180 s nach **jedem** Start, `index.js:220`); optional `TERMINE_TOKEN_SECRET` mit Fallback `SESSION_SECRET`; `engines >=20`; axe-/Screenreader-Lauf von Widget und Gästeseite; CSV-Export und `loescheAbgeschaltete` seitenweise für sehr große Einzelbetriebe. **Kann warten:** Lock-Wartezeiten, `halte` ohne `termine-cfg`-Lock, Fetch-Timeouts, Widget-Monolith.
|
||||
|
||||
**Mailweg (D8):** Terminplaner-Mails gehen weiter über den Hetzner-SMTP, mit Kontingent je Betrieb und Alarm (Punkt #4/#5).
|
||||
Ab **25 aktiven Terminplaner-Betrieben** (Zählung im Tagesbericht; Warnlog und Team-Mail ab 20) wird der Umstieg auf einen
|
||||
EU-Versanddienst geplant (eigenes kleines Vorhaben: Anbieterwahl, AVV/6c-Nachtrag, DKIM/SPF).
|
||||
|
||||
**Betriebsannahme:** Ein Portal-Prozess (Locks, Limiter, Mailbudget liegen im Speicher; Zellen bleiben durch atomares Bulk-Create sicher). Jeder zweite Prozess bricht das; im Livegang-Plan festhalten.
|
||||
|
||||
---
|
||||
|
||||
## 9. Livegang TP2 (je Schritt Freigabe des Inhabers)
|
||||
|
||||
1. **Rechtsrunde:** Texte (Abschnitt 7.1) und Kanzlei-Paket (7.2) fertigstellen; der Inhaber beauftragt die Kanzlei; Hausrunde für den Rest. Kein Push auf `main` durch Claude, das macht der Inhaber.
|
||||
2. **Code in Worktrees** (Portal, WOMS), TDD je Block: Register + Checkout, Fragebogen, Anleitung/WOMS, Abo-Kopplung, TP1-Schuld (Abschnitt 8). Test-Pins aus den Berichten mit ändern (`funktionenRoutes`, `termineRoutes`, `termineSetup`, `termineView`, `kundenansichtGuard`, `legalTexts`, `mailTemplates`, `termineJobs`).
|
||||
3. **Schema in Produktions-Appwrite** (Schreibzugriff, Freigabe): `scripts/termine-setup.mjs --check`, dann anwenden (`termineConfig.fragebogen`, Index `termineBookings(status,endsAt)`, `termineOutbox(bookingId)`); `setup-abo-collections.mjs` (`portalFeatures.disabledReason`, `aboEndeMailAt`, neue Collection `featureConsents`). **Vor** dem Code-Deploy (Indexaufbau ist asynchron, Abfragen funktionieren auch ohne).
|
||||
4. **Stripe (Aufruf, Freigabe):** `stripe-products.mjs --pruefen`, dann `--anlegen` für `termineSetup`; Inhaber prüft im Dashboard, dass der Live-Webhook `async_payment_succeeded/failed` und `expired` abonniert hat (Annahme: ja, News läuft seit 2026-09-30).
|
||||
5. **Messprüfungen** (Freigabe): `details`-Größe, Seitenlisting > 500, Hetzner-Mailantwort (Abschnitt 8 #7); Hetzner-AV-Prüfung durch den Inhaber (7.2).
|
||||
6. **Deploy Portal + WOMS** (Inhaber pusht): Modus bleibt `pilot` mit leerem `TERMINE_PILOT_EMAILS` plus Pilotkunde „test“; Abo-Abgleich läuft zunächst nur beobachtend (Zählung im Tagesbericht, Pilotkunde ausgenommen).
|
||||
7. **Rechtstexte live:** PDFs bauen, HTML/PDF unter `/rechtliches`, 6c + PDF-Datenschutz, WOMS-Karten (Skript mit API-Key, Freigabe) und Uploads durch den Inhaber, Gästetexte final (`RECHTSTEXTE_VERSION`).
|
||||
8. **Ende-zu-Ende-Test** mit einem Testkunden und echtem Wartungs-Abo: Fragebogen → Bestellung (echte 149 €, Rückerstattung durch den Inhaber danach; Annahme, es gibt kein Test-Konto für diesen Preis) → Ticket + Anleitung → „Freischalten (Abnahme)“ → Probebuchung → Abo-Ende-Simulation (Testkunde) → K22 → Rückkehr K23 → Löschlauf auf Wegwerfdaten.
|
||||
9. **Erster fremder Kunde im Pilot** (erst nach dem Kanzleibefund, D5): nur seine Adresse in `TERMINE_PILOT_EMAILS`; Beobachtung Tagesbericht, Mailbudget, Status-Route (Freigabe der Adresse).
|
||||
10. **`TERMINE_ENABLED=on`** (Compose-Änderung, nur `kundenbereich-server` neu erstellen) als **letzter** Schritt; danach 1 Woche Beobachtung, News-Altfälle (D7) und Entwurfs-Löschjob prüfen.
|
||||
|
||||
---
|
||||
|
||||
## 9b. Umsetzung in einem Plan, sechs Blöcke
|
||||
A Technik-Voraussetzungen aus TP1 (Abschnitt 8, MUSS-Liste) · B Register + Checkout + Zustimmungsnachweis (4) ·
|
||||
C Fragebogen (3) · D Ticket, Einbauanleitung, WOMS (5) · E Abo-Kopplung (6) · F Rechtstexte + Kanzlei-Paket + News-Korrektur (7).
|
||||
Reihenfolge A → B → E → C → D → F (F kann parallel zu C/D laufen, da überwiegend Text). Jeder Block endet mit grünen Tests;
|
||||
der Livegang (Abschnitt 9) beginnt erst nach F.
|
||||
|
||||
## 10. Entscheidungen des Inhabers (2026-10-03)
|
||||
|
||||
| # | Frage | Entscheidung |
|
||||
|---|---|---|
|
||||
| D1 | Verbraucher oder Unternehmer bei der 149-€-Bestellung | **Alle wie Verbraucher**: Haken „Beginn vor Fristablauf, Erlöschen, Wertersatz“, volle Belehrung + Muster-Formular für alle (4.3) |
|
||||
| D2 | Abo endet und kommt wieder | **Innerhalb von 30 Tagen automatisch und gratis wieder an**; nach der Löschung neuer Fragebogen und neue 149 € (Abschnitt 6) |
|
||||
| D3 | Erstattung der 149 € | **Gesetzlicher Widerruf** (mit Wertersatz für begonnenen Einbau) **und volle Erstattung, wenn das Abo vor der Fertigstellung endet** (4.6) |
|
||||
| D4 | Umfang der 149 € | **Je Website-Projekt ein Widget an einer Stelle**, Gestaltung nach Fragebogen, Einbau; Mehrwünsche nach Stunden (Abschnitt 1) |
|
||||
| D5 | Rechtsprüfung | **Kanzlei für das Kernpaket**, Rest Hausrunde; erster fremder Betrieb erst nach dem Befund (7.2) |
|
||||
| D6 | Gesundheits-Ausschluss | **Branchenfrage + Haken + Stichwort-Warnung ans Team**; Team kann ablehnen (3.5) |
|
||||
| D7 | News-Altkunden nach Textkorrektur | **Nach dem Kanzleibefund entscheiden**; Belehrung nur nachholen, wenn die Frist nicht lief (7.3) |
|
||||
| D8 | Mailweg bei mehr Betrieben | **Hetzner-SMTP mit Kontingent und Alarm, Umstieg auf EU-Versanddienst ab 25 aktiven Betrieben** (Abschnitt 8) |
|
||||
|
||||
## 11. TP3 (Ausblick, eigener Plan nach TP2)
|
||||
Online-Zahlung der Gäste über Stripe Connect (Standard-ähnliches Konto, direkte Zahlungen, Hosted Checkout, eigener
|
||||
Connect-Webhook, Erstattungen nach Regel des Betriebs; PayPal nicht möglich, SEPA ausgeschlossen; Haltezeit ≥ 32 min).
|
||||
Voraussetzung: Connect-Plattform im Stripe-Live-Modus eingerichtet (bisher nur Sandbox, Konto acct_1UM7AVBONHfkVuDM).
|
||||
Reference in new issue
Block a user