# K5-KONZEPT - Termine-Buchungsmodul (Kurato)

Stand: 23.07.2026. Erstellt von Fable 5 als Architektur- und Etappenkonzept fuer die
Umsetzung durch Opus-Bau-Subagenten. Grundlage: BAUPLAN.md Abschnitt 13 ("Termine"),
UX-KONZEPT.md Abschnitt 5 (Termine-Bereich) und 6 (Hans-Ausloeser "erste Terminbuchung"),
DECISIONS.md K1-K4e, bestehender Code (LinkToken, PreferenceCenter, EmailRenderer,
DriverFactory, WelcomeMailService, LeadIntake, PrivacyBridge).

## 1. Ziel und Positionierung

Selbst gehostete Calendly-Alternative direkt im CRM. Das Alleinstellungsmerkmal ist die
Verzahnung: Buchung -> Kontakt -> Pipeline -> Nachfassen. Komplett self-hosted,
DSGVO-first, keine Drittdienste. v1 baut den Free-Umfang: 1 Ansprechpartner (Host) mit
2 Terminarten, voll nutzbar inklusive ICS und Erinnerungsmail. Pro (unbegrenzte Hosts/
Terminarten, Kalender-Sync, Besucher-Zeitzonen-Automatik, Round-Robin) ist Phase 3+;
die Limits laufen ueber Filter (`kurato_booking_limits`), damit Pro sie nur anheben muss.

## 2. Datenmodell (Migration 1.2.0)

| Tabelle | Spalten (Kern) | Zweck |
|---|---|---|
| `hs_hosts` | id, label, slug (UNIQUE), role, photo_id (Attachment), user_id NULL, active, created_at | Ansprechpartner/Experte |
| `hs_booking_types` | id, host_id, label, slug, duration_min, location_type (phone/video/in_person), location_detail, buffer_before_min, buffer_after_min, lead_time_hours (Vorlauf), horizon_days (Maximalfrist), active | Terminart |
| `hs_availability` | id, host_id, booking_type_id NULL (= alle Arten), kind (weekly/exception), weekday 0-6 NULL, start_time, end_time, date_from NULL, date_to NULL | Wochenraster + Ausnahmen/Urlaub (kind=exception blockt Zeitraum) |
| `hs_bookings` | id, contact_id, host_id, type_id, start_utc DATETIME, end_utc DATETIME, status (confirmed/cancelled), note TEXT, created_at, cancelled_at NULL, UNIQUE(host_id, start_utc, status-relevant s.u.) | Buchung; Zeiten IMMER UTC |

Doppelbuchungs-Schutz auf DB-Ebene: UNIQUE-Index ueber (host_id, start_utc) ist mit
Storno nicht vertraeglich (stornierter Slot muss wieder buchbar sein). Loesung v1:
UNIQUE(host_id, start_utc, status) plus Insert-Muster wie beim K3c-Race-Fallback
(insert versuchen, bei Duplikat freundlich "Slot ist soeben vergeben worden" zeigen).
Status-Historie braucht v1 nicht (nur confirmed/cancelled; Umbuchung = Storno + Neubuchung
mit Verweis in note, s. Abschnitt 5).

Alles als gekapseltes Modul unter `src/Booking/` (+ `src/Db/Host-/BookingType-/
Availability-/BookingRepository.php`), damit es spaeter als eigenes Produkt auskoppelbar
bleibt (BAUPLAN 13). Repositories nicht final, prepared, Muster Bestand.

## 3. Slot-Logik (Kern, rein testbar)

`SlotCalculator` (src/Booking/SlotCalculator.php), frei von wpdb/HTTP:
Eingabe = Terminart (Dauer, Puffer, Vorlauf, Horizont), Wochenraster, Ausnahmen,
bestehende Buchungen (Start/Ende UTC), Zeitfenster (z.B. eine Woche), "jetzt" (UTC),
Site-Zeitzone. Ausgabe = buchbare Slot-Startzeiten (UTC).

Regeln:
- Raster: Slots im Takt der Termindauer innerhalb der Wochenfenster (Site-Zeitzone,
  intern nach UTC konvertiert; DST-Wechsel ueber DateTimeZone, nie Offset-Arithmetik).
- Abzug: Ausnahmen (kind=exception), bestehende confirmed-Buchungen INKLUSIVE Puffer
  vor/nach (Puffer der Terminart der BESTEHENDEN Buchung zaehlt), Vorlauf (jetzt +
  lead_time_hours), Horizont (heute + horizon_days).
- Anzeige immer in Site-Zeitzone mit explizitem Zeitzonen-Hinweis auf der Buchungsseite
  ("Alle Zeiten in Europe/Berlin"). Besucher-Zeitzonen-Automatik ist Pro, v1 NICHT.

Tests: Rasterbildung, Puffer-Kollisionen, Vorlauf/Horizont-Grenzen, Ausnahme-Zeitraum,
DST-Uebergang (Maerz/Oktober), leere Verfuegbarkeit.

## 4. Oeffentliche Buchungsseite

