Files
Webklar-Kundenbereich/docs/superpowers/plans/2026-10-01-finanzen-plan-3-woms-livegang.md
T

209 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Finanzen Plan 3: WOMS-Oberfläche und Livegang 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:** Die WOMS-Seite „Finanzen“ zeigt Stripe-Rechnungen und das Finom-Konto nach dem Seitenaufbau der Spec, Projektrechnungen werden in WOMS geschrieben, InvoiceNinja verschwindet aus der Oberfläche, und das Ganze geht kontrolliert live.
**Architecture:** WOMS spricht ausschließlich die Portal-Routen `/api/admin/finanzen/*` aus Plan 1 und 2 an (Appwrite-JWT, wiederverwendeter JWT-Cache wie Serverpuls). Reine Formatierungs- und Parse-Logik in `src/lib/finance*.js` (mit `node --test` geprüft), Darstellung in `src/components/finance/*` und drei Seiten.
**Tech Stack:** React 18 + Vite (WOMS), vorhandene UI-Bausteine `src/components/ui` (PageHeader, Card, Button, Badge, Tabs, EmptyState, Field, Kpi, AutocompleteInput), `styles/global.css`, `node --test` mit `tests/unit/load.mjs`.
**Spec:** `docs/superpowers/specs/2026-10-01-finanzen-design.md` (im Portal-Repo, Worktree `kundenbereich.webklar.com/dev-finanzen`)
## Global Constraints
- WOMS-Worktree: `/home/webklar/apps/ticket.webklar.com/dev-finanzen`, Branch `feature/finanzen` von `origin/test`, `node_modules` als Symlink auf `../build/node_modules`. `node_modules` ist in Git getrackt: **nur einzelne Dateien adden, nie `git add -A`**. **Nie pushen** ohne OK des Nutzers (der Webhook stellt jede gepushte Branch live).
- Tests: `npm test` (`node --test tests/*.test.mjs tests/unit/*.test.mjs`) grün vor jedem Commit. Reine Logik gehört nach `src/lib/` (ohne Appwrite-Import), damit `loadModule` sie bündeln kann.
- Build-Prüfung unter Speichergrenze: `systemd-run --scope -p MemoryMax=1500M -p MemorySwapMax=0 npm run build` im Worktree.
- Portal-Basis-URL: `import.meta.env.VITE_PROJECT_ADMIN_URL || 'https://project.webklar.com'`; Auth `Authorization: Bearer <JWT>` über `createJwtCache` + `fetchWithRetry` aus `src/lib/serverpulsAuth.js`. Kein `PUT` (Portal-CORS erlaubt GET, POST, PATCH, DELETE).
- Nur Admins (`isAdmin` aus `useAuth()`) sehen Menüpunkt und Seiten; Nicht-Admins werden auf `/tickets` umgeleitet.
- Darstellung: Beträge de-DE mit `€`, rechtsbündig, Klasse mit `font-variant-numeric: tabular-nums`; Daten `TT.MM.JJJJ`; nie Farbe allein (immer Text oder Symbol dazu); keine Prozent-Veränderungen, keine Diagramme außer optionaler Treppenlinie (nicht in diesem Plan).
- Texte für Nutzer auf Deutsch mit echten Umlauten.
- InvoiceNinja-Server und `ticket-integrations` bleiben unangetastet.
- Seitenblöcke, Aufgabenarten und Zuordnungsregeln exakt wie in der Spec.
## Review Focus
- **Rückkehrseite doppelt geladen** (React StrictMode, Neuladen, Zurück-Taste): der Code darf nur einmal an `/bank/rueckkehr` gehen; zweiter Versuch zeigt die Serverantwort verständlich statt „Fehler“. Test in Task 2 (`parseCallback` + Einmal-Schutz in der Seite).
- **Betragseingabe** „1.234,50“, „50“, „50,5“, „ 12 “, „-5“, „abc“ → 123450, 5000, 5050, 1200, Fehler, Fehler. Test in Task 1.
- **Portal nicht erreichbar oder 403** (Admin ohne 2FA-Sitzung): Seite zeigt eine Fehlerkarte mit Text und „Erneut versuchen“, kein weißer Bildschirm. Test in Task 7 (Smoke).
- **Unumkehrbare Zuordnung:** Bestätigen einer Rechnungszuordnung verlangt einen zweiten Klick mit dem Hinweis „wird in Stripe als bezahlt markiert, nicht umkehrbar“. Test in Task 7 (Smoke).
- **Kunde ohne E-Mail:** „Rechnung schreiben“ für diesen Kunden ist gesperrt mit Hinweis „E-Mail fehlt“ (Stripe verschickt per Mail). Test in Task 6 (`canInvoice`).
---
### Task 1: Formatierung, Parser und API-Client
**Files:**
- Create: `src/lib/financeFormat.js`
- Create: `src/lib/financeApi.js`
- Test: `tests/unit/financeFormat.test.mjs`
**Interfaces:**
- Produces (`financeFormat.js`, rein):
- `euro(cents) → string` (`euro(123456) === '1.234,56 €'`, `euro(null) === '–'`)
- `euroWhole(cents) → string` (`euroWhole(10893) === '109 €'`)
- `signedEuro(cents) → string` (`'+168,79 €'`, `'−4,49 €'` mit U+2212)
- `dateDe('2027-03-29') === '29.03.2027'`; `sinceLabel(iso, now) → 'gerade eben' | 'vor 12 Min.' | 'vor 3 Std.' | 'vor 2 Tagen'`
- `parseEuro(text) → number` (Cent) oder wirft `Error('Ungültiger Betrag')`; erlaubt Tausenderpunkte und Dezimalkomma, keine negativen Werte
- `INVOICE_STATUS = { entwurf: { label: 'Entwurf', tone: 'muted' }, offen: { label: 'Offen', tone: 'info' }, ueberfaellig: { label: 'Überfällig', tone: 'warn' }, bezahlt: { label: 'Bezahlt', tone: 'ok' }, storniert: { label: 'Storniert', tone: 'muted' }, uneinbringlich: { label: 'Uneinbringlich', tone: 'danger' } }`
- `CATEGORY_LABEL = { rechnung: 'Rechnung', 'rechnung-extern': 'Einnahme ohne Stripe-Rechnung', 'stripe-auszahlung': 'Stripe-Auszahlung', fixkosten: 'Fixkosten', privat: 'Privat/Umbuchung', sonstiges: 'Sonstiges', '': 'Offen' }`
- `URGENCY = { hoch: { label: 'Dringend', tone: 'danger' }, mittel: { label: 'Bald', tone: 'warn' }, info: { label: 'Hinweis', tone: 'muted' } }`
- `AGE_GROUP_LABEL = { 'nicht-faellig': 'Nicht fällig', '1-30': '1–30 Tage', '31-60': '31–60 Tage', 'ueber-60': 'Über 60 Tage' }`
- `pageOf(list, page, size = 50) → { items, page, pages }`
- `canInvoice(customer) → { ok: boolean, reason?: string }` – `customerStatus` `lead`/`lost` → „Noch kein fester Kunde“; ohne `email` → „E-Mail fehlt“.
- Produces (`financeApi.js`): `getOverview()`, `getInvoices({ status, kunde })`, `createInvoice(body)`, `sendInvoice(id)`, `voidInvoice(id)`, `deleteInvoice(id)`, `getTransactions()`, `assignTransaction(id, target)`, `unassignTransaction(id)`, `getBank()`, `refreshBank()`, `startBankAuth()`, `completeBankAuth({ code, state, error })`, `getSettings()`, `patchSettings(patch)`, `getFixedCosts()`, `createFixedCost(body)`, `updateFixedCost(id, patch)`, `deleteFixedCost(id)` – Pfade und Methoden wie in Plan 1/2; Fehler werfen `Error(data.error || 'API-Fehler <status>')` mit `.status`.
- [ ] **Step 1: Worktree anlegen**
```bash
cd /home/webklar/apps/ticket.webklar.com && git -C build fetch -q origin && git -C build worktree add ../dev-finanzen -b feature/finanzen origin/test && ln -s ../build/node_modules dev-finanzen/node_modules
```
- [ ] **Step 2: Write the failing tests** in `tests/unit/financeFormat.test.mjs` mit allen Beispielwerten oben (inkl. Review-Focus-Fälle für `parseEuro` und `canInvoice`).
- [ ] **Step 3: Run** `npm test` – Expected: FAIL (Modul fehlt).
- [ ] **Step 4: Implement** beide Dateien (`financeApi.js` ohne Test, folgt dem Muster von `serverpulsApi.js`).
- [ ] **Step 5: Run** `npm test` – Expected: PASS.
- [ ] **Step 6: Commit** `git add src/lib/financeFormat.js src/lib/financeApi.js tests/unit/financeFormat.test.mjs && git commit -m "Finanzen: Formatierung, Betragsparser und Portal-API-Client"`
### Task 2: Zugriff nur für Admins, Routen, Rückkehrseite
**Files:**
- Create: `src/components/AdminRoute.jsx`
- Create: `src/lib/bankCallback.js`
- Create: `src/pages/BankCallbackPage.jsx`
- Modify: `src/App.jsx` (Routen `/finance`, `/finance/einstellungen`, `/finanzen/bank-callback`, alle in `ProtectedRoute` + `AdminRoute`)
- Modify: `src/components/Navbar.jsx:64` (Eintrag „Finanzen“ nur bei `isAdmin`)
- Test: `tests/unit/bankCallback.test.mjs`
**Interfaces:**
- Produces:
- `AdminRoute({ children })` – wartet auf geladenen Nutzer, sonst `<Navigate to="/tickets" replace />` für Nicht-Admins.
- `parseCallback(search) → { code, state, error } | null` (`null`, wenn weder `code` noch `error`); `error_description` wird an `error` angehängt („access_denied: …“).
- `BankCallbackPage`: liest `location.search`, ruft `completeBankAuth` genau einmal (`useRef`-Schutz), zeigt „Bankzugang wird eingerichtet …“, dann Erfolg „Bankzugang erneuert, gültig bis TT.MM.JJJJ“ oder die Fehlermeldung des Servers, jeweils mit Knopf „Zu den Finanzen“ (`/finance`). Entfernt danach `code`/`state` aus der Adresszeile (`navigate('/finanzen/bank-callback', { replace: true, state: result })`).
- [ ] **Step 1: Write the failing tests:** `parseCallback('?state=a&code=b') → { code: 'b', state: 'a', error: '' }`; `'?state=a&error=access_denied&error_description=abgebrochen' → error 'access_denied: abgebrochen'`; `'' → null`.
- [ ] **Step 2: Run** `npm test` – Expected: FAIL.
- [ ] **Step 3: Implement** Dateien und Routen.
- [ ] **Step 4: Run** `npm test` und den Build-Befehl aus Global Constraints – Expected: PASS, Build ohne Fehler.
- [ ] **Step 5: Commit** `git commit -m "Finanzen: nur fuer Admins, Rueckkehrseite fuer die Bankfreigabe"`
### Task 3: Seitenkopf – Statuszeile, Liquidität, Zu erledigen
**Files:**
- Modify: `src/pages/FinancePage.jsx` (komplett neu, InvoiceNinja-Inhalt entfällt)
- Create: `src/components/finance/StatusLine.jsx`, `LiquidityHead.jsx`, `TodoList.jsx`
- Create: `src/hooks/useFinanceOverview.js` (`{ data, loading, error, reload }` über `getOverview`)
- Modify: `src/styles/global.css` (Klassen `.fin-amount` mit `tabular-nums` und rechtsbündig, `.fin-todo`, `.fin-status`; Farben nur über vorhandene Variablen)
**Interfaces:**
- Consumes: Task 1, Übersicht-Format aus Plan 2 Task 8.
- Produces:
- `StatusLine({ stand, onRefresh, refreshing })` – Text nach Spec Block 0; „Aktualisieren“ ruft `refreshBank`, zeigt 429-Meldung des Servers als Hinweis.
- `LiquidityHead({ kopf })` – drei `Kpi`: „Kontostand“ (`euroWhole`), „Verfügbar nach Rücklagen“ (`euroWhole`, Unterzeile „Aufschlüsselung“ klappt `kontostand − Steuer-Rücklage − Fixkosten 30 Tage − Mindestpuffer` als Liste auf), „Tiefster Stand in 30 Tagen“ (`sicher: … am TT.MM.` / `erwartet: … am TT.MM.`).
- `TodoList({ aufgaben, onAction })` – höchstens 7 Zeilen, danach „Alle N anzeigen“; je Zeile Badge (`URGENCY`), Titel, Text, ein Knopf. `onAction(aktion)` wird in `FinancePage` behandelt: `bank-verbinden` → `startBankAuth()` und `window.location.assign(url)`; `bank-aktualisieren` → Refresh; `rechnung-oeffnen` → `hostedUrl` in neuem Tab bzw. Sprung zum Rechnungsblock; `zuordnen` → öffnet Zuordnen-Dialog (Task 5); `kunde-oeffnen` → `/customers/:id`; `fixkosten-uebernehmen` → Bestätigung, dann `createFixedCost({ name, amountCents, interval, nextDate: <lastDate + Rhythmus>, counterpartyMatch: key })`; `einstellungen` → `/finance/einstellungen`. Leerzustand: `EmptyState` „Nichts offen“.
- `FinancePage`: Ladezustand, Fehlerkarte mit Meldung und „Erneut versuchen“, danach Blöcke 0–2.
- [ ] **Step 1: Implement** Komponenten, Hook, Seite (Blöcke 0–2).
- [ ] **Step 2: Run** `npm test` und Build – Expected: PASS.
- [ ] **Step 3: Commit** `git commit -m "Finanzen: Statuszeile, Liquiditaetskopf und Aufgabenliste"`
### Task 4: Vorschau, Steuer, Abos
**Files:**
- Create: `src/components/finance/ForecastTable.jsx`, `TaxCard.jsx`, `AboCard.jsx`
- Modify: `src/pages/FinancePage.jsx` (Blöcke 3–5; Desktop: Vorschau links, Steuer rechts; Abos halbe Breite)
**Interfaces:**
- Produces:
- `ForecastTable({ vorschau })` – Spalten Datum, Posten, „sicher“/„erwartet“ (Text, nicht nur Farbe), Betrag (`signedEuro`), Saldo sicher, Saldo erwartet (laufend ab Kontostand); darunter `vorschau.annahme`; leer → „Keine Zahlungen in den nächsten 30 Tagen bekannt.“
- `TaxCard({ steuer })` – Rücklage (`euro`, „30 % von <Gewinn> Gewinn 2026“), Kleinunternehmer-Zeile „<Betrag> von 25.000 € (2026)“ und „Grenze 100.000 €“, Vorjahr nur wenn gesetzt, Gewerbesteuer-Hinweis nur wenn `gewerbesteuerHinweis`, nächste ESt-Vorauszahlung nur wenn gesetzt.
- `AboCard({ abos })` – „Abo-Einnahmen pro Monat“, „zahlende Kunden“, „gefährdet“ (nur > 0), Liste Kunde · Produkt · Status · Betrag · nächste Abbuchung; leer → „Noch keine Abos.“
- [ ] **Step 1: Implement.**
- [ ] **Step 2: Run** `npm test` und Build – Expected: PASS.
- [ ] **Step 3: Commit** `git commit -m "Finanzen: Vorschau, Steuer und Abos"`
### Task 5: Rechnungen, Rechnung schreiben, Buchungen, Zuordnen
**Files:**
- Create: `src/components/finance/InvoiceTable.jsx`, `InvoiceCreateModal.jsx`, `TransactionTable.jsx`, `AssignDialog.jsx`
- Modify: `src/pages/FinancePage.jsx` (Blöcke 6–7, Dialoge)
**Interfaces:**
- Consumes: Task 1 (`financeApi`, `financeFormat`), `useCustomers` (vorhanden) für die Kundenauswahl.
- Produces:
- `InvoiceTable({ altersgruppen, customerId = null, onChanged })` – lädt `getInvoices({ status, kunde: customerId })`; Filter `.seg` Offen/Überfällig/Bezahlt/Entwürfe/Alle (Standard Offen); Altersgruppen-Zeile mit Anzahl und Summe; Spalten nach Spec Block 6; Aktionen je Status: Entwurf → „Senden“, „Löschen“ (mit Rückfrage); offen/überfällig → „Zahlungsseite“, „PDF“, „Stornieren“ (Rückfrage, nur `source 'projekt'`); bezahlt → „PDF“, bei `outOfBand` Hinweis „per Überweisung“. Knopf „Rechnung schreiben“.
- `InvoiceCreateModal({ open, onClose, onCreated, fixedCustomer = null })` – Kunde (AutocompleteInput über Kunden mit `canInvoice(c).ok`, gesperrte mit Grund), Positionen (Beschreibung, Menge, Einzelpreis in Euro → `parseEuro`), „+ Position“, Zahlungsziel (Standard 14 Tage), Notiz; Summe; Hinweis „Gemäß § 19 UStG ohne Umsatzsteuer.“; Knöpfe „Als Entwurf speichern“ und „Senden“ (`senden: true`); Serverfehler im Dialog anzeigen.
- `TransactionTable({ onAssign })` – `getTransactions()`, 50 je Seite (`pageOf`), neueste zuerst; Spalten Datum, Gegenpartei + Verwendungszweck (zweizeilig), Betrag (`signedEuro`), Zuordnung (`CATEGORY_LABEL`, bei `rechnung` mit Nummer); offene Eingänge mit stärkstem Vorschlag als Text und Knopf „Zuordnen“.
- `AssignDialog({ transaction, invoices, onClose, onDone })` – Auswahl: Vorschläge, andere offene Rechnung, Stripe-Auszahlung (aus Vorschlägen), Fixkosten (nur Ausgänge, aus `getFixedCosts`), Kategorie (Einnahme ohne Stripe-Rechnung nur bei Eingang, Privat/Umbuchung, Sonstiges), Notiz; bei Rechnung zweistufig: erst „Zuordnen“, dann Bestätigungstext „Die Rechnung <Nr.> wird in Stripe als bezahlt markiert. Das lässt sich nicht rückgängig machen.“ mit „Ja, als bezahlt markieren“; bei bestehender Zuordnung (außer `rechnung`) Knopf „Zuordnung lösen“. Serverfehler (409 Betrag weicht ab usw.) im Dialog.
- [ ] **Step 1: Implement.**
- [ ] **Step 2: Run** `npm test` und Build – Expected: PASS.
- [ ] **Step 3: Commit** `git commit -m "Finanzen: Rechnungen schreiben und verwalten, Buchungen zuordnen"`
### Task 6: Einstellungen, Kunden-Tab, InvoiceNinja aus der Oberfläche
**Files:**
- Create: `src/pages/FinanceSettingsPage.jsx`
- Create: `src/components/finance/CustomerInvoices.jsx`
- Modify: `src/pages/CustomerDetailPage.jsx` (Tab „Rechnungen“ → `CustomerInvoices`; Ninja-Sync beim Speichern `:133-156` entfernen; Karte „Rechnungsdaten (für InvoiceNinja)“ → „Rechnungsdaten“; Badge „verknüpft“ entfernen; `syntheticTicket` entfernen)
- Modify: `src/components/TicketCard.jsx` (Tab `invoices` und `InvoicePanel` entfernen)
- Modify: `src/pages/CustomersPage.jsx` (Untertitel „Sync mit InvoiceNinja“ und Badge „Ninja“ entfernen)
- Modify: `src/lib/leads.js` (`ninjaPayload`, `syncCustomerToNinja` und den Ninja-Teil von `upgradeToCustomer` entfernen; `isInvoiceReady` entfernen, falls danach unbenutzt)
- Delete: `src/components/InvoicePanel.jsx`, `src/hooks/useInvoices.js`, `src/hooks/useFinanceSummary.js`, `src/lib/integrationsApi.js` (nur wenn `grep -rn integrationsApi src` danach leer ist)
**Interfaces:**
- Produces:
- `FinanceSettingsPage` (`/finance/einstellungen`): Formular Einstellungen (Mindestpuffer und Vorjahresumsatz/ESt-Vorauszahlung in Euro über `parseEuro`, Rücklage in %, Bankverbindungstext) mit „Speichern“; Fixkostenliste (anlegen, bearbeiten, aktiv/inaktiv, löschen mit Rückfrage); Karte „Bankzugang“ (Status, IBAN maskiert, gültig bis, letzter Abruf, Fehler) mit „Jetzt abrufen“ und „Neu verbinden“; Link zurück zu `/finance`.
- `CustomerInvoices({ customer })` – `canInvoice(customer)`: nicht ok → `EmptyState` mit Grund und „Daten erfassen“ (wechselt zum Übersicht-Tab); ok → `InvoiceTable({ customerId })` + `InvoiceCreateModal({ fixedCustomer: customer })`.
- [ ] **Step 1: Implement** Seite und Komponente.
- [ ] **Step 2: Remove** InvoiceNinja-Stellen wie oben; danach `grep -rni "ninja\|invoicePanel\|useInvoices\|useFinanceSummary" src` – Expected: keine Treffer.
- [ ] **Step 3: Run** `npm test` und Build – Expected: PASS.
- [ ] **Step 4: Commit** (geänderte und gelöschte Dateien einzeln: `git add <dateien>` und `git rm <dateien>`) `git commit -m "Finanzen: Einstellungen, Rechnungen am Kunden, InvoiceNinja aus der Oberflaeche entfernt"`
### Task 7: Oberflächentest (Smoke) gegen Nachbau
**Files:**
- Create (nicht committen): Scratchpad `finanzen-ui/` mit `fixtures.json` (Übersicht, Rechnungen, Buchungen aus Plan-2-Testdaten) und `smoke.mjs`
**Interfaces:**
- Consumes: gebautes `dist/` aus dem Worktree.
- [ ] **Step 1: Write** `smoke.mjs`: statischer Server für `dist/` (SPA-Fallback auf `index.html`) unter einer festen Herkunft; Playwright (`headless shell`, unter `systemd-run --scope -p MemoryMax=700M`) mit `page.route` für `**/v1/account*` (Admin-Nutzer mit Label `admin`), `**/v1/account/jwt*` und `https://project.webklar.com/api/admin/finanzen/**` aus `fixtures.json`. Prüfungen:
1. `/finance` zeigt „Kontostand“ mit „109 €“, „Zu erledigen“ mit der Aufgabe „Zahlungseingang zuordnen“, Vorschau-Annahmezeile, Rechnungsfilter „Offen“.
2. Zuordnen eines Eingangs zu einer Rechnung: nach erstem Klick erscheint der Satz „lässt sich nicht rückgängig machen“, erst der zweite Klick sendet `POST …/zuordnen` (Anfrage mitzählen).
3. Übersicht antwortet 403 → Fehlerkarte mit „Erneut versuchen“, kein leerer Bildschirm.
4. Nutzer ohne Label `admin` → `/finance` landet auf `/tickets`, Menüpunkt „Finanzen“ fehlt.
5. `/finanzen/bank-callback?state=s&code=c` → genau ein `POST …/bank/rueckkehr`, Text „Bankzugang erneuert, gültig bis 29.03.2027“.
6. Breite 390 px: keine horizontale Scrollleiste auf `/finance`.
- [ ] **Step 2: Run** `node smoke.mjs` – Expected: alle 6 Prüfungen „ok“.
- [ ] **Step 3:** Gefundene Fehler in den Komponenten beheben, `npm test` + Build, Commit `git commit -m "Finanzen: Korrekturen aus dem Oberflaechentest"` (nur falls Änderungen).
### Task 8: Livegang (jeder Schritt mit OK des Nutzers)
**Files:**
- Modify: `/home/webklar/apps/docker-compose.yml` (Sicherung vorher: `docker-compose.yml.bak-2026-10-XX-finanzen`)
- Move: `/home/webklar/apps/ticket-integrations/enablebanking.pem` und `.pub` → `/home/webklar/apps/secrets/enablebanking/`
- [ ] **Step 1: Schlüssel verschieben** (Ordner 700, Schlüssel 600; Inhalt nie ausgeben):
```bash
install -d -m 700 /home/webklar/apps/secrets/enablebanking && mv /home/webklar/apps/ticket-integrations/enablebanking.pem /home/webklar/apps/ticket-integrations/enablebanking.pub /home/webklar/apps/secrets/enablebanking/ && chmod 600 /home/webklar/apps/secrets/enablebanking/enablebanking.pem
```
- [ ] **Step 2: Compose** beim Dienst `kundenbereich-server` ergänzen: Volume `- ./secrets/enablebanking:/run/secrets/enablebanking:ro`; Umgebung `- ENABLE_BANKING_APP_ID=b2582270-11bf-4489-affb-1a151f076c07` und `- ENABLE_BANKING_KEY_PATH=/run/secrets/enablebanking/enablebanking.pem`. Prüfen: `docker compose config -q` ohne Fehler.
- [ ] **Step 3: Portal live** (OK des Nutzers): `git -C /home/webklar/apps/kundenbereich.webklar.com/dev-finanzen push origin feature/finanzen:main`; warten, bis `build/` auf dem neuen Stand ist (`git -C …/build log -1`); dann `cd /home/webklar/apps && docker compose up -d kundenbereich-server` (übernimmt Volume und Umgebung). Erwartet: `docker logs webklar-kundenbereich-server --since 2m` zeigt den Start ohne Fehler; `curl -s https://project.webklar.com/api/health` → `ok: true`.
- [ ] **Step 4: Collections und Sitzung übernehmen** (Werte aus `ticket-integrations/enablebanking-session.txt`, ohne sie auszugeben):
```bash
F=/home/webklar/apps/ticket-integrations/enablebanking-session.txt; docker exec -w /app webklar-kundenbereich-server node scripts/setup-finanzen.mjs --session "$(sed -n 's/^session_id=//p' $F)" --account "$(sed -n 's/^account_uid=//p' $F)" --valid-until "$(sed -n 's/^valid_until=//p' $F)"
```
Erwartet: „Sitzung übernommen, gültig bis 29.03.2027“. Danach `enablebanking-session.txt` und `enablebanking-app-id.txt` löschen. Nach höchstens 2 Minuten (Job) oder per `POST /bank/aktualisieren`: 9 Buchungen, Kontostand 108,93 €.
- [ ] **Step 5: Stripe-Dashboard** (Nutzer): Rechnungsnummern fortlaufend auf Kontoebene; Zahlarten für Rechnungen Karte und SEPA-Lastschrift; in WOMS-Einstellungen den Bankverbindungstext (IBAN, BIC) eintragen.
- [ ] **Step 6: WOMS live** (OK des Nutzers): `git -C /home/webklar/apps/ticket.webklar.com/dev-finanzen push origin feature/finanzen:test`. Erwartet: neues Bundle in `ticket.webklar.com/out` enthält „Verfügbar nach Rücklagen“; Seite lädt für Kenso.
- [ ] **Step 7: Daten nachziehen:** Buchung „Rechnung 0005 vom 28.07.2026“ als „Einnahme ohne Stripe-Rechnung“ zuordnen; prüfen, dass Justin das Label `admin` hat (sonst Nutzer fragen).
- [ ] **Step 8: Merken:** Memory-Datei `webklar-finanzen.md` mit Livestand, Commits und offenen Punkten aktualisieren (Enable-Banking-Vertrag, Kundenportal-Tab „Rechnungen“ noch InvoiceNinja).