Files
Webklar-Kundenbereich/docs/superpowers/specs/2026-10-01-finanzen-design.md
T

197 lines
14 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: Stripe-Rechnungen und Finom-Konto im Ticketsystem
Stand: 2026-10-01 · Status: Entwurf zur Freigabe
Grundlage: Recherche `/root/reports/Finanzübersicht für kleine Webagentur.md` (Notizen in
`/root/research_notes/Finanzübersicht für kleine Webagentur/`) und die Machbarkeitsprüfung vom
30.09./01.10.2026 (Enable Banking produktiv getestet: Finom-Konto, Salden, Umsätze).
## Ziel
Die Seite „Finanzen“ im Ticketsystem (WOMS, `ticket.webklar.com/finance`) zeigt nicht mehr
InvoiceNinja, sondern **Stripe-Rechnungen** und das **Finom-Geschäftskonto**. Sie beantwortet beim
wöchentlichen Blick drei Fragen: Wie viel Geld ist wirklich frei? Was muss ich heute tun? Reicht das
Geld die nächsten 30 Tage? Projektrechnungen werden direkt aus WOMS über Stripe geschrieben und
verschickt.
Erfolg: Kein Zahlungseingang bleibt unzugeordnet, keine überfällige Rechnung bleibt unbemerkt, und
der Bankzugang läuft nie unbemerkt ab.
## Feststehende Rahmenbedingungen (geprüft)
- **WEBklar GbR, Kleinunternehmer nach § 19 UStG** (Impressum, Stripe-Rechnungsfuß im Portal). Daraus
folgt: keine Umsatzsteuer auf Rechnungen, keine USt-Voranmeldung, keine USt-Rücklage, **keine
E-Rechnungspflicht beim Ausstellen** – das Stripe-PDF genügt. Einkommensteuer zahlen die
Gesellschafter privat; Gewerbesteuer erst über 24.500 € Gewinn.
- **Bank:** Enable Banking, App „WEBklar Finanzen“, ID `b2582270-11bf-4489-affb-1a151f076c07`,
Produktion, nur Kontoinformationen (AIS). Schlüssel auf dem Server erzeugt
(`/home/webklar/apps/ticket-integrations/enablebanking.pem`, verlässt den Server nie). Sitzung zum
Finom-Konto „Webklar“ besteht, gültig bis **29.03.2027**; Sitzungs-ID und Konto-UID in
`/home/webklar/apps/ticket-integrations/enablebanking-session.txt`. Finom liefert bei Eingängen
Zahlername, IBAN und Verwendungszweck; Kartenzahlungen haben eine Händlerkategorie (MCC).
PSD2-Grenzen: etwa 4 automatische Abrufe pro Tag, Umsätze nur der letzten ~90 Tage, Zugang
höchstens 180 Tage.
- **Enable Banking ohne Vertrag ist nur zur Erprobung erlaubt** („solely for evaluation purposes or
for the personal use of private individuals“, Terms → Restriction of Use). Für den Dauerbetrieb
braucht es einen Vertrag (support.api@enablebanking.com). Bauen und testen ist gedeckt.
- **Stripe** ist im Kundenportal eingerichtet (Live-Schlüssel, Abos per Karte und SEPA-Lastschrift,
Rechnungsfuß § 19 UStG). Bisher hat noch keine Stripe-Auszahlung das Finom-Konto erreicht.
- **InvoiceNinja bleibt installiert** und unverändert in Betrieb, verschwindet aber aus der
WOMS-Oberfläche. Einzige relevante Altrechnung: 0005 (EWV Software GmbH, 168,79 €), am 05.08.2026
per Überweisung bezahlt und in InvoiceNinja seit 01.10.2026 als bezahlt markiert.
- Die Kundenstammdaten (`customers` in `woms-database`) teilen sich WOMS und Portal; Stripe-Kunden
hängen über `metadata.webklarCustomerId` daran (`getOrCreateStripeCustomer` im Portal).
## Entscheidungen (Standardwerte zur Freigabe)
1. **Backend im Kundenportal-Server** unter `/api/admin/finanzen/*`, geschützt mit
`requireAppwriteStaff` (Appwrite-JWT, Label `admin`, Sitzung mit zweitem Faktor). Dort liegen schon
Stripe, Jobs, Mails, Appwrite-Setup-Skripte und Tests. `ticket-integrations` bleibt unverändert.
2. **Nur Admins** sehen „Finanzen“ (Menüpunkt und Seiten). Justin braucht als Gesellschafter das Label
`admin` (Prüfung beim Livegang).
3. **Schlüssel** wandert nach `/home/webklar/apps/secrets/enablebanking/` und wird schreibgeschützt in
den Portal-Container eingebunden. Die Sitzungs-ID steht in Appwrite (ohne Schlüssel wertlos).
4. **Bankdaten werden dauerhaft gespeichert** (Collection `bankTransactions`), weil die Bank nur 90 Tage
liefert. Abruf automatisch alle 6 Stunden (4× täglich) und per Knopf „Aktualisieren“ (höchstens alle
10 Minuten; mit PSU-Kopfzeilen, damit es nicht auf das Tageslimit zählt).
5. **Bankzugang erneuern** direkt aus WOMS: Knopf → Finom-Freigabe → Rückkehr auf
`https://ticket.webklar.com/finanzen/bank-callback` (bereits als Redirect-URL registriert).
Hinweis in „Zu erledigen“ ab 30 Tagen vor Ablauf, Team-Mail (Vorlage T18) bei 30, 14 und 3 Tagen.
6. **Projektrechnungen** entstehen in WOMS über Stripe: Kunde wählen, Positionen (Beschreibung, Menge,
Einzelpreis), Zahlungsziel 14 Tage, Versand per Stripe-Mail mit PDF und Zahlungsseite. Zahlarten
stellt der Nutzer im Stripe-Dashboard ein (Empfehlung: Karte und SEPA-Lastschrift). Im Rechnungsfuß
stehen der § 19-Hinweis und die Bankverbindung mit der Bitte, die Rechnungsnummer als
Verwendungszweck anzugeben. Entwürfe lassen sich löschen, versendete Rechnungen nur stornieren.
7. **Zahlungsabgleich als Vorschlag mit Bestätigung**, nie still automatisch: Eine Bestätigung markiert
die Stripe-Rechnung „außerhalb von Stripe bezahlt“ (`paid_out_of_band`, nicht umkehrbar) und merkt
sich die Bankbuchung. Stripe-Auszahlungen werden der Auszahlung zugeordnet (Umbuchung, kein Umsatz).
Teilzahlungen werden nicht unterstützt (Hinweis: in Stripe direkt erfassen).
8. **Fixkosten** pflegt der Nutzer als Liste (Name, Betrag, Rhythmus, nächstes Datum, Suchbegriff für die
Gegenpartei). Erkannte wiederkehrende Ausgaben werden nur vorgeschlagen (mindestens 3 Buchungen).
9. **Vorschau 30 Tage** mit zwei Linien: „nur sicher“ (Kontostand, unterwegs befindliche
Stripe-Auszahlungen, Fixkosten, ESt-Vorauszahlungen) und „erwartet“ (zusätzlich offene Rechnungen
und Abo-Verlängerungen). Kein Konfidenzkegel, eine Annahmezeile in Klartext.
10. **Steuer:** Rücklage = einstellbarer Prozentsatz (Standard 30 %) vom Gewinn des laufenden Jahres
(Einnahmen minus Ausgaben laut Bank). Kleinunternehmer-Zähler nach Zahlungseingang: laufendes Jahr
gegen 25.000 € (Status im Folgejahr) und 100.000 € (harte Grenze), Vorjahr als Einstellung.
ESt-Vorauszahlungen als optionale Einstellung (Betrag je Quartal, fällig 10.3./10.6./10.9./10.12.).
11. **Kundenansicht:** Der Kunden-Tab „Rechnungen“ zeigt Stripe-Rechnungen des Kunden und „Rechnung
schreiben“. Der Ticket-Tab „Rechnungen“ entfällt.
12. **Nicht im Umfang:** Kundenportal-Tab „Rechnungen“ (zeigt weiter InvoiceNinja, eigener Folgeschritt),
Stundenguthaben je Kunde, Projektmarge, Kundenkonzentration, Angebote, Teilzahlungen, E-Rechnung,
Ausgabenbelege, mehrere Bankkonten.
## Seitenaufbau (WOMS `/finance`)
Reihenfolge von oben nach unten; Blöcke 0–2 stehen im ersten Bildschirm. Beträge in de-DE
(`1.234,50 €`, rechtsbündig, `tabular-nums`), Daten `01.10.2026`, Farbe nie allein als Signal.
| Nr. | Block | Inhalt |
|---|---|---|
| 0 | Statuszeile | „Bank: Stand vor 3 Std. · jüngste Buchung 29.09. · Zugang bis 29.03.2027“, „Stripe: Stand …“; bei Fehler „Abruf fehlgeschlagen: …“; Knopf „Aktualisieren“ |
| 1 | Liquiditätskopf | Kontostand (gebucht), „Verfügbar nach Rücklagen“ (Kontostand − Steuer-Rücklage − Fixkosten der nächsten 30 Tage − Mindestpuffer; negativ erlaubt; Aufschlüsselung per Klick), „Tiefster Stand in 30 Tagen“ (nur sicher / erwartet, jeweils mit Datum) |
| 2 | Zu erledigen | Höchstens 7 Zeilen, je mit genau einer Aktion; Leerzustand „Nichts offen“ |
| 3 | Vorschau 30 Tage | Tabelle nach Datum: Posten, sicher/erwartet, Betrag, laufender Saldo beider Linien; Annahmezeile |
| 4 | Steuer | Rücklage (Betrag, Prozentsatz, Gewinn lfd. Jahr), Kleinunternehmer-Zähler, nächste ESt-Vorauszahlung (falls eingestellt) |
| 5 | Abos | Monatliche Abo-Einnahmen (aktive + `past_due`, Jahrespreise ÷ 12), Anzahl zahlender Kunden, gefährdete Einnahmen (`past_due`), Liste der Abos |
| 6 | Rechnungen | Filter Offen / Überfällig / Bezahlt / Entwürfe / Alle; Altersgruppen (nicht fällig, 1–30, 31–60, über 60 Tage) mit Summe und Anzahl; Tabelle Nr., Kunde, Datum, Fällig, Betrag, Offen, Status, Aktionen (PDF, Zahlungsseite, Senden, Stornieren, Löschen); Knopf „Rechnung schreiben“ |
| 7 | Buchungen | Alle gespeicherten Buchungen, neueste zuerst, 50 je Seite: Datum, Gegenpartei + Verwendungszweck, Betrag mit Vorzeichen, Zuordnung (Rechnung, Stripe-Auszahlung, Fixkosten, Einnahme ohne Stripe-Rechnung, Privat/Umbuchung, Sonstiges, offen) |
Unterseite `/finance/einstellungen`: Mindestpuffer, Rücklage-Prozentsatz, Vorjahresumsatz,
ESt-Vorauszahlung je Quartal, Bankverbindungstext für den Rechnungsfuß, Fixkostenliste, Bankzugang
(Status, „Neu verbinden“).
Mobil: Blöcke gestapelt in derselben Reihenfolge, Tabellen als zweizeilige Zeilen.
### „Zu erledigen“ – Arten
| Art | Wann | Aktion | Dringlichkeit |
|---|---|---|---|
| `bank-zugang` | Zugang läuft in ≤ 30 Tagen ab oder ist abgelaufen | Neu verbinden | hoch ab ≤ 14 Tagen |
| `bank-abruf` | Letzter erfolgreicher Abruf älter als 24 h | Aktualisieren | mittel |
| `ueberfaellig` | Offene Rechnung, Fälligkeit < heute | Zahlungsseite öffnen / Erinnerung über Stripe senden | hoch ab 30 Tagen (Verzug) |
| `zuordnung` | Eingang (Betrag > 0) ohne Zuordnung | Zuordnen (mit Vorschlag, falls vorhanden) | mittel |
| `abo-zahlung` | Abo im Status `past_due` oder `unpaid` | Kunde öffnen | hoch |
| `entwurf` | Rechnungsentwurf älter als 3 Tage | Öffnen | info |
| `fixkosten-vorschlag` | Wiederkehrende Ausgabe erkannt, nicht in der Fixkostenliste | Übernehmen | info |
| `kleinunternehmer` | Einnahmen lfd. Jahr ≥ 70 % von 25.000 € | Einstellungen | mittel, hoch ab 100 % |
Sortierung: hoch vor mittel vor info, innerhalb nach Datum (älteste zuerst).
## Datenmodell (Appwrite, `woms-database`)
- `bankConnections` – ein Dokument `finom`: `status` (`aktiv` | `abgelaufen` | `fehler`), `sessionId`, `accountUid`, `accountIdHash`, `ibanMasked`,
`iban`, `validUntil` (ISO), `lastSyncAt`, `lastSyncOk` (bool), `lastError`, `lastManualSyncAt`,
`balanceBookedCents`, `balanceAvailableCents`, `balanceDate`, `latestBookingDate`, `authState`,
`authStartedAt`, `warnedThresholds` (z. B. `30,14`).
- `bankTransactions` – Dokument-ID = erste 36 Zeichen des SHA-256 aus `entry_reference` (sonst
`transaction_id`, sonst Datum|Betrag|Gegenpartei|Verwendungszweck): `accountUid`, `status`
(`BOOK`/`PDNG`), `bookingDate`, `valueDate` (`YYYY-MM-DD`), `amountCents` (Integer, Eingang +,
Ausgang −), `currency`, `counterpartyName`, `counterpartyIban`, `remittance`, `mcc`, `entryReference`,
`category` (leer | `rechnung` | `rechnung-extern` | `stripe-auszahlung` | `fixkosten` | `privat` |
`sonstiges`), `matchedInvoiceId`, `matchedInvoiceNumber`, `matchedPayoutId`, `fixedCostId`, `note`,
`syncedAt`.
- `financeSettings` – ein Dokument `main`: `minBufferCents` (0), `taxReservePercent` (30),
`previousYearRevenueCents` (leer), `estPrepaymentCents` (0), `bankDetailsText`.
- `financeFixedCosts`: `name`, `amountCents` (positiv), `interval` (`monat` | `quartal` | `jahr`),
`nextDate`, `counterpartyMatch`, `active`.
Stripe-Rechnungen, -Auszahlungen und Abos werden **nicht** gespiegelt: Rechnungen und Auszahlungen
liest der Server live (Zwischenspeicher 60 s, nach eigenen Änderungen geleert), Abos kommen aus der
vorhandenen Collection `subscriptions`.
## Schnittstellen (`/api/admin/finanzen`)
| Methode, Pfad | Zweck |
|---|---|
| GET `/uebersicht` | Alles für Blöcke 0–6 in einer Antwort (Status, Kopf, Aufgaben, Vorschau, Steuer, Abos, Rechnungs-Altersgruppen) |
| GET `/rechnungen?status=&kunde=` | Stripe-Rechnungen (status: `offen`, `ueberfaellig`, `bezahlt`, `entwurf`, `alle`) |
| POST `/rechnungen` | Rechnung anlegen (`{ customerId, items, daysUntilDue, memo, senden }`) |
| POST `/rechnungen/:id/senden` | Entwurf festschreiben und senden |
| POST `/rechnungen/:id/stornieren` | Offene Rechnung stornieren |
| DELETE `/rechnungen/:id` | Entwurf löschen |
| GET `/buchungen` | Gespeicherte Buchungen (höchstens 1000, neueste zuerst; die Oberfläche blättert in 50ern) mit Zuordnungsvorschlägen für offene Eingänge |
| POST `/buchungen/:id/zuordnen` | `{ invoiceId }` oder `{ payoutId }` oder `{ fixedCostId }` oder `{ category }` |
| DELETE `/buchungen/:id/zuordnung` | Zuordnung lösen (nicht bei Rechnungen, die schon als bezahlt markiert sind) |
| POST `/bank/aktualisieren` | Sofortabruf |
| POST `/bank/verbinden` | Freigabe starten → `{ url }` |
| POST `/bank/rueckkehr` | `{ code, state }` oder `{ error, state }` → neue Sitzung |
| GET, PATCH `/einstellungen` | Einstellungen |
| GET, POST `/fixkosten`; PATCH, DELETE `/fixkosten/:id` | Fixkostenliste |
## Zuordnungsregeln (Vorschläge, höchstens 3 je Buchung)
1. **Stripe-Auszahlung (hoch):** Gegenpartei oder Verwendungszweck enthält „stripe“, Betrag = Betrag
einer Auszahlung, Buchungsdatum höchstens 3 Tage von `arrival_date` entfernt.
2. **Rechnungsnummer (hoch / mittel):** normalisierter Verwendungszweck (Großbuchstaben, ohne
Leerzeichen und Bindestriche) enthält die normalisierte Stripe-Rechnungsnummer. Betrag = offener
Betrag → hoch, sonst mittel mit Hinweis „Betrag weicht ab“.
3. **Betrag und Name (mittel):** Betrag = offener Betrag und ein Wort (≥ 4 Zeichen, ohne GmbH, UG, GbR,
AG, KG, e.K., und, der, die) aus Firmen- oder Kundenname kommt im Zahlernamen vor.
4. **Nur Betrag (niedrig):** Betrag = offener Betrag genau einer offenen Rechnung.
Bestätigen mit Rechnung ist nur möglich, wenn der Betrag dem offenen Betrag entspricht.
## Fehlerverhalten
- Bankabruf scheitert: alte Daten bleiben sichtbar und werden als veraltet markiert; `lastError` in der
Statuszeile; kein Datenverlust.
- Zugang abgelaufen (Enable Banking 401/403 oder Sitzung `EXPIRED`): Statuszeile + Aufgabe
`bank-zugang` (hoch), Abrufe pausieren bis zur Neuverbindung.
- Stripe nicht erreichbar: Übersicht liefert Bankteil trotzdem; Stripe-Blöcke zeigen „Stripe nicht
erreichbar“.
- Rückkehr mit falschem `state` oder Fehler von Finom: Meldung auf der Rückkehrseite, bestehende Sitzung
bleibt gültig.
## Livegang (Reihenfolge)
1. Schlüssel nach `secrets/enablebanking/` verschieben, Compose: Mount und Umgebungsvariablen
`ENABLE_BANKING_APP_ID`, `ENABLE_BANKING_KEY_PATH`.
2. Setup-Skript legt Collections an und übernimmt die bestehende Sitzung (kein neuer Finom-Login).
3. Portal `main` pushen (Nutzer-OK), Container neu starten, erster Abruf prüfen.
4. Stripe-Dashboard: Rechnungsnummern fortlaufend auf Kontoebene, Zahlarten für Rechnungen (Karte,
SEPA-Lastschrift) – Nutzer.
5. WOMS `test` pushen (Nutzer-OK).
6. Buchung „Rechnung 0005“ als „Einnahme ohne Stripe-Rechnung“ zuordnen.
7. Vor dem Dauerbetrieb: Vertrag mit Enable Banking (Nutzer).