Plan: Buchungskalender (23 Tasks, 4 parallele Stränge)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
668bc62c55
commit
18369a348c
2 files changed
+492
No files matched your search
@@ -0,0 +1,491 @@
|
||||
# Buchungskalender (Funktion `unterkunft`) Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Neue Portal-Funktion „Buchungskalender“ für Ferienwohnungen: Kalender-Widget mit Anfrage, Freigabe durch den Vermieter, verbindliche Buchung per Link, Rechnung im Namen des Vermieters, Zahlung über Stripe Connect oder vor Ort, iCal-Abgleich; erste Kundin Donau Home.
|
||||
|
||||
**Architecture:** Eigenes Modul `server/services/unterkunft/` nach dem Muster von Webshop (`shop`) und Terminplaner (`termine`). Ein serieller Kern (Strang 0) legt Schema, Konfiguration, reine Logik, Statusmaschine und **alle** Verdrahtungen in gemeinsam genutzten Dateien an; danach laufen vier Stränge parallel in eigenen Worktrees und berühren nur noch ihre eigenen Dateien (Ausnahmen sind je Task genannt). Abschluss: Zusammenführen, UI-Durchlauf, Review.
|
||||
|
||||
**Tech Stack:** Node 20/24 ESM, Express 4, Appwrite (node-appwrite, Server-Key), Stripe Connect (stripe ^19), nodemailer, Vanilla-ES-Module im Frontend, `node:test`, Playwright-core für UI.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-07-buchungskalender-design.md` (Abschnitte 1–16 + Nachträge N1–N6 in Abschnitt 17; N1–N6 gehen vor).
|
||||
**Code-Fakten:** `/tmp/claude-0/-root/b360f353-cb78-442e-ada0-d24927a23169/scratchpad/plan/fakten-portal.md` und `fakten-woms.md` (vor jedem Task die genannten Muster dort nachschlagen; bei Abweichung zählt der Code).
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- **Sicherheit (in JEDEN Agenten-Prompt kopieren):** Kein `rm -rf`, `rsync --delete`, `git reset --hard`, `git clean`, `git checkout -- .`, `git push`, kein `--force`; nichts außerhalb des eigenen Worktrees und des Scratchpads schreiben; keine destruktiven Proben; nie `/dev/null` o. ä. löschen oder ersetzen; nie Produktions-Container, `.env`, compose-Dateien oder Live-Datenbanken anfassen.
|
||||
- **Speicher:** Server hat wenig RAM. Jeder Node-Testlauf: `flock /tmp/claude-0/uk-test.lock nice timeout 600 node --max-old-space-size=1024 --test <datei>`; nie `npm test` (alles parallel); UI-Läufe vorher `free -m`, nur wenn „available“ > 1500 MB.
|
||||
- **node_modules:** Worktrees haben keins → `ln -s ../dev-termine/node_modules node_modules` (Portal), untracked lassen, **nie** `git add -A`, immer Dateien einzeln stagen. Keine neuen Abhängigkeiten.
|
||||
- **Commits:** Autor `Kenso Grimm <kenso@webklar.com>`, Nachricht deutsch, endet mit `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`. Kein Push (macht der Inhaber).
|
||||
- Funktionsschlüssel `unterkunft`; UI-Name „Buchungskalender“; Portal-Hash `#buchungen`; Fassung Rechtstexte `2026-10-07`.
|
||||
- Schalter `UNTERKUNFT_ENABLED` (`off`|`pilot`|`on`), `UNTERKUNFT_PILOT_EMAILS` (Komma, `*` = alle).
|
||||
- Stripe-Produkt `unterkunftSetup`: 19900 Cent, Lookup-Key `webklar_unterkunft_einrichtung_19900`, `priceLabel '199 €'`, `setupEstimate 'meist innerhalb von 1–2 Wochen'`.
|
||||
- Collections: `ukUnterkuenfte`, `ukPreise`, `ukSperren`, `ukAnfragen`, `ukRechnungen`, `ukEinstellungen` (Dokument-ID = `featureId`). Kein Feld heißt `data`. JSON-Felder als Text.
|
||||
- Datumswerte im Modul sind Kalendertage `YYYY-MM-DD` (Berlin); `bis` = Abreisetag, exklusiv. Nie mit lokaler Zeitzone rechnen; „heute“ über `termine/zeit.js` `heute()`.
|
||||
- Geld immer ganze Cent (Integer), Bruttobeträge.
|
||||
- Mail-IDs: G16–G22, K31–K34, T22 (vor Anlage gegen `server/mail-templates/` prüfen).
|
||||
- Gast-Mails: Sie-Form, Absender `config.smtp.from` mit Anzeigename = Firma des Vermieters, Reply-To = E-Mail des Vermieters. K-Mails: Du-Form.
|
||||
- Öffentliche Antworten enthalten nie Kunden-, Projekt-, Feature-, Anfrage-IDs (außer der Anfrage-ID im Buchungslink), keine E-Mails außer Anbieterdaten.
|
||||
- Sperren nur über `withLocks([key], fn)` aus `server/services/keyedLock.js`; Schlüssel `unterkunft:<unterkunftId>` (Belegung/Status) und `uk-rechnungsnr:<featureId>`.
|
||||
- Statuswerte exakt: `neu`, `freigegeben`, `gebucht`, `bezahlt`, `abgelehnt`, `abgelaufen`, `storniert`; `zahlart` ∈ `online|vorort`; `bezahltVia` ∈ `stripe|vorort`.
|
||||
|
||||
## Review Focus
|
||||
|
||||
1. **Sommerzeit/Datumsgrenzen:** Aufenthalt 2026-10-24 → 2026-10-26 (Zeitumstellung 25.10.) = genau 2 Nächte, Preis für 2 Nächte; Anreise heute (Berlin) ist erlaubt, gestern nicht. → Tests in Task 2 und Task 3.
|
||||
2. **Gleichzeitige Anfragen für dieselben Nächte:** Zwei Anfragen überlappen; Freigabe der ersten klappt, Freigabe der zweiten scheitert mit `409 zeitraum_belegt`, ohne Mail; zwei gleichzeitige `bucheVerbindlich` derselben Anfrage ergeben genau eine Rechnung. → Tests in Task 4.
|
||||
3. **iCal-Formate aus der Praxis:** Booking.com (`DTSTART;VALUE=DATE`), Airbnb, `DATE-TIME` mit `Z` oder `TZID`, gefaltete Zeilen, `STATUS:CANCELLED`, fehlendes `DTEND`, HTML-Fehlerseite statt ICS (→ Fehler, alter Stand bleibt). → Tests in Task 9.
|
||||
4. **Buchungslink erneut geöffnet / doppelt geklickt:** nach Buchung zeigt die Seite den Status statt erneut „buchen“; Doppelklick erzeugt keine zweite Rechnung; abgelaufener Link → klare Meldung, keine Buchung; falsches Token → 404 ohne Unterschied zu unbekannter ID. → Tests in Task 7.
|
||||
5. **Unsinnige Posten des Vermieters:** leere Postenliste, Gesamtsumme ≤ 0, Text leer/über 200 Zeichen, mehr als 30 Posten, ust ≠ 0 bei Kleinunternehmer → 400 mit Feldfehler, nichts gespeichert. → Tests in Task 2 und Task 8.
|
||||
|
||||
---
|
||||
|
||||
## Ablauf der Stränge
|
||||
|
||||
```
|
||||
Strang 0 (seriell): Task 1 → 2 → 3 → 4 → 5 Branch feature/unterkunft (dev-unterkunft)
|
||||
danach parallel, je eigener Worktree von feature/unterkunft:
|
||||
Strang 1 Server : Task 6 → 7 → 8 → 9 → 10 → 11 feature/uk-server (dev-uk-server)
|
||||
Strang 2 Oberfl. : Task 12 → 13 → 14 → 15 feature/uk-ui (dev-uk-ui) + WOMS feature/unterkunft
|
||||
Strang 3 Widget : Task 16 → 17 → 18 feature/uk-widget (dev-uk-widget) + Donau-Repo
|
||||
Strang 4 Mail/PDF : Task 19 → 20 → 21 feature/uk-mail (dev-uk-mail)
|
||||
Abschluss (seriell): Task 22 (Merge + UI-Durchlauf) → Task 23 (Gesamtreview)
|
||||
```
|
||||
|
||||
Stränge 2 und 3 bauen gegen die API-Verträge aus Task 5 (Routen + Antwortformen sind dort festgelegt) und testen mit Attrappen; echte Daten erst in Task 22. Worktree je Strang: `git -C /home/webklar/apps/kundenbereich.webklar.com/build worktree add -b feature/uk-<x> ../dev-uk-<x> feature/unterkunft` (Upstream danach mit `git branch --unset-upstream` lösen).
|
||||
|
||||
---
|
||||
|
||||
## Strang 0: Kern
|
||||
|
||||
### Task 1: Konfiguration, Schalter, Schema, Setup-Skript
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/config.js` (Env-Lesen neben `SHOP_ENABLED` ~Z.32; Produkt neben `shopSetup` ~Z.131 und Export ~Z.653; `collections` ~Z.612-636; neuer Block `unterkunft` neben `shop` ~Z.884)
|
||||
- Create: `server/services/unterkunft/schalter.js`, `server/services/unterkunft/schema.js`, `scripts/unterkunft-setup.mjs`
|
||||
- Modify: `test/support/schema.js` (`UNTERKUNFT_COLLECTIONS` in `schemaKeys`)
|
||||
- Test: `test/unterkunftSchalter.test.js`, `test/unterkunftSetup.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `config.unterkunft = { mode, pilotEmails, fassung:'2026-10-07', formMinSekunden:3, minAntwortMs:300, icalIntervallMin:15, icalMaxBytes:1_000_000, icalTimeoutMs:10_000, icalFehlerStunden:6, horizontMonate:18, maxPosten:30, postenTextMax:200, nachrichtMax:2000, anfragenJeSiteStunde:30, ipHashTage:30, unverbindlichMonate:6, neuVerfallTage:60, anonymisierenJahre:3, rechnungJahre:8, loeschTageNachAbschalten:90 }` (Werte bei jedem Zugriff aus Env, Muster `config.shop`).
|
||||
- `config.unterkunftSetup = { key:'unterkunftSetup', name:'Buchungskalender (Einrichtung)', amountCents:19900, lookupKey:'webklar_unterkunft_einrichtung_19900', oneTime:true, priceNickname }`.
|
||||
- `config.collections.{ukUnterkuenfte,ukPreise,ukSperren,ukAnfragen,ukRechnungen,ukEinstellungen}` mit Env-Override `APPWRITE_COLLECTION_UK_*`.
|
||||
- `schalter.js`: `unterkunftModus(): 'off'|'pilot'|'on'`, `unterkunftFuerKunde(email): boolean`.
|
||||
- `schema.js`: `export const UNTERKUNFT_COLLECTIONS` (reine Daten, Muster `shop/schema.js`, Helfer `str/int/bool/dt/index`).
|
||||
- `scripts/unterkunft-setup.mjs`: `export async function main({ check=false, client, log })`.
|
||||
|
||||
- [ ] **Step 1: Schalter-Test schreiben** (Muster `test/shopSchalter.test.js`): `off` bei leer/unbekannt; `pilot` → nur `UNTERKUNFT_PILOT_EMAILS` (case-insensitiv), `*` = alle; `on` → alle.
|
||||
- [ ] **Step 2: Setup-Test schreiben** (Muster `test/shopSetup.test.js`): `--check` gegen leeren Mock meldet alle 6 Collections als FEHLT und schreibt nichts; ohne `check` legt es alle an, zweiter Lauf legt nichts an; jede Collection hat die Attribute aus Spec §3 + N2, u. a.:
|
||||
- `ukUnterkuenfte`: featureId, customerId, projectId, name(120), beschreibung(500), maxPersonen int, mindestNaechte int, checkIn(5), checkOut(5), sort int, aktiv bool, icalImporte(4000), icalExportToken(64); Index featureId.
|
||||
- `ukPreise`: unterkunftId, featureId, staffeln(2000), reinigungCent, personenInklusive, aufpreisPersonNachtCent, direktrabattProzent, steuerart(20), ustUebernachtung, ustReinigung, fristTage, stornobedingungen(5000); Index unterkunftId.
|
||||
- `ukSperren`: unterkunftId, featureId, von(10), bis(10), quelle(60), notiz(200); Indizes unterkunftId, (unterkunftId,quelle).
|
||||
- `ukAnfragen`: featureId, customerId, projectId, unterkunftId, nummer(12), von, bis, personen int, status(12), gast(3000), rechnungsadresse(1500), posten(8000), summen(1000), nachrichtVermieter(2000), fristAt dt, tokenHash(64), zahlart(8), bezahltVia(8), bezahltAt dt, stripeSessionId(100), stripePaymentIntentId(100), erstattetCent int, stornoGrund(500), stornobedingungenText(5000), rechtstexteFassung(20), ipHash(16), freigegebenAt/gebuchtAt/abgelehntAt/abgelaufenAt/storniertAt dt, rechnungId(36), anonymisiertAt dt; Indizes featureId, unterkunftId, (unterkunftId,status), status.
|
||||
- `ukRechnungen`: featureId, anfrageId, art(8), nummer(24), datum(10), bruttoCent int, inhalt(20000); Indizes featureId, anfrageId, (featureId,nummer).
|
||||
- `ukEinstellungen`: featureId, firma(150), inhaber(150), strasse(150), plz(10), ort(100), email(200), telefon(50), steuernummer(30), ustId(20), iban(34), bic(11), bank(100), rechnungsPraefix(8), rechnungZaehler(100), akzentfarbe(7), stripeAccountId(40), stripeStatus(4000), onlineZahlung bool, rechnungAuftragAt dt, anfrage(2000), anfrageStatus(16), anfrageAt dt, anfrageEntschiedenAt dt, anfrageEntschiedenVon(120), anfrageBegruendung(1000), icalFehlerMailAt dt.
|
||||
- [ ] **Step 3: Tests laufen lassen, FAIL erwartet** (`flock … --test test/unterkunftSchalter.test.js test/unterkunftSetup.test.js`).
|
||||
- [ ] **Step 4: Implementieren** (config-Block, Produkt, Collections, schalter.js, schema.js, Setup-Skript als Kopie des Shop-Musters mit `createAppwriteClient()` aus `scripts/analyse-setup.mjs`), `test/support/schema.js` ergänzen.
|
||||
- [ ] **Step 5: Tests PASS**; zusätzlich `test/shopSetup.test.js` und `test/shopSchalter.test.js` weiter grün.
|
||||
- [ ] **Step 6: Commit** `Buchungskalender: Konfiguration, Schalter, Schema und Setup-Skript`.
|
||||
|
||||
### Task 2: Preisrechnung
|
||||
|
||||
**Files:**
|
||||
- Create: `server/services/unterkunft/datum.js`, `server/services/unterkunft/preis.js`
|
||||
- Test: `test/unterkunftPreis.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `datum.js`: `istTag(s): boolean` (`/^\d{4}-\d{2}-\d{2}$/` + gültig), `naechte(von, bis): number` (UTC-Datumsarithmetik auf Tagesstrings), `plusTage(tag, n): string`, `tageVon(von, bis): string[]` (belegte Nächte, `bis` exklusiv), `formatTag(tag): '12.11.2026'`.
|
||||
- `preis.js`:
|
||||
- `berechnePreis({ preise, von, bis, personen }) → { naechte, posten:[{text, menge, einzelCent, ustProzent}], summen }`
|
||||
- `summiere(posten, steuerart) → { bruttoCent, nettoCent, ust:[{ satz, nettoCent, steuerCent }] }` (bei `kleinunternehmer` `ust: []`, netto = brutto)
|
||||
- `pruefePosten(posten, steuerart) → { posten } | { fehler: { posten: string } }`
|
||||
- `formatEuro(cent) → '1.234,50 €'`
|
||||
- `preiseVollstaendig(preise) → boolean`
|
||||
|
||||
- [ ] **Step 1: Failing tests** mit `PREISE = { staffeln:[{abNaechten:1,preisProNachtCent:6500},{abNaechten:7,preisProNachtCent:5500},{abNaechten:28,preisProNachtCent:4500}], reinigungCent:6000, personenInklusive:2, aufpreisPersonNachtCent:1000, direktrabattProzent:10, steuerart:'regel', ustUebernachtung:7, ustReinigung:7 }`:
|
||||
- `naechte('2026-10-24','2026-10-26') === 2`; `naechte('2026-12-30','2027-01-02') === 3`.
|
||||
- 2026-11-02→2026-11-09, 2 Personen: Posten `[Übernachtung 7×5500, Endreinigung 1×6000, Direktbuchungsrabatt 10 % 1×-3850]`, `bruttoCent 40650`, `ust [{satz:7, nettoCent:37991, steuerCent:2659}]`.
|
||||
- 30 Nächte, 3 Personen: Übernachtung 30×4500, „Zusatzperson“ menge 30 × 1000, Reinigung 6000, Rabatt -16500 → `bruttoCent 184500`.
|
||||
- 3 Nächte, 2 Personen: `bruttoCent 23550`.
|
||||
- Unterschiedliche Sätze (ustReinigung 19): zwei `ust`-Einträge, Summe netto+steuer = brutto.
|
||||
- `kleinunternehmer`: `ust: []`, `nettoCent === bruttoCent`.
|
||||
- Rabatt 0 / Reinigung 0 → kein Posten dafür.
|
||||
- `pruefePosten`: `[]` → Fehler; Summe 0 → Fehler; Text '' oder 201 Zeichen → Fehler; 31 Posten → Fehler; `ustProzent:7` bei `kleinunternehmer` → Fehler; `ustProzent` außerhalb {0,7,19} → Fehler; `menge` kein positiver Integer → Fehler; gültige Liste → `{posten}` mit getrimmten Texten.
|
||||
- `formatEuro(123450) === '1.234,50 €'`, `formatEuro(-3850) === '−38,50 €'` (U+2212).
|
||||
- [ ] **Step 2: FAIL** prüfen.
|
||||
- [ ] **Step 3: Implementieren.** Staffel = höchste `abNaechten` ≤ `naechte` für alle Nächte (U11). Rabatt-Basis = Übernachtung + Zusatzpersonen, `-Math.round(basis*p/100)`, ust = `ustUebernachtung` (U12). USt je Satz: `netto = Math.round(bruttoSatz*100/(100+satz))`, `steuer = bruttoSatz - netto`. Texte: `Übernachtung, ${n} Nächte`, `Zusatzperson${k>1?'en':''} (${k}), ${n} Nächte`, `Endreinigung`, `Direktbuchungsrabatt ${p} %`.
|
||||
- [ ] **Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Preisrechnung und Datumshilfen`.
|
||||
|
||||
### Task 3: Speicher und Belegung
|
||||
|
||||
**Files:**
|
||||
- Create: `server/services/unterkunft/speicher.js`, `server/services/unterkunft/belegung.js`
|
||||
- Test: `test/unterkunftBelegung.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `datum.js` (Task 2), `appwriteAdmin.js` (`listAllDocuments`, `getDocument`, `createDocument`, `updateDocument`, `deleteDocument`, `Query`).
|
||||
- Produces:
|
||||
- `speicher.js` (alle JSON-Felder geparst/serialisiert; Rückgaben sind „flache“ Objekte mit `$id`):
|
||||
`ladeEinstellungen(featureId) → obj|null`, `speichereEinstellungen(featureId, felder) → obj` (legt an, falls fehlt),
|
||||
`listeUnterkuenfte(featureId, { nurAktive=false }) → obj[]` (nach `sort`), `ladeUnterkunft(id) → obj|null`, `speichereUnterkunft(featureId, felder, id?) → obj`,
|
||||
`ladePreise(unterkunftId) → obj|null`, `speicherePreise(unterkunftId, featureId, felder) → obj`,
|
||||
`listeSperren(unterkunftId) → obj[]`, `legeSperreAn(...)`, `loescheSperre(id)`,
|
||||
`ladeAnfrage(id) → obj|null`, `legeAnfrageDokAn(felder) → obj`, `aktualisiereAnfrage(id, felder) → obj`, `listeAnfragen(featureId, { status?: string[], unterkunftId? }) → obj[]`.
|
||||
- `belegung.js`:
|
||||
- `AKTIVE_STATUS = ['gebucht','bezahlt']`
|
||||
- `belegteBereiche(unterkunftId, von, bis, { ohneAnfrage, now }) → [{ von, bis, quelle:'manuell'|'ical'|'reserviert'|'gebucht' }]` (Sperren + Anfragen `gebucht|bezahlt` + `freigegeben` mit `fristAt > now`; Ausschnitt auf [von,bis))
|
||||
- `istFrei(unterkunftId, von, bis, { ohneAnfrage, now }) → boolean`
|
||||
- `ueberschneidet(a, b) → boolean` (rein, Halboffen-Intervalle)
|
||||
|
||||
- [ ] **Step 1: Failing tests** (Appwrite-Mock mit `ukSperren`, `ukAnfragen`):
|
||||
- `ueberschneidet({von:'2026-11-01',bis:'2026-11-05'},{von:'2026-11-05',bis:'2026-11-07'}) === false` (Abreise = Anreise ok); `…'2026-11-04'…` → true.
|
||||
- Sperre manuell 11-10..11-12 → `istFrei(u,'2026-11-11','2026-11-13')` false; `'2026-11-12','2026-11-14'` true.
|
||||
- Anfrage `freigegeben` mit `fristAt` in Zukunft blockiert; mit `fristAt` in Vergangenheit nicht; `neu`, `abgelehnt`, `abgelaufen`, `storniert` blockieren nicht; `gebucht`/`bezahlt` blockieren.
|
||||
- `ohneAnfrage: id` ignoriert genau diese Anfrage.
|
||||
- `belegteBereiche` liefert die Quelle `ical` für `quelle:'ical:abc'`.
|
||||
- [ ] **Step 2: FAIL.**
|
||||
- [ ] **Step 3: Implementieren** (Abfragen per `Query.equal('unterkunftId', id)` + Status-Filter, `listAllDocuments`).
|
||||
- [ ] **Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Speicher und Belegung`.
|
||||
|
||||
### Task 4: Statusmaschine, Token, Rechnungsnummern
|
||||
|
||||
**Files:**
|
||||
- Create: `server/services/unterkunft/fehler.js`, `token.js`, `rechnungen.js`, `ablauf.js`
|
||||
- Create (Stubs, die die Stränge ersetzen): `server/services/unterkunft/mails.js`, `server/services/unterkunft/zahlung.js`
|
||||
- Test: `test/unterkunftAblauf.test.js`, `test/unterkunftRechnungen.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Tasks 1–3.
|
||||
- Produces:
|
||||
- `fehler.js`: `class UkFehler extends Error { constructor(status, code, felder = {}) }` — Codes u. a. `zeitraum_belegt` (409), `status_ungueltig` (409), `frist_abgelaufen` (410), `nicht_gefunden` (404), `ungueltig` (400), `mindestnaechte` (400), `zu_viele_personen` (400), `online_aus` (409).
|
||||
- `token.js`: `neuesToken() → { token, hash }` (32 Byte base64url, sha256 hex), `tokenPasst(hash, token) → boolean` (timingSafeEqual), `formToken(siteKey, now)`, `pruefeFormToken(siteKey, token, now) → 'ok'|'zu_schnell'|'abgelaufen'|'ungueltig'` (Kopie `shop/token.js`, Label `'uk-form'`, Min `config.unterkunft.formMinSekunden`), `ipHashVon(ip)` (Präfix `uk-ip:`).
|
||||
- `rechnungen.js`:
|
||||
- `naechsteNummer(featureId, now) → 'RE-2026-0001'` (Sperre `uk-rechnungsnr:<featureId>`, Zähler `{jahr,n}` in `ukEinstellungen.rechnungZaehler`, Jahreswechsel → n=1; Präfix aus Einstellungen, Standard `RE`)
|
||||
- `stelleAus(ctx, anfrage, { art:'rechnung'|'storno', bezugNummer?, now }) → rechnungDoc` (`inhalt` = `{ aussteller:{firma,inhaber,strasse,plz,ort,email,telefon,steuernummer,ustId,iban,bic,bank}, empfaenger: anfrage.rechnungsadresse, unterkunft:{name}, von, bis, naechte, personen, posten (bei storno: einzelCent negiert), summen, steuerart, zahlart, bezugNummer, anfrageNummer }`)
|
||||
- `listeRechnungen(featureId) → doc[]`, `ladeRechnung(id) → doc|null`
|
||||
- `mails.js` (Stub, Task 20 ersetzt den Inhalt, Signaturen bleiben): `anGast(art:'G16'|'G17'|'G18'|'G19'|'G20'|'G21'|'G22', ctx, anfrage, extra={}) → Promise<void>`, `anVermieter(template:'K31'|'K32'|'K33', ctx, values={}) → Promise<void>`; Stub protokolliert Aufrufe in `export const _aufrufe = []`.
|
||||
- `zahlung.js` (Stub, Task 7 ersetzt): `erstatte(ctx, anfrage, betragCent, { now }) → Promise<{ erstattetCent }>`; Stub wirft `UkFehler(409,'online_aus')`, wenn `ctx.einstellungen.stripeAccountId` fehlt, sonst `{erstattetCent: betragCent}`.
|
||||
- `ablauf.js` (jede Änderung unter `withLocks(['unterkunft:'+anfrage.unterkunftId])`, Status vor Schreiben frisch lesen):
|
||||
- `ctx` = `{ featureId, customerId, projectId, siteKey, einstellungen }` (Task 5 liefert ihn)
|
||||
- `legeAnfrageAn(ctx, { unterkunftId, von, bis, personen, gast:{name,firma,email,telefon,nachricht} }, { now, ipHash }) → anfrage` → prüft Tage, `von ≥ heute`, `bis ≤ heute+horizontMonate`, `mindestNaechte`, `maxPersonen`, `istFrei`; `nummer` = `A-` + 4 Zeichen `[A-Z2-9]`; Mails G16 + K31.
|
||||
- `gibFrei(ctx, anfrageId, { posten?, fristTage?, nachricht?, now }) → { anfrage, token }` → nur aus `neu`; Posten = `posten` (über `pruefePosten`) oder `berechnePreis`; `istFrei(…, {ohneAnfrage})`; `fristAt = now + fristTage*24h`; `tokenHash`; `stornobedingungenText` + `rechtstexteFassung` als Schnappschuss; `ipHash` = ''; Mail G17 (`extra.token`).
|
||||
- `lehneAb(ctx, anfrageId, { text, now }) → anfrage` → nur aus `neu`; `ipHash` = ''; Mail G18.
|
||||
- `bucheVerbindlich(ctx, anfrageId, token, { rechnungsadresse, zahlart, now }) → { anfrage, rechnung }` → Token prüfen (sonst `nicht_gefunden`), Status `freigegeben` (bei `gebucht|bezahlt` mit gleichem Token: bestehendes Ergebnis zurückgeben, idempotent), `fristAt > now` (sonst `frist_abgelaufen`), `zahlart==='online'` nur mit `einstellungen.onlineZahlung && stripeAccountId`, Pflichtfelder Adresse (name|firma, strasse, plz, ort, land), `istFrei(…,{ohneAnfrage})`; → `gebucht`, `stelleAus(rechnung)`, `rechnungId`, Mails G19 + K32.
|
||||
- `markiereBezahlt(ctx, anfrageId, { via, paymentIntentId?, now }) → anfrage` → aus `gebucht`; idempotent bei `bezahlt`; Mail G20 nur bei `via==='stripe'`.
|
||||
- `storniere(ctx, anfrageId, { grund, erstattungCent=0, now }) → { anfrage, stornoRechnung|null }` → aus `freigegeben|gebucht|bezahlt`; Erstattung nur bei `bezahltVia==='stripe'`, 0 ≤ cent ≤ brutto − bereits erstattet, über `zahlung.erstatte`; Stornorechnung nur, wenn Rechnung existiert; Mail G22.
|
||||
- `laufeFristenAb(now) → { abgelaufen: number }` → alle `freigegeben` mit `fristAt ≤ now` → `abgelaufen`, Mail G21.
|
||||
- `UEBERGAENGE` (Objekt Status → erlaubte Ziele), `darfUebergang(von, nach) → boolean`.
|
||||
|
||||
- [ ] **Step 1: Failing tests Rechnungen:** erste Nummer `RE-2026-0001`, zweite `RE-2026-0002`; 10 parallele `naechsteNummer` → 10 verschiedene, lückenlos 1..10; Jahreswechsel (now 2027-01-01T00:30+01:00 Berlin) → `RE-2027-0001`; Präfix `DH` → `DH-2026-0001`; `stelleAus(storno)` negiert alle `einzelCent` und `summen.bruttoCent`, trägt `bezugNummer`.
|
||||
- [ ] **Step 2: Failing tests Ablauf** (Appwrite-Mock, `mails._aufrufe` prüfen):
|
||||
- Anfrage → `neu`, Nummer-Format `/^A-[A-Z2-9]{4}$/`, `_aufrufe` enthält G16 und K31.
|
||||
- Anfrage in der Vergangenheit / unter Mindestnächten / mehr Personen als max / über belegte Nacht → `UkFehler` mit passendem Code, nichts gespeichert.
|
||||
- Review Focus 2: Anfragen A und B überlappen; `gibFrei(A)` ok; `gibFrei(B)` → 409 `zeitraum_belegt`, keine G17 für B.
|
||||
- `gibFrei` mit Posten `[{text:'Pauschale',menge:1,einzelCent:50000,ustProzent:7}]` → summen.bruttoCent 50000.
|
||||
- `bucheVerbindlich` falsches Token → 404 `nicht_gefunden`; nach `fristAt` → 410; zweimal parallel mit richtigem Token → genau ein `ukRechnungen`-Dokument, beide Aufrufe liefern dieselbe `rechnung.nummer`.
|
||||
- `zahlart:'online'` ohne Stripe-Konto → 409 `online_aus`.
|
||||
- `laufeFristenAb` setzt `abgelaufen` und schickt G21; Zeitraum danach frei.
|
||||
- `storniere` gebuchter Anfrage → Stornorechnung mit eigener Nummer, G22; `storniere` mit `erstattungCent` > brutto → 400.
|
||||
- `darfUebergang('neu','bezahlt') === false`.
|
||||
- [ ] **Step 3: FAIL.**
|
||||
- [ ] **Step 4: Implementieren.**
|
||||
- [ ] **Step 5: PASS.**
|
||||
- [ ] **Step 6: Commit** `Buchungskalender: Statusmaschine, Token und Rechnungsnummern`.
|
||||
|
||||
### Task 5: Verdrahtung in gemeinsamen Dateien + API-Verträge
|
||||
|
||||
Dieser Task berührt alle geteilten Dateien, damit die Stränge danach konfliktfrei arbeiten. Router-Dateien entstehen hier als leere `express.Router()` mit Kommentar, welcher Task sie füllt.
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/config.js` (`features.unterkunft` neben `shop` ~Z.461-500), `server/services/featureRegistry.js` (`STANDARD_HAKEN.unterkunft`), `server/services/legalDocs.js` (`SAETZE.unterkunft = { lb:{slug:'leistungsbeschreibung-unterkunft', pdf:'Leistungsbeschreibung-Buchungskalender'}, avv:{slug:'avv-unterkunft', pdf:'AVV-Buchungskalender'}, widerruf: <gemeinsam> }`, `FUNKTION_SATZ`, `aktuelleFassung`), `server/services/stripeBilling.js` (PRODUCT_METADATA `unterkunftSetup:'unterkunft-einrichtung'`), `scripts/stripe-products.mjs` (KEYS), `server/routes/abo.js` (ORDERS, FEATURE_OF_ORDER `unterkunft:'unterkunft'`, Text 'Buchungskalender (Einrichtung)'), `server/routes/funktionen.js` (Antwort `unterkunft:{verfuegbar}`), `server/index.js` (Mounts + Job-Registrierung), `server/routes/shopWebhook.js` (Weiche `wkUk` vor Z.54, `charge.refunded` nach Metadaten trennen, `account.updated` auch an Uk), `server/services/termine/outbox.js` + `server/services/termine/schema.js` (optionales Feld `anhang` 500, wird nur gespeichert/durchgereicht), `public/dashboard.html` (Section `view-buchungen` mit `<div id="buchungen-body">`), `public/app.js` (`VIEW_HASHES.buchungen`, `viewRefreshers.buchungen = () => import('/buchungen.js').then(m => m.initBuchungen(document.getElementById('buchungen-body')))`, `APP_PARENT.buchungen='funktionen'`, `FEATURE_LABELS.unterkunft='Buchungskalender'`)
|
||||
- Create: `server/services/unterkunft/kontext.js`, `server/services/unterkunft/jobs.js` (Gerüst), `server/routes/publicUnterkunft.js`, `server/routes/unterkunft.js`, `server/routes/team/unterkunft.js`, `server/routes/unterkunftSeite.js`, `server/routes/unterkunftWebhook.js` (exportiert `verarbeiteUkEreignis(event, now)`, leer), `public/buchungen.js` (Platzhalter `export function initBuchungen(el){ el.textContent = 'Buchungskalender wird geladen …' }`), `docs/funktionen/unterkunft-api.md` (API-Vertrag, s. u.)
|
||||
- Test: `test/unterkunftWiring.test.js` (Muster `test/shopWiring.test.js`), Anpassung `test/featureRegistry.test.js`, `test/legalDocs.test.js`, `test/termineOutbox.test.js` falls Pins brechen
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `kontext.js`: `unterkunftPerSiteKey(siteKey, { now }) → ctx|null` (30-s-Cache, Feature-Dokument `featureKey:'unterkunft'`, nur state `active`; im Modus `pilot` nur, wenn die Besitzer-E-Mail in der Pilotliste steht; Modus `off` → null), `ladeFuerProjekt(customerId, projectId) → ctx|null` (beliebiger state ≠ off), `leereCache(siteKey)`, `SITE_KEY_RE`. `ctx = { featureId, customerId, projectId, siteKey, state, besitzerEmail, einstellungen, project }`.
|
||||
- `jobs.js`: `minutenJob(now)`, `viertelstundenJob(now)`, `taeglicherJob(now)` mit `einzeln()`-Wrapper; bei Modus `off` → `{aus:true}`. Minutenjob ruft schon `laufeFristenAb(now)`; die anderen sind leer (Task 9/10 füllen).
|
||||
- Mounts in `server/index.js`: `app.use('/api/public/unterkunft/v1', publicUnterkunftRoutes)` vor dem globalen Parser (Z.77); `app.use('/api/unterkunft', unterkunftRoutes)` (Kunden-Session); Team-Router `router.use('/unterkunft', unterkunftTeamRoutes)` (wie Shop, index.js:28); `app.use(unterkunftSeiteRouter())` vor `express.static`. Jobs: 80 s Start/60 s, 140 s Start/15 min, 210 s Start/24 h.
|
||||
- Register `config.features.unterkunft` = Kopie `shop` mit: `product:'unterkunftSetup'`, `label {name:'Buchungskalender', kurz:'Buchungskalender', keine:…}`, `priceLabel '199 €'`, `setupEstimate`, `consent` (items `unterlagen`, `verantwortung` [Text Spec §9], `rechnung` [„Ich beauftrage WEBklar, in meinem Namen und auf meine Rechnung Rechnungen an meine Gäste auszustellen (§ 14 Abs. 2 Satz 4 UStG). Für Richtigkeit von Preisen und Steuerangaben bin ich verantwortlich.“], `stripe`, `beginn`, `information`), `returnHash:'buchungen'`, `returnProjekt:true`, `ticket.topic = (p) => 'Buchungskalender einbauen – ' + name`, `ticket.details` → dyn. Import `services/unterkunft/ticketDetails.js` (Task 11), `requiresWartungsAbo:false`, `ticketFreischaltung:true`.
|
||||
- `STANDARD_HAKEN.unterkunft = { verfuegbar: unterkunftFuerKunde, bestellpruefung: (args) => import('./unterkunft/anfrageFunktion.js').then(m => m.anfrageOffenePunkte(args)), unterlagenBereit: () => featureLegalReady('unterkunft'), belehrung: termineBelehrung, freischaltbar: (doc) => import('./unterkunft/freischaltung.js').then(m => m.freischaltbar(doc)) }` — die zwei Module entstehen in Task 11; bis dahin existieren sie als Stubs (`anfrageOffenePunkte → ['Anfrage noch nicht freigegeben']`, `freischaltbar → false`).
|
||||
- **API-Vertrag** `docs/funktionen/unterkunft-api.md` (Stränge 2 und 3 bauen dagegen; Task 6–8 erfüllen ihn). Fehlerform überall `{ fehler: code, felder?: {feld: text} }`.
|
||||
- Öffentlich (`/api/public/unterkunft/v1`):
|
||||
- `GET /:siteKey` → `{ aktiv:true, anbieter:{ name, ort, datenschutzUrl }, akzentfarbe, formToken, unterkuenfte:[{ id, name, maxPersonen, mindestNaechte, checkIn, checkOut }] }` oder `{ aktiv:false }`.
|
||||
- `GET /:siteKey/belegung?unterkunft=<id>&von=YYYY-MM-DD&bis=YYYY-MM-DD` → `{ belegt:[{von,bis}], heute:'YYYY-MM-DD', bisMax:'YYYY-MM-DD' }` (Quellen nicht offengelegt).
|
||||
- `POST /:siteKey/anfrage` Body `{ unterkunft, von, bis, personen, name, firma, email, telefon, nachricht, formToken, kontakt_url }` → `201 { nummer }`.
|
||||
- `GET /:siteKey/buchung/:id?t=` → `{ status, nummer, unterkunft:{name,checkIn,checkOut}, von, bis, naechte, personen, posten, summen, steuerart, fristAt, stornobedingungen, anbieter:{firma,inhaber,strasse,plz,ort,email,telefon}, gast:{name,firma}, rechnungsadresse?, zahlarten:['vorort'|'online'], zahlart?, bezahlt:boolean, rechnungNummer? }`.
|
||||
- `POST /:siteKey/buchung/:id?t=` Body `{ rechnungsadresse:{name,firma,strasse,plz,ort,land}, zahlart }` → `{ status:'gebucht', rechnungNummer, zahlungUrl? }` (bei `online` direkt Checkout-URL).
|
||||
- `POST /:siteKey/buchung/:id/zahlung?t=` → `{ url }`.
|
||||
- `GET /:siteKey/buchung/:id/rechnung.pdf?t=&art=rechnung|storno` → PDF.
|
||||
- Kunde (`/api/unterkunft`, Session, `?projectId=`): `GET /status`; `GET|PUT /einstellungen`; `GET /unterkuenfte`; `POST /unterkuenfte`; `PUT|DELETE /unterkuenfte/:id`; `GET|PUT /unterkuenfte/:id/preise`; `GET /unterkuenfte/:id/kalender?von&bis` → `{ bereiche:[{von,bis,quelle,anfrageNummer?,sperreId?,notiz?}] }`; `POST /unterkuenfte/:id/sperren`; `DELETE /sperren/:id`; `POST /unterkuenfte/:id/ical-importe`; `DELETE /unterkuenfte/:id/ical-importe/:importId`; `POST /unterkuenfte/:id/ical-export/erneuern`; `GET /anfragen?filter=offen|gebucht|alle`; `GET /anfragen/:id` (inkl. `vorschlag` = `berechnePreis`); `POST /anfragen/:id/freigeben` `{posten,fristTage,nachricht}`; `POST /anfragen/:id/ablehnen` `{text}`; `POST /anfragen/:id/bezahlt`; `POST /anfragen/:id/stornieren` `{grund, erstattungCent}`; `POST /preisvorschau` `{unterkunftId,von,bis,personen}`; `GET /rechnungen`; `GET /rechnungen/:id.pdf` (nur Einzel-PDF, kein ZIP — Spec N7); `POST /stripe/verbinden`, `POST /stripe/vorhandenes-konto`, `POST /stripe/aktualisieren`; `POST /anfrage-funktion` `{art,einheiten,portale,nachricht}`; `POST /anfrage-funktion/zurueckziehen`.
|
||||
- Team (`/api/team/unterkunft`): `GET /:customerId`; `POST /:customerId/anfrage` `{projectId, entscheidung, begruendung}`.
|
||||
|
||||
- [ ] **Step 1: Wiring-Test schreiben:** Registereintrag vorhanden mit Produkt `unterkunftSetup` und `returnHash 'buchungen'`; `featureHaken('unterkunft')` hat alle 5 Haken; `FUNKTION_SATZ.unterkunft === 'unterkunft'`; `GET /api/public/unterkunft/v1/aaaaaaaaaaaaaaaa` → 200 `{aktiv:false}` (Stub-Router liefert das schon); `public/app.js` enthält `buchungen` in VIEW_HASHES (Quelltext-Prüfung); `dashboard.html` enthält `id="buchungen-body"`; Shop-Webhook mit `metadata.wkUk:'1'` ruft `verarbeiteUkEreignis` statt Shop-Logik.
|
||||
- [ ] **Step 2: FAIL.**
|
||||
- [ ] **Step 3: Implementieren** inkl. API-Vertrag-Datei (Text oben 1:1).
|
||||
- [ ] **Step 4: PASS**; zusätzlich grün: `test/shopWiring.test.js`, `test/featureRegistry.test.js`, `test/legalDocs.test.js`, `test/termineOutbox.test.js`, `test/funktionenRoutes.test.js`, `test/shopWebhook*.test.js` (einzeln nacheinander).
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Verdrahtung, Router-Gerüste und API-Vertrag`.
|
||||
- [ ] **Step 6: Worktrees der vier Stränge anlegen** (Befehl oben), je `node_modules`-Symlink.
|
||||
|
||||
---
|
||||
|
||||
## Strang 1: Server (`dev-uk-server`)
|
||||
|
||||
### Task 6: Öffentliche API – Konfiguration, Belegung, Anfrage
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/routes/publicUnterkunft.js`
|
||||
- Test: `test/unterkunftPublic.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `kontext.unterkunftPerSiteKey`, `ablauf.legeAnfrageAn`, `belegung.belegteBereiche`, `token.formToken/pruefeFormToken/ipHashVon`, `termine/limits.js` (`zaehle`, `darfAnfrage`, `ipSchluessel`).
|
||||
- Produces: die drei öffentlichen Endpunkte `GET /:siteKey`, `GET /:siteKey/belegung`, `POST /:siteKey/anfrage` laut Vertrag.
|
||||
|
||||
- [ ] **Step 1: Failing tests:** unbekannter/inaktiver siteKey → 200 `{aktiv:false}`; aktiver → Unterkünfte nur `aktiv`, sortiert, ohne `featureId/customerId`; Belegung über 18 Monate hinaus → 400; Belegung fasst angrenzende Bereiche zusammen; `POST anfrage` gültig → 201 `{nummer}` + Dokument `neu`; Honeypot `kontakt_url` gefüllt → 201 mit Schein-Nummer, nichts gespeichert; Form-Token `zu_schnell` → ebenso stiller Erfolg; fehlende E-Mail → 400 `felder.email`; mehr als `anfragenJeSiteStunde` je Site → 429; CORS: `OPTIONS` von erlaubter Origin (Domain des Projekts) → 204 mit Origin, fremde Origin bei POST → 403; GET mit `Access-Control-Allow-Origin: *`.
|
||||
- [ ] **Step 2: FAIL.**
|
||||
- [ ] **Step 3: Implementieren** (Helfer `route/schreibe/fehlerMit/erlaubteHerkunft/vorabantwort` aus `publicShop.js` in eine eigene lokale Kopie; eigener 16-KB-JSON-Parser; `UkFehler` → Status/Code).
|
||||
- [ ] **Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: öffentliche API für Kalender und Anfrage`.
|
||||
|
||||
### Task 7: Buchungsseite, Stripe-Zahlung, Webhook
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/routes/publicUnterkunft.js` (Buchungs-Endpunkte), `server/routes/unterkunftWebhook.js`, `server/services/unterkunft/zahlung.js` (Stub ersetzen), `server/services/shop/connect.js` (`erstatten(..., { webklar = 'shop' })` Parameter, Standard unverändert)
|
||||
- Test: `test/unterkunftBuchung.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `ablauf.bucheVerbindlich/markiereBezahlt`, `connect.checkoutAnlegen/sessionHolen/erstatten`, `rechnungen.ladeRechnung`, `rechnungPdf` aus Task 19 (bis zum Merge: `GET …/rechnung.pdf` darf in diesem Strang mit `501` antworten, wenn `server/services/unterkunft/rechnungPdf.js` fehlt — dynamischer Import mit Fallback, Test prüft nur 200/501).
|
||||
- Produces:
|
||||
- `zahlung.js`: `checkoutFuer(ctx, anfrage, { now }) → { url }` (Session auf `ctx.einstellungen.stripeAccountId`, `mode:'payment'`, ein Posten `Aufenthalt <Unterkunft> <von>–<bis>, Rechnung <Nr>` Betrag `summen.bruttoCent`, `payment_method_types` `['card','sepa_debit']` wenn SEPA im Status aktiv sonst `['card']`, `locale:'de'`, `expires_at` +30 min, `metadata:{wkUk:'1', anfrageId}`, `payment_intent_data.metadata` gleich, `success_url`/`cancel_url` = Buchungsseite `…/unterkunft/<siteKey>/buchung/<id>?t=<token>&wk_zahlung=ok|abbruch`, `idempotencyKey:'uk-zahlung-'+anfrageId+'-'+minute`), speichert `stripeSessionId`; `schliesseAbWennBezahlt(ctx, anfrage, { now }) → anfrage` (Session holen, bei `complete`+`paid` → `markiereBezahlt(via:'stripe')`); `erstatte(...)` echt mit `webklar:'unterkunft'`.
|
||||
- `unterkunftWebhook.verarbeiteUkEreignis(event, now)`: `checkout.session.completed` (paid) / `async_payment_succeeded` → `markiereBezahlt`; `charge.refunded` → `erstattetCent` nachtragen; idempotent.
|
||||
|
||||
- [ ] **Step 1: Failing tests** (Fake-Stripe): `GET buchung` falsches Token und unbekannte ID → identisch 404; gültig → Vertrag-Antwort, `zahlarten` ohne `online`, wenn Stripe fehlt; `POST buchung` vor Ort → `{status:'gebucht', rechnungNummer:'RE-2026-0001'}`; Review Focus 4: zweites `POST` → gleiche Antwort, keine zweite Rechnung; `GET` danach zeigt `status:'gebucht'` und `rechnungNummer`; abgelaufene Frist → 410 `frist_abgelaufen`; `online` → Antwort enthält `zahlungUrl` vom Fake-Stripe; Webhook `checkout.session.completed` mit `metadata.wkUk` → `bezahlt` + G20 im Stub-Protokoll; Buchungsseite mit `wk_zahlung=ok` schließt ohne Webhook selbst ab; Webhook zweimal → ein Übergang.
|
||||
- [ ] **Step 2: FAIL.**
|
||||
- [ ] **Step 3: Implementieren.**
|
||||
- [ ] **Step 4: PASS**; `test/shopWebhook*.test.js` und Erstattungs-Tests des Shops weiter grün.
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Buchungsseite, Stripe-Zahlung und Webhook`.
|
||||
|
||||
### Task 8: Kunden-API (Vermieter)
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/routes/unterkunft.js`
|
||||
- Create: `server/services/unterkunft/verwaltung.js` (Validierung + Speichern von Einstellungen, Unterkünften, Preisen, Sperren; Stripe-Verbinden über `shop/connect.js` `kontoAnlegen`/`onboardingLink` mit `idempotencyKey 'uk-konto-'+projectId`)
|
||||
- Test: `test/unterkunftVerwaltung.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Tasks 1–4, `siteFeatures.assertOwnProject`, `writeLimiter.createWriteLimiter`, `shop/connect.js`; PDF-Download mit demselben 501-Fallback wie Task 7, bis Task 19 gemergt ist.
|
||||
- Produces: alle Kunden-Endpunkte laut Vertrag außer `anfrage-funktion*` (Task 11). Validierung `verwaltung.js`: `pruefeEinstellungen(eingabe) → {werte}|{fehler}`, `pruefeUnterkunft`, `pruefePreise` (Staffeln aufsteigend, erste `abNaechten:1`, max 6 Staffeln, Preise 100–100000 Cent, Rabatt 0–50, Frist 1–14, Stornobedingungen 20–5000 Zeichen), `pruefeSperre`, `pruefeIcalUrl` (nur `https:`, max 500 Zeichen). `rechnungsPraefix` nur änderbar, solange `rechnungZaehler` leer.
|
||||
|
||||
- [ ] **Step 1: Failing tests:** fremdes Projekt → 403; Einstellungen ohne Steuernummer und ohne USt-ID → 400 `felder.steuernummer`; Präfix nach erster Rechnung ändern → 409; Unterkunft anlegen/sortieren/deaktivieren; Preise mit Staffel `abNaechten:2` als erste → 400; Kalender liefert Sperren mit `notiz` und Anfragen mit `anfrageNummer`; Sperre über gebuchte Nacht → 409 `zeitraum_belegt`; Review Focus 5 über `POST /anfragen/:id/freigeben` (leere Posten, Summe ≤ 0, Text 201 Zeichen, 31 Posten, ust 7 bei Kleinunternehmer → je 400, Anfrage bleibt `neu`); `GET /anfragen/:id` enthält `vorschlag`; `POST /preisvorschau` = `berechnePreis`; `POST /stripe/vorhandenes-konto` übernimmt `shopConfig.stripeAccountId` desselben Projekts, ohne eins → 409; `GET /rechnungen/:id.pdf` fremde Rechnung → 404.
|
||||
- [ ] **Step 2: FAIL.**
|
||||
- [ ] **Step 3: Implementieren.**
|
||||
- [ ] **Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Verwaltungs-API für Vermieter`.
|
||||
|
||||
### Task 9: iCal-Import und -Export
|
||||
|
||||
**Files:**
|
||||
- Create: `server/services/unterkunft/ical.js`, `server/services/unterkunft/icalAbgleich.js`
|
||||
- Modify: `server/services/unterkunft/jobs.js` (`viertelstundenJob` → `gleicheAlleAb(now)`), `server/routes/unterkunftSeite.js` (Export-Route)
|
||||
- Test: `test/unterkunftIcal.test.js` (Fixtures `test/fixtures/ical/booking.ics`, `airbnb.ics`, `tzid.ics`, `gefaltet.ics`, `fehlerseite.html`)
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `ical.js` (rein): `parseIcs(text, { heute, horizontTage }) → { ereignisse:[{von,bis,uid}] } | throws Error('kein_ics')`; `baueIcs({ name, ereignisse:[{uid,von,bis}] , jetzt }) → string` (CRLF, Zeilen ≤ 75 Oktette gefaltet, `PRODID:-//WEBklar//Buchungskalender//DE`, `SUMMARY:Belegt`, `DTSTART;VALUE=DATE`).
|
||||
- `icalAbgleich.js`: `holeIcs(url, { fetch, now }) → string` (nur `https:`, DNS-Auflösung → Abbruch bei privaten/loopback/link-local Adressen v4+v6, Timeout `icalTimeoutMs`, Abbruch über `icalMaxBytes`, max 3 Weiterleitungen je erneut geprüft); `gleicheAb(unterkunft, importEintrag, { now, fetch }) → { ok, anzahl }|{ ok:false, fehler }`; `gleicheAlleAb(now)`; nach `icalFehlerStunden` ohne Erfolg `mails.anVermieter('K33', …)` höchstens alle 24 h (`ukEinstellungen.icalFehlerMailAt`).
|
||||
- Export: `GET /unterkunft/ical/:token.ics` → `text/calendar; charset=utf-8`, `Cache-Control: no-store`; Ereignisse = Buchungen `gebucht|bezahlt`, Reservierungen `freigegeben` mit Frist, `manuell`-Sperren; nie `ical:*`; `UID` = `<anfrageId|sperreId>@project.webklar.com`; unbekanntes Token → 404.
|
||||
|
||||
- [ ] **Step 1: Failing tests** (Review Focus 3): Booking-Fixture → Ereignisse mit DATE-Werten; `DTSTART:20261110T140000Z` → `2026-11-10`; `TZID=Europe/Berlin:20261110T000000` → `2026-11-10`; fehlendes DTEND → 1 Nacht; `STATUS:CANCELLED` ignoriert; gefaltete `SUMMARY` stört nicht; Ereignisse vor gestern/hinter Horizont fallen weg; HTML → `kein_ics`; `holeIcs('http://…')` → abgelehnt; `https://127.0.0.1/…`, `https://10.0.0.5/`, `https://[::1]/` → abgelehnt; zu große Antwort → abgelehnt; `gleicheAb` Erfolg ersetzt alte `ical:<id>`-Sperren (neue zuerst), Fehler lässt alte stehen und setzt `fehler`; K33 erst nach 6 h, dann höchstens 1× je 24 h; Export enthält Buchung + manuelle Sperre, nicht die importierte, Zeilen mit CRLF.
|
||||
- [ ] **Step 2: FAIL.**
|
||||
- [ ] **Step 3: Implementieren** (DNS über `node:dns/promises` `lookup(host,{all:true})`; `fetch` injizierbar für Tests).
|
||||
- [ ] **Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: iCal-Import und -Export`.
|
||||
|
||||
### Task 10: Fristen-, Aufräum- und Löschjobs
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/services/unterkunft/jobs.js`, `server/services/retention.js` (Uk-Zweig analog Shop), `server/services/customerAdmin.js` (~Z.299-305: `disableCustomerFeatures` auch `unterkunft`, `schliesseUkVonKunde`)
|
||||
- Create: `server/services/unterkunft/aufbewahrung.js`
|
||||
- Test: `test/unterkunftAufbewahrung.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `aufbewahrung.js`: `anonymisiere(now, { deckel }) → { anfragen:n, gaeste:n }` — `ipHash` leeren nach `ipHashTage`; `neu` älter als `neuVerfallTage` → `abgelaufen` (ohne Mail); Anfragen ohne Buchung (`abgelehnt|abgelaufen`) älter als `unverbindlichMonate` → `gast`, `nachrichtVermieter` leeren, `anonymisiertAt`; gebuchte Aufenthalte 3 Jahre nach Jahresende der Abreise → `gast` leeren (Rechnungsadresse und `ukRechnungen` bleiben); `ukRechnungen` 8 Jahre nach Jahresende löschen. `loescheUkDaten({ featureId, deckel })` (90 Tage nach Abschalten, Marker in `portalFeatures.adminNote` wie Shop). `schliesseUkVonKunde(customerId)`.
|
||||
|
||||
- [ ] **Step 1: Failing tests:** je Frist ein Dokument knapp davor und knapp danach (now fest); Deckel begrenzt Anzahl; Rechnung bleibt nach Gast-Anonymisierung lesbar und PDF-Schnappschuss unverändert; Kundenlöschung schaltet Funktion ab.
|
||||
- [ ] **Step 2: FAIL.** **Step 3: Implementieren** (Tagesjob ruft `anonymisiere` + `loescheUkDaten` für abgeschaltete). **Step 4: PASS**; `test/retention*.test.js` weiter grün.
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Aufbewahrung und Löschfristen`.
|
||||
|
||||
### Task 11: Funktion bestellen „auf Anfrage“, Freischaltung, Ticket
|
||||
|
||||
**Files:**
|
||||
- Create (ersetzt Stubs aus Task 5): `server/services/unterkunft/anfrageFunktion.js`, `server/services/unterkunft/freischaltung.js`, `server/services/unterkunft/ticketDetails.js`
|
||||
- Modify: `server/routes/unterkunft.js` (`anfrage-funktion*`), `server/routes/team/unterkunft.js`
|
||||
- Test: `test/unterkunftAnfrageFunktion.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `anfrageFunktion.js` (Kopie-Muster `shop/anfrage.js`, Status in `ukEinstellungen.anfrage*`, Sperre `uk-anfrage:<projectId>`): `ARTEN = ['ferienwohnung','monteurzimmer','pension','sonstiges']`, `anfrageOffenePunkte({customerId,projectId}) → string[]`, `stelleAnfrage({customerId,projectId,eingabe:{art,einheiten(1–50),portale:boolean,nachricht},person,now})` → T22 ans Team, K34 an Kunden (über `sendAdminNotice`/`sendCustomerMail` direkt; Vorlagen kommen aus Task 20 — Test prüft nur den Aufruf mit Template-ID), `zieheZurueck`, `entscheideAnfrage({…, entscheidung, begruendung})`, `anfrageAnsicht(einstellungen)`.
|
||||
- `freischaltung.js`: `freischaltbar(doc) → boolean` (≥ 1 aktive Unterkunft mit `preiseVollstaendig` und Stornobedingungen; Einstellungen firma, strasse, plz, ort, email und (steuernummer|ustId); fail-closed).
|
||||
- `ticketDetails.js`: `ticketDetails(ctx) → string` (siteKey, Snippet `<div data-webklar-unterkunft="SITEKEY" data-api="https://project.webklar.com"></div><script src="https://project.webklar.com/embed/unterkunft.js" defer></script>`, Schritte aus Spec §9, Verweis `docs/funktionen/unterkunft-einbau.md`).
|
||||
- Team: `GET /api/team/unterkunft/:customerId` → je Projekt `{ projectId, name, state, anfrage, stripe, unterkuenfte:n, offeneAnfragen:n }`; `POST …/anfrage`.
|
||||
|
||||
- [ ] **Step 1: Failing tests:** Anfrage nur bei state `off`; doppelte Anfrage → 409; Entscheidung `annehmen` → `anfrageOffenePunkte` leer; `freischaltbar` false ohne Preise, true mit vollständigen Daten, false bei Lesefehler; Ticket-Details enthalten den siteKey.
|
||||
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Bestellung auf Anfrage, Freischaltung und Ticket`.
|
||||
|
||||
---
|
||||
|
||||
## Strang 2: Oberfläche (`dev-uk-ui`, WOMS-Worktree `ticket.webklar.com/dev-unterkunft`)
|
||||
|
||||
Alle Portal-Module nach `public/shop.js`-Muster: `public/buchungen.js` (`initBuchungen(rootEl, { projectId })`, `REITER`), Reiter-Module `public/buchungen-tab-*.js` mit `render(panel, ctx)`, DOM-Helfer aus `public/shop-dom.js` wiederverwenden (`h`, `request`, `fehlerKarte`, `infoKarte`), Stile `public/buchungen.css`. Texte deutsch, Du-Form. Tests: Quelltext-/fakeDom-Tests nach Muster `test/shop*.test.js` mit `test/support/fakeDom.js`; API mit `request`-Attrappe gemäß Vertrag.
|
||||
|
||||
### Task 12: Grundgerüst + Reiter „Anfragen“
|
||||
|
||||
**Files:**
|
||||
- Modify: `public/buchungen.js` (Platzhalter ersetzen)
|
||||
- Create: `public/buchungen-tab-anfragen.js`, `public/buchungen-posten.js` (Posten-Editor, reine Funktionen `summeLive(posten, steuerart)` = Spiegel von `summiere`), `public/buchungen.css`
|
||||
- Test: `test/buchungenUi.test.js`
|
||||
|
||||
- [ ] **Step 1: Failing tests:** `REITER` = `anfragen, kalender, unterkuenfte, rechnungen, einstellungen` (Labels „Anfragen“, „Kalender“, „Unterkünfte & Preise“, „Rechnungen“, „Einstellungen“); bei state `off` statt Reitern Anfrage-/Bestellkarte (Task 14); Anfragenliste rendert Nummer, Zeitraum „12.–30.11.2026 · 18 Nächte“, Gast, Status-Pille, Betrag; Detail zeigt Vorschlag-Posten editierbar, Summe aktualisiert sich (`summeLive` = Server-Werte aus Task 2 Beispielen: 40650); „Freigeben“ sendet `{posten,fristTage,nachricht}`; 409-Antwort zeigt „Der Zeitraum ist inzwischen belegt.“; gebuchte Anfrage zeigt Rechnung-Link, „Als bezahlt (vor Ort) markieren“, „Stornieren“ (Dialog mit Grund + Erstattungsbetrag nur bei Online-Zahlung).
|
||||
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Portal-Bereich und Anfragen`.
|
||||
|
||||
### Task 13: Reiter „Kalender“ und „Unterkünfte & Preise“
|
||||
|
||||
**Files:**
|
||||
- Create: `public/buchungen-tab-kalender.js`, `public/buchungen-tab-unterkuenfte.js`
|
||||
- Test: `test/buchungenUi.test.js` (erweitern)
|
||||
|
||||
- [ ] **Step 1: Failing tests:** Monatsraster beginnt Montag; Bereiche gefärbt nach `quelle` (Klassen `ist-gebucht`, `ist-reserviert`, `ist-ical`, `ist-manuell`) mit Legende; zwei Klicks auf freie Tage öffnen „Zeitraum sperren“ mit Notiz → `POST sperren`; Klick auf manuelle Sperre bietet „Sperre aufheben“; Unterkunft-Formular mit allen Feldern aus Spec §3; Preis-Editor mit Staffelzeilen (+/−), Beispielrechnung „7 Nächte, 2 Personen = 406,50 €“ über `POST /preisvorschau`; iCal-Bereich: Import hinzufügen (Name, URL), Status „zuletzt abgeglichen …“ bzw. Fehlertext, Export-Link mit „Kopieren“, Kurzanleitung Booking.com („Unterkunft → Tarife & Verfügbarkeit → Kalender synchronisieren → Kalender importieren → Link einfügen“) und Airbnb, „Link erneuern“ mit Bestätigung.
|
||||
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Kalender und Unterkünfte im Portal`.
|
||||
|
||||
### Task 14: Rechnungen, Einstellungen, Funktionen-Kachel, Anfrageformular
|
||||
|
||||
**Files:**
|
||||
- Create: `public/buchungen-tab-rechnungen.js`, `public/buchungen-tab-einstellungen.js`, `public/buchungen-anfrage.js` (Formular der Funktion, Muster `shop-anfrage.js`)
|
||||
- Modify: `public/funktionen.js` (Kachel „Buchungskalender“ nach `shopTile` ~Z.216-260, sichtbar nur bei `unterkunft.verfuegbar`; Wunschprojekt-Klick wie Z.405 auf `buchungen-body`)
|
||||
- Test: `test/buchungenUi.test.js`, `test/funktionen*.test.js` falls Pins
|
||||
|
||||
- [ ] **Step 1: Failing tests:** Rechnungsliste mit PDF-Link `/api/unterkunft/rechnungen/<id>.pdf?projectId=`; Einstellungen: Pflichtfelder markiert, Präfix-Feld gesperrt mit Hinweis, sobald Rechnungen existieren; Stripe-Karte mit Zuständen nicht verbunden / Onboarding offen / aktiv und Knopf „Vorhandenes Webshop-Konto verwenden“ nur wenn API es meldet; Schalter „Online-Zahlung anbieten“ nur bei aktivem Konto; Knopf „Mustertext übernehmen“ füllt Stornobedingungen (Text aus `docs/funktionen/unterkunft-storno-muster.md`, Task 21 — bis dahin Konstante in `buchungen-tab-einstellungen.js`, Task 22 gleicht ab); Kachel zeigt Preis „199 €“ und Zustände wie Shop-Kachel; Anfrageformular sendet `{art,einheiten,portale,nachricht}`.
|
||||
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Rechnungen, Einstellungen und Funktionen-Kachel`.
|
||||
|
||||
### Task 15: WOMS
|
||||
|
||||
**Files (WOMS-Repo, Worktree von `origin/test` als `feature/unterkunft`, `git ls-files | grep -c node_modules` prüfen, nur Dateien einzeln stagen):**
|
||||
- Modify: `src/lib/funktionenView.js` (`FUNKTIONEN` Eintrag `{ key:'unterkunft', label:'Buchungskalender', bestaetigen:{…wie shop} }`, `aktionen()`, `siteKey`, Hilfen für Anfrage-Zeilen generalisieren statt kopieren: `anfrageZeilen(eintrag, art)`), `src/lib/teamApi.js` (`getUnterkunft(customerId)`, `entscheideUnterkunftAnfrage(customerId, body)`), `src/components/customer/CustomerFunktionen.jsx` (Panel `UnterkunftPanel` nach `ShopPanel`-Muster: Anfrage-Zeilen, Annehmen/Ablehnen, Stripe-Status, Zahl Unterkünfte/offene Anfragen), `scripts/setup-legal.mjs` (DOCUMENTS anhängen: `leistungsbeschreibung-unterkunft` „Leistungsbeschreibung Buchungskalender“, `avv-unterkunft` „AVV Buchungskalender“, `liveUrl https://project.webklar.com/rechtliches/<key>.html` → sort 13 und 14)
|
||||
- Test: `tests/unit/funktionenView.test.mjs`, `tests/setup-legal.test.mjs` (Länge 14, letzter Eintrag `avv-unterkunft`)
|
||||
|
||||
- [ ] **Step 1: Failing tests** (WOMS `node --test` mit `node_modules`-Symlink auf `../build/node_modules`): `FUNKTIONEN` enthält `unterkunft`; `aktionen()` für `unterkunft` wie Shop; `anfrageZeilen` liefert für beide Arten die richtigen Zeilen; setup-legal Länge 14.
|
||||
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS**; Vite-Build: `NODE_OPTIONS=--max-old-space-size=1200 node node_modules/vite/bin/vite.js build --outDir /tmp/claude-0/uk-woms-build` läuft durch.
|
||||
- [ ] **Step 5: Commit** (WOMS) `Buchungskalender: Funktionsanfragen und Rechtliches-Karten`.
|
||||
|
||||
---
|
||||
|
||||
## Strang 3: Widget (`dev-uk-widget`, Donau-Repo `/root/reports/donau-home-website`)
|
||||
|
||||
### Task 16: Widget `public/embed/unterkunft.js`
|
||||
|
||||
**Files:**
|
||||
- Create: `public/embed/unterkunft.js`, `public/embed/unterkunft-demo.html`
|
||||
- Test: `test/unterkunftEmbedStatic.test.js` (Muster `shopEmbedStatic.test.js`), `test/unterkunftKalenderLogik.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: öffentlicher Vertrag `GET /:siteKey`, `GET /:siteKey/belegung`, `POST /:siteKey/anfrage`.
|
||||
- Produces: IIFE; Einbau `<div data-webklar-unterkunft="SITEKEY" data-api="https://project.webklar.com"></div>`. Die reine Kalenderlogik steht im Widget in einem Objekt `WKU_LOGIK` und wird nur dann als `globalThis.__wkuLogik` bereitgestellt, wenn `document.currentScript?.dataset.test === '1'`; der Test lädt die Datei per `node:vm` mit `test/support/fakeDom.js` und diesem Attribut. Bei `{aktiv:false}` oder Ladefehler zeigt das Widget nur „Anfragen gerne per Telefon oder E-Mail.“ Funktionen: `monatsRaster(jahr, monat) → [{tag|null}] (Montag zuerst)`, `nachtBelegt(tag, belegt) → boolean`, `waehle(zustand, tag, belegt, mindestNaechte) → zustand` (erster Klick Anreise; zweiter Klick nach Anreise → Abreise, wenn keine belegte Nacht dazwischen, sonst neue Anreise; Klick vor Anreise → neue Anreise), `zusammenfassung(von, bis) → '12.–30. Nov. · 18 Nächte'`.
|
||||
- Gestaltung: N1 (`@layer wku`, `.wku-`, Variablen). Zwei Spalten ab 760 px (Container-Query), Kalender links (1 Monat, 2 Monate ab 1100 px), Formular rechts: Unterkunft (nur bei > 1), Personen (1…max), Firma (opt.), Name, E-Mail, Telefon (opt.), Nachricht (opt.), Honeypot `kontakt_url` (versteckt, `tabindex=-1`, `autocomplete=off`), Datenschutzhinweis mit Link `anbieter.datenschutzUrl`, Knopf „Anfrage senden“. Ohne Zeitraum → Meldung „Bitte wählen Sie Anreise und Abreise im Kalender.“. Erfolg: „Danke! Ihre Anfrage A-XXXX ist eingegangen. Sie erhalten gleich eine Bestätigung per E-Mail.“ Tastatur: Pfeile bewegen Fokus, Enter/Leertaste wählt; `aria-label` „Dienstag, 10. November 2026, frei“ / „belegt“; `aria-live` für Zusammenfassung. Alle Texte per `textContent`.
|
||||
|
||||
- [ ] **Step 1: Failing tests:** Statik: kein `innerHTML` mit Variablen, kein `eval`, API-Pfad `/api/public/unterkunft/v1/`, Präfix `.wku-`, `@layer wku`; Logik: `monatsRaster(2026,11)` beginnt mit 6 Leerfeldern (1.11.2026 ist Sonntag); `waehle` über belegte Nacht → neue Anreise; Abreise auf belegter Anreisetag-Grenze (Nacht davor frei) erlaubt; unter Mindestnächten → Hinweis-Zustand `{fehler:'mindestnaechte'}`; `zusammenfassung('2026-11-12','2026-11-30') === '12.–30. Nov. · 18 Nächte'`, über Monatsgrenze `'28. Nov. – 3. Dez. · 5 Nächte'`.
|
||||
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Website-Widget mit Kalender und Anfrageformular`.
|
||||
|
||||
### Task 17: Standalone-Seiten und Buchungsseite
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/routes/unterkunftSeite.js` (GET `/unterkunft/:siteKey` → Widget auf neutraler Seite; GET `/unterkunft/:siteKey/buchung/:id` → Buchungsseite; Export-Route kommt aus Task 9 — Reihenfolge `ical` zuerst)
|
||||
- Create: `public/unterkunft-seite.html` (Platzhalter `__SITEKEY__`, `__ANSICHT__` wie `shop-seite.html`, CSP `script-src 'self'`, `frame-ancestors 'none'`, `noindex`), `public/unterkunft-buchung.js`, `public/unterkunft-buchung.css`
|
||||
- Test: `test/unterkunftSeite.test.js`
|
||||
|
||||
- [ ] **Step 1: Failing tests:** Routen liefern HTML mit escaptem siteKey, CSP-Header, `Cache-Control: no-store`; ungültiger siteKey → 404; `unterkunft-buchung.js` (fakeDom, Vertrag-Attrappe): Status `freigegeben` zeigt alle Pflichtangaben aus Spec §6 in dieser Reihenfolge direkt über dem Knopf — Anbieter, Unterkunft + Zeitraum mit Check-in/-out, Personen, Posten, Gesamtpreis (inkl. USt-Ausweis bzw. „Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.“), Zahlarten, Stornobedingungen, „Ein Widerrufsrecht besteht nicht (§ 312g Abs. 2 Nr. 9 BGB).“, Datenschutzhinweis, „Reserviert bis …“; Knopftext exakt „Zahlungspflichtig buchen“; Option „online“ fehlt, wenn nicht in `zahlarten`; nach Buchung: „Ihre Buchung ist bestätigt“ + Rechnung-Link + bei unbezahlt online „Jetzt online bezahlen“; `abgelaufen` → „Die Reservierung ist abgelaufen. Bitte fragen Sie erneut an.“; 404 → „Dieser Link ist ungültig.“; `wk_zahlung=abbruch` → Hinweis „Zahlung abgebrochen – Sie können jetzt oder vor Ort bezahlen.“
|
||||
- [ ] **Step 2: FAIL.** **Step 3: Implementieren** (Akzentfarbe aus Antwort als CSS-Variable). **Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Buchungsseite für Gäste`.
|
||||
|
||||
### Task 18: Einbau auf der Donau-Home-Seite
|
||||
|
||||
**Files (Repo `/root/reports/donau-home-website`, Branch `main`, auf 5f8465a; vorher `git status` sauber und `git log -1` prüfen — hat der Inhaber gepusht, auf `origin/main` aufsetzen, nie überschreiben):**
|
||||
- Modify: `index.html` Abschnitt `#anfrage` (Z.~555-590) und Skript (Z.~668-690): altes Formular und Datums-Skript entfernen; linke Spalte = Kopf + Einleitung „Wählen Sie Ihren Zeitraum im Kalender, nennen Sie die Zahl der Personen – wir melden uns mit Verfügbarkeit und Preis.“ + Kontaktliste; rechte/ganze Breite = `<div data-webklar-unterkunft="SITEKEY_FOLGT" data-api="https://project.webklar.com"></div>` + Script-Tag. Da das Widget selbst zweispaltig ist (Kalender links, Formular rechts), wird `.anfrage` zu: Kopf + Kontakt oben (zweispaltig: Text links, Kontakt rechts), darunter das Widget über volle Breite — so steht der Kalender links.
|
||||
- Modify: CSS-Block `/* Anfrage */`: Variablen `--wku-akzent: var(--eiche)`, `--wku-hintergrund: var(--leinen)`, `--wku-text: var(--tinte)`, `--wku-rand: var(--linie)`, `--wku-radius: 10px`, `--wku-schrift: 'Figtree', system-ui, sans-serif` (genaue Token-Namen aus dem `:root` der Seite übernehmen).
|
||||
- Fallback ohne aktives Widget liefert das Widget selbst (Task 16); ohne JS bleibt die Kontaktliste sichtbar.
|
||||
|
||||
- [ ] **Step 1:** Mit lokalem Widget-Demo prüfen: `public/embed/unterkunft-demo.html` um eine Variante „Donau“ mit den Variablen ergänzen; Screenshot 390 px und 1280 px per Playwright nach `/root/reports/buchungskalender-2026-10-07/donau/`.
|
||||
- [ ] **Step 2:** `index.html` ändern; Seite lokal statisch öffnen (Widget mit API-Attrappe über `page.route`), Screenshots wie oben; keine horizontale Scrollleiste bei 390 px.
|
||||
- [ ] **Step 3: Commit** im Donau-Repo `Anfrage: Buchungskalender statt Anreise/Abreise-Feldern` (Autor Kenso Grimm). Kein Push. siteKey wird beim Livegang eingesetzt (Livegang-Notiz).
|
||||
|
||||
---
|
||||
|
||||
## Strang 4: Mails, PDF, Recht (`dev-uk-mail`)
|
||||
|
||||
### Task 19: PDF-Writer und Rechnungs-PDF
|
||||
|
||||
**Files:**
|
||||
- Create: `server/services/unterkunft/pdf.js`, `server/services/unterkunft/rechnungPdf.js`
|
||||
- Test: `test/unterkunftPdf.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces:
|
||||
- `pdf.js`: `class PdfDoc { constructor({ breite=595.28, hoehe=841.89 }); seite(); text(x, y, str, { groesse=10, fett=false, rechts=false }); linie(x1,y1,x2,y2,{dicke=0.5}); breite(str, groesse, fett) → number; umbrechen(str, maxBreite, groesse) → string[]; toBuffer() → Buffer }` — Standardschriften Helvetica/Helvetica-Bold, `WinAnsiEncoding`, Breitentabelle der Helvetica-AFM für 32–255 eingebettet, Zeichen außerhalb WinAnsi → `?`; `€` = 0x80, Umlaute/ß nach CP1252; Strings PDF-escaped (`\`, `(`, `)`); xref mit korrekten Offsets; kein Kompressionsfilter.
|
||||
- `rechnungPdf.js`: `rechnungPdf(inhalt, { art, nummer, datum }) → Buffer` — Layout A4: Absenderzeile klein, Empfängeradresse, rechts Rechnungsnummer/Datum/Leistungszeitraum, Titel „Rechnung“ bzw. „Stornorechnung zu <bezugNummer>“, Leistung „Beherbergung <Unterkunft>, <von>–<bis> (<n> Nächte, <p> Personen)“, Posten-Tabelle (Text, Menge, Einzelpreis, Betrag), Summen (netto je Satz, USt je Satz, Gesamt) bzw. § 19-Satz, Zahlart („Zahlung vor Ort“ / „Online-Zahlung“), Fuß: Firma, Anschrift, Steuernummer/USt-ID, Bank (falls vorhanden). Seitenumbruch bei vielen Posten.
|
||||
- [ ] **Step 1: Failing tests:** Buffer beginnt `%PDF-1.4`, endet `%%EOF\n`; `startxref`-Wert zeigt auf `xref`; jeder xref-Offset zeigt auf `n 0 obj`; Text „Übernachtung“ als CP1252-Bytes enthalten; `€` als Byte 0x80; `(` im Text escaped; 40 Posten → 2 Seiten (`/Count 2`); Rechnung enthält Nummer, „Gemäß § 19 UStG“ bei Kleinunternehmer, „USt 7 %“ bei Regel; gleicher `inhalt` → byte-gleiches PDF (deterministisch, kein Zeitstempel außer `datum`).
|
||||
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS**; zusätzlich ein Beispiel-PDF nach `/root/reports/buchungskalender-2026-10-07/beispiel-rechnung.pdf` schreiben und mit `pdftotext` (falls vorhanden) oder Chrome prüfen.
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: PDF-Writer und Rechnungs-PDF`.
|
||||
|
||||
### Task 20: Mail-Vorlagen und Versand
|
||||
|
||||
**Files:**
|
||||
- Create: `server/mail-templates/G16 Buchungsanfrage eingegangen.html`, `G17 Buchungsanfrage freigegeben.html`, `G18 Buchungsanfrage abgelehnt.html`, `G19 Buchungsbestätigung.html`, `G20 Zahlung eingegangen (Unterkunft).html`, `G21 Reservierung abgelaufen.html`, `G22 Buchung storniert.html`, `K31 Neue Buchungsanfrage.html`, `K32 Neue Buchung.html`, `K33 Kalenderabgleich fehlgeschlagen.html`, `K34 Anfrage Buchungskalender.html`, `T22 Neue Anfrage Buchungskalender.html`
|
||||
- Modify: `server/services/unterkunft/mails.js` (Stub ersetzen, Signaturen aus Task 4 bleiben; `_aufrufe` bleibt als Testhilfe erhalten), `server/mailer.js` bzw. `server/services/mailer.js` (`sendGuestMail({ …, attachments })`), `server/services/termine/outbox.js` (beim Senden: `anhang.art==='ukRechnung'` → `rechnungPdf` erzeugen und als `attachments:[{filename:'Rechnung-<nr>.pdf', content, contentType:'application/pdf'}]` übergeben)
|
||||
- Test: `test/unterkunftMails.test.js`, `test/mailTemplates.test.js` (Pins), `test/termineOutbox.test.js`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `mails.anGast` reiht über `einreihen({ featureId, bookingId: anfrage.$id, art: 'uk-'+art, an: gast.email, fromName: einstellungen.firma, replyTo: einstellungen.email, template: art, values, blocks, anhang? }, now)` ein; G19/G22 mit `anhang:{art:'ukRechnung', rechnungId}`. Platzhalter je Vorlage im Kopfkommentar; gemeinsame Werte: `gast_name`, `firma`, `unterkunft`, `zeitraum` („12.11.2026 – 30.11.2026“), `naechte`, `personen`, `nummer`, `gesamt`, `posten_text` (mehrzeilig), `frist`, `buchungs_link`, `stornobedingungen`, `anbieter_kontakt`, `rechnung_nummer`, `erstattung`, `nachricht_vermieter`. K-Mails an `besitzerEmail` mit `portal_url` = `${publicBaseUrl}/dashboard.html#buchungen?projekt=<id>`.
|
||||
- Inhalt: G17 enthält Preis, Frist, Stornobedingungen, Knopf-Link „Jetzt verbindlich buchen“ und Satz „Erst mit dem Klick auf „Zahlungspflichtig buchen“ auf der Buchungsseite ist Ihre Buchung verbindlich.“; G19 (dauerhafter Datenträger) alle Angaben der Buchungsseite + Stornobedingungen vollständig + Hinweis kein Widerrufsrecht + bei online unbezahlt Bezahl-Link.
|
||||
|
||||
- [ ] **Step 1: Failing tests:** `templateIds()` enthält alle 12 IDs; jede Vorlage rendert mit Beispielwerten ohne `problems`; G-Vorlagen in Sie-Form ohne WEBklar-Logo; Outbox-Eintrag mit `anhang` → gesendete Mail hat 1 Anhang `Rechnung-RE-2026-0001.pdf` beginnend mit `%PDF`; ohne `anhang` unverändert (Termine-Tests grün); `anGast('G17',…)` Mail hat `fromName` Firma und `replyTo` Vermieter-E-Mail.
|
||||
- [ ] **Step 2: FAIL.** **Step 3: Implementieren.** **Step 4: PASS.**
|
||||
- [ ] **Step 5: Commit** `Buchungskalender: Mails an Gäste, Vermieter und Team`.
|
||||
|
||||
### Task 21: Rechtstexte und Doku
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/rechtliches/leistungsbeschreibung-unterkunft.md`, `docs/rechtliches/avv-unterkunft.md` (Muster der Webshop-Texte; Fassung 2026-10-07; Inhalte: Leistung, 199 € einmalig ohne USt § 19, Laufzeit solange Website bei WEBklar, Pflichten Vermieter [Preise, Steuer, Stornobedingungen, Meldeschein, Kurtaxe], Rechnungsstellung im Auftrag § 14 Abs. 2 S. 4 UStG, Verfügbarkeit iCal ohne Gewähr, Stripe als eigener Vertrag), `docs/rechtliches/unterkunft-datenschutz-6f-entwurf.md`, `docs/rechtliches/unterkunft-kanzlei-fragen.md` (mind.: Button-Lösung per Mail-Link, Pflichtinfos Art. 246a, Widerrufsausschluss § 312g Abs. 2 Nr. 9 auch bei Monteursunterkünften für Unternehmer/Verbraucher, USt Endreinigung 7 %, Rechnung im Auftrag, Aufbewahrung 8 Jahre durch WEBklar, Rolle AVV, Haftung bei Doppelbuchung durch iCal-Verzögerung), `docs/funktionen/unterkunft-datenschutz-absatz.md`, `docs/funktionen/unterkunft-storno-muster.md`, `docs/funktionen/unterkunft-einbau.md`, `docs/funktionen/unterkunft-livegang.md`
|
||||
- Modify: `scripts/build-legal-html.mjs` (Modus `unterkunft`), danach `node scripts/build-legal-html.mjs unterkunft` → `public/rechtliches/*-2026-10-07.html`; PDFs über `scripts/build-legal-pdfs.sh unterkunft` (Host-Chrome; falls nicht vorhanden: im Livegang-Dokument als Schritt vermerken)
|
||||
- Test: `test/legalDocs.test.js` (featureLegalReady('unterkunft') true, wenn Dateien da)
|
||||
|
||||
- [ ] **Step 1:** Texte schreiben (Kennzeichnung „Entwurf – Prüfung durch Kanzlei offen“ in LB/AVV-Kopf).
|
||||
- [ ] **Step 2:** HTML/PDF bauen, `--check` grün; `featureLegalReady('unterkunft')` Test PASS.
|
||||
- [ ] **Step 3:** Livegang-Notiz mit Schritten: (1) Inhaber prüft Texte; (2) `node scripts/unterkunft-setup.mjs --check` dann ohne; Termine-Setup für Outbox-Feld `anhang`; (3) `stripe-products.mjs --anlegen` (unterkunftSetup); (4) compose/.env `UNTERKUNFT_ENABLED=pilot`, `UNTERKUNFT_PILOT_EMAILS=kenso.gri@gmail.com,<Mimi>`; (5) Push `feature/unterkunft:main` (Portal), `feature/unterkunft:test` (WOMS) — nie unter eigenem Namen; (6) Rechtstexte in WOMS Rechtliches hochladen (macht Claude, Schlüssel `/root/.appwrite-rechtliches.key`); (7) Mimi im WOMS freischalten (`activate`), siteKey in Donau-`index.html` einsetzen, Donau pushen; (8) Pilot: Testanfrage → Freigabe → vor Ort buchen → Rechnung prüfen → stornieren; (9) Datenschutz § 6f in `public/datenschutz.html` übernehmen; (10) nach Kanzlei `on`.
|
||||
- [ ] **Step 4: Commit** `Buchungskalender: Rechtstexte, Einbau- und Livegang-Notiz`.
|
||||
|
||||
---
|
||||
|
||||
## Abschluss
|
||||
|
||||
### Task 22: Zusammenführen und Gesamtdurchlauf
|
||||
|
||||
**Files:**
|
||||
- Merge in `feature/unterkunft`: `feature/uk-server`, `feature/uk-mail`, `feature/uk-widget`, `feature/uk-ui` (in dieser Reihenfolge; Konflikte nur in `server/routes/unterkunft.js`, `unterkunftSeite.js` erwartet)
|
||||
- Create: `test/ui/unterkunftTestServer.mjs` (Muster `shopTestServer.mjs`), `test/ui/unterkunft.ui.mjs`
|
||||
|
||||
- [ ] **Step 1:** Merges, danach alle `test/unterkunft*.test.js`, `test/buchungen*.test.js` und die in Tasks 1–21 genannten Bestands-Tests einzeln nacheinander grün.
|
||||
- [ ] **Step 2:** Abgleich: Stornomuster-Konstante in `buchungen-tab-einstellungen.js` = `docs/funktionen/unterkunft-storno-muster.md`; Stub-Reste (`_aufrufe` nur in Tests genutzt, 501-Fallback in Task 7 entfernen).
|
||||
- [ ] **Step 3: UI-Durchlauf** (`free -m` vorher): Widget auf Demo-Seite → Zeitraum + Anfrage → Portal „Anfragen“ → Posten ändern → Freigeben → Link aus Mail-Sink → Buchungsseite „vor Ort“ → „Zahlungspflichtig buchen“ → Rechnung-PDF abrufbar → Portal „bezahlt (vor Ort)“ → zweite Anfrage online → Fake-Stripe-Zahlung → `bezahlt` → Stornieren mit Teilerstattung. Screenshots nach `/root/reports/buchungskalender-2026-10-07/ui/`.
|
||||
- [ ] **Step 4: Commit** `Buchungskalender: Zusammenführung und UI-Durchlauf`.
|
||||
|
||||
### Task 23: Gesamtreview
|
||||
|
||||
- [ ] **Step 1:** Ein Review-Agent (sonnet) prüft den ganzen Branch gegen Spec + Review Focus; Funde beheben, je Fund ein Commit.
|
||||
- [ ] **Step 2:** Ledger-Abschluss, Memory-Notiz aktualisieren, dem Inhaber Livegang-Notiz verlinken.
|
||||
@@ -291,3 +291,4 @@ Abschluss: Zusammenführen, Gesamttests, Review, UI-Durchlauf.
|
||||
- **N5:** Datenschutz-Abschnitt bleibt § 6f (6e ist für Öffnungszeiten reserviert, noch nicht live).
|
||||
- **N6:** Kein Textform-Nachweis (nicht in `TEXTFORM_KEYS`); Mimi wird im WOMS über „Freischalten“ (`action:'activate'`)
|
||||
ohne Zahlung freigeschaltet.
|
||||
- **N7:** Kein Jahres-ZIP der Rechnungen (YAGNI); Einzel-PDF-Download je Rechnung. Bei Abschaltung Hinweis-Mail mit der Bitte, die PDFs herunterzuladen.
|
||||
Reference in new issue
Block a user