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

21 KiB
Raw Permalink Blame History

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

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 2026“), Kleinunternehmer-Zeile „ 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):

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):
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).