# Finanzen Plan 2: Stripe-Rechnungen, Zahlungsabgleich und Auswertung 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:** Der Portal-Server liest und schreibt Stripe-Rechnungen, schlägt Zuordnungen von Bankeingängen vor, verwaltet Einstellungen und Fixkosten und liefert unter `GET /api/admin/finanzen/uebersicht` alles, was die WOMS-Seite braucht (Kopf, Aufgaben, Vorschau, Steuer, Abos, Altersgruppen). **Architecture:** Reine Rechenmodule ohne I/O (`matching.js`, `recurring.js`, `forecast.js`, `tax.js`, `todos.js`) plus dünne I/O-Dienste (`stripeFinance.js`, `assign.js`, `settings.js`, `overview.js`) unter `server/services/finance/`; Routen in `server/routes/admin/finanzen.js` (aus Plan 1). Stripe wird nicht gespiegelt, sondern live gelesen (Zwischenspeicher 60 s). **Tech Stack:** Node 20, Express 4, stripe-node ^19.3 (API 2025-10-29.clover), Appwrite REST, `node:test`, `test/support/fakeStripe.js`. **Spec:** `docs/superpowers/specs/2026-10-01-finanzen-design.md` ## Global Constraints - Setzt Plan 1 voraus (gleicher Worktree `dev-finanzen`, Branch `feature/finanzen`). **Nie pushen** ohne OK des Nutzers. - `npm test` grün vor jedem Commit. Keine neuen Abhängigkeiten. - Beträge in ganzen Cent; Stripe-Beträge sind schon Cent (EUR). - Statuswerte nach außen (deutsch): Rechnung `entwurf` | `offen` | `ueberfaellig` | `bezahlt` | `storniert` | `uneinbringlich`; Sicherheit `sicher` | `erwartet`; Dringlichkeit `hoch` | `mittel` | `info`; Zuordnungskategorien `rechnung` | `rechnung-extern` | `stripe-auszahlung` | `fixkosten` | `privat` | `sonstiges`. - Kleinunternehmer-Grenzen: 2.500.000 Cent (Folgejahr), 10.000.000 Cent (hart); Warnung ab 70 %. - Gewerbesteuer-Hinweis ab Gewinn > 2.450.000 Cent. ESt-Vorauszahlungstermine: 10.03., 10.06., 10.09., 10.12. - Standard-Einstellungen: `minBufferCents 0`, `taxReservePercent 30`, `previousYearRevenueCents null`, `estPrepaymentCents 0`, `bankDetailsText ''`. - Rechnungsfuß = `config.stripe.invoiceFooter` + Zeilenumbruch + `bankDetailsText` (falls gesetzt) + Zeilenumbruch + `Bitte geben Sie bei Überweisung die Rechnungsnummer als Verwendungszweck an.` - Projektrechnungen: `collection_method: 'send_invoice'`, `days_until_due` Standard 14 (0–60), `auto_advance: false`, `pending_invoice_items_behavior: 'exclude'`, `metadata: { webklarCustomerId, quelle: 'woms', erstelltVon }`. - Abo-Rechnungen (aus Abos entstanden) lassen sich in WOMS weder stornieren noch löschen (409 „Abo-Rechnungen verwaltet Stripe.“). - Eine Zuordnung zu einer Rechnung markiert sie mit `paid_out_of_band: true` und ist nicht umkehrbar. - Hilfsfunktionen aus Plan 1: `berlinDate`, `addDays`, `daysBetween`, `formatDe` (`server/services/finance/dates.js`). ## Review Focus - **Stripe nicht erreichbar:** `GET /uebersicht` liefert trotzdem Bankteil, Kopf (ohne Stripe-Posten) und Aufgaben aus Bankdaten; `stand.stripe.ok = false` mit Meldung. Test in Task 8. - **Doppelte Bestätigung:** Zwei schnelle Klicks auf „Zuordnen“ für dieselbe Buchung → die zweite bekommt 409, Stripe wird nur einmal als bezahlt markiert. Test in Task 4. - **Betrag weicht ab:** Eingang 100 € für Rechnung mit offenen 120 € → Vorschlag „mittel, Betrag weicht ab“, Bestätigen → 409 mit Hinweis auf Teilzahlung. Test in Tasks 3 und 4. - **Monatsende in Vorschau:** Fixkosten am 31. → nächste Termine 30.11., 31.12., 28.02. (kein Überlauf in den Folgemonat). Test in Task 6. - **Leere Daten:** Keine Rechnungen, keine Abos, keine Fixkosten, nur Kontostand → Übersicht ohne Fehler, Vorschau = flache Linie, keine Aufgaben außer Bank. Test in Task 8. --- ### Task 1: Stripe-Rechnungen und Auszahlungen lesen **Files:** - Modify: `test/support/fakeStripe.js` (Routen `GET /v1/invoices` mit `expand[]=data.customer`, `GET /v1/payouts`; Rückgabeobjekt `payouts: Map`) - Create: `server/services/finance/format.js` - Create: `server/services/finance/stripeFinance.js` - Test: `test/financeStripeRead.test.js` **Interfaces:** - Produces: - `format.js`: `euroDe(cents) → string` (`euroDe(123456) === '1.234,56 €'`, `euroDe(-449) === '−4,49 €'` mit echtem Minuszeichen U+2212, geschütztes Leerzeichen wie `Intl` es liefert). - `mapInvoice(raw, { today }) → { id, number, status, customerName, customerEmail, webklarCustomerId, createdDate, dueDate, totalCents, remainingCents, paidCents, paidDate, overdueDays, source, hostedUrl, pdfUrl, outOfBand, bankTransactionId }` - `status`: `draft`→`entwurf`, `open`→`offen` bzw. `ueberfaellig` wenn `dueDate < today`, `paid`→`bezahlt`, `void`→`storniert`, `uncollectible`→`uneinbringlich` - `webklarCustomerId` = `raw.metadata.webklarCustomerId` oder `raw.customer.metadata.webklarCustomerId` (bei expandiertem Kunden) - `paidDate` = `raw.metadata.bankBookingDate` oder Berlin-Datum von `status_transitions.paid_at` - `overdueDays` = `daysBetween(dueDate, today)` bei `ueberfaellig`, sonst 0 - `source` = `'abo'` wenn `subscriptionIdOfInvoice(raw)` (aus `stripeBilling.js`) etwas liefert, sonst `'projekt'` - `outOfBand` = `Boolean(raw.metadata?.bankTransactionId)` - `listInvoices({ force = false } = {}) → Promise` – `stripe.invoices.list({ limit: 100, created: { gte: }, expand: ['data.customer'] })` mit Auto-Paging, höchstens 1000; Zwischenspeicher 60 s. - `listPayouts({ force = false } = {}) → Promise>` – `stripe.payouts.list({ limit: 50 })`, 60 s. - `invalidateFinanceCache() → void` - [ ] **Step 1: Write the failing tests:** `mapInvoice` für je einen Rohdatensatz pro Stripe-Status; offene Rechnung fällig `2026-09-20`, `today = '2026-10-01'` → `ueberfaellig`, `overdueDays 11`; Rechnung mit `parent.subscription_details.subscription` → `source 'abo'`; `metadata.bankBookingDate '2026-08-05'` → `paidDate '2026-08-05'`, `outOfBand true`; `listInvoices` zweimal in 60 s → ein Stripe-Aufruf (`st.calls`), nach `invalidateFinanceCache()` zwei; `euroDe`-Fälle oben. - [ ] **Step 2: Run** `node --test test/financeStripeRead.test.js` – Expected: FAIL. - [ ] **Step 3: Implement** Fake-Routen, `format.js`, `stripeFinance.js` (`getStripe()` aus `stripeBilling.js`). - [ ] **Step 4: Run** Testdatei und `npm test` – Expected: PASS. - [ ] **Step 5: Commit** `git commit -m "Finanzen: Stripe-Rechnungen und Auszahlungen lesen (deutsch gemappt, 60-s-Cache)"` ### Task 2: Rechnungen schreiben, senden, stornieren, löschen **Files:** - Modify: `server/services/stripeBilling.js` (`getOrCreateStripeCustomer` exportieren, sonst unverändert) - Modify: `test/support/fakeStripe.js` (`POST /v1/invoices`, `POST /v1/invoiceitems`, `POST /v1/invoices/:id` (Metadaten), `POST /v1/invoices/:id/finalize`, `/send`, `/void`, `/pay`, `DELETE /v1/invoices/:id`, `GET /v1/customers/search` falls fehlend; Statusübergänge wie Stripe: finalize nur `draft`→`open`, void nur `open`, pay nur `open`, del nur `draft`, sonst 400) - Modify: `server/services/finance/stripeFinance.js` - Test: `test/financeStripeWrite.test.js` **Interfaces:** - Consumes: Task 1; `getSettings()` aus Task 5 – bis dahin `bankDetailsText` als Parameter: `invoiceFooter(bankDetailsText) → string`. - Produces: - `invoiceFooter(bankDetailsText = '') → string` (Regel in Global Constraints) - `createProjectInvoice({ customerId, items, daysUntilDue = 14, memo = '', senden = false, createdBy, bankDetailsText = '' }) → Promise`; Validierung (Fehler mit `status 400`): Kunde existiert und `customerStatus` ist nicht `lead`/`lost` („Rechnungen nur für feste Kunden.“); 1–20 Positionen; `description` 1–200 Zeichen; `quantity` ganze Zahl 1–999; `unitAmountCents` ganze Zahl 1–10.000.000; `daysUntilDue` 0–60. - `sendInvoiceNow(invoiceId) → Promise` (finalize mit `auto_advance: false`, dann send); nur `entwurf`, sonst 409 „Nur Entwürfe können gesendet werden.“ - `voidInvoice(invoiceId) → Promise`; nur `offen`/`ueberfaellig` und `source 'projekt'`, sonst 409. - `deleteDraft(invoiceId) → Promise`; nur `entwurf` und `source 'projekt'`, sonst 409. - Alle schreibenden Funktionen rufen `invalidateFinanceCache()`. - Positionen: `stripe.invoiceItems.create({ customer, invoice, description, quantity, unit_amount_decimal: String(unitAmountCents), currency: 'eur' })`. **Vor dem Implementieren** in der Stripe-API-Referenz für die gepinnte Version prüfen, ob `unit_amount_decimal` beim Invoice Item noch erlaubt ist; falls nicht, `pricing`/`price_data` nach Referenz verwenden und den Test-Assert entsprechend anpassen. Der Test prüft, dass auf der Rechnung Menge und Einzelpreis ankommen. - [ ] **Step 1: Write the failing tests:** Kunde `c1` (Fixtures) mit zwei Positionen (`'Website-Anpassung', 2, 5000` und `'Domain', 1, 1200`) → `st.calls` enthält `POST /v1/invoices` mit `collection_method=send_invoice`, `days_until_due=14`, `auto_advance=false`, `metadata[quelle]=woms`, `footer` beginnt mit dem §-19-Hinweis und endet mit dem Verwendungszweck-Satz; zwei `POST /v1/invoiceitems`; Ergebnis `status 'entwurf'`; mit `senden: true` → finalize + send, Ergebnis `offen`; Lead-Kunde → 400; 21 Positionen → 400; `voidInvoice` auf Entwurf → 409; `deleteDraft` auf Abo-Rechnung → 409; `sendInvoiceNow` auf offene → 409. - [ ] **Step 2: Run** `node --test test/financeStripeWrite.test.js` – Expected: FAIL. - [ ] **Step 3: Implement.** - [ ] **Step 4: Run** Testdatei und `npm test` (insb. `stripeBilling.test.js`, `aboRoutes.test.js` unverändert grün) – Expected: PASS. - [ ] **Step 5: Commit** `git commit -m "Finanzen: Projektrechnungen in Stripe anlegen, senden, stornieren, Entwurf loeschen"` ### Task 3: Zuordnungsvorschläge und wiederkehrende Ausgaben (rein) **Files:** - Create: `server/services/finance/matching.js` - Create: `server/services/finance/recurring.js` - Test: `test/financeMatching.test.js`, `test/financeRecurring.test.js` **Interfaces:** - Produces: - `normalizeRef(text) → string` (Großbuchstaben, ohne Leerraum, `-`, `_`, `.`, `/`) - `nameTokens(name) → string[]` (klein, an Nicht-Buchstaben trennen, Umlaute bleiben, Länge ≥ 4, ohne `gmbh gbr ohg mbh und der die das kg ag ug haftungsbeschränkt`) - `suggestMatches(tx, { invoices, payouts }) → Array<{ kind: 'invoice'|'payout', id, number, confidence: 'hoch'|'mittel'|'niedrig', reason, amountDiffers }>` – nur für `tx.amountCents > 0` ohne `category`; Regeln 1–4 aus Spec „Zuordnungsregeln“ in dieser Reihenfolge; je Rechnung/Auszahlung höchstens ein Vorschlag (der stärkste); sortiert hoch → niedrig; höchstens 3. Nur Rechnungen mit Status `offen`/`ueberfaellig`; Nummern mit normalisierter Länge < 4 ignorieren. `reason` deutsch, z. B. „Rechnungsnummer im Verwendungszweck“, „Betrag und Name passen“, „Nur der Betrag passt“, „Stripe-Auszahlung gleicher Betrag“. - `detectRecurring(transactions, { fixedCosts = [] } = {}) → Array<{ key, name, amountCents, interval: 'monat'|'quartal'|'jahr', lastDate, count }>` – nur `DBIT`-Buchungen (`amountCents < 0`, `status 'BOOK'`); Gruppierung nach normalisierter Gegenpartei (klein, ohne Ziffern und Satzzeichen, Leerraum zusammengefasst); ≥ 3 Buchungen; Median der Abstände 25–35 Tage → `monat`, 80–100 → `quartal`, 350–380 → `jahr`, sonst keine; Variationskoeffizient der Beträge ≤ 0,15; Gruppen, deren Name einen `counterpartyMatch` einer aktiven Fixkostenposition enthält, entfallen; `amountCents` = Median als positiver Wert. - [ ] **Step 1: Write the failing tests:** - Eingang 16879 „EWV SOFTWARE GMBH“ mit Zweck „Rechnung WK-0007 vom …“, offene Rechnung `WK-0007` über 16879 → ein Vorschlag `hoch`; gleiche Rechnung über 20000 → `mittel`, `amountDiffers true`. - Zweck ohne Nummer, Betrag passt, Kunde „EWV Software GmbH“ → `mittel` („Betrag und Name passen“); Kunde „Döner Nation“ → `niedrig`; zwei offene Rechnungen gleichen Betrags ohne Namensbezug → keine `niedrig`-Vorschläge (Regel 4 nur bei genau einer). - Gegenpartei „Stripe Technologies Europe Ltd“, Betrag = Auszahlung, Datum 2 Tage nach `arrivalDate` → `payout`, `hoch`; 5 Tage → kein Vorschlag. - Ausgang (`amountCents < 0`) oder schon zugeordnet → `[]`. - `detectRecurring`: drei PORKBUN-Buchungen 449 am 27.09., 29.09., 29.09. → `[]`; OPENROUTER 4358 am 16.06., 16.07., 16.08. → `monat`, `amountCents 4358`; Beträge 1000/2000/3000 monatlich → `[]`; aktive Fixkosten mit `counterpartyMatch 'openrouter'` → `[]`. - [ ] **Step 2: Run** `node --test test/financeMatching.test.js test/financeRecurring.test.js` – Expected: FAIL. - [ ] **Step 3: Implement.** - [ ] **Step 4: Run** beide Testdateien und `npm test` – Expected: PASS. - [ ] **Step 5: Commit** `git commit -m "Finanzen: Zuordnungsvorschlaege und Erkennung wiederkehrender Ausgaben"` ### Task 4: Buchungen zuordnen **Files:** - Create: `server/services/finance/assign.js` - Modify: `server/routes/admin/finanzen.js` (`GET /buchungen` mit Vorschlägen; `POST /buchungen/:id/zuordnen`; `DELETE /buchungen/:id/zuordnung`) - Test: `test/financeAssign.test.js` **Interfaces:** - Consumes: Plan 1 `bankStore` (`getTransaction`, `updateTransaction`, `listTransactions`); Task 1 (`listInvoices`, `listPayouts`, `invalidateFinanceCache`); Task 3 (`suggestMatches`); Task 5 `getFixedCost(id)` (bis dahin über `getDocument('financeFixedCosts', id)`). - Produces: - `assignTransaction(txId, { invoiceId, payoutId, fixedCostId, category, note }) → Promise` – genau eines der vier Ziele, sonst 400. Regeln: - bereits `category` gesetzt → 409 „Bereits zugeordnet.“ (vor jedem Stripe-Aufruf geprüft; gleichzeitige Aufrufe für dieselbe Buchung mit `withLocks([`finanzen-buchung:${txId}`], fn)` aus `server/services/keyedLock.js` serialisieren) - `invoiceId`: Eingang; Rechnung per `stripe.invoices.retrieve`; Status `open`, sonst 409; `amount_remaining === tx.amountCents`, sonst 409 „Betrag weicht vom offenen Betrag ab (). Teilzahlungen bitte direkt in Stripe erfassen.“; dann `stripe.invoices.pay(id, { paid_out_of_band: true })`, `stripe.invoices.update(id, { metadata: { bankTransactionId, bankBookingDate } })`, Buchung `{ category: 'rechnung', matchedInvoiceId, matchedInvoiceNumber }` - `payoutId`: Auszahlung aus `listPayouts({ force: true })`, gleicher Betrag, sonst 409 → `{ category: 'stripe-auszahlung', matchedPayoutId }` - `fixedCostId`: Ausgang, Position existiert → `{ category: 'fixkosten', fixedCostId }` - `category` ∈ `rechnung-extern` (nur Eingang), `privat`, `sonstiges` → `{ category }` - `note` optional, ≤ 500 Zeichen - `unassignTransaction(txId) → Promise` – `category 'rechnung'` → 409 „Die Rechnung ist in Stripe schon als bezahlt markiert und lässt sich nicht zurücksetzen.“; sonst `category`, `matchedInvoiceId`, `matchedInvoiceNumber`, `matchedPayoutId`, `fixedCostId` leeren. - `GET /buchungen` → `{ buchungen: Array, stripeFehler?: string }` - Routen: `POST /buchungen/:id/zuordnen` Body = Ziel → `{ buchung }`; `DELETE /buchungen/:id/zuordnung` → `{ buchung }`. - [ ] **Step 1: Write the failing tests:** Zuordnen zu offener Rechnung → Stripe-Aufrufe `pay` (mit `paid_out_of_band=true`) und Update mit `metadata[bankTransactionId]`, Buchung `category 'rechnung'`; zwei parallele Zuordnungen derselben Buchung (`Promise.allSettled`) → eine 200, eine 409, genau ein `pay`-Aufruf; Betrag weicht ab → 409, kein `pay`; Zuordnung lösen bei `rechnung` → 409, bei `privat` → Felder leer; `rechnung-extern` auf Ausgang → 400; `GET /buchungen` liefert für den EWV-Eingang einen Vorschlag; Stripe-Fehler (`st.failNext({ path: '/v1/invoices', status: 500 })` und `/v1/payouts`) → `GET /buchungen` 200 mit `stripeFehler` und leeren `vorschlaege`. - [ ] **Step 2: Run** `node --test test/financeAssign.test.js` – Expected: FAIL. - [ ] **Step 3: Implement.** - [ ] **Step 4: Run** Testdatei und `npm test` – Expected: PASS. - [ ] **Step 5: Commit** `git commit -m "Finanzen: Bankbuchungen Rechnungen, Auszahlungen, Fixkosten und Kategorien zuordnen"` ### Task 5: Einstellungen und Fixkosten **Files:** - Create: `server/services/finance/settings.js` - Modify: `server/routes/admin/finanzen.js` (`GET`/`PATCH /einstellungen`, `GET`/`POST /fixkosten`, `PATCH`/`DELETE /fixkosten/:id`) - Modify: `server/services/finance/stripeFinance.js` (`createProjectInvoice` holt `bankDetailsText` selbst über `getSettings()`, Parameter entfällt) - Test: `test/financeSettings.test.js` **Interfaces:** - Produces: - `getSettings() → Promise` (Dokument `financeSettings/main` mit Standardwerten zusammengeführt; fehlt es, nur Standardwerte, nichts anlegen) - `patchSettings(patch) → Promise`; erlaubte Schlüssel und Grenzen: `minBufferCents` 0–100.000.000, `taxReservePercent` 0–60, `previousYearRevenueCents` ≥ 0 oder `null`, `estPrepaymentCents` ≥ 0, `bankDetailsText` ≤ 500 Zeichen; unbekannter Schlüssel oder Verstoß → 400 mit Feldname in der Meldung. Legt `main` an, falls nötig. - `listFixedCosts() → Promise`, `getFixedCost(id)`, `createFixedCost(input)`, `updateFixedCost(id, patch)`, `deleteFixedCost(id)`; Validierung: `name` 1–120, `amountCents` ganze Zahl > 0, `interval` ∈ `monat|quartal|jahr`, `nextDate` `YYYY-MM-DD`, `counterpartyMatch` ≤ 120 (wird klein gespeichert), `active` Standard `true`. - [ ] **Step 1: Write the failing tests:** ohne Dokument → Standardwerte; `PATCH { taxReservePercent: 25 }` → 25 und Dokument angelegt; `{ taxReservePercent: 80 }` → 400 mit „taxReservePercent“; `{ foo: 1 }` → 400; Fixkosten anlegen mit `counterpartyMatch 'PORKBUN'` → gespeichert `porkbun`; `interval 'woche'` → 400; Rechnung über Task-2-Funktion nach `PATCH { bankDetailsText: 'IBAN DE00 …' }` → Fuß enthält die IBAN-Zeile. Routen: `jwt-staff` → 403. - [ ] **Step 2: Run** `node --test test/financeSettings.test.js` – Expected: FAIL. - [ ] **Step 3: Implement.** - [ ] **Step 4: Run** Testdatei und `npm test` – Expected: PASS. - [ ] **Step 5: Commit** `git commit -m "Finanzen: Einstellungen und Fixkostenliste"` ### Task 6: Liquiditätsvorschau (rein) **Files:** - Create: `server/services/finance/forecast.js` - Test: `test/financeForecast.test.js` **Interfaces:** - Produces: `buildForecast({ today, horizonDays = 30, balanceCents, payouts = [], invoices = [], subscriptions = [], fixedCosts = [], settings }) → { posten: Array<{ date, label, amountCents, sicherheit }>, minSicher: { amountCents, date }, minErwartet: { amountCents, date }, endeSicherCents, endeErwartetCents, fixkosten30Cents, annahme }` - `sicher`: Auszahlungen `pending`/`in_transit` mit `arrivalDate` im Zeitraum (+); aktive Fixkosten (−) an allen Terminen ab `nextDate` (vor heute liegende Termine vorspulen) im Rhythmus +1/+3/+12 Monate, Tag auf Monatsende begrenzt, aber vom ursprünglichen Tag aus gerechnet (31. → 30.11. → 31.12.); ESt-Vorauszahlung (−) am 10.03./10.06./10.09./10.12., wenn `estPrepaymentCents > 0`. - `erwartet`: Rechnungen `offen` (+ `remainingCents`) am `dueDate`, `ueberfaellig` am `today + 14` mit Label-Zusatz „(überfällig, angenommen)“; Abos mit Status `active` (+ `priceAmount`) an `currentPeriodEnd` und monatlich danach, jeweils + 4 Tage (Auszahlung), ohne Termine ab `cancelAt` bzw. nach `currentPeriodEnd` bei `cancelAtPeriodEnd`. - Zeitraum: `today` bis `today + horizonDays` einschließlich; Posten nach Datum, bei gleichem Datum Abflüsse zuerst. - `minSicher`/`minErwartet`: tiefster laufender Saldo der jeweiligen Linie inklusive Startwert (`date = today`, falls nie tiefer); bei Gleichstand das früheste Datum. - `fixkosten30Cents` = Summe der `sicher`-Abflüsse (positiv). - `annahme` = `'Erwartet: offene Rechnungen zum Fälligkeitstag, überfällige in 14 Tagen, Abo-Verlängerungen 4 Tage nach Abbuchung, vor Stripe-Gebühren.'` - [ ] **Step 1: Write the failing tests** (`today '2026-10-01'`, `balanceCents 10893`): - nur Kontostand → `posten []`, beide Minima 10893 am `2026-10-01`, `fixkosten30Cents 0`. - Fixkosten 449 monatlich ab `2026-09-27` → Termin `2026-10-27` (−449), `minSicher 10444` am `2026-10-27`. - Fixkosten am 31. ab `2026-10-31`, Horizont 120 → Termine `2026-10-31`, `2026-11-30`, `2026-12-31`, `2027-01-31`. - Rechnung offen 50000 fällig `2026-10-14` → `erwartet` am `2026-10-14`, `endeErwartetCents 60893`, `endeSicherCents 10893`. - Rechnung überfällig → am `2026-10-15` mit „(überfällig, angenommen)“. - Abo 2000, `currentPeriodEnd 2026-10-05` → Posten am `2026-10-09`; mit `cancelAtPeriodEnd: true` und `currentPeriodEnd 2026-10-05` → kein Posten am 09.11. - `estPrepaymentCents 50000` → Posten am `2026-12-10` bei Horizont 90, keiner bei 30. - Auszahlung `in_transit` 1500 am `2026-10-03` → `sicher` +1500. - [ ] **Step 2: Run** `node --test test/financeForecast.test.js` – Expected: FAIL. - [ ] **Step 3: Implement.** - [ ] **Step 4: Run** Testdatei und `npm test` – Expected: PASS. - [ ] **Step 5: Commit** `git commit -m "Finanzen: Liquiditaetsvorschau 30 Tage (sicher/erwartet)"` ### Task 7: Steuer und Aufgabenliste (rein) **Files:** - Create: `server/services/finance/tax.js` - Create: `server/services/finance/todos.js` - Test: `test/financeTax.test.js`, `test/financeTodos.test.js` **Interfaces:** - Produces: - `computeTax({ today, transactions, invoices, settings }) → { jahr, einnahmenCents, ausgabenCents, gewinnCents, ruecklageCents, prozent, gewerbesteuerHinweis, kleinunternehmer: { jahrCents, vorjahrCents, grenzeFolgejahrCents: 2500000, grenzeHartCents: 10000000, anteilFolgejahr }, naechsteVorauszahlung: { date, amountCents } | null }` - Einnahmen/Ausgaben: Buchungen `BOOK` im Kalenderjahr von `today`, ohne `category 'privat'`; Eingänge positiv summiert, Ausgänge als positiver Betrag. - `ruecklageCents` = `Math.round(max(0, gewinnCents) * prozent / 100)` - `kleinunternehmer.jahrCents` = Summe `paidCents` der Stripe-Rechnungen mit `paidDate` im Jahr + Buchungen `category 'rechnung-extern'` im Jahr (Stripe-Auszahlungen zählen nicht, sonst doppelt). - `anteilFolgejahr` = `jahrCents / 2500000` - `buildTodos({ today, now, connection, invoices, transactions, subscriptions, recurring, tax }) → Array<{ art, dringlichkeit, titel, text, datum, aktion }>` nach Spec-Tabelle „Zu erledigen – Arten“, sortiert hoch → mittel → info, darin nach `datum` aufsteigend. `aktion.typ` ∈ `bank-verbinden`, `bank-aktualisieren`, `rechnung-oeffnen` (`invoiceId`, `hostedUrl`), `zuordnen` (`transactionId`), `kunde-oeffnen` (`customerId`), `fixkosten-uebernehmen` (`vorschlag`), `einstellungen`. Texte mit `euroDe` und `formatDe`. `transactions` enthalten bereits `vorschlaege`; bei Vorschlag `hoch` endet `text` mit „Vorschlag: Rechnung “ bzw. „Vorschlag: Stripe-Auszahlung“. - [ ] **Step 1: Write the failing tests:** - Steuer: Eingänge 16879 (`rechnung-extern`) und Ausgänge 449+449+449+50+50+50+131+4358 im Jahr 2026, eine Buchung `privat` −10000 → `einnahmenCents 16879`, `ausgabenCents 5986`, `gewinnCents 10893`, `ruecklageCents 3268` (30 %), `kleinunternehmer.jahrCents 16879`; Gewinn negativ → Rücklage 0; Stripe-Rechnung bezahlt 2026-10-02 über 5000 → `jahrCents` + 5000; `estPrepaymentCents 50000` und `today 2026-10-01` → `naechsteVorauszahlung { date: '2026-12-10', amountCents: 50000 }`. - Aufgaben: Zugang gültig bis `2026-10-20` (19 Tage) → `bank-zugang` `mittel`; bis `2026-10-10` → `hoch`; `status 'abgelaufen'` → „Bankzugang abgelaufen“ `hoch`; `lastSyncAt` vor 25 h → `bank-abruf`; Rechnung 31 Tage überfällig → `hoch`, 5 Tage → `mittel`; ungeordneter Eingang → `zuordnung` mit `aktion.transactionId`; Abo `past_due` → `abo-zahlung` `hoch`; Entwurf von vor 4 Tagen → `entwurf` `info`, von gestern → keine Aufgabe; `anteilFolgejahr 0.72` → `kleinunternehmer` `mittel`, `1.05` → `hoch`; Reihenfolge hoch vor mittel vor info. - [ ] **Step 2: Run** `node --test test/financeTax.test.js test/financeTodos.test.js` – Expected: FAIL. - [ ] **Step 3: Implement.** - [ ] **Step 4: Run** beide Testdateien und `npm test` – Expected: PASS. - [ ] **Step 5: Commit** `git commit -m "Finanzen: Steuerruecklage, Kleinunternehmer-Zaehler und Aufgabenliste"` ### Task 8: Übersicht und Rechnungsrouten **Files:** - Create: `server/services/finance/overview.js` - Modify: `server/routes/admin/finanzen.js` (`GET /uebersicht`, `GET /rechnungen`, `POST /rechnungen`, `POST /rechnungen/:id/senden`, `POST /rechnungen/:id/stornieren`, `DELETE /rechnungen/:id`) - Test: `test/financeOverview.test.js` **Interfaces:** - Consumes: alle vorigen Tasks; `listDocuments(config.collections.subscriptions, [Query.limit(500)])` und Kundennamen per `getDocument(config.collections.customers, id)` (nur für Abo-Kunden). - Produces: - `buildOverview({ now = new Date() } = {}) → Promise` mit - `stand: { bank: <öffentliche Verbindung wie GET /bank>, stripe: { ok, fehler?, abgerufenUm } }` - `kopf: { kontostandCents, verfuegbarCents, aufschluesselung: { kontostandCents, steuerRuecklageCents, fixkosten30Cents, mindestpufferCents }, tiefsterSicher, tiefsterErwartet }` mit `verfuegbarCents = kontostand − steuerRuecklage − fixkosten30 − mindestpuffer` - `aufgaben` (Task 7), `vorschau` (Task 6), `steuer` (Task 7) - `abos: { monatlichCents, zahlendeKunden, gefaehrdetCents, liste: Array<{ customerId, kunde, produkt, status, betragCents, naechsteAbbuchung }> }` – `monatlichCents` = Summe `priceAmount` der Status `active` und `past_due`; `gefaehrdetCents` = Summe `past_due`; `zahlendeKunden` = verschiedene `customerId` mit `active`; `produkt` = `productKey ?? planKey` - `rechnungen: { altersgruppen: Array<{ key: 'nicht-faellig'|'1-30'|'31-60'|'ueber-60', anzahl, summeCents }>, entwuerfe }` aus `offen`/`ueberfaellig` - Stripe-Fehler: Rechnungen/Auszahlungen als leere Listen weiterrechnen, `stand.stripe.ok false`. - `GET /rechnungen?status=offen|ueberfaellig|bezahlt|entwurf|alle&kunde=` → `{ rechnungen }` (Standard `alle`, neueste zuerst; `kunde` filtert auf `webklarCustomerId`) - `POST /rechnungen` Body `{ customerId, items: [{ description, quantity, unitAmountCents }], daysUntilDue, memo, senden }` → `{ rechnung }`; `createdBy` = `req.appwriteUser.email` - `POST /rechnungen/:id/senden` → `{ rechnung }`; `POST /rechnungen/:id/stornieren` → `{ rechnung }`; `DELETE /rechnungen/:id` → `{ ok: true }` - [ ] **Step 1: Write the failing tests** (Appwrite-Mock mit Verbindung, 9 Buchungen, Einstellungen; Fake-Stripe mit einer offenen Projekt-, einer überfälligen und einer bezahlten Rechnung; ein Abo `active` 2000 und ein `past_due` 5000): - `GET /uebersicht` → `kopf.kontostandCents 10893`; `verfuegbarCents` = 10893 − Rücklage − Fixkosten30 − Puffer (konkrete Zahl aus den Testdaten ausrechnen und festschreiben); `abos.monatlichCents 7000`, `gefaehrdetCents 5000`, `zahlendeKunden 1`; Altersgruppen enthalten die überfällige Rechnung in der richtigen Gruppe; `aufgaben` enthält `ueberfaellig` und `abo-zahlung`. - Leere Daten (keine Rechnungen, Abos, Fixkosten) → 200, `vorschau.posten []`, `aufgaben` ohne Rechnungs-/Abo-Arten. - Stripe 500 → 200, `stand.stripe.ok false`, `kopf.kontostandCents 10893`. - `GET /rechnungen?status=ueberfaellig` → nur die überfällige; `?kunde=c1` → nur Rechnungen mit `webklarCustomerId c1`. - `POST /rechnungen` → 200 Entwurf; `POST /rechnungen/:id/senden` → `offen`; `DELETE` auf offene → 409. - `jwt-staff` auf `/uebersicht` → 403. - [ ] **Step 2: Run** `node --test test/financeOverview.test.js` – Expected: FAIL. - [ ] **Step 3: Implement.** - [ ] **Step 4: Run** Testdatei und `npm test` – Expected: PASS (gesamte Suite). - [ ] **Step 5: Commit** `git commit -m "Finanzen: Uebersicht und Rechnungsrouten fuer WOMS"`