- **Zugang**: Rewrite `/termin/{host-slug}` (Query-Var + template_redirect,
  eigenstaendige escapte HTML-Seite, Muster PreferenceCenter: kein Theme-Zwang, noindex
  optional NICHT gesetzt - die Seite DARF gefunden werden, Inhaber-Seite). Zusaetzlich
  Gutenberg-Block (No-Build, D13) + Shortcode `[kurato_termin host="slug"]`, die dieselbe
  Render-Komponente einbetten.
- **Drei Schritte, serverseitig (POST/Redirect/GET, kein JS-Zwang; leichtes Inline-JS
  nur als Komfort)**: (1) Terminart + Slot waehlen (Wochenansicht, vor/zurueck),
  (2) Daten eingeben: Name, E-Mail, optional Telefon und Anliegen (Datenminimierung,
  BAUPLAN 13) + Pflicht-Checkbox Datenschutzhinweis (nicht vorangekreuzt, Text mit
  Datenschutz-Link, Consent-Log purpose `booking`), (3) Bestaetigung mit ICS-Download-
  Link und "Link fuer Storno/Umbuchung" (auch in der Mail).
- **Spam-Schutz**: Honeypot + Zeitfalle (Muster Leadlotse SpamGuard-Idee, lokal
  nachgebaut, kein Leadlotse-Import), Rate-Limit pro IP. KEIN Double-Opt-in fuer die
  Buchung selbst (Huerde toetet Conversion; die E-Mail wird durch die Bestaetigungsmail
  faktisch verifiziert - eine tote Adresse bekommt keinen Termin-Nachweis).
- **Race**: Beim Submit Slot erneut berechnen + Insert mit Duplikat-Abfang (Abschnitt 2).

## 5. Bestaetigung, Storno, Umbuchung, Erinnerung

- **Mails sind Transaktionsmails**: direkt ueber `DriverFactory->current()` (Muster
  WelcomeMailService), NIE durch die Marketing-Queue, kein Abmeldelink, kein Tracking.
  (BAUPLAN sagt "Erinnerung ueber die Mail-Queue" - bewusste Abweichung: K4-Konzept
  Abschnitt 6 verbietet Transaktionsmails in der Marketing-Queue; die Erinnerung ist
  eine Transaktionsmail. In DECISIONS festhalten.)
- **Bestaetigungsmail** an Besucher UND Host, beide mit ICS-Anhang (`IcsBuilder`,
  rein testbar: VCALENDAR/VEVENT, UID = booking-{id}@{host}, DTSTART/DTEND UTC,
  METHOD:REQUEST; Storno-Version METHOD:CANCEL). Core-Message braucht Anhaenge:
  pruefen, ob `Message` Attachments traegt - sonst ICS als Download-Link ueber
  signierten Token (Fallback, KEINE Core-Aenderung in K5; Entscheidung im Bau
  dokumentieren).
- **Storno/Umbuchung ohne Konto**: signierter Link (LinkToken-MUSTER, eigene Klasse
  `BookingToken` mit eigenem HMAC-Praefix `kurato_booking`, Zweckbindung wie K4d-2;
  kein Ablauf, aber wirkungslos sobald Termin vorbei/storniert). Storno-Seite zeigt
  den Termin, ein Klick storniert (POST), Host bekommt Info-Mail, ICS-CANCEL an beide.
  Umbuchung v1 = Storno + direkter Link zurueck auf die Buchungsseite (vorbefuellte
  Terminart) - KEIN eigener Umbuchungs-Flow (bewusste v1-Vereinfachung).
- **Erinnerung**: WP-Cron (stuendlich) sucht confirmed-Buchungen, deren Start zwischen
  jetzt+23h und jetzt+24h liegt (Fenster verhindert Doppelversand zusammen mit
  reminder_sent-Flag auf der Buchung), Default 24 h, Setting `booking.reminder_hours`
  (0 = aus). Erinnerung an Besucher; Host optional (Setting).

## 6. CRM-Verzahnung (das Alleinstellungsmerkmal)

Bei Buchung:
1. Kontakt anlegen/aktualisieren (Dedup per E-Mail, Muster LeadIntake inkl.
   UNIQUE-Race-Fallback; Telefon/Name nur ergaenzen, nie ueberschreiben).
2. Flotten-Event `hafenstudios/booking_created` (contact_id, booking_id, host, type,
   start_utc) - Adjet/Kompass docken in Phase 3 an.
3. Optional (Settings, Default an): Pipeline-Stufe "Termin gebucht" setzen, wenn der
   Kontakt in einer frueheren Stufe steht (nie zurueckstufen); Wiedervorlage
   "Nachfassen: {Terminart}" faellig 1 Tag nach dem Termin (TaskRepository).
4. Timeline: Buchung erscheint im Kontaktprofil (eigener Abschnitt "Termine" in
   contact.js, REST /contacts/{id}/bookings).
Storno setzt die Wiedervorlage nicht zurueck (bewusst simpel), schreibt aber ein
Timeline-sichtbares Storno-Datum.

## 7. Admin-UI (UX-KONZEPT Abschnitt 5)

- Menuepunkt "Termine" im Kurato-Menue (Cap: hs_manage_contacts fuer Verwaltung,
  neue Cap NICHT noetig - begruenden, falls der Bau anders entscheidet).
- **Buchungsseiten-Verwaltung** (serverseitig, admin-post, Muster ListsPage):
  Host anlegen (Free: 1), Terminarten (Free: 2, Limits via Filter), Wochenraster
  (je Wochentag von/bis, mehrere Fenster), Ausnahmen/Urlaub (Datum von/bis),
  oeffentliche URL zum Kopieren.
- **Buchungsliste** nach Kalenderwoche (Filter Host/Woche), Status-Badges,
  Storno durch den Betreiber (mit Mail an Besucher), Link zum Kontaktprofil.
- **Hans**: leerer Zustand ("Noch keine Buchungsseite"), Meilenstein-Toast bei der
  ersten eingegangenen Buchung (UX-Katalog v1), Hinweis auf den oeffentlichen Link.

## 8. DSGVO

- Consent-Log purpose `booking` bei jeder Buchung (Checkbox-Text + Version).
- PrivacyBridge: Buchungen in Export (Termin, Art, Status, Zeiten) und Loeschung
  (Kontaktloeschung entfernt/anonymisiert seine Buchungen).
- Retention: vergangene Buchungen nach Frist loeschen (Setting
  `booking.retention_days`, Default 365, Cron im PrivacyBridge-Muster).
- Datenminimierung im Formular (nur Name, E-Mail, optional Telefon/Anliegen).
- Keine Drittdienste; Video-Ort ist ein freies Link-Feld des Betreibers (Jitsi/eigenes
  Tool), Kurato laedt nichts extern.

## 9. Bau-Etappen

1. **K5a - Fundament**: Migration 1.2.0 (4 Tabellen), 4 Repositories, SlotCalculator,
   IcsBuilder, BookingToken. Tests: Slot-Regeln (inkl. DST), ICS-Struktur,
   Token-Zweckbindung, Repository-Basisverhalten.
2. **K5b - Admin**: Termine-Menue, Host-/Terminarten-/Verfuegbarkeits-Verwaltung
   (Free-Limits ueber Filter), Buchungsliste mit Betreiber-Storno, Hans-Zustaende.
3. **K5c - Oeffentlich + Verzahnung**: Buchungsseite (/termin/{slug} + Block +
   Shortcode), 3-Schritte-Flow mit Consent-Log + Spam-Schutz + Race-Abfang,
   Bestaetigungs-/Storno-/Erinnerungsmails + ICS, Storno-Seite, CRM-Verzahnung
   (Kontakt, Event, Stufe, Wiedervorlage, Timeline, REST bookings),
   PrivacyBridge + Retention.

Jede Etappe: Subagent baut, Projektlead prueft gegen (Tests, php -l, Kernlogik lesen).

## 10. Offene Entscheidungen fuer den Inhaber

1. **Buchung sofort fix oder mit Bestaetigungspflicht?** Vorschlag: sofort confirmed
   (+ Honeypot/Rate-Limit); eine E-Mail-Bestaetigungspflicht (DOI-light) kostet
   Conversions und schuetzt kaum besser.
2. **Erinnerungszeitpunkt**: Default 24 h vorher, einstellbar (0 = aus). Host-Erinnerung
   zusaetzlich an/aus?
3. **URL-Namespace**: `/termin/{slug}` fest oder einstellbar (Vorschlag: fest in v1,
   Filter fuer Entwickler)?
4. **Retention-Frist** vergangener Buchungen: Default 365 Tage ok?
5. **Stufen-Automatik**: Default-Name der Pipeline-Stufe "Termin gebucht" - vorhandene
   Stufe verwenden/anlegen oder nur, wenn der Betreiber sie zuordnet? Vorschlag:
   Setting mit Stufen-Dropdown, Default "aus", Hans empfiehlt es im Onboarding.

## 11. Getroffene Entscheidungen (Inhaber, 23.07.2026)

- **Buchung sofort verbindlich** (kein Bestaetigungslink); Schutz ueber Honeypot,
  Zeitfalle und Rate-Limit.
- **Erinnerung 24 h vorher an Besucher UND Host** (Default; Stunden einstellbar,
  0 = aus).
- **Stufen-Automatik als Setting mit Stufen-Dropdown, Default aus**; Hans empfiehlt
  die Aktivierung im Onboarding. Nie zurueckstufen.
- Defaults ohne Einwand uebernommen: URL-Namespace fest `/termin/` (Filter fuer
  Entwickler), Retention vergangener Buchungen 365 Tage (Setting).

Konzept damit abgenommen; Bau startet mit K5a.

---

*Referenzen: BAUPLAN.md Abschn. 13, 25; UX-KONZEPT.md Abschn. 5-6; DECISIONS.md D2, D6,
D13, K3c (Race-Fallback), K4c-1 (LinkToken), K4d-2 (Zweckbindung), K4e-2
(WelcomeMailService als Transaktionsmail-Muster); Code: PreferenceCenter (oeffentliche
Seite), LeadIntake (Kontakt-Dedup), PrivacyBridge (Export/Retention).*
