28 KiB
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, Branchfeature/finanzen). Nie pushen ohne OK des Nutzers. npm testgrü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; Sicherheitsicher|erwartet; Dringlichkeithoch|mittel|info; Zuordnungskategorienrechnung|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_dueStandard 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: trueund ist nicht umkehrbar. - Hilfsfunktionen aus Plan 1:
berlinDate,addDays,daysBetween,formatDe(server/services/finance/dates.js).
Review Focus
- Stripe nicht erreichbar:
GET /uebersichtliefert trotzdem Bankteil, Kopf (ohne Stripe-Posten) und Aufgaben aus Bankdaten;stand.stripe.ok = falsemit 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(RoutenGET /v1/invoicesmitexpand[]=data.customer,GET /v1/payouts; Rückgabeobjektpayouts: 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 wieIntles 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→offenbzw.ueberfaelligwenndueDate < today,paid→bezahlt,void→storniert,uncollectible→uneinbringlichwebklarCustomerId=raw.metadata.webklarCustomerIdoderraw.customer.metadata.webklarCustomerId(bei expandiertem Kunden)paidDate=raw.metadata.bankBookingDateoder Berlin-Datum vonstatus_transitions.paid_atoverdueDays=daysBetween(dueDate, today)beiueberfaellig, sonst 0source='abo'wennsubscriptionIdOfInvoice(raw)(ausstripeBilling.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:
mapInvoicefür je einen Rohdatensatz pro Stripe-Status; offene Rechnung fällig2026-09-20,today = '2026-10-01'→ueberfaellig,overdueDays 11; Rechnung mitparent.subscription_details.subscription→source 'abo';metadata.bankBookingDate '2026-08-05'→paidDate '2026-08-05',outOfBand true;listInvoiceszweimal in 60 s → ein Stripe-Aufruf (st.calls), nachinvalidateFinanceCache()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()ausstripeBilling.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(getOrCreateStripeCustomerexportieren, 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/searchfalls fehlend; Statusübergänge wie Stripe: finalize nurdraft→open, void nuropen, pay nuropen, del nurdraft, sonst 400) - Modify:
server/services/finance/stripeFinance.js - Test:
test/financeStripeWrite.test.js
Interfaces:
-
Consumes: Task 1;
getSettings()aus Task 5 – bis dahinbankDetailsTextals 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 mitstatus 400): Kunde existiert undcustomerStatusist nichtlead/lost(„Rechnungen nur für feste Kunden.“); 1–20 Positionen;description1–200 Zeichen;quantityganze Zahl 1–999;unitAmountCentsganze Zahl 1–10.000.000;daysUntilDue0–60.sendInvoiceNow(invoiceId) → Promise<Invoice>(finalize mitauto_advance: false, dann send); nurentwurf, sonst 409 „Nur Entwürfe können gesendet werden.“voidInvoice(invoiceId) → Promise<Invoice>; nuroffen/ueberfaelligundsource 'projekt', sonst 409.deleteDraft(invoiceId) → Promise<void>; nurentwurfundsource '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, obunit_amount_decimalbeim Invoice Item noch erlaubt ist; falls nicht,pricing/price_datanach 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, 5000und'Domain', 1, 1200) →st.callsenthältPOST /v1/invoicesmitcollection_method=send_invoice,days_until_due=14,auto_advance=false,metadata[quelle]=woms,footerbeginnt mit dem §-19-Hinweis und endet mit dem Verwendungszweck-Satz; zweiPOST /v1/invoiceitems; Ergebnisstatus 'entwurf'; mitsenden: true→ finalize + send, Ergebnisoffen; Lead-Kunde → 400; 21 Positionen → 400;voidInvoiceauf Entwurf → 409;deleteDraftauf Abo-Rechnung → 409;sendInvoiceNowauf 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.jsunverä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, ohnegmbh 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ürtx.amountCents > 0ohnecategory; 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 Statusoffen/ueberfaellig; Nummern mit normalisierter Länge < 4 ignorieren.reasondeutsch, 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 }>– nurDBIT-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 einencounterpartyMatcheiner 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 Vorschlaghoch; 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 → keineniedrig-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 mitcounterpartyMatch 'openrouter'→[].
- Eingang 16879 „EWV SOFTWARE GMBH“ mit Zweck „Rechnung WK-0007 vom …“, offene Rechnung
-
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 /buchungenmit 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 5getFixedCost(id)(bis dahin übergetDocument('financeFixedCosts', id)). -
Produces:
assignTransaction(txId, { invoiceId, payoutId, fixedCostId, category, note }) → Promise<Transaction>– genau eines der vier Ziele, sonst 400. Regeln:- bereits
categorygesetzt → 409 „Bereits zugeordnet.“ (vor jedem Stripe-Aufruf geprüft; gleichzeitige Aufrufe für dieselbe Buchung mitwithLocks([finanzen-buchung:${txId}], fn)ausserver/services/keyedLock.jsserialisieren) invoiceId: Eingang; Rechnung perstripe.invoices.retrieve; Statusopen, sonst 409;amount_remaining === tx.amountCents, sonst 409 „Betrag weicht vom offenen Betrag ab (). Teilzahlungen bitte direkt in Stripe erfassen.“; dannstripe.invoices.pay(id, { paid_out_of_band: true }),stripe.invoices.update(id, { metadata: { bankTransactionId, bankBookingDate } }), Buchung{ category: 'rechnung', matchedInvoiceId, matchedInvoiceNumber }payoutId: Auszahlung auslistPayouts({ 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 }noteoptional, ≤ 500 Zeichen
- bereits
unassignTransaction(txId) → Promise<Transaction>–category 'rechnung'→ 409 „Die Rechnung ist in Stripe schon als bezahlt markiert und lässt sich nicht zurücksetzen.“; sonstcategory,matchedInvoiceId,matchedInvoiceNumber,matchedPayoutId,fixedCostIdleeren.GET /buchungen→{ buchungen: Array<Transaction & { vorschlaege: Suggestion[] }>, stripeFehler?: string }- Routen:
POST /buchungen/:id/zuordnenBody = Ziel →{ buchung };DELETE /buchungen/:id/zuordnung→{ buchung }.
-
Step 1: Write the failing tests: Zuordnen zu offener Rechnung → Stripe-Aufrufe
pay(mitpaid_out_of_band=true) und Update mitmetadata[bankTransactionId], Buchungcategory 'rechnung'; zwei parallele Zuordnungen derselben Buchung (Promise.allSettled) → eine 200, eine 409, genau einpay-Aufruf; Betrag weicht ab → 409, keinpay; Zuordnung lösen beirechnung→ 409, beiprivat→ Felder leer;rechnung-externauf Ausgang → 400;GET /buchungenliefert für den EWV-Eingang einen Vorschlag; Stripe-Fehler (st.failNext({ path: '/v1/invoices', status: 500 })und/v1/payouts) →GET /buchungen200 mitstripeFehlerund leerenvorschlaege. -
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(createProjectInvoiceholtbankDetailsTextselbst übergetSettings(), Parameter entfällt) - Test:
test/financeSettings.test.js
Interfaces:
-
Produces:
getSettings() → Promise<Settings>(DokumentfinanceSettings/mainmit Standardwerten zusammengeführt; fehlt es, nur Standardwerte, nichts anlegen)patchSettings(patch) → Promise<Settings>; erlaubte Schlüssel und Grenzen:minBufferCents0–100.000.000,taxReservePercent0–60,previousYearRevenueCents≥ 0 odernull,estPrepaymentCents≥ 0,bankDetailsText≤ 500 Zeichen; unbekannter Schlüssel oder Verstoß → 400 mit Feldname in der Meldung. Legtmainan, falls nötig.listFixedCosts() → Promise<FixedCost[]>,getFixedCost(id),createFixedCost(input),updateFixedCost(id, patch),deleteFixedCost(id); Validierung:name1–120,amountCentsganze Zahl > 0,interval∈monat|quartal|jahr,nextDateYYYY-MM-DD,counterpartyMatch≤ 120 (wird klein gespeichert),activeStandardtrue.
-
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 mitcounterpartyMatch 'PORKBUN'→ gespeichertporkbun;interval 'woche'→ 400; Rechnung über Task-2-Funktion nachPATCH { 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: Auszahlungenpending/in_transitmitarrivalDateim Zeitraum (+); aktive Fixkosten (−) an allen Terminen abnextDate(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., wennestPrepaymentCents > 0.erwartet: Rechnungenoffen(+remainingCents) amdueDate,ueberfaelligamtoday + 14mit Label-Zusatz „(überfällig, angenommen)“; Abos mit Statusactive(+priceAmount) ancurrentPeriodEndund monatlich danach, jeweils + 4 Tage (Auszahlung), ohne Termine abcancelAtbzw. nachcurrentPeriodEndbeicancelAtPeriodEnd.- Zeitraum:
todaybistoday + horizonDayseinschließ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 dersicher-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 am2026-10-01,fixkosten30Cents 0. - Fixkosten 449 monatlich ab
2026-09-27→ Termin2026-10-27(−449),minSicher 10444am2026-10-27. - Fixkosten am 31. ab
2026-10-31, Horizont 120 → Termine2026-10-31,2026-11-30,2026-12-31,2027-01-31. - Rechnung offen 50000 fällig
2026-10-14→erwartetam2026-10-14,endeErwartetCents 60893,endeSicherCents 10893. - Rechnung überfällig → am
2026-10-15mit „(überfällig, angenommen)“. - Abo 2000,
currentPeriodEnd 2026-10-05→ Posten am2026-10-09; mitcancelAtPeriodEnd: trueundcurrentPeriodEnd 2026-10-05→ kein Posten am 09.11. estPrepaymentCents 50000→ Posten am2026-12-10bei Horizont 90, keiner bei 30.- Auszahlung
in_transit1500 am2026-10-03→sicher+1500.
- nur Kontostand →
-
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
BOOKim Kalenderjahr vontoday, ohnecategory 'privat'; Eingänge positiv summiert, Ausgänge als positiver Betrag. ruecklageCents=Math.round(max(0, gewinnCents) * prozent / 100)kleinunternehmer.jahrCents= SummepaidCentsder Stripe-Rechnungen mitpaidDateim Jahr + Buchungencategory 'rechnung-extern'im Jahr (Stripe-Auszahlungen zählen nicht, sonst doppelt).anteilFolgejahr=jahrCents / 2500000
- Einnahmen/Ausgaben: Buchungen
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 nachdatumaufsteigend.aktion.typ∈bank-verbinden,bank-aktualisieren,rechnung-oeffnen(invoiceId,hostedUrl),zuordnen(transactionId),kunde-oeffnen(customerId),fixkosten-uebernehmen(vorschlag),einstellungen. Texte miteuroDeundformatDe.transactionsenthalten bereitsvorschlaege; bei Vorschlaghochendettextmit „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 Buchungprivat−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 50000undtoday 2026-10-01→naechsteVorauszahlung { date: '2026-12-10', amountCents: 50000 }. - Aufgaben: Zugang gültig bis
2026-10-20(19 Tage) →bank-zugangmittel; bis2026-10-10→hoch;status 'abgelaufen'→ „Bankzugang abgelaufen“hoch;lastSyncAtvor 25 h →bank-abruf; Rechnung 31 Tage überfällig →hoch, 5 Tage →mittel; ungeordneter Eingang →zuordnungmitaktion.transactionId; Abopast_due→abo-zahlunghoch; Entwurf von vor 4 Tagen →entwurfinfo, von gestern → keine Aufgabe;anteilFolgejahr 0.72→kleinunternehmermittel,1.05→hoch; Reihenfolge hoch vor mittel vor info.
- Steuer: Eingänge 16879 (
-
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 pergetDocument(config.collections.customers, id)(nur für Abo-Kunden). -
Produces:
buildOverview({ now = new Date() } = {}) → Promise<Overview>mitstand: { bank: <öffentliche Verbindung wie GET /bank>, stripe: { ok, fehler?, abgerufenUm } }kopf: { kontostandCents, verfuegbarCents, aufschluesselung: { kontostandCents, steuerRuecklageCents, fixkosten30Cents, mindestpufferCents }, tiefsterSicher, tiefsterErwartet }mitverfuegbarCents = kontostand − steuerRuecklage − fixkosten30 − mindestpufferaufgaben(Task 7),vorschau(Task 6),steuer(Task 7)abos: { monatlichCents, zahlendeKunden, gefaehrdetCents, liste: Array<{ customerId, kunde, produkt, status, betragCents, naechsteAbbuchung }> }–monatlichCents= SummepriceAmountder Statusactiveundpast_due;gefaehrdetCents= Summepast_due;zahlendeKunden= verschiedenecustomerIdmitactive;produkt=productKey ?? planKeyrechnungen: { altersgruppen: Array<{ key: 'nicht-faellig'|'1-30'|'31-60'|'ueber-60', anzahl, summeCents }>, entwuerfe }ausoffen/ueberfaellig- Stripe-Fehler: Rechnungen/Auszahlungen als leere Listen weiterrechnen,
stand.stripe.ok false.
GET /rechnungen?status=offen|ueberfaellig|bezahlt|entwurf|alle&kunde=<customerId>→{ rechnungen }(Standardalle, neueste zuerst;kundefiltert aufwebklarCustomerId)POST /rechnungenBody{ customerId, items: [{ description, quantity, unitAmountCents }], daysUntilDue, memo, senden }→{ rechnung };createdBy=req.appwriteUser.emailPOST /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
active2000 und einpast_due5000):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;aufgabenenthältueberfaelligundabo-zahlung.- Leere Daten (keine Rechnungen, Abos, Fixkosten) → 200,
vorschau.posten [],aufgabenohne 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 mitwebklarCustomerId c1.POST /rechnungen→ 200 Entwurf;POST /rechnungen/:id/senden→offen;DELETEauf offene → 409.jwt-staffauf/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"