65 KiB
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/nullo. ä. 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>; nienpm test(alles parallel); UI-Läufe vorherfree -m, nur wenn „available“ > 1500 MB. - node_modules: Worktrees haben keins →
ln -s ../dev-termine/node_modules node_modules(Portal), untracked lassen, niegit add -A, immer Dateien einzeln stagen. Keine neuen Abhängigkeiten. - Commits: Autor
Kenso Grimm <kenso@webklar.com>, Nachricht deutsch, endet mitCo-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>. Kein Push (macht der Inhaber). - Funktionsschlüssel
unterkunft; UI-Name „Buchungskalender“; Portal-Hash#buchungen; Fassung Rechtstexte2026-10-07. - Schalter
UNTERKUNFT_ENABLED(off|pilot|on),UNTERKUNFT_PILOT_EMAILS(Komma,*= alle). - Stripe-Produkt
unterkunftSetup: 19900 Cent, Lookup-Keywebklar_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ßtdata. JSON-Felder als Text. - Datumswerte im Modul sind Kalendertage
YYYY-MM-DD(Berlin);bis= Abreisetag, exklusiv. Nie mit lokaler Zeitzone rechnen; „heute“ übertermine/zeit.jsheute(). - 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.frommit 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)ausserver/services/keyedLock.js; Schlüsselunterkunft:<unterkunftId>(Belegung/Status) unduk-rechnungsnr:<featureId>. - Statuswerte exakt:
neu,freigegeben,gebucht,bezahlt,abgelehnt,abgelaufen,storniert;zahlart∈online|vorort;bezahltVia∈stripe|vorort.
Review Focus
- 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.
- 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 gleichzeitigebucheVerbindlichderselben Anfrage ergeben genau eine Rechnung. → Tests in Task 4. - iCal-Formate aus der Praxis: Booking.com (
DTSTART;VALUE=DATE), Airbnb,DATE-TIMEmitZoderTZID, gefaltete Zeilen,STATUS:CANCELLED, fehlendesDTEND, HTML-Fehlerseite statt ICS (→ Fehler, alter Stand bleibt). → Tests in Task 9. - 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.
- 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 nebenSHOP_ENABLED~Z.32; Produkt nebenshopSetup~Z.131 und Export ~Z.653;collections~Z.612-636; neuer Blockunterkunftnebenshop~Z.884) - Create:
server/services/unterkunft/schalter.js,server/services/unterkunft/schema.js,scripts/unterkunft-setup.mjs - Modify:
test/support/schema.js(UNTERKUNFT_COLLECTIONSinschemaKeys) - 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, Musterconfig.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-OverrideAPPWRITE_COLLECTION_UK_*.schalter.js:unterkunftModus(): 'off'|'pilot'|'on',unterkunftFuerKunde(email): boolean.schema.js:export const UNTERKUNFT_COLLECTIONS(reine Daten, Mustershop/schema.js, Helferstr/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):offbei leer/unbekannt;pilot→ nurUNTERKUNFT_PILOT_EMAILS(case-insensitiv),*= alle;on→ alle. -
Step 2: Setup-Test schreiben (Muster
test/shopSetup.test.js):--checkgegen leeren Mock meldet alle 6 Collections als FEHLT und schreibt nichts; ohnechecklegt 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()ausscripts/analyse-setup.mjs),test/support/schema.jsergänzen. -
Step 5: Tests PASS; zusätzlich
test/shopSetup.test.jsundtest/shopSchalter.test.jsweiter 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,bisexklusiv),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 }] }(beikleinunternehmerust: [], 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:7beikleinunternehmer→ Fehler;ustProzentaußerhalb {0,7,19} → Fehler;mengekein 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≤naechtefü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[](nachsort),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 + Anfragengebucht|bezahlt+freigegebenmitfristAt > now; Ausschnitt auf [von,bis))istFrei(unterkunftId, von, bis, { ohneAnfrage, now }) → booleanueberschneidet(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
freigegebenmitfristAtin Zukunft blockiert; mitfristAtin Vergangenheit nicht;neu,abgelehnt,abgelaufen,storniertblockieren nicht;gebucht/bezahltblockieren. ohneAnfrage: idignoriert genau diese Anfrage.belegteBereicheliefert die Quelleicalfürquelle:'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'(Kopieshop/token.js, Label'uk-form', Minconfig.unterkunft.formMinSekunden),ipHashVon(ip)(Präfixuk-ip:).rechnungen.js:naechsteNummer(featureId, now) → 'RE-2026-0001'(Sperreuk-rechnungsnr:<featureId>, Zähler{jahr,n}inukEinstellungen.rechnungZaehler, Jahreswechsel → n=1; Präfix aus Einstellungen, StandardRE)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 inexport const _aufrufe = [].zahlung.js(Stub, Task 7 ersetzt):erstatte(ctx, anfrage, betragCent, { now }) → Promise<{ erstattetCent }>; Stub wirftUkFehler(409,'online_aus'), wennctx.einstellungen.stripeAccountIdfehlt, sonst{erstattetCent: betragCent}.ablauf.js(jede Änderung unterwithLocks(['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 ausneu; Posten =posten(überpruefePosten) oderberechnePreis;istFrei(…, {ohneAnfrage});fristAt = now + fristTage*24h;tokenHash;stornobedingungenText+rechtstexteFassungals Schnappschuss;ipHash= ''; Mail G17 (extra.token).lehneAb(ctx, anfrageId, { text, now }) → anfrage→ nur ausneu;ipHash= ''; Mail G18.bucheVerbindlich(ctx, anfrageId, token, { rechnungsadresse, zahlart, now }) → { anfrage, rechnung }→ Token prüfen (sonstnicht_gefunden), Statusfreigegeben(beigebucht|bezahltmit gleichem Token: bestehendes Ergebnis zurückgeben, idempotent),fristAt > now(sonstfrist_abgelaufen),zahlart==='online'nur miteinstellungen.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→ ausgebucht; idempotent beibezahlt; Mail G20 nur beivia==='stripe'.storniere(ctx, anfrageId, { grund, erstattungCent=0, now }) → { anfrage, stornoRechnung|null }→ ausfreigegeben|gebucht|bezahlt; Erstattung nur beibezahltVia==='stripe', 0 ≤ cent ≤ brutto − bereits erstattet, überzahlung.erstatte; Stornorechnung nur, wenn Rechnung existiert; Mail G22.laufeFristenAb(now) → { abgelaufen: number }→ allefreigegebenmitfristAt ≤ now→abgelaufen, Mail G21.UEBERGAENGE(Objekt Status → erlaubte Ziele),darfUebergang(von, nach) → boolean.
-
Step 1: Failing tests Rechnungen: erste Nummer
RE-2026-0001, zweiteRE-2026-0002; 10 parallelenaechsteNummer→ 10 verschiedene, lückenlos 1..10; Jahreswechsel (now 2027-01-01T00:30+01:00 Berlin) →RE-2027-0001; PräfixDH→DH-2026-0001;stelleAus(storno)negiert alleeinzelCentundsummen.bruttoCent, trägtbezugNummer. -
Step 2: Failing tests Ablauf (Appwrite-Mock,
mails._aufrufeprüfen):- Anfrage →
neu, Nummer-Format/^A-[A-Z2-9]{4}$/,_aufrufeenthält G16 und K31. - Anfrage in der Vergangenheit / unter Mindestnächten / mehr Personen als max / über belegte Nacht →
UkFehlermit passendem Code, nichts gespeichert. - Review Focus 2: Anfragen A und B überlappen;
gibFrei(A)ok;gibFrei(B)→ 409zeitraum_belegt, keine G17 für B. gibFreimit Posten[{text:'Pauschale',menge:1,einzelCent:50000,ustProzent:7}]→ summen.bruttoCent 50000.bucheVerbindlichfalsches Token → 404nicht_gefunden; nachfristAt→ 410; zweimal parallel mit richtigem Token → genau einukRechnungen-Dokument, beide Aufrufe liefern dieselberechnung.nummer.zahlart:'online'ohne Stripe-Konto → 409online_aus.laufeFristenAbsetztabgelaufenund schickt G21; Zeitraum danach frei.stornieregebuchter Anfrage → Stornorechnung mit eigener Nummer, G22;stornieremiterstattungCent> brutto → 400.darfUebergang('neu','bezahlt') === false.
- Anfrage →
-
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.unterkunftnebenshop~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_METADATAunterkunftSetup:'unterkunft-einrichtung'),scripts/stripe-products.mjs(KEYS),server/routes/abo.js(ORDERS, FEATURE_OF_ORDERunterkunft:'unterkunft', Text 'Buchungskalender (Einrichtung)'),server/routes/funktionen.js(Antwortunterkunft:{verfuegbar}),server/index.js(Mounts + Job-Registrierung),server/routes/shopWebhook.js(WeichewkUkvor Z.54,charge.refundednach Metadaten trennen,account.updatedauch an Uk),server/services/termine/outbox.js+server/services/termine/schema.js(optionales Feldanhang500, wird nur gespeichert/durchgereicht),public/dashboard.html(Sectionview-buchungenmit<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(exportiertverarbeiteUkEreignis(event, now), leer),public/buchungen.js(Platzhalterexport function initBuchungen(el){ el.textContent = 'Buchungskalender wird geladen …' }),docs/funktionen/unterkunft-api.md(API-Vertrag, s. u.) - Test:
test/unterkunftWiring.test.js(Mustertest/shopWiring.test.js), Anpassungtest/featureRegistry.test.js,test/legalDocs.test.js,test/termineOutbox.test.jsfalls Pins brechen
Interfaces:
-
Produces:
kontext.js:unterkunftPerSiteKey(siteKey, { now }) → ctx|null(30-s-Cache, Feature-DokumentfeatureKey:'unterkunft', nur stateactive; im Moduspilotnur, wenn die Besitzer-E-Mail in der Pilotliste steht; Modusoff→ 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)miteinzeln()-Wrapper; bei Modusoff→{aus:true}. Minutenjob ruft schonlaufeFristenAb(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-Routerrouter.use('/unterkunft', unterkunftTeamRoutes)(wie Shop, index.js:28);app.use(unterkunftSeiteRouter())vorexpress.static. Jobs: 80 s Start/60 s, 140 s Start/15 min, 210 s Start/24 h. - Register
config.features.unterkunft= Kopieshopmit:product:'unterkunftSetup',label {name:'Buchungskalender', kurz:'Buchungskalender', keine:…},priceLabel '199 €',setupEstimate,consent(itemsunterlagen,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. Importservices/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/anfrageBody{ 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? }(beionlinedirekt 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}.
- Öffentlich (
-
Step 1: Wiring-Test schreiben: Registereintrag vorhanden mit Produkt
unterkunftSetupundreturnHash '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.jsenthältbuchungenin VIEW_HASHES (Quelltext-Prüfung);dashboard.htmlenthältid="buchungen-body"; Shop-Webhook mitmetadata.wkUk:'1'ruftverarbeiteUkEreignisstatt 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/anfragelaut Vertrag. -
Step 1: Failing tests: unbekannter/inaktiver siteKey → 200
{aktiv:false}; aktiver → Unterkünfte nuraktiv, sortiert, ohnefeatureId/customerId; Belegung über 18 Monate hinaus → 400; Belegung fasst angrenzende Bereiche zusammen;POST anfragegültig → 201{nummer}+ Dokumentneu; Honeypotkontakt_urlgefüllt → 201 mit Schein-Nummer, nichts gespeichert; Form-Tokenzu_schnell→ ebenso stiller Erfolg; fehlende E-Mail → 400felder.email; mehr alsanfragenJeSiteStundeje Site → 429; CORS:OPTIONSvon erlaubter Origin (Domain des Projekts) → 204 mit Origin, fremde Origin bei POST → 403; GET mitAccess-Control-Allow-Origin: *. -
Step 2: FAIL.
-
Step 3: Implementieren (Helfer
route/schreibe/fehlerMit/erlaubteHerkunft/vorabantwortauspublicShop.jsin 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,rechnungPdfaus Task 19 (bis zum Merge:GET …/rechnung.pdfdarf in diesem Strang mit501antworten, wennserver/services/unterkunft/rechnungPdf.jsfehlt — dynamischer Import mit Fallback, Test prüft nur 200/501). -
Produces:
zahlung.js:checkoutFuer(ctx, anfrage, { now }) → { url }(Session aufctx.einstellungen.stripeAccountId,mode:'payment', ein PostenAufenthalt <Unterkunft> <von>–<bis>, Rechnung <Nr>Betragsummen.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.metadatagleich,success_url/cancel_url= Buchungsseite…/unterkunft/<siteKey>/buchung/<id>?t=<token>&wk_zahlung=ok|abbruch,idempotencyKey:'uk-zahlung-'+anfrageId+'-'+minute), speichertstripeSessionId;schliesseAbWennBezahlt(ctx, anfrage, { now }) → anfrage(Session holen, beicomplete+paid→markiereBezahlt(via:'stripe'));erstatte(...)echt mitwebklar:'unterkunft'.unterkunftWebhook.verarbeiteUkEreignis(event, now):checkout.session.completed(paid) /async_payment_succeeded→markiereBezahlt;charge.refunded→erstattetCentnachtragen; idempotent.
-
Step 1: Failing tests (Fake-Stripe):
GET buchungfalsches Token und unbekannte ID → identisch 404; gültig → Vertrag-Antwort,zahlartenohneonline, wenn Stripe fehlt;POST buchungvor Ort →{status:'gebucht', rechnungNummer:'RE-2026-0001'}; Review Focus 4: zweitesPOST→ gleiche Antwort, keine zweite Rechnung;GETdanach zeigtstatus:'gebucht'undrechnungNummer; abgelaufene Frist → 410frist_abgelaufen;online→ Antwort enthältzahlungUrlvom Fake-Stripe; Webhookcheckout.session.completedmitmetadata.wkUk→bezahlt+ G20 im Stub-Protokoll; Buchungsseite mitwk_zahlung=okschließt ohne Webhook selbst ab; Webhook zweimal → ein Übergang. -
Step 2: FAIL.
-
Step 3: Implementieren.
-
Step 4: PASS;
test/shopWebhook*.test.jsund 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 übershop/connect.jskontoAnlegen/onboardingLinkmitidempotencyKey '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). Validierungverwaltung.js:pruefeEinstellungen(eingabe) → {werte}|{fehler},pruefeUnterkunft,pruefePreise(Staffeln aufsteigend, ersteabNaechten:1, max 6 Staffeln, Preise 100–100000 Cent, Rabatt 0–50, Frist 1–14, Stornobedingungen 20–5000 Zeichen),pruefeSperre,pruefeIcalUrl(nurhttps:, max 500 Zeichen).rechnungsPraefixnur änderbar, solangerechnungZaehlerleer. -
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 StaffelabNaechten:2als erste → 400; Kalender liefert Sperren mitnotizund Anfragen mitanfrageNummer; Sperre über gebuchte Nacht → 409zeitraum_belegt; Review Focus 5 überPOST /anfragen/:id/freigeben(leere Posten, Summe ≤ 0, Text 201 Zeichen, 31 Posten, ust 7 bei Kleinunternehmer → je 400, Anfrage bleibtneu);GET /anfragen/:identhältvorschlag;POST /preisvorschau=berechnePreis;POST /stripe/vorhandenes-kontoübernimmtshopConfig.stripeAccountIddesselben Projekts, ohne eins → 409;GET /rechnungen/:id.pdffremde 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(Fixturestest/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(nurhttps:, DNS-Auflösung → Abbruch bei privaten/loopback/link-local Adressen v4+v6, TimeouticalTimeoutMs, Abbruch übericalMaxBytes, max 3 Weiterleitungen je erneut geprüft);gleicheAb(unterkunft, importEintrag, { now, fetch }) → { ok, anzahl }|{ ok:false, fehler };gleicheAlleAb(now); nachicalFehlerStundenohne Erfolgmails.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 = Buchungengebucht|bezahlt, Reservierungenfreigegebenmit Frist,manuell-Sperren; nieical:*;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:CANCELLEDignoriert; gefalteteSUMMARYstö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;gleicheAbErfolg ersetzt alteical:<id>-Sperren (neue zuerst), Fehler lässt alte stehen und setztfehler; 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/promiseslookup(host,{all:true});fetchinjizierbar 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:disableCustomerFeaturesauchunterkunft,schliesseUkVonKunde) - Create:
server/services/unterkunft/aufbewahrung.js - Test:
test/unterkunftAufbewahrung.test.js
Interfaces:
-
Produces:
aufbewahrung.js:anonymisiere(now, { deckel }) → { anfragen:n, gaeste:n }—ipHashleeren nachipHashTage;neuälter alsneuVerfallTage→abgelaufen(ohne Mail); Anfragen ohne Buchung (abgelehnt|abgelaufen) älter alsunverbindlichMonate→gast,nachrichtVermieterleeren,anonymisiertAt; gebuchte Aufenthalte 3 Jahre nach Jahresende der Abreise →gastleeren (Rechnungsadresse undukRechnungenbleiben);ukRechnungen8 Jahre nach Jahresende löschen.loescheUkDaten({ featureId, deckel })(90 Tage nach Abschalten, Marker inportalFeatures.adminNotewie 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+loescheUkDatenfür abgeschaltete). Step 4: PASS;test/retention*.test.jsweiter 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-Mustershop/anfrage.js, Status inukEinstellungen.anfrage*, Sperreuk-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 (übersendAdminNotice/sendCustomerMaildirekt; 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 mitpreiseVollstaendigund 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, Verweisdocs/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; Entscheidungannehmen→anfrageOffenePunkteleer;freischaltbarfalse 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 FunktionensummeLive(posten, steuerart)= Spiegel vonsummiere),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 stateoffstatt 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(Klassenist-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 €“ überPOST /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, Mustershop-anfrage.js) -
Modify:
public/funktionen.js(Kachel „Buchungskalender“ nachshopTile~Z.216-260, sichtbar nur beiunterkunft.verfuegbar; Wunschprojekt-Klick wie Z.405 aufbuchungen-body) -
Test:
test/buchungenUi.test.js,test/funktionen*.test.jsfalls 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 ausdocs/funktionen/unterkunft-storno-muster.md, Task 21 — bis dahin Konstante inbuchungen-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(FUNKTIONENEintrag{ 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(PanelUnterkunftPanelnachShopPanel-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 Eintragavv-unterkunft) -
Step 1: Failing tests (WOMS
node --testmitnode_modules-Symlink auf../build/node_modules):FUNKTIONENenthältunterkunft;aktionen()fürunterkunftwie Shop;anfrageZeilenliefert 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-buildlä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(MustershopEmbedStatic.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 ObjektWKU_LOGIKund wird nur dann alsglobalThis.__wkuLogikbereitgestellt, wenndocument.currentScript?.dataset.test === '1'; der Test lädt die Datei pernode:vmmittest/support/fakeDom.jsund 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.), Honeypotkontakt_url(versteckt,tabindex=-1,autocomplete=off), Datenschutzhinweis mit Linkanbieter.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-livefür Zusammenfassung. Alle Texte pertextContent. -
Step 1: Failing tests: Statik: kein
innerHTMLmit Variablen, keineval, 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 — Reihenfolgeicalzuerst) -
Create:
public/unterkunft-seite.html(Platzhalter__SITEKEY__,__ANSICHT__wieshop-seite.html, CSPscript-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): Statusfreigegebenzeigt 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 inzahlarten; 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.htmlAbschnitt#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.anfragezu: 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:rootder 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.htmlum 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 überpage.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 aufxref; jeder xref-Offset zeigt aufn 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; gleicherinhalt→ byte-gleiches PDF (deterministisch, kein Zeitstempel außerdatum). - Step 2: FAIL. Step 3: Implementieren. Step 4: PASS; zusätzlich ein Beispiel-PDF nach
/root/reports/buchungskalender-2026-10-07/beispiel-rechnung.pdfschreiben und mitpdftotext(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;_aufrufebleibt als Testhilfe erhalten),server/mailer.jsbzw.server/services/mailer.js(sendGuestMail({ …, attachments })),server/services/termine/outbox.js(beim Senden:anhang.art==='ukRechnung'→rechnungPdferzeugen und alsattachments:[{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.anGastreiht übereinreihen({ 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 mitanhang:{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 anbesitzerEmailmitportal_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 ohneproblems; G-Vorlagen in Sie-Form ohne WEBklar-Logo; Outbox-Eintrag mitanhang→ gesendete Mail hat 1 AnhangRechnung-RE-2026-0001.pdfbeginnend mit%PDF; ohneanhangunverändert (Termine-Tests grün);anGast('G17',…)Mail hatfromNameFirma undreplyToVermieter-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(Modusunterkunft), danachnode scripts/build-legal-html.mjs unterkunft→public/rechtliches/*-2026-10-07.html; PDFs überscripts/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,
--checkgrün;featureLegalReady('unterkunft')Test PASS. -
Step 3: Livegang-Notiz mit Schritten: (1) Inhaber prüft Texte; (2)
node scripts/unterkunft-setup.mjs --checkdann ohne; Termine-Setup für Outbox-Feldanhang; (3)stripe-products.mjs --anlegen(unterkunftSetup); (4) compose/.envUNTERKUNFT_ENABLED=pilot,UNTERKUNFT_PILOT_EMAILS=kenso.gri@gmail.com,<Mimi>; (5) Pushfeature/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.htmleinsetzen, Donau pushen; (8) Pilot: Testanfrage → Freigabe → vor Ort buchen → Rechnung prüfen → stornieren; (9) Datenschutz § 6f inpublic/datenschutz.htmlübernehmen; (10) nach Kanzleion. -
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 inserver/routes/unterkunft.js,unterkunftSeite.jserwartet) -
Create:
test/ui/unterkunftTestServer.mjs(MustershopTestServer.mjs),test/ui/unterkunft.ui.mjs -
Step 1: Merges, danach alle
test/unterkunft*.test.js,test/buchungen*.test.jsund 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 (_aufrufenur in Tests genutzt, 501-Fallback in Task 7 entfernen). -
Step 3: UI-Durchlauf (
free -mvorher): 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.