Files
Webklar-Kundenbereich/docs/superpowers/plans/2026-10-07-buchungskalender.md
T
2026-10-07 13:41:26 +00:00

65 KiB
Raw Blame History

Buchungskalender (Funktion unterkunft) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Neue Portal-Funktion „Buchungskalender“ für Ferienwohnungen: Kalender-Widget mit Anfrage, Freigabe durch den Vermieter, verbindliche Buchung per Link, Rechnung im Namen des Vermieters, Zahlung über Stripe Connect oder vor Ort, iCal-Abgleich; erste Kundin Donau Home.

Architecture: Eigenes Modul server/services/unterkunft/ nach dem Muster von Webshop (shop) und Terminplaner (termine). Ein serieller Kern (Strang 0) legt Schema, Konfiguration, reine Logik, Statusmaschine und alle Verdrahtungen in gemeinsam genutzten Dateien an; danach laufen vier Stränge parallel in eigenen Worktrees und berühren nur noch ihre eigenen Dateien (Ausnahmen sind je Task genannt). Abschluss: Zusammenführen, UI-Durchlauf, Review.

Tech Stack: Node 20/24 ESM, Express 4, Appwrite (node-appwrite, Server-Key), Stripe Connect (stripe ^19), nodemailer, Vanilla-ES-Module im Frontend, node:test, Playwright-core für UI.

Spec: docs/superpowers/specs/2026-10-07-buchungskalender-design.md (Abschnitte 1–16 + Nachträge N1–N6 in Abschnitt 17; N1–N6 gehen vor). Code-Fakten: /tmp/claude-0/-root/b360f353-cb78-442e-ada0-d24927a23169/scratchpad/plan/fakten-portal.md und fakten-woms.md (vor jedem Task die genannten Muster dort nachschlagen; bei Abweichung zählt der Code).

Global Constraints

  • Sicherheit (in JEDEN Agenten-Prompt kopieren): Kein rm -rf, rsync --delete, git reset --hard, git clean, git checkout -- ., git push, kein --force; nichts außerhalb des eigenen Worktrees und des Scratchpads schreiben; keine destruktiven Proben; nie /dev/null o. ä. löschen oder ersetzen; nie Produktions-Container, .env, compose-Dateien oder Live-Datenbanken anfassen.
  • Speicher: Server hat wenig RAM. Jeder Node-Testlauf: flock /tmp/claude-0/uk-test.lock nice timeout 600 node --max-old-space-size=1024 --test <datei>; nie npm test (alles parallel); UI-Läufe vorher free -m, nur wenn „available“ > 1500 MB.
  • node_modules: Worktrees haben keins → ln -s ../dev-termine/node_modules node_modules (Portal), untracked lassen, nie git add -A, immer Dateien einzeln stagen. Keine neuen Abhängigkeiten.
  • Commits: Autor Kenso Grimm <kenso@webklar.com>, Nachricht deutsch, endet mit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>. Kein Push (macht der Inhaber).
  • Funktionsschlüssel unterkunft; UI-Name „Buchungskalender“; Portal-Hash #buchungen; Fassung Rechtstexte 2026-10-07.
  • Schalter UNTERKUNFT_ENABLED (off|pilot|on), UNTERKUNFT_PILOT_EMAILS (Komma, * = alle).
  • Stripe-Produkt unterkunftSetup: 19900 Cent, Lookup-Key webklar_unterkunft_einrichtung_19900, priceLabel '199 €', setupEstimate 'meist innerhalb von 1–2 Wochen'.
  • Collections: ukUnterkuenfte, ukPreise, ukSperren, ukAnfragen, ukRechnungen, ukEinstellungen (Dokument-ID = featureId). Kein Feld heißt data. JSON-Felder als Text.
  • Datumswerte im Modul sind Kalendertage YYYY-MM-DD (Berlin); bis = Abreisetag, exklusiv. Nie mit lokaler Zeitzone rechnen; „heute“ über termine/zeit.js heute().
  • Geld immer ganze Cent (Integer), Bruttobeträge.
  • Mail-IDs: G16–G22, K31–K34, T22 (vor Anlage gegen server/mail-templates/ prüfen).
  • Gast-Mails: Sie-Form, Absender config.smtp.from mit Anzeigename = Firma des Vermieters, Reply-To = E-Mail des Vermieters. K-Mails: Du-Form.
  • Öffentliche Antworten enthalten nie Kunden-, Projekt-, Feature-, Anfrage-IDs (außer der Anfrage-ID im Buchungslink), keine E-Mails außer Anbieterdaten.
  • Sperren nur über withLocks([key], fn) aus server/services/keyedLock.js; Schlüssel unterkunft:<unterkunftId> (Belegung/Status) und uk-rechnungsnr:<featureId>.
  • Statuswerte exakt: neu, freigegeben, gebucht, bezahlt, abgelehnt, abgelaufen, storniert; zahlart ∈ online|vorort; bezahltVia ∈ stripe|vorort.

Review Focus

  1. Sommerzeit/Datumsgrenzen: Aufenthalt 2026-10-24 → 2026-10-26 (Zeitumstellung 25.10.) = genau 2 Nächte, Preis für 2 Nächte; Anreise heute (Berlin) ist erlaubt, gestern nicht. → Tests in Task 2 und Task 3.
  2. Gleichzeitige Anfragen für dieselben Nächte: Zwei Anfragen überlappen; Freigabe der ersten klappt, Freigabe der zweiten scheitert mit 409 zeitraum_belegt, ohne Mail; zwei gleichzeitige bucheVerbindlich derselben Anfrage ergeben genau eine Rechnung. → Tests in Task 4.
  3. iCal-Formate aus der Praxis: Booking.com (DTSTART;VALUE=DATE), Airbnb, DATE-TIME mit Z oder TZID, gefaltete Zeilen, STATUS:CANCELLED, fehlendes DTEND, HTML-Fehlerseite statt ICS (→ Fehler, alter Stand bleibt). → Tests in Task 9.
  4. Buchungslink erneut geöffnet / doppelt geklickt: nach Buchung zeigt die Seite den Status statt erneut „buchen“; Doppelklick erzeugt keine zweite Rechnung; abgelaufener Link → klare Meldung, keine Buchung; falsches Token → 404 ohne Unterschied zu unbekannter ID. → Tests in Task 7.
  5. Unsinnige Posten des Vermieters: leere Postenliste, Gesamtsumme ≤ 0, Text leer/über 200 Zeichen, mehr als 30 Posten, ust ≠ 0 bei Kleinunternehmer → 400 mit Feldfehler, nichts gespeichert. → Tests in Task 2 und Task 8.

Ablauf der Stränge

Strang 0 (seriell): Task 1 → 2 → 3 → 4 → 5        Branch feature/unterkunft (dev-unterkunft)
danach parallel, je eigener Worktree von feature/unterkunft:
  Strang 1 Server   : Task 6 → 7 → 8 → 9 → 10 → 11  feature/uk-server  (dev-uk-server)
  Strang 2 Oberfl.  : Task 12 → 13 → 14 → 15        feature/uk-ui      (dev-uk-ui)  + WOMS feature/unterkunft
  Strang 3 Widget   : Task 16 → 17 → 18             feature/uk-widget  (dev-uk-widget) + Donau-Repo
  Strang 4 Mail/PDF : Task 19 → 20 → 21             feature/uk-mail    (dev-uk-mail)
Abschluss (seriell): Task 22 (Merge + UI-Durchlauf) → Task 23 (Gesamtreview)

Stränge 2 und 3 bauen gegen die API-Verträge aus Task 5 (Routen + Antwortformen sind dort festgelegt) und testen mit Attrappen; echte Daten erst in Task 22. Worktree je Strang: git -C /home/webklar/apps/kundenbereich.webklar.com/build worktree add -b feature/uk-<x> ../dev-uk-<x> feature/unterkunft (Upstream danach mit git branch --unset-upstream lösen).


Strang 0: Kern

Task 1: Konfiguration, Schalter, Schema, Setup-Skript

Files:

  • Modify: server/config.js (Env-Lesen neben SHOP_ENABLED ~Z.32; Produkt neben shopSetup ~Z.131 und Export ~Z.653; collections ~Z.612-636; neuer Block unterkunft neben shop ~Z.884)
  • Create: server/services/unterkunft/schalter.js, server/services/unterkunft/schema.js, scripts/unterkunft-setup.mjs
  • Modify: test/support/schema.js (UNTERKUNFT_COLLECTIONS in schemaKeys)
  • Test: test/unterkunftSchalter.test.js, test/unterkunftSetup.test.js

Interfaces:

  • Produces:

    • config.unterkunft = { mode, pilotEmails, fassung:'2026-10-07', formMinSekunden:3, minAntwortMs:300, icalIntervallMin:15, icalMaxBytes:1_000_000, icalTimeoutMs:10_000, icalFehlerStunden:6, horizontMonate:18, maxPosten:30, postenTextMax:200, nachrichtMax:2000, anfragenJeSiteStunde:30, ipHashTage:30, unverbindlichMonate:6, neuVerfallTage:60, anonymisierenJahre:3, rechnungJahre:8, loeschTageNachAbschalten:90 } (Werte bei jedem Zugriff aus Env, Muster config.shop).
    • config.unterkunftSetup = { key:'unterkunftSetup', name:'Buchungskalender (Einrichtung)', amountCents:19900, lookupKey:'webklar_unterkunft_einrichtung_19900', oneTime:true, priceNickname }.
    • config.collections.{ukUnterkuenfte,ukPreise,ukSperren,ukAnfragen,ukRechnungen,ukEinstellungen} mit Env-Override APPWRITE_COLLECTION_UK_*.
    • schalter.js: unterkunftModus(): 'off'|'pilot'|'on', unterkunftFuerKunde(email): boolean.
    • schema.js: export const UNTERKUNFT_COLLECTIONS (reine Daten, Muster shop/schema.js, Helfer str/int/bool/dt/index).
    • scripts/unterkunft-setup.mjs: export async function main({ check=false, client, log }).
  • Step 1: Schalter-Test schreiben (Muster test/shopSchalter.test.js): off bei leer/unbekannt; pilot → nur UNTERKUNFT_PILOT_EMAILS (case-insensitiv), * = alle; on → alle.

  • Step 2: Setup-Test schreiben (Muster test/shopSetup.test.js): --check gegen leeren Mock meldet alle 6 Collections als FEHLT und schreibt nichts; ohne check legt es alle an, zweiter Lauf legt nichts an; jede Collection hat die Attribute aus Spec §3 + N2, u. a.:

    • ukUnterkuenfte: featureId, customerId, projectId, name(120), beschreibung(500), maxPersonen int, mindestNaechte int, checkIn(5), checkOut(5), sort int, aktiv bool, icalImporte(4000), icalExportToken(64); Index featureId.
    • ukPreise: unterkunftId, featureId, staffeln(2000), reinigungCent, personenInklusive, aufpreisPersonNachtCent, direktrabattProzent, steuerart(20), ustUebernachtung, ustReinigung, fristTage, stornobedingungen(5000); Index unterkunftId.
    • ukSperren: unterkunftId, featureId, von(10), bis(10), quelle(60), notiz(200); Indizes unterkunftId, (unterkunftId,quelle).
    • ukAnfragen: featureId, customerId, projectId, unterkunftId, nummer(12), von, bis, personen int, status(12), gast(3000), rechnungsadresse(1500), posten(8000), summen(1000), nachrichtVermieter(2000), fristAt dt, tokenHash(64), zahlart(8), bezahltVia(8), bezahltAt dt, stripeSessionId(100), stripePaymentIntentId(100), erstattetCent int, stornoGrund(500), stornobedingungenText(5000), rechtstexteFassung(20), ipHash(16), freigegebenAt/gebuchtAt/abgelehntAt/abgelaufenAt/storniertAt dt, rechnungId(36), anonymisiertAt dt; Indizes featureId, unterkunftId, (unterkunftId,status), status.
    • ukRechnungen: featureId, anfrageId, art(8), nummer(24), datum(10), bruttoCent int, inhalt(20000); Indizes featureId, anfrageId, (featureId,nummer).
    • ukEinstellungen: featureId, firma(150), inhaber(150), strasse(150), plz(10), ort(100), email(200), telefon(50), steuernummer(30), ustId(20), iban(34), bic(11), bank(100), rechnungsPraefix(8), rechnungZaehler(100), akzentfarbe(7), stripeAccountId(40), stripeStatus(4000), onlineZahlung bool, rechnungAuftragAt dt, anfrage(2000), anfrageStatus(16), anfrageAt dt, anfrageEntschiedenAt dt, anfrageEntschiedenVon(120), anfrageBegruendung(1000), icalFehlerMailAt dt.
  • Step 3: Tests laufen lassen, FAIL erwartet (flock … --test test/unterkunftSchalter.test.js test/unterkunftSetup.test.js).

  • Step 4: Implementieren (config-Block, Produkt, Collections, schalter.js, schema.js, Setup-Skript als Kopie des Shop-Musters mit createAppwriteClient() aus scripts/analyse-setup.mjs), test/support/schema.js ergänzen.

  • Step 5: Tests PASS; zusätzlich test/shopSetup.test.js und test/shopSchalter.test.js weiter grün.

  • Step 6: Commit Buchungskalender: Konfiguration, Schalter, Schema und Setup-Skript.

Task 2: Preisrechnung

Files:

  • Create: server/services/unterkunft/datum.js, server/services/unterkunft/preis.js
  • Test: test/unterkunftPreis.test.js

Interfaces:

  • Produces:

    • datum.js: istTag(s): boolean (/^\d{4}-\d{2}-\d{2}$/ + gültig), naechte(von, bis): number (UTC-Datumsarithmetik auf Tagesstrings), plusTage(tag, n): string, tageVon(von, bis): string[] (belegte Nächte, bis exklusiv), formatTag(tag): '12.11.2026'.
    • preis.js:
      • berechnePreis({ preise, von, bis, personen }) → { naechte, posten:[{text, menge, einzelCent, ustProzent}], summen }
      • summiere(posten, steuerart) → { bruttoCent, nettoCent, ust:[{ satz, nettoCent, steuerCent }] } (bei kleinunternehmer ust: [], netto = brutto)
      • pruefePosten(posten, steuerart) → { posten } | { fehler: { posten: string } }
      • formatEuro(cent) → '1.234,50 €'
      • preiseVollstaendig(preise) → boolean
  • Step 1: Failing tests mit PREISE = { staffeln:[{abNaechten:1,preisProNachtCent:6500},{abNaechten:7,preisProNachtCent:5500},{abNaechten:28,preisProNachtCent:4500}], reinigungCent:6000, personenInklusive:2, aufpreisPersonNachtCent:1000, direktrabattProzent:10, steuerart:'regel', ustUebernachtung:7, ustReinigung:7 }:

    • naechte('2026-10-24','2026-10-26') === 2; naechte('2026-12-30','2027-01-02') === 3.
    • 2026-11-02→2026-11-09, 2 Personen: Posten [Übernachtung 7×5500, Endreinigung 1×6000, Direktbuchungsrabatt 10 % 1×-3850], bruttoCent 40650, ust [{satz:7, nettoCent:37991, steuerCent:2659}].
    • 30 Nächte, 3 Personen: Übernachtung 30×4500, „Zusatzperson“ menge 30 × 1000, Reinigung 6000, Rabatt -16500 → bruttoCent 184500.
    • 3 Nächte, 2 Personen: bruttoCent 23550.
    • Unterschiedliche Sätze (ustReinigung 19): zwei ust-Einträge, Summe netto+steuer = brutto.
    • kleinunternehmer: ust: [], nettoCent === bruttoCent.
    • Rabatt 0 / Reinigung 0 → kein Posten dafür.
    • pruefePosten: [] → Fehler; Summe 0 → Fehler; Text '' oder 201 Zeichen → Fehler; 31 Posten → Fehler; ustProzent:7 bei kleinunternehmer → Fehler; ustProzent außerhalb {0,7,19} → Fehler; menge kein positiver Integer → Fehler; gültige Liste → {posten} mit getrimmten Texten.
    • formatEuro(123450) === '1.234,50 €', formatEuro(-3850) === '−38,50 €' (U+2212).
  • Step 2: FAIL prüfen.

  • Step 3: Implementieren. Staffel = höchste abNaechten ≤ naechte für alle Nächte (U11). Rabatt-Basis = Übernachtung + Zusatzpersonen, -Math.round(basis*p/100), ust = ustUebernachtung (U12). USt je Satz: netto = Math.round(bruttoSatz*100/(100+satz)), steuer = bruttoSatz - netto. Texte: Übernachtung, ${n} Nächte, Zusatzperson${k>1?'en':''} (${k}), ${n} Nächte, Endreinigung, Direktbuchungsrabatt ${p} %.

  • Step 4: PASS.

  • Step 5: Commit Buchungskalender: Preisrechnung und Datumshilfen.

Task 3: Speicher und Belegung

Files:

  • Create: server/services/unterkunft/speicher.js, server/services/unterkunft/belegung.js
  • Test: test/unterkunftBelegung.test.js

Interfaces:

  • Consumes: datum.js (Task 2), appwriteAdmin.js (listAllDocuments, getDocument, createDocument, updateDocument, deleteDocument, Query).

  • Produces:

    • speicher.js (alle JSON-Felder geparst/serialisiert; Rückgaben sind „flache“ Objekte mit $id): ladeEinstellungen(featureId) → obj|null, speichereEinstellungen(featureId, felder) → obj (legt an, falls fehlt), listeUnterkuenfte(featureId, { nurAktive=false }) → obj[] (nach sort), ladeUnterkunft(id) → obj|null, speichereUnterkunft(featureId, felder, id?) → obj, ladePreise(unterkunftId) → obj|null, speicherePreise(unterkunftId, featureId, felder) → obj, listeSperren(unterkunftId) → obj[], legeSperreAn(...), loescheSperre(id), ladeAnfrage(id) → obj|null, legeAnfrageDokAn(felder) → obj, aktualisiereAnfrage(id, felder) → obj, listeAnfragen(featureId, { status?: string[], unterkunftId? }) → obj[].
    • belegung.js:
      • AKTIVE_STATUS = ['gebucht','bezahlt']
      • belegteBereiche(unterkunftId, von, bis, { ohneAnfrage, now }) → [{ von, bis, quelle:'manuell'|'ical'|'reserviert'|'gebucht' }] (Sperren + Anfragen gebucht|bezahlt + freigegeben mit fristAt > now; Ausschnitt auf [von,bis))
      • istFrei(unterkunftId, von, bis, { ohneAnfrage, now }) → boolean
      • ueberschneidet(a, b) → boolean (rein, Halboffen-Intervalle)
  • Step 1: Failing tests (Appwrite-Mock mit ukSperren, ukAnfragen):

    • ueberschneidet({von:'2026-11-01',bis:'2026-11-05'},{von:'2026-11-05',bis:'2026-11-07'}) === false (Abreise = Anreise ok); …'2026-11-04'… → true.
    • Sperre manuell 11-10..11-12 → istFrei(u,'2026-11-11','2026-11-13') false; '2026-11-12','2026-11-14' true.
    • Anfrage freigegeben mit fristAt in Zukunft blockiert; mit fristAt in Vergangenheit nicht; neu, abgelehnt, abgelaufen, storniert blockieren nicht; gebucht/bezahlt blockieren.
    • ohneAnfrage: id ignoriert genau diese Anfrage.
    • belegteBereiche liefert die Quelle ical für quelle:'ical:abc'.
  • Step 2: FAIL.

  • Step 3: Implementieren (Abfragen per Query.equal('unterkunftId', id) + Status-Filter, listAllDocuments).

  • Step 4: PASS.

  • Step 5: Commit Buchungskalender: Speicher und Belegung.

Task 4: Statusmaschine, Token, Rechnungsnummern

Files:

  • Create: server/services/unterkunft/fehler.js, token.js, rechnungen.js, ablauf.js
  • Create (Stubs, die die Stränge ersetzen): server/services/unterkunft/mails.js, server/services/unterkunft/zahlung.js
  • Test: test/unterkunftAblauf.test.js, test/unterkunftRechnungen.test.js

Interfaces:

  • Consumes: Tasks 1–3.

  • Produces:

    • fehler.js: class UkFehler extends Error { constructor(status, code, felder = {}) } — Codes u. a. zeitraum_belegt (409), status_ungueltig (409), frist_abgelaufen (410), nicht_gefunden (404), ungueltig (400), mindestnaechte (400), zu_viele_personen (400), online_aus (409).
    • token.js: neuesToken() → { token, hash } (32 Byte base64url, sha256 hex), tokenPasst(hash, token) → boolean (timingSafeEqual), formToken(siteKey, now), pruefeFormToken(siteKey, token, now) → 'ok'|'zu_schnell'|'abgelaufen'|'ungueltig' (Kopie shop/token.js, Label 'uk-form', Min config.unterkunft.formMinSekunden), ipHashVon(ip) (Präfix uk-ip:).
    • rechnungen.js:
      • naechsteNummer(featureId, now) → 'RE-2026-0001' (Sperre uk-rechnungsnr:<featureId>, Zähler {jahr,n} in ukEinstellungen.rechnungZaehler, Jahreswechsel → n=1; Präfix aus Einstellungen, Standard RE)
      • stelleAus(ctx, anfrage, { art:'rechnung'|'storno', bezugNummer?, now }) → rechnungDoc (inhalt = { aussteller:{firma,inhaber,strasse,plz,ort,email,telefon,steuernummer,ustId,iban,bic,bank}, empfaenger: anfrage.rechnungsadresse, unterkunft:{name}, von, bis, naechte, personen, posten (bei storno: einzelCent negiert), summen, steuerart, zahlart, bezugNummer, anfrageNummer })
      • listeRechnungen(featureId) → doc[], ladeRechnung(id) → doc|null
    • mails.js (Stub, Task 20 ersetzt den Inhalt, Signaturen bleiben): anGast(art:'G16'|'G17'|'G18'|'G19'|'G20'|'G21'|'G22', ctx, anfrage, extra={}) → Promise<void>, anVermieter(template:'K31'|'K32'|'K33', ctx, values={}) → Promise<void>; Stub protokolliert Aufrufe in export const _aufrufe = [].
    • zahlung.js (Stub, Task 7 ersetzt): erstatte(ctx, anfrage, betragCent, { now }) → Promise<{ erstattetCent }>; Stub wirft UkFehler(409,'online_aus'), wenn ctx.einstellungen.stripeAccountId fehlt, sonst {erstattetCent: betragCent}.
    • ablauf.js (jede Änderung unter withLocks(['unterkunft:'+anfrage.unterkunftId]), Status vor Schreiben frisch lesen):
      • ctx = { featureId, customerId, projectId, siteKey, einstellungen } (Task 5 liefert ihn)
      • legeAnfrageAn(ctx, { unterkunftId, von, bis, personen, gast:{name,firma,email,telefon,nachricht} }, { now, ipHash }) → anfrage → prüft Tage, von ≥ heute, bis ≤ heute+horizontMonate, mindestNaechte, maxPersonen, istFrei; nummer = A- + 4 Zeichen [A-Z2-9]; Mails G16 + K31.
      • gibFrei(ctx, anfrageId, { posten?, fristTage?, nachricht?, now }) → { anfrage, token } → nur aus neu; Posten = posten (über pruefePosten) oder berechnePreis; istFrei(…, {ohneAnfrage}); fristAt = now + fristTage*24h; tokenHash; stornobedingungenText + rechtstexteFassung als Schnappschuss; ipHash = ''; Mail G17 (extra.token).
      • lehneAb(ctx, anfrageId, { text, now }) → anfrage → nur aus neu; ipHash = ''; Mail G18.
      • bucheVerbindlich(ctx, anfrageId, token, { rechnungsadresse, zahlart, now }) → { anfrage, rechnung } → Token prüfen (sonst nicht_gefunden), Status freigegeben (bei gebucht|bezahlt mit gleichem Token: bestehendes Ergebnis zurückgeben, idempotent), fristAt > now (sonst frist_abgelaufen), zahlart==='online' nur mit einstellungen.onlineZahlung && stripeAccountId, Pflichtfelder Adresse (name|firma, strasse, plz, ort, land), istFrei(…,{ohneAnfrage}); → gebucht, stelleAus(rechnung), rechnungId, Mails G19 + K32.
      • markiereBezahlt(ctx, anfrageId, { via, paymentIntentId?, now }) → anfrage → aus gebucht; idempotent bei bezahlt; Mail G20 nur bei via==='stripe'.
      • storniere(ctx, anfrageId, { grund, erstattungCent=0, now }) → { anfrage, stornoRechnung|null } → aus freigegeben|gebucht|bezahlt; Erstattung nur bei bezahltVia==='stripe', 0 ≤ cent ≤ brutto − bereits erstattet, über zahlung.erstatte; Stornorechnung nur, wenn Rechnung existiert; Mail G22.
      • laufeFristenAb(now) → { abgelaufen: number } → alle freigegeben mit fristAt ≤ now → abgelaufen, Mail G21.
      • UEBERGAENGE (Objekt Status → erlaubte Ziele), darfUebergang(von, nach) → boolean.
  • Step 1: Failing tests Rechnungen: erste Nummer RE-2026-0001, zweite RE-2026-0002; 10 parallele naechsteNummer → 10 verschiedene, lückenlos 1..10; Jahreswechsel (now 2027-01-01T00:30+01:00 Berlin) → RE-2027-0001; Präfix DH → DH-2026-0001; stelleAus(storno) negiert alle einzelCent und summen.bruttoCent, trägt bezugNummer.

  • Step 2: Failing tests Ablauf (Appwrite-Mock, mails._aufrufe prüfen):

    • Anfrage → neu, Nummer-Format /^A-[A-Z2-9]{4}$/, _aufrufe enthält G16 und K31.
    • Anfrage in der Vergangenheit / unter Mindestnächten / mehr Personen als max / über belegte Nacht → UkFehler mit passendem Code, nichts gespeichert.
    • Review Focus 2: Anfragen A und B überlappen; gibFrei(A) ok; gibFrei(B) → 409 zeitraum_belegt, keine G17 für B.
    • gibFrei mit Posten [{text:'Pauschale',menge:1,einzelCent:50000,ustProzent:7}] → summen.bruttoCent 50000.
    • bucheVerbindlich falsches Token → 404 nicht_gefunden; nach fristAt → 410; zweimal parallel mit richtigem Token → genau ein ukRechnungen-Dokument, beide Aufrufe liefern dieselbe rechnung.nummer.
    • zahlart:'online' ohne Stripe-Konto → 409 online_aus.
    • laufeFristenAb setzt abgelaufen und schickt G21; Zeitraum danach frei.
    • storniere gebuchter Anfrage → Stornorechnung mit eigener Nummer, G22; storniere mit erstattungCent > brutto → 400.
    • darfUebergang('neu','bezahlt') === false.
  • Step 3: FAIL.

  • Step 4: Implementieren.

  • Step 5: PASS.

  • Step 6: Commit Buchungskalender: Statusmaschine, Token und Rechnungsnummern.

Task 5: Verdrahtung in gemeinsamen Dateien + API-Verträge

Dieser Task berührt alle geteilten Dateien, damit die Stränge danach konfliktfrei arbeiten. Router-Dateien entstehen hier als leere express.Router() mit Kommentar, welcher Task sie füllt.

Files:

  • Modify: server/config.js (features.unterkunft neben shop ~Z.461-500), server/services/featureRegistry.js (STANDARD_HAKEN.unterkunft), server/services/legalDocs.js (SAETZE.unterkunft = { lb:{slug:'leistungsbeschreibung-unterkunft', pdf:'Leistungsbeschreibung-Buchungskalender'}, avv:{slug:'avv-unterkunft', pdf:'AVV-Buchungskalender'}, widerruf: <gemeinsam> }, FUNKTION_SATZ, aktuelleFassung), server/services/stripeBilling.js (PRODUCT_METADATA unterkunftSetup:'unterkunft-einrichtung'), scripts/stripe-products.mjs (KEYS), server/routes/abo.js (ORDERS, FEATURE_OF_ORDER unterkunft:'unterkunft', Text 'Buchungskalender (Einrichtung)'), server/routes/funktionen.js (Antwort unterkunft:{verfuegbar}), server/index.js (Mounts + Job-Registrierung), server/routes/shopWebhook.js (Weiche wkUk vor Z.54, charge.refunded nach Metadaten trennen, account.updated auch an Uk), server/services/termine/outbox.js + server/services/termine/schema.js (optionales Feld anhang 500, wird nur gespeichert/durchgereicht), public/dashboard.html (Section view-buchungen mit <div id="buchungen-body">), public/app.js (VIEW_HASHES.buchungen, viewRefreshers.buchungen = () => import('/buchungen.js').then(m => m.initBuchungen(document.getElementById('buchungen-body'))), APP_PARENT.buchungen='funktionen', FEATURE_LABELS.unterkunft='Buchungskalender')
  • Create: server/services/unterkunft/kontext.js, server/services/unterkunft/jobs.js (Gerüst), server/routes/publicUnterkunft.js, server/routes/unterkunft.js, server/routes/team/unterkunft.js, server/routes/unterkunftSeite.js, server/routes/unterkunftWebhook.js (exportiert verarbeiteUkEreignis(event, now), leer), public/buchungen.js (Platzhalter export function initBuchungen(el){ el.textContent = 'Buchungskalender wird geladen …' }), docs/funktionen/unterkunft-api.md (API-Vertrag, s. u.)
  • Test: test/unterkunftWiring.test.js (Muster test/shopWiring.test.js), Anpassung test/featureRegistry.test.js, test/legalDocs.test.js, test/termineOutbox.test.js falls Pins brechen

Interfaces:

  • Produces:

    • kontext.js: unterkunftPerSiteKey(siteKey, { now }) → ctx|null (30-s-Cache, Feature-Dokument featureKey:'unterkunft', nur state active; im Modus pilot nur, wenn die Besitzer-E-Mail in der Pilotliste steht; Modus off → null), ladeFuerProjekt(customerId, projectId) → ctx|null (beliebiger state ≠ off), leereCache(siteKey), SITE_KEY_RE. ctx = { featureId, customerId, projectId, siteKey, state, besitzerEmail, einstellungen, project }.
    • jobs.js: minutenJob(now), viertelstundenJob(now), taeglicherJob(now) mit einzeln()-Wrapper; bei Modus off → {aus:true}. Minutenjob ruft schon laufeFristenAb(now); die anderen sind leer (Task 9/10 füllen).
    • Mounts in server/index.js: app.use('/api/public/unterkunft/v1', publicUnterkunftRoutes) vor dem globalen Parser (Z.77); app.use('/api/unterkunft', unterkunftRoutes) (Kunden-Session); Team-Router router.use('/unterkunft', unterkunftTeamRoutes) (wie Shop, index.js:28); app.use(unterkunftSeiteRouter()) vor express.static. Jobs: 80 s Start/60 s, 140 s Start/15 min, 210 s Start/24 h.
    • Register config.features.unterkunft = Kopie shop mit: product:'unterkunftSetup', label {name:'Buchungskalender', kurz:'Buchungskalender', keine:…}, priceLabel '199 €', setupEstimate, consent (items unterlagen, verantwortung [Text Spec §9], rechnung [„Ich beauftrage WEBklar, in meinem Namen und auf meine Rechnung Rechnungen an meine Gäste auszustellen (§ 14 Abs. 2 Satz 4 UStG). Für Richtigkeit von Preisen und Steuerangaben bin ich verantwortlich.“], stripe, beginn, information), returnHash:'buchungen', returnProjekt:true, ticket.topic = (p) => 'Buchungskalender einbauen – ' + name, ticket.details → dyn. Import services/unterkunft/ticketDetails.js (Task 11), requiresWartungsAbo:false, ticketFreischaltung:true.
    • STANDARD_HAKEN.unterkunft = { verfuegbar: unterkunftFuerKunde, bestellpruefung: (args) => import('./unterkunft/anfrageFunktion.js').then(m => m.anfrageOffenePunkte(args)), unterlagenBereit: () => featureLegalReady('unterkunft'), belehrung: termineBelehrung, freischaltbar: (doc) => import('./unterkunft/freischaltung.js').then(m => m.freischaltbar(doc)) } — die zwei Module entstehen in Task 11; bis dahin existieren sie als Stubs (anfrageOffenePunkte → ['Anfrage noch nicht freigegeben'], freischaltbar → false).
    • API-Vertrag docs/funktionen/unterkunft-api.md (Stränge 2 und 3 bauen dagegen; Task 6–8 erfüllen ihn). Fehlerform überall { fehler: code, felder?: {feld: text} }.
      • Öffentlich (/api/public/unterkunft/v1):
        • GET /:siteKey → { aktiv:true, anbieter:{ name, ort, datenschutzUrl }, akzentfarbe, formToken, unterkuenfte:[{ id, name, maxPersonen, mindestNaechte, checkIn, checkOut }] } oder { aktiv:false }.
        • GET /:siteKey/belegung?unterkunft=<id>&von=YYYY-MM-DD&bis=YYYY-MM-DD → { belegt:[{von,bis}], heute:'YYYY-MM-DD', bisMax:'YYYY-MM-DD' } (Quellen nicht offengelegt).
        • POST /:siteKey/anfrage Body { unterkunft, von, bis, personen, name, firma, email, telefon, nachricht, formToken, kontakt_url } → 201 { nummer }.
        • GET /:siteKey/buchung/:id?t= → { status, nummer, unterkunft:{name,checkIn,checkOut}, von, bis, naechte, personen, posten, summen, steuerart, fristAt, stornobedingungen, anbieter:{firma,inhaber,strasse,plz,ort,email,telefon}, gast:{name,firma}, rechnungsadresse?, zahlarten:['vorort'|'online'], zahlart?, bezahlt:boolean, rechnungNummer? }.
        • POST /:siteKey/buchung/:id?t= Body { rechnungsadresse:{name,firma,strasse,plz,ort,land}, zahlart } → { status:'gebucht', rechnungNummer, zahlungUrl? } (bei online direkt Checkout-URL).
        • POST /:siteKey/buchung/:id/zahlung?t= → { url }.
        • GET /:siteKey/buchung/:id/rechnung.pdf?t=&art=rechnung|storno → PDF.
      • Kunde (/api/unterkunft, Session, ?projectId=): GET /status; GET|PUT /einstellungen; GET /unterkuenfte; POST /unterkuenfte; PUT|DELETE /unterkuenfte/:id; GET|PUT /unterkuenfte/:id/preise; GET /unterkuenfte/:id/kalender?von&bis → { bereiche:[{von,bis,quelle,anfrageNummer?,sperreId?,notiz?}] }; POST /unterkuenfte/:id/sperren; DELETE /sperren/:id; POST /unterkuenfte/:id/ical-importe; DELETE /unterkuenfte/:id/ical-importe/:importId; POST /unterkuenfte/:id/ical-export/erneuern; GET /anfragen?filter=offen|gebucht|alle; GET /anfragen/:id (inkl. vorschlag = berechnePreis); POST /anfragen/:id/freigeben {posten,fristTage,nachricht}; POST /anfragen/:id/ablehnen {text}; POST /anfragen/:id/bezahlt; POST /anfragen/:id/stornieren {grund, erstattungCent}; POST /preisvorschau {unterkunftId,von,bis,personen}; GET /rechnungen; GET /rechnungen/:id.pdf (nur Einzel-PDF, kein ZIP — Spec N7); POST /stripe/verbinden, POST /stripe/vorhandenes-konto, POST /stripe/aktualisieren; POST /anfrage-funktion {art,einheiten,portale,nachricht}; POST /anfrage-funktion/zurueckziehen.
      • Team (/api/team/unterkunft): GET /:customerId; POST /:customerId/anfrage {projectId, entscheidung, begruendung}.
  • Step 1: Wiring-Test schreiben: Registereintrag vorhanden mit Produkt unterkunftSetup und returnHash 'buchungen'; featureHaken('unterkunft') hat alle 5 Haken; FUNKTION_SATZ.unterkunft === 'unterkunft'; GET /api/public/unterkunft/v1/aaaaaaaaaaaaaaaa → 200 {aktiv:false} (Stub-Router liefert das schon); public/app.js enthält buchungen in VIEW_HASHES (Quelltext-Prüfung); dashboard.html enthält id="buchungen-body"; Shop-Webhook mit metadata.wkUk:'1' ruft verarbeiteUkEreignis statt Shop-Logik.

  • Step 2: FAIL.

  • Step 3: Implementieren inkl. API-Vertrag-Datei (Text oben 1:1).

  • Step 4: PASS; zusätzlich grün: test/shopWiring.test.js, test/featureRegistry.test.js, test/legalDocs.test.js, test/termineOutbox.test.js, test/funktionenRoutes.test.js, test/shopWebhook*.test.js (einzeln nacheinander).

  • Step 5: Commit Buchungskalender: Verdrahtung, Router-Gerüste und API-Vertrag.

  • Step 6: Worktrees der vier Stränge anlegen (Befehl oben), je node_modules-Symlink.


Strang 1: Server (dev-uk-server)

Task 6: Öffentliche API – Konfiguration, Belegung, Anfrage

Files:

  • Modify: server/routes/publicUnterkunft.js
  • Test: test/unterkunftPublic.test.js

Interfaces:

  • Consumes: kontext.unterkunftPerSiteKey, ablauf.legeAnfrageAn, belegung.belegteBereiche, token.formToken/pruefeFormToken/ipHashVon, termine/limits.js (zaehle, darfAnfrage, ipSchluessel).

  • Produces: die drei öffentlichen Endpunkte GET /:siteKey, GET /:siteKey/belegung, POST /:siteKey/anfrage laut Vertrag.

  • Step 1: Failing tests: unbekannter/inaktiver siteKey → 200 {aktiv:false}; aktiver → Unterkünfte nur aktiv, sortiert, ohne featureId/customerId; Belegung über 18 Monate hinaus → 400; Belegung fasst angrenzende Bereiche zusammen; POST anfrage gültig → 201 {nummer} + Dokument neu; Honeypot kontakt_url gefüllt → 201 mit Schein-Nummer, nichts gespeichert; Form-Token zu_schnell → ebenso stiller Erfolg; fehlende E-Mail → 400 felder.email; mehr als anfragenJeSiteStunde je Site → 429; CORS: OPTIONS von erlaubter Origin (Domain des Projekts) → 204 mit Origin, fremde Origin bei POST → 403; GET mit Access-Control-Allow-Origin: *.

  • Step 2: FAIL.

  • Step 3: Implementieren (Helfer route/schreibe/fehlerMit/erlaubteHerkunft/vorabantwort aus publicShop.js in eine eigene lokale Kopie; eigener 16-KB-JSON-Parser; UkFehler → Status/Code).

  • Step 4: PASS.

  • Step 5: Commit Buchungskalender: öffentliche API für Kalender und Anfrage.

Task 7: Buchungsseite, Stripe-Zahlung, Webhook

Files:

  • Modify: server/routes/publicUnterkunft.js (Buchungs-Endpunkte), server/routes/unterkunftWebhook.js, server/services/unterkunft/zahlung.js (Stub ersetzen), server/services/shop/connect.js (erstatten(..., { webklar = 'shop' }) Parameter, Standard unverändert)
  • Test: test/unterkunftBuchung.test.js

Interfaces:

  • Consumes: ablauf.bucheVerbindlich/markiereBezahlt, connect.checkoutAnlegen/sessionHolen/erstatten, rechnungen.ladeRechnung, rechnungPdf aus Task 19 (bis zum Merge: GET …/rechnung.pdf darf in diesem Strang mit 501 antworten, wenn server/services/unterkunft/rechnungPdf.js fehlt — dynamischer Import mit Fallback, Test prüft nur 200/501).

  • Produces:

    • zahlung.js: checkoutFuer(ctx, anfrage, { now }) → { url } (Session auf ctx.einstellungen.stripeAccountId, mode:'payment', ein Posten Aufenthalt <Unterkunft> <von>–<bis>, Rechnung <Nr> Betrag summen.bruttoCent, payment_method_types ['card','sepa_debit'] wenn SEPA im Status aktiv sonst ['card'], locale:'de', expires_at +30 min, metadata:{wkUk:'1', anfrageId}, payment_intent_data.metadata gleich, success_url/cancel_url = Buchungsseite …/unterkunft/<siteKey>/buchung/<id>?t=<token>&wk_zahlung=ok|abbruch, idempotencyKey:'uk-zahlung-'+anfrageId+'-'+minute), speichert stripeSessionId; schliesseAbWennBezahlt(ctx, anfrage, { now }) → anfrage (Session holen, bei complete+paid → markiereBezahlt(via:'stripe')); erstatte(...) echt mit webklar:'unterkunft'.
    • unterkunftWebhook.verarbeiteUkEreignis(event, now): checkout.session.completed (paid) / async_payment_succeeded → markiereBezahlt; charge.refunded → erstattetCent nachtragen; idempotent.
  • Step 1: Failing tests (Fake-Stripe): GET buchung falsches Token und unbekannte ID → identisch 404; gültig → Vertrag-Antwort, zahlarten ohne online, wenn Stripe fehlt; POST buchung vor Ort → {status:'gebucht', rechnungNummer:'RE-2026-0001'}; Review Focus 4: zweites POST → gleiche Antwort, keine zweite Rechnung; GET danach zeigt status:'gebucht' und rechnungNummer; abgelaufene Frist → 410 frist_abgelaufen; online → Antwort enthält zahlungUrl vom Fake-Stripe; Webhook checkout.session.completed mit metadata.wkUk → bezahlt + G20 im Stub-Protokoll; Buchungsseite mit wk_zahlung=ok schließt ohne Webhook selbst ab; Webhook zweimal → ein Übergang.

  • Step 2: FAIL.

  • Step 3: Implementieren.

  • Step 4: PASS; test/shopWebhook*.test.js und Erstattungs-Tests des Shops weiter grün.

  • Step 5: Commit Buchungskalender: Buchungsseite, Stripe-Zahlung und Webhook.

Task 8: Kunden-API (Vermieter)

Files:

  • Modify: server/routes/unterkunft.js
  • Create: server/services/unterkunft/verwaltung.js (Validierung + Speichern von Einstellungen, Unterkünften, Preisen, Sperren; Stripe-Verbinden über shop/connect.js kontoAnlegen/onboardingLink mit idempotencyKey 'uk-konto-'+projectId)
  • Test: test/unterkunftVerwaltung.test.js

Interfaces:

  • Consumes: Tasks 1–4, siteFeatures.assertOwnProject, writeLimiter.createWriteLimiter, shop/connect.js; PDF-Download mit demselben 501-Fallback wie Task 7, bis Task 19 gemergt ist.

  • Produces: alle Kunden-Endpunkte laut Vertrag außer anfrage-funktion* (Task 11). Validierung verwaltung.js: pruefeEinstellungen(eingabe) → {werte}|{fehler}, pruefeUnterkunft, pruefePreise (Staffeln aufsteigend, erste abNaechten:1, max 6 Staffeln, Preise 100–100000 Cent, Rabatt 0–50, Frist 1–14, Stornobedingungen 20–5000 Zeichen), pruefeSperre, pruefeIcalUrl (nur https:, max 500 Zeichen). rechnungsPraefix nur änderbar, solange rechnungZaehler leer.

  • Step 1: Failing tests: fremdes Projekt → 403; Einstellungen ohne Steuernummer und ohne USt-ID → 400 felder.steuernummer; Präfix nach erster Rechnung ändern → 409; Unterkunft anlegen/sortieren/deaktivieren; Preise mit Staffel abNaechten:2 als erste → 400; Kalender liefert Sperren mit notiz und Anfragen mit anfrageNummer; Sperre über gebuchte Nacht → 409 zeitraum_belegt; Review Focus 5 über POST /anfragen/:id/freigeben (leere Posten, Summe ≤ 0, Text 201 Zeichen, 31 Posten, ust 7 bei Kleinunternehmer → je 400, Anfrage bleibt neu); GET /anfragen/:id enthält vorschlag; POST /preisvorschau = berechnePreis; POST /stripe/vorhandenes-konto übernimmt shopConfig.stripeAccountId desselben Projekts, ohne eins → 409; GET /rechnungen/:id.pdf fremde Rechnung → 404.

  • Step 2: FAIL.

  • Step 3: Implementieren.

  • Step 4: PASS.

  • Step 5: Commit Buchungskalender: Verwaltungs-API für Vermieter.

Task 9: iCal-Import und -Export

Files:

  • Create: server/services/unterkunft/ical.js, server/services/unterkunft/icalAbgleich.js
  • Modify: server/services/unterkunft/jobs.js (viertelstundenJob → gleicheAlleAb(now)), server/routes/unterkunftSeite.js (Export-Route)
  • Test: test/unterkunftIcal.test.js (Fixtures test/fixtures/ical/booking.ics, airbnb.ics, tzid.ics, gefaltet.ics, fehlerseite.html)

Interfaces:

  • Produces:

    • ical.js (rein): parseIcs(text, { heute, horizontTage }) → { ereignisse:[{von,bis,uid}] } | throws Error('kein_ics'); baueIcs({ name, ereignisse:[{uid,von,bis}] , jetzt }) → string (CRLF, Zeilen ≤ 75 Oktette gefaltet, PRODID:-//WEBklar//Buchungskalender//DE, SUMMARY:Belegt, DTSTART;VALUE=DATE).
    • icalAbgleich.js: holeIcs(url, { fetch, now }) → string (nur https:, DNS-Auflösung → Abbruch bei privaten/loopback/link-local Adressen v4+v6, Timeout icalTimeoutMs, Abbruch über icalMaxBytes, max 3 Weiterleitungen je erneut geprüft); gleicheAb(unterkunft, importEintrag, { now, fetch }) → { ok, anzahl }|{ ok:false, fehler }; gleicheAlleAb(now); nach icalFehlerStunden ohne Erfolg mails.anVermieter('K33', …) höchstens alle 24 h (ukEinstellungen.icalFehlerMailAt).
    • Export: GET /unterkunft/ical/:token.ics → text/calendar; charset=utf-8, Cache-Control: no-store; Ereignisse = Buchungen gebucht|bezahlt, Reservierungen freigegeben mit Frist, manuell-Sperren; nie ical:*; UID = <anfrageId|sperreId>@project.webklar.com; unbekanntes Token → 404.
  • Step 1: Failing tests (Review Focus 3): Booking-Fixture → Ereignisse mit DATE-Werten; DTSTART:20261110T140000Z → 2026-11-10; TZID=Europe/Berlin:20261110T000000 → 2026-11-10; fehlendes DTEND → 1 Nacht; STATUS:CANCELLED ignoriert; gefaltete SUMMARY stört nicht; Ereignisse vor gestern/hinter Horizont fallen weg; HTML → kein_ics; holeIcs('http://…') → abgelehnt; https://127.0.0.1/…, https://10.0.0.5/, https://[::1]/ → abgelehnt; zu große Antwort → abgelehnt; gleicheAb Erfolg ersetzt alte ical:<id>-Sperren (neue zuerst), Fehler lässt alte stehen und setzt fehler; K33 erst nach 6 h, dann höchstens 1× je 24 h; Export enthält Buchung + manuelle Sperre, nicht die importierte, Zeilen mit CRLF.

  • Step 2: FAIL.

  • Step 3: Implementieren (DNS über node:dns/promises lookup(host,{all:true}); fetch injizierbar für Tests).

  • Step 4: PASS.

  • Step 5: Commit Buchungskalender: iCal-Import und -Export.

Task 10: Fristen-, Aufräum- und Löschjobs

Files:

  • Modify: server/services/unterkunft/jobs.js, server/services/retention.js (Uk-Zweig analog Shop), server/services/customerAdmin.js (~Z.299-305: disableCustomerFeatures auch unterkunft, schliesseUkVonKunde)
  • Create: server/services/unterkunft/aufbewahrung.js
  • Test: test/unterkunftAufbewahrung.test.js

Interfaces:

  • Produces: aufbewahrung.js: anonymisiere(now, { deckel }) → { anfragen:n, gaeste:n } — ipHash leeren nach ipHashTage; neu älter als neuVerfallTage → abgelaufen (ohne Mail); Anfragen ohne Buchung (abgelehnt|abgelaufen) älter als unverbindlichMonate → gast, nachrichtVermieter leeren, anonymisiertAt; gebuchte Aufenthalte 3 Jahre nach Jahresende der Abreise → gast leeren (Rechnungsadresse und ukRechnungen bleiben); ukRechnungen 8 Jahre nach Jahresende löschen. loescheUkDaten({ featureId, deckel }) (90 Tage nach Abschalten, Marker in portalFeatures.adminNote wie Shop). schliesseUkVonKunde(customerId).

  • Step 1: Failing tests: je Frist ein Dokument knapp davor und knapp danach (now fest); Deckel begrenzt Anzahl; Rechnung bleibt nach Gast-Anonymisierung lesbar und PDF-Schnappschuss unverändert; Kundenlöschung schaltet Funktion ab.

  • Step 2: FAIL. Step 3: Implementieren (Tagesjob ruft anonymisiere + loescheUkDaten für abgeschaltete). Step 4: PASS; test/retention*.test.js weiter grün.

  • Step 5: Commit Buchungskalender: Aufbewahrung und Löschfristen.

Task 11: Funktion bestellen „auf Anfrage“, Freischaltung, Ticket

Files:

  • Create (ersetzt Stubs aus Task 5): server/services/unterkunft/anfrageFunktion.js, server/services/unterkunft/freischaltung.js, server/services/unterkunft/ticketDetails.js
  • Modify: server/routes/unterkunft.js (anfrage-funktion*), server/routes/team/unterkunft.js
  • Test: test/unterkunftAnfrageFunktion.test.js

Interfaces:

  • Produces:

    • anfrageFunktion.js (Kopie-Muster shop/anfrage.js, Status in ukEinstellungen.anfrage*, Sperre uk-anfrage:<projectId>): ARTEN = ['ferienwohnung','monteurzimmer','pension','sonstiges'], anfrageOffenePunkte({customerId,projectId}) → string[], stelleAnfrage({customerId,projectId,eingabe:{art,einheiten(1–50),portale:boolean,nachricht},person,now}) → T22 ans Team, K34 an Kunden (über sendAdminNotice/sendCustomerMail direkt; Vorlagen kommen aus Task 20 — Test prüft nur den Aufruf mit Template-ID), zieheZurueck, entscheideAnfrage({…, entscheidung, begruendung}), anfrageAnsicht(einstellungen).
    • freischaltung.js: freischaltbar(doc) → boolean (≥ 1 aktive Unterkunft mit preiseVollstaendig und Stornobedingungen; Einstellungen firma, strasse, plz, ort, email und (steuernummer|ustId); fail-closed).
    • ticketDetails.js: ticketDetails(ctx) → string (siteKey, Snippet <div data-webklar-unterkunft="SITEKEY" data-api="https://project.webklar.com"></div><script src="https://project.webklar.com/embed/unterkunft.js" defer></script>, Schritte aus Spec §9, Verweis docs/funktionen/unterkunft-einbau.md).
    • Team: GET /api/team/unterkunft/:customerId → je Projekt { projectId, name, state, anfrage, stripe, unterkuenfte:n, offeneAnfragen:n }; POST …/anfrage.
  • Step 1: Failing tests: Anfrage nur bei state off; doppelte Anfrage → 409; Entscheidung annehmen → anfrageOffenePunkte leer; freischaltbar false ohne Preise, true mit vollständigen Daten, false bei Lesefehler; Ticket-Details enthalten den siteKey.

  • Step 2: FAIL. Step 3: Implementieren. Step 4: PASS.

  • Step 5: Commit Buchungskalender: Bestellung auf Anfrage, Freischaltung und Ticket.


Strang 2: Oberfläche (dev-uk-ui, WOMS-Worktree ticket.webklar.com/dev-unterkunft)

Alle Portal-Module nach public/shop.js-Muster: public/buchungen.js (initBuchungen(rootEl, { projectId }), REITER), Reiter-Module public/buchungen-tab-*.js mit render(panel, ctx), DOM-Helfer aus public/shop-dom.js wiederverwenden (h, request, fehlerKarte, infoKarte), Stile public/buchungen.css. Texte deutsch, Du-Form. Tests: Quelltext-/fakeDom-Tests nach Muster test/shop*.test.js mit test/support/fakeDom.js; API mit request-Attrappe gemäß Vertrag.

Task 12: Grundgerüst + Reiter „Anfragen“

Files:

  • Modify: public/buchungen.js (Platzhalter ersetzen)

  • Create: public/buchungen-tab-anfragen.js, public/buchungen-posten.js (Posten-Editor, reine Funktionen summeLive(posten, steuerart) = Spiegel von summiere), public/buchungen.css

  • Test: test/buchungenUi.test.js

  • Step 1: Failing tests: REITER = anfragen, kalender, unterkuenfte, rechnungen, einstellungen (Labels „Anfragen“, „Kalender“, „Unterkünfte & Preise“, „Rechnungen“, „Einstellungen“); bei state off statt Reitern Anfrage-/Bestellkarte (Task 14); Anfragenliste rendert Nummer, Zeitraum „12.–30.11.2026 · 18 Nächte“, Gast, Status-Pille, Betrag; Detail zeigt Vorschlag-Posten editierbar, Summe aktualisiert sich (summeLive = Server-Werte aus Task 2 Beispielen: 40650); „Freigeben“ sendet {posten,fristTage,nachricht}; 409-Antwort zeigt „Der Zeitraum ist inzwischen belegt.“; gebuchte Anfrage zeigt Rechnung-Link, „Als bezahlt (vor Ort) markieren“, „Stornieren“ (Dialog mit Grund + Erstattungsbetrag nur bei Online-Zahlung).

  • Step 2: FAIL. Step 3: Implementieren. Step 4: PASS.

  • Step 5: Commit Buchungskalender: Portal-Bereich und Anfragen.

Task 13: Reiter „Kalender“ und „Unterkünfte & Preise“

Files:

  • Create: public/buchungen-tab-kalender.js, public/buchungen-tab-unterkuenfte.js

  • Test: test/buchungenUi.test.js (erweitern)

  • Step 1: Failing tests: Monatsraster beginnt Montag; Bereiche gefärbt nach quelle (Klassen ist-gebucht, ist-reserviert, ist-ical, ist-manuell) mit Legende; zwei Klicks auf freie Tage öffnen „Zeitraum sperren“ mit Notiz → POST sperren; Klick auf manuelle Sperre bietet „Sperre aufheben“; Unterkunft-Formular mit allen Feldern aus Spec §3; Preis-Editor mit Staffelzeilen (+/−), Beispielrechnung „7 Nächte, 2 Personen = 406,50 €“ über POST /preisvorschau; iCal-Bereich: Import hinzufügen (Name, URL), Status „zuletzt abgeglichen …“ bzw. Fehlertext, Export-Link mit „Kopieren“, Kurzanleitung Booking.com („Unterkunft → Tarife & Verfügbarkeit → Kalender synchronisieren → Kalender importieren → Link einfügen“) und Airbnb, „Link erneuern“ mit Bestätigung.

  • Step 2: FAIL. Step 3: Implementieren. Step 4: PASS.

  • Step 5: Commit Buchungskalender: Kalender und Unterkünfte im Portal.

Task 14: Rechnungen, Einstellungen, Funktionen-Kachel, Anfrageformular

Files:

  • Create: public/buchungen-tab-rechnungen.js, public/buchungen-tab-einstellungen.js, public/buchungen-anfrage.js (Formular der Funktion, Muster shop-anfrage.js)

  • Modify: public/funktionen.js (Kachel „Buchungskalender“ nach shopTile ~Z.216-260, sichtbar nur bei unterkunft.verfuegbar; Wunschprojekt-Klick wie Z.405 auf buchungen-body)

  • Test: test/buchungenUi.test.js, test/funktionen*.test.js falls Pins

  • Step 1: Failing tests: Rechnungsliste mit PDF-Link /api/unterkunft/rechnungen/<id>.pdf?projectId=; Einstellungen: Pflichtfelder markiert, Präfix-Feld gesperrt mit Hinweis, sobald Rechnungen existieren; Stripe-Karte mit Zuständen nicht verbunden / Onboarding offen / aktiv und Knopf „Vorhandenes Webshop-Konto verwenden“ nur wenn API es meldet; Schalter „Online-Zahlung anbieten“ nur bei aktivem Konto; Knopf „Mustertext übernehmen“ füllt Stornobedingungen (Text aus docs/funktionen/unterkunft-storno-muster.md, Task 21 — bis dahin Konstante in buchungen-tab-einstellungen.js, Task 22 gleicht ab); Kachel zeigt Preis „199 €“ und Zustände wie Shop-Kachel; Anfrageformular sendet {art,einheiten,portale,nachricht}.

  • Step 2: FAIL. Step 3: Implementieren. Step 4: PASS.

  • Step 5: Commit Buchungskalender: Rechnungen, Einstellungen und Funktionen-Kachel.

Task 15: WOMS

Files (WOMS-Repo, Worktree von origin/test als feature/unterkunft, git ls-files | grep -c node_modules prüfen, nur Dateien einzeln stagen):

  • Modify: src/lib/funktionenView.js (FUNKTIONEN Eintrag { key:'unterkunft', label:'Buchungskalender', bestaetigen:{…wie shop} }, aktionen(), siteKey, Hilfen für Anfrage-Zeilen generalisieren statt kopieren: anfrageZeilen(eintrag, art)), src/lib/teamApi.js (getUnterkunft(customerId), entscheideUnterkunftAnfrage(customerId, body)), src/components/customer/CustomerFunktionen.jsx (Panel UnterkunftPanel nach ShopPanel-Muster: Anfrage-Zeilen, Annehmen/Ablehnen, Stripe-Status, Zahl Unterkünfte/offene Anfragen), scripts/setup-legal.mjs (DOCUMENTS anhängen: leistungsbeschreibung-unterkunft „Leistungsbeschreibung Buchungskalender“, avv-unterkunft „AVV Buchungskalender“, liveUrl https://project.webklar.com/rechtliches/<key>.html → sort 13 und 14)

  • Test: tests/unit/funktionenView.test.mjs, tests/setup-legal.test.mjs (Länge 14, letzter Eintrag avv-unterkunft)

  • Step 1: Failing tests (WOMS node --test mit node_modules-Symlink auf ../build/node_modules): FUNKTIONEN enthält unterkunft; aktionen() für unterkunft wie Shop; anfrageZeilen liefert für beide Arten die richtigen Zeilen; setup-legal Länge 14.

  • Step 2: FAIL. Step 3: Implementieren. Step 4: PASS; Vite-Build: NODE_OPTIONS=--max-old-space-size=1200 node node_modules/vite/bin/vite.js build --outDir /tmp/claude-0/uk-woms-build läuft durch.

  • Step 5: Commit (WOMS) Buchungskalender: Funktionsanfragen und Rechtliches-Karten.


Strang 3: Widget (dev-uk-widget, Donau-Repo /root/reports/donau-home-website)

Task 16: Widget public/embed/unterkunft.js

Files:

  • Create: public/embed/unterkunft.js, public/embed/unterkunft-demo.html
  • Test: test/unterkunftEmbedStatic.test.js (Muster shopEmbedStatic.test.js), test/unterkunftKalenderLogik.test.js

Interfaces:

  • Consumes: öffentlicher Vertrag GET /:siteKey, GET /:siteKey/belegung, POST /:siteKey/anfrage.

  • Produces: IIFE; Einbau <div data-webklar-unterkunft="SITEKEY" data-api="https://project.webklar.com"></div>. Die reine Kalenderlogik steht im Widget in einem Objekt WKU_LOGIK und wird nur dann als globalThis.__wkuLogik bereitgestellt, wenn document.currentScript?.dataset.test === '1'; der Test lädt die Datei per node:vm mit test/support/fakeDom.js und diesem Attribut. Bei {aktiv:false} oder Ladefehler zeigt das Widget nur „Anfragen gerne per Telefon oder E-Mail.“ Funktionen: monatsRaster(jahr, monat) → [{tag|null}] (Montag zuerst), nachtBelegt(tag, belegt) → boolean, waehle(zustand, tag, belegt, mindestNaechte) → zustand (erster Klick Anreise; zweiter Klick nach Anreise → Abreise, wenn keine belegte Nacht dazwischen, sonst neue Anreise; Klick vor Anreise → neue Anreise), zusammenfassung(von, bis) → '12.–30. Nov. · 18 Nächte'.

  • Gestaltung: N1 (@layer wku, .wku-, Variablen). Zwei Spalten ab 760 px (Container-Query), Kalender links (1 Monat, 2 Monate ab 1100 px), Formular rechts: Unterkunft (nur bei > 1), Personen (1…max), Firma (opt.), Name, E-Mail, Telefon (opt.), Nachricht (opt.), Honeypot kontakt_url (versteckt, tabindex=-1, autocomplete=off), Datenschutzhinweis mit Link anbieter.datenschutzUrl, Knopf „Anfrage senden“. Ohne Zeitraum → Meldung „Bitte wählen Sie Anreise und Abreise im Kalender.“. Erfolg: „Danke! Ihre Anfrage A-XXXX ist eingegangen. Sie erhalten gleich eine Bestätigung per E-Mail.“ Tastatur: Pfeile bewegen Fokus, Enter/Leertaste wählt; aria-label „Dienstag, 10. November 2026, frei“ / „belegt“; aria-live für Zusammenfassung. Alle Texte per textContent.

  • Step 1: Failing tests: Statik: kein innerHTML mit Variablen, kein eval, API-Pfad /api/public/unterkunft/v1/, Präfix .wku-, @layer wku; Logik: monatsRaster(2026,11) beginnt mit 6 Leerfeldern (1.11.2026 ist Sonntag); waehle über belegte Nacht → neue Anreise; Abreise auf belegter Anreisetag-Grenze (Nacht davor frei) erlaubt; unter Mindestnächten → Hinweis-Zustand {fehler:'mindestnaechte'}; zusammenfassung('2026-11-12','2026-11-30') === '12.–30. Nov. · 18 Nächte', über Monatsgrenze '28. Nov. – 3. Dez. · 5 Nächte'.

  • Step 2: FAIL. Step 3: Implementieren. Step 4: PASS.

  • Step 5: Commit Buchungskalender: Website-Widget mit Kalender und Anfrageformular.

Task 17: Standalone-Seiten und Buchungsseite

Files:

  • Modify: server/routes/unterkunftSeite.js (GET /unterkunft/:siteKey → Widget auf neutraler Seite; GET /unterkunft/:siteKey/buchung/:id → Buchungsseite; Export-Route kommt aus Task 9 — Reihenfolge ical zuerst)

  • Create: public/unterkunft-seite.html (Platzhalter __SITEKEY__, __ANSICHT__ wie shop-seite.html, CSP script-src 'self', frame-ancestors 'none', noindex), public/unterkunft-buchung.js, public/unterkunft-buchung.css

  • Test: test/unterkunftSeite.test.js

  • Step 1: Failing tests: Routen liefern HTML mit escaptem siteKey, CSP-Header, Cache-Control: no-store; ungültiger siteKey → 404; unterkunft-buchung.js (fakeDom, Vertrag-Attrappe): Status freigegeben zeigt alle Pflichtangaben aus Spec §6 in dieser Reihenfolge direkt über dem Knopf — Anbieter, Unterkunft + Zeitraum mit Check-in/-out, Personen, Posten, Gesamtpreis (inkl. USt-Ausweis bzw. „Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.“), Zahlarten, Stornobedingungen, „Ein Widerrufsrecht besteht nicht (§ 312g Abs. 2 Nr. 9 BGB).“, Datenschutzhinweis, „Reserviert bis …“; Knopftext exakt „Zahlungspflichtig buchen“; Option „online“ fehlt, wenn nicht in zahlarten; nach Buchung: „Ihre Buchung ist bestätigt“ + Rechnung-Link + bei unbezahlt online „Jetzt online bezahlen“; abgelaufen → „Die Reservierung ist abgelaufen. Bitte fragen Sie erneut an.“; 404 → „Dieser Link ist ungültig.“; wk_zahlung=abbruch → Hinweis „Zahlung abgebrochen – Sie können jetzt oder vor Ort bezahlen.“

  • Step 2: FAIL. Step 3: Implementieren (Akzentfarbe aus Antwort als CSS-Variable). Step 4: PASS.

  • Step 5: Commit Buchungskalender: Buchungsseite für Gäste.

Task 18: Einbau auf der Donau-Home-Seite

Files (Repo /root/reports/donau-home-website, Branch main, auf 5f8465a; vorher git status sauber und git log -1 prüfen — hat der Inhaber gepusht, auf origin/main aufsetzen, nie überschreiben):

  • Modify: index.html Abschnitt #anfrage (Z.~555-590) und Skript (Z.~668-690): altes Formular und Datums-Skript entfernen; linke Spalte = Kopf + Einleitung „Wählen Sie Ihren Zeitraum im Kalender, nennen Sie die Zahl der Personen – wir melden uns mit Verfügbarkeit und Preis.“ + Kontaktliste; rechte/ganze Breite = <div data-webklar-unterkunft="SITEKEY_FOLGT" data-api="https://project.webklar.com"></div> + Script-Tag. Da das Widget selbst zweispaltig ist (Kalender links, Formular rechts), wird .anfrage zu: Kopf + Kontakt oben (zweispaltig: Text links, Kontakt rechts), darunter das Widget über volle Breite — so steht der Kalender links.

  • Modify: CSS-Block /* Anfrage */: Variablen --wku-akzent: var(--eiche), --wku-hintergrund: var(--leinen), --wku-text: var(--tinte), --wku-rand: var(--linie), --wku-radius: 10px, --wku-schrift: 'Figtree', system-ui, sans-serif (genaue Token-Namen aus dem :root der Seite übernehmen).

  • Fallback ohne aktives Widget liefert das Widget selbst (Task 16); ohne JS bleibt die Kontaktliste sichtbar.

  • Step 1: Mit lokalem Widget-Demo prüfen: public/embed/unterkunft-demo.html um eine Variante „Donau“ mit den Variablen ergänzen; Screenshot 390 px und 1280 px per Playwright nach /root/reports/buchungskalender-2026-10-07/donau/.

  • Step 2: index.html ändern; Seite lokal statisch öffnen (Widget mit API-Attrappe über page.route), Screenshots wie oben; keine horizontale Scrollleiste bei 390 px.

  • Step 3: Commit im Donau-Repo Anfrage: Buchungskalender statt Anreise/Abreise-Feldern (Autor Kenso Grimm). Kein Push. siteKey wird beim Livegang eingesetzt (Livegang-Notiz).


Strang 4: Mails, PDF, Recht (dev-uk-mail)

Task 19: PDF-Writer und Rechnungs-PDF

Files:

  • Create: server/services/unterkunft/pdf.js, server/services/unterkunft/rechnungPdf.js
  • Test: test/unterkunftPdf.test.js

Interfaces:

  • Produces:
    • pdf.js: class PdfDoc { constructor({ breite=595.28, hoehe=841.89 }); seite(); text(x, y, str, { groesse=10, fett=false, rechts=false }); linie(x1,y1,x2,y2,{dicke=0.5}); breite(str, groesse, fett) → number; umbrechen(str, maxBreite, groesse) → string[]; toBuffer() → Buffer } — Standardschriften Helvetica/Helvetica-Bold, WinAnsiEncoding, Breitentabelle der Helvetica-AFM für 32–255 eingebettet, Zeichen außerhalb WinAnsi → ?; € = 0x80, Umlaute/ß nach CP1252; Strings PDF-escaped (\, (, )); xref mit korrekten Offsets; kein Kompressionsfilter.
    • rechnungPdf.js: rechnungPdf(inhalt, { art, nummer, datum }) → Buffer — Layout A4: Absenderzeile klein, Empfängeradresse, rechts Rechnungsnummer/Datum/Leistungszeitraum, Titel „Rechnung“ bzw. „Stornorechnung zu “, Leistung „Beherbergung , – ( Nächte,

      Personen)“, Posten-Tabelle (Text, Menge, Einzelpreis, Betrag), Summen (netto je Satz, USt je Satz, Gesamt) bzw. § 19-Satz, Zahlart („Zahlung vor Ort“ / „Online-Zahlung“), Fuß: Firma, Anschrift, Steuernummer/USt-ID, Bank (falls vorhanden). Seitenumbruch bei vielen Posten.

  • Step 1: Failing tests: Buffer beginnt %PDF-1.4, endet %%EOF\n; startxref-Wert zeigt auf xref; jeder xref-Offset zeigt auf n 0 obj; Text „Übernachtung“ als CP1252-Bytes enthalten; € als Byte 0x80; ( im Text escaped; 40 Posten → 2 Seiten (/Count 2); Rechnung enthält Nummer, „Gemäß § 19 UStG“ bei Kleinunternehmer, „USt 7 %“ bei Regel; gleicher inhalt → byte-gleiches PDF (deterministisch, kein Zeitstempel außer datum).
  • Step 2: FAIL. Step 3: Implementieren. Step 4: PASS; zusätzlich ein Beispiel-PDF nach /root/reports/buchungskalender-2026-10-07/beispiel-rechnung.pdf schreiben und mit pdftotext (falls vorhanden) oder Chrome prüfen.
  • Step 5: Commit Buchungskalender: PDF-Writer und Rechnungs-PDF.

Task 20: Mail-Vorlagen und Versand

Files:

  • Create: server/mail-templates/G16 Buchungsanfrage eingegangen.html, G17 Buchungsanfrage freigegeben.html, G18 Buchungsanfrage abgelehnt.html, G19 Buchungsbestätigung.html, G20 Zahlung eingegangen (Unterkunft).html, G21 Reservierung abgelaufen.html, G22 Buchung storniert.html, K31 Neue Buchungsanfrage.html, K32 Neue Buchung.html, K33 Kalenderabgleich fehlgeschlagen.html, K34 Anfrage Buchungskalender.html, T22 Neue Anfrage Buchungskalender.html
  • Modify: server/services/unterkunft/mails.js (Stub ersetzen, Signaturen aus Task 4 bleiben; _aufrufe bleibt als Testhilfe erhalten), server/mailer.js bzw. server/services/mailer.js (sendGuestMail({ …, attachments })), server/services/termine/outbox.js (beim Senden: anhang.art==='ukRechnung' → rechnungPdf erzeugen und als attachments:[{filename:'Rechnung-<nr>.pdf', content, contentType:'application/pdf'}] übergeben)
  • Test: test/unterkunftMails.test.js, test/mailTemplates.test.js (Pins), test/termineOutbox.test.js

Interfaces:

  • Produces: mails.anGast reiht über einreihen({ featureId, bookingId: anfrage.$id, art: 'uk-'+art, an: gast.email, fromName: einstellungen.firma, replyTo: einstellungen.email, template: art, values, blocks, anhang? }, now) ein; G19/G22 mit anhang:{art:'ukRechnung', rechnungId}. Platzhalter je Vorlage im Kopfkommentar; gemeinsame Werte: gast_name, firma, unterkunft, zeitraum („12.11.2026 – 30.11.2026“), naechte, personen, nummer, gesamt, posten_text (mehrzeilig), frist, buchungs_link, stornobedingungen, anbieter_kontakt, rechnung_nummer, erstattung, nachricht_vermieter. K-Mails an besitzerEmail mit portal_url = ${publicBaseUrl}/dashboard.html#buchungen?projekt=<id>.

  • Inhalt: G17 enthält Preis, Frist, Stornobedingungen, Knopf-Link „Jetzt verbindlich buchen“ und Satz „Erst mit dem Klick auf „Zahlungspflichtig buchen“ auf der Buchungsseite ist Ihre Buchung verbindlich.“; G19 (dauerhafter Datenträger) alle Angaben der Buchungsseite + Stornobedingungen vollständig + Hinweis kein Widerrufsrecht + bei online unbezahlt Bezahl-Link.

  • Step 1: Failing tests: templateIds() enthält alle 12 IDs; jede Vorlage rendert mit Beispielwerten ohne problems; G-Vorlagen in Sie-Form ohne WEBklar-Logo; Outbox-Eintrag mit anhang → gesendete Mail hat 1 Anhang Rechnung-RE-2026-0001.pdf beginnend mit %PDF; ohne anhang unverändert (Termine-Tests grün); anGast('G17',…) Mail hat fromName Firma und replyTo Vermieter-E-Mail.

  • Step 2: FAIL. Step 3: Implementieren. Step 4: PASS.

  • Step 5: Commit Buchungskalender: Mails an Gäste, Vermieter und Team.

Task 21: Rechtstexte und Doku

Files:

  • Create: docs/rechtliches/leistungsbeschreibung-unterkunft.md, docs/rechtliches/avv-unterkunft.md (Muster der Webshop-Texte; Fassung 2026-10-07; Inhalte: Leistung, 199 € einmalig ohne USt § 19, Laufzeit solange Website bei WEBklar, Pflichten Vermieter [Preise, Steuer, Stornobedingungen, Meldeschein, Kurtaxe], Rechnungsstellung im Auftrag § 14 Abs. 2 S. 4 UStG, Verfügbarkeit iCal ohne Gewähr, Stripe als eigener Vertrag), docs/rechtliches/unterkunft-datenschutz-6f-entwurf.md, docs/rechtliches/unterkunft-kanzlei-fragen.md (mind.: Button-Lösung per Mail-Link, Pflichtinfos Art. 246a, Widerrufsausschluss § 312g Abs. 2 Nr. 9 auch bei Monteursunterkünften für Unternehmer/Verbraucher, USt Endreinigung 7 %, Rechnung im Auftrag, Aufbewahrung 8 Jahre durch WEBklar, Rolle AVV, Haftung bei Doppelbuchung durch iCal-Verzögerung), docs/funktionen/unterkunft-datenschutz-absatz.md, docs/funktionen/unterkunft-storno-muster.md, docs/funktionen/unterkunft-einbau.md, docs/funktionen/unterkunft-livegang.md

  • Modify: scripts/build-legal-html.mjs (Modus unterkunft), danach node scripts/build-legal-html.mjs unterkunft → public/rechtliches/*-2026-10-07.html; PDFs über scripts/build-legal-pdfs.sh unterkunft (Host-Chrome; falls nicht vorhanden: im Livegang-Dokument als Schritt vermerken)

  • Test: test/legalDocs.test.js (featureLegalReady('unterkunft') true, wenn Dateien da)

  • Step 1: Texte schreiben (Kennzeichnung „Entwurf – Prüfung durch Kanzlei offen“ in LB/AVV-Kopf).

  • Step 2: HTML/PDF bauen, --check grün; featureLegalReady('unterkunft') Test PASS.

  • Step 3: Livegang-Notiz mit Schritten: (1) Inhaber prüft Texte; (2) node scripts/unterkunft-setup.mjs --check dann ohne; Termine-Setup für Outbox-Feld anhang; (3) stripe-products.mjs --anlegen (unterkunftSetup); (4) compose/.env UNTERKUNFT_ENABLED=pilot, UNTERKUNFT_PILOT_EMAILS=kenso.gri@gmail.com,<Mimi>; (5) Push feature/unterkunft:main (Portal), feature/unterkunft:test (WOMS) — nie unter eigenem Namen; (6) Rechtstexte in WOMS Rechtliches hochladen (macht Claude, Schlüssel /root/.appwrite-rechtliches.key); (7) Mimi im WOMS freischalten (activate), siteKey in Donau-index.html einsetzen, Donau pushen; (8) Pilot: Testanfrage → Freigabe → vor Ort buchen → Rechnung prüfen → stornieren; (9) Datenschutz § 6f in public/datenschutz.html übernehmen; (10) nach Kanzlei on.

  • Step 4: Commit Buchungskalender: Rechtstexte, Einbau- und Livegang-Notiz.


Abschluss

Task 22: Zusammenführen und Gesamtdurchlauf

Files:

  • Merge in feature/unterkunft: feature/uk-server, feature/uk-mail, feature/uk-widget, feature/uk-ui (in dieser Reihenfolge; Konflikte nur in server/routes/unterkunft.js, unterkunftSeite.js erwartet)

  • Create: test/ui/unterkunftTestServer.mjs (Muster shopTestServer.mjs), test/ui/unterkunft.ui.mjs

  • Step 1: Merges, danach alle test/unterkunft*.test.js, test/buchungen*.test.js und die in Tasks 1–21 genannten Bestands-Tests einzeln nacheinander grün.

  • Step 2: Abgleich: Stornomuster-Konstante in buchungen-tab-einstellungen.js = docs/funktionen/unterkunft-storno-muster.md; Stub-Reste (_aufrufe nur in Tests genutzt, 501-Fallback in Task 7 entfernen).

  • Step 3: UI-Durchlauf (free -m vorher): Widget auf Demo-Seite → Zeitraum + Anfrage → Portal „Anfragen“ → Posten ändern → Freigeben → Link aus Mail-Sink → Buchungsseite „vor Ort“ → „Zahlungspflichtig buchen“ → Rechnung-PDF abrufbar → Portal „bezahlt (vor Ort)“ → zweite Anfrage online → Fake-Stripe-Zahlung → bezahlt → Stornieren mit Teilerstattung. Screenshots nach /root/reports/buchungskalender-2026-10-07/ui/.

  • Step 4: Commit Buchungskalender: Zusammenführung und UI-Durchlauf.

Task 23: Gesamtreview

  • Step 1: Ein Review-Agent (sonnet) prüft den ganzen Branch gegen Spec + Review Focus; Funde beheben, je Fund ein Commit.
  • Step 2: Ledger-Abschluss, Memory-Notiz aktualisieren, dem Inhaber Livegang-Notiz verlinken.