diff --git a/docs/superpowers/plans/2026-10-07-buchungskalender.md b/docs/superpowers/plans/2026-10-07-buchungskalender.md new file mode 100644 index 0000000..33918de --- /dev/null +++ b/docs/superpowers/plans/2026-10-07-buchungskalender.md @@ -0,0 +1,491 @@ +# 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 `; 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 `, Nachricht deutsch, endet mit `Co-Authored-By: Claude Opus 5.5 `. 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:` (Belegung/Status) und `uk-rechnungsnr:`. +- 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- ../dev-uk- 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:`, 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`, `anVermieter(template:'K31'|'K32'|'K33', ctx, values={}) → Promise`; 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: }`, `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 `
`), `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=&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 –, Rechnung ` 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//buchung/?t=&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` = `@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:`-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:`): `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 `
`, 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/.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/.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 `
`. 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 = `
` + 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-.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=`. +- 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,`; (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. diff --git a/docs/superpowers/specs/2026-10-07-buchungskalender-design.md b/docs/superpowers/specs/2026-10-07-buchungskalender-design.md index 9ea7eb4..c57f789 100644 --- a/docs/superpowers/specs/2026-10-07-buchungskalender-design.md +++ b/docs/superpowers/specs/2026-10-07-buchungskalender-design.md @@ -291,3 +291,4 @@ Abschluss: Zusammenführen, Gesamttests, Review, UI-Durchlauf. - **N5:** Datenschutz-Abschnitt bleibt § 6f (6e ist für Öffnungszeiten reserviert, noch nicht live). - **N6:** Kein Textform-Nachweis (nicht in `TEXTFORM_KEYS`); Mimi wird im WOMS über „Freischalten“ (`action:'activate'`) ohne Zahlung freigeschaltet. +- **N7:** Kein Jahres-ZIP der Rechnungen (YAGNI); Einzel-PDF-Download je Rechnung. Bei Abschaltung Hinweis-Mail mit der Bitte, die PDFs herunterzuladen.