Files
Webklar-Kundenbereich/docs/superpowers/plans/2026-10-01-finanzen-plan-2-stripe-auswertung.md
T

28 KiB
Raw Blame History

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<Invoice[]> – stripe.invoices.list({ limit: 100, created: { gte: <jetzt − 400 Tage> }, expand: ['data.customer'] }) mit Auto-Paging, höchstens 1000; Zwischenspeicher 60 s.
    • listPayouts({ force = false } = {}) → Promise<Array<{ id, amountCents, arrivalDate, status }>> – 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<Invoice>; 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<Invoice> (finalize mit auto_advance: false, dann send); nur entwurf, sonst 409 „Nur Entwürfe können gesendet werden.“
    • voidInvoice(invoiceId) → Promise<Invoice>; nur offen/ueberfaellig und source 'projekt', sonst 409.
    • deleteDraft(invoiceId) → Promise<void>; 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<Transaction> – 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<Transaction> – 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<Transaction & { vorschlaege: Suggestion[] }>, 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<Settings> (Dokument financeSettings/main mit Standardwerten zusammengeführt; fehlt es, nur Standardwerte, nichts anlegen)
    • patchSettings(patch) → Promise<Settings>; 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<FixedCost[]>, 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 <Nr.>“ 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<Overview> 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=<customerId> → { 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"