21 KiB
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, Branchfeature/finanzenvonorigin/test,node_modulesals Symlink auf../build/node_modules.node_modulesist in Git getrackt: nur einzelne Dateien adden, niegit 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 nachsrc/lib/(ohne Appwrite-Import), damitloadModulesie bündeln kann. - Build-Prüfung unter Speichergrenze:
systemd-run --scope -p MemoryMax=1500M -p MemorySwapMax=0 npm run buildim Worktree. - Portal-Basis-URL:
import.meta.env.VITE_PROJECT_ADMIN_URL || 'https://project.webklar.com'; AuthAuthorization: Bearer <JWT>übercreateJwtCache+fetchWithRetryaussrc/lib/serverpulsAuth.js. KeinPUT(Portal-CORS erlaubt GET, POST, PATCH, DELETE). - Nur Admins (
isAdminaususeAuth()) sehen Menüpunkt und Seiten; Nicht-Admins werden auf/ticketsumgeleitet. - Darstellung: Beträge de-DE mit
€, rechtsbündig, Klasse mitfont-variant-numeric: tabular-nums; DatenTT.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-integrationsbleiben 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/rueckkehrgehen; 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 wirftError('Ungültiger Betrag'); erlaubt Tausenderpunkte und Dezimalkomma, keine negativen WerteINVOICE_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 }–customerStatuslead/lost→ „Noch kein fester Kunde“; ohneemail→ „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 werfenError(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.mjsmit allen Beispielwerten oben (inkl. Review-Focus-Fälle fürparseEuroundcanInvoice). - Step 3: Run
npm test– Expected: FAIL (Modul fehlt). - Step 4: Implement beide Dateien (
financeApi.jsohne Test, folgt dem Muster vonserverpulsApi.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 inProtectedRoute+AdminRoute) - Modify:
src/components/Navbar.jsx:64(Eintrag „Finanzen“ nur beiisAdmin) - 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 wedercodenocherror);error_descriptionwird anerrorangehängt („access_denied: …“).BankCallbackPage: liestlocation.search, ruftcompleteBankAuthgenau 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 danachcode/stateaus 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 testund 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 }übergetOverview) - Modify:
src/styles/global.css(Klassen.fin-amountmittabular-numsund 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“ ruftrefreshBank, zeigt 429-Meldung des Servers als Hinweis.LiquidityHead({ kopf })– dreiKpi: „Kontostand“ (euroWhole), „Verfügbar nach Rücklagen“ (euroWhole, Unterzeile „Aufschlüsselung“ klapptkontostand − Steuer-Rücklage − Fixkosten 30 Tage − Mindestpufferals 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 inFinancePagebehandelt:bank-verbinden→startBankAuth()undwindow.location.assign(url);bank-aktualisieren→ Refresh;rechnung-oeffnen→hostedUrlin neuem Tab bzw. Sprung zum Rechnungsblock;zuordnen→ öffnet Zuordnen-Dialog (Task 5);kunde-oeffnen→/customers/:id;fixkosten-uebernehmen→ Bestätigung, danncreateFixedCost({ 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 testund 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); daruntervorschau.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 wenngewerbesteuerHinweis, 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 testund 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ädtgetInvoices({ status, kunde: customerId }); Filter.segOffen/Ü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, nursource 'projekt'); bezahlt → „PDF“, beioutOfBandHinweis „per Überweisung“. Knopf „Rechnung schreiben“.InvoiceCreateModal({ open, onClose, onCreated, fixedCustomer = null })– Kunde (AutocompleteInput über Kunden mitcanInvoice(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, beirechnungmit 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, ausgetFixedCosts), 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ßerrechnung) Knopf „Zuordnung lösen“. Serverfehler (409 Betrag weicht ab usw.) im Dialog.
-
Step 1: Implement.
-
Step 2: Run
npm testund 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-156entfernen; Karte „Rechnungsdaten (für InvoiceNinja)“ → „Rechnungsdaten“; Badge „verknüpft“ entfernen;syntheticTicketentfernen) - Modify:
src/components/TicketCard.jsx(TabinvoicesundInvoicePanelentfernen) - Modify:
src/pages/CustomersPage.jsx(Untertitel „Sync mit InvoiceNinja“ und Badge „Ninja“ entfernen) - Modify:
src/lib/leads.js(ninjaPayload,syncCustomerToNinjaund den Ninja-Teil vonupgradeToCustomerentfernen;isInvoiceReadyentfernen, falls danach unbenutzt) - Delete:
src/components/InvoicePanel.jsx,src/hooks/useInvoices.js,src/hooks/useFinanceSummary.js,src/lib/integrationsApi.js(nur wenngrep -rn integrationsApi srcdanach leer ist)
Interfaces:
-
Produces:
FinanceSettingsPage(/finance/einstellungen): Formular Einstellungen (Mindestpuffer und Vorjahresumsatz/ESt-Vorauszahlung in Euro überparseEuro, 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 →EmptyStatemit 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 testund Build – Expected: PASS. -
Step 4: Commit (geänderte und gelöschte Dateien einzeln:
git add <dateien>undgit 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/mitfixtures.json(Übersicht, Rechnungen, Buchungen aus Plan-2-Testdaten) undsmoke.mjs
Interfaces:
-
Consumes: gebautes
dist/aus dem Worktree. -
Step 1: Write
smoke.mjs: statischer Server fürdist/(SPA-Fallback aufindex.html) unter einer festen Herkunft; Playwright (headless shell, untersystemd-run --scope -p MemoryMax=700M) mitpage.routefür**/v1/account*(Admin-Nutzer mit Labeladmin),**/v1/account/jwt*undhttps://project.webklar.com/api/admin/finanzen/**ausfixtures.json. Prüfungen:/financezeigt „Kontostand“ mit „109 €“, „Zu erledigen“ mit der Aufgabe „Zahlungseingang zuordnen“, Vorschau-Annahmezeile, Rechnungsfilter „Offen“.- 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). - Übersicht antwortet 403 → Fehlerkarte mit „Erneut versuchen“, kein leerer Bildschirm.
- Nutzer ohne Label
admin→/financelandet auf/tickets, Menüpunkt „Finanzen“ fehlt. /finanzen/bank-callback?state=s&code=c→ genau einPOST …/bank/rueckkehr, Text „Bankzugang erneuert, gültig bis 29.03.2027“.- 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, Commitgit 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.pemund.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-serverergänzen: Volume- ./secrets/enablebanking:/run/secrets/enablebanking:ro; Umgebung- ENABLE_BANKING_APP_ID=b2582270-11bf-4489-affb-1a151f076c07und- ENABLE_BANKING_KEY_PATH=/run/secrets/enablebanking/enablebanking.pem. Prüfen:docker compose config -qohne Fehler. - Step 3: Portal live (OK des Nutzers):
git -C /home/webklar/apps/kundenbereich.webklar.com/dev-finanzen push origin feature/finanzen:main; warten, bisbuild/auf dem neuen Stand ist (git -C …/build log -1); danncd /home/webklar/apps && docker compose up -d kundenbereich-server(übernimmt Volume und Umgebung). Erwartet:docker logs webklar-kundenbereich-server --since 2mzeigt 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 inticket.webklar.com/outenthä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
adminhat (sonst Nutzer fragen). - Step 8: Merken: Memory-Datei
webklar-finanzen.mdmit Livestand, Commits und offenen Punkten aktualisieren (Enable-Banking-Vertrag, Kundenportal-Tab „Rechnungen“ noch InvoiceNinja).