65 KiB
Buchungskalender (Funktion unterkunft) Implementation Plan
Hinweis (2026-10-07, Livegang-Vorbereitung): Die Mail-Nummern K31, K32 und T22 gehören seit dem Merge mit
origin/mainden Aufträgen (Auftragsbestätigung). Im Buchungskalender heißen die Mails jetzt K36 (Neue Buchungsanfrage, vorher K31), K37 (Neue Buchung, vorher K32) und T23 (Neue Anfrage Buchungskalender, vorher T22). Der folgende Text nennt noch die alten Nummern.
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.