Files
Webklar-Kundenbereich/docs/superpowers/plans/2026-10-01-finanzen-plan-1-bank.md
T

23 KiB
Raw Blame History

Finanzen Plan 1: Bankanbindung (Enable Banking → Finom) 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 Kundenportal-Server ruft Kontostand und Umsätze des Finom-Kontos über Enable Banking ab, speichert sie dauerhaft in Appwrite, erneuert den Bankzugang auf Knopfdruck und warnt vor dessen Ablauf.

Architecture: Ein dünner Enable-Banking-Client (node:crypto signiert das RS256-JWT, fetch spricht die API) unter server/services/finance/, ein Sync-Dienst, der Buchungen idempotent in bankTransactions schreibt, und ein Admin-Router /api/admin/finanzen (requireAppwriteStaff). Jobs laufen wie die übrigen Portal-Jobs per setInterval in server/index.js.

Tech Stack: Node 20 (ESM), Express 4, Appwrite REST (services/appwriteAdmin.js), node:test, Nachbauten in test/support/.

Spec: docs/superpowers/specs/2026-10-01-finanzen-design.md

Global Constraints

  • Arbeitsverzeichnis: Worktree /home/webklar/apps/kundenbereich.webklar.com/dev-finanzen, Branch feature/finanzen (von main c37dd49). Nie pushen ohne ausdrückliches OK des Nutzers (der Deploy-Webhook kann jede gepushte Branch live stellen).
  • Tests: npm test (= node --test test/*.test.js) muss vor jedem Commit grün sein; einzelne Datei: node --test test/<datei>.test.js.
  • Keine neuen npm-Abhängigkeiten.
  • Enable Banking: API https://api.enablebanking.com, JWT-Header { typ: 'JWT', alg: 'RS256', kid: <App-ID> }, Payload { iss: 'enablebanking.com', aud: 'api.enablebanking.com', iat, exp: iat + 3600 }.
  • ASPSP { name: 'Finom', country: 'DE' }, psu_type: 'business', Redirect https://ticket.webklar.com/finanzen/bank-callback, Zugang valid_until = jetzt + 179 Tage.
  • ENABLE_BANKING_API_BASE (nur Tests) wird nur für 127.0.0.1/localhost akzeptiert, sonst ignoriert (wie STRIPE_API_BASE).
  • Beträge intern immer als ganze Cent (amountCents, Integer); Eingang positiv, Ausgang negativ.
  • Daten im Format YYYY-MM-DD; Berechnungen „heute“ in Europe/Berlin.
  • Der private Schlüssel und Sitzungs-IDs erscheinen nie in Logs, Fehlermeldungen oder API-Antworten.
  • Kommentare und Fehlermeldungen auf Deutsch, Stil wie die umgebenden Dateien (ASCII-Umlaute in Code-Kommentaren wie im Bestand, echte Umlaute in Texten für Menschen).

Review Focus

  • Doppelte Abrufe: Dieselbe Buchung kommt bei jedem Abruf erneut → darf nie doppelt gespeichert werden und darf Nutzerfelder (category, matched*, fixedCostId, note) nie überschreiben. Test in Task 3.
  • Vorgemerkte Buchungen: Eine PDNG-Buchung verschwindet beim nächsten Abruf (wird gebucht oder storniert) → die alte Vormerkung wird gelöscht, die gebuchte erscheint einmal. Test in Task 3.
  • Abgelaufener Zugang: Enable Banking antwortet 401/403 → Status abgelaufen, alte Daten bleiben, kein Absturz des Jobs, keine Wiederholungsschleife. Test in Task 3.
  • Fremde Rückkehr: state passt nicht oder ist älter als 30 Minuten, oder Finom meldet error → 400 mit verständlicher Meldung, bestehende Sitzung bleibt unverändert. Test in Task 4.
  • Paging: Enable Banking liefert continuation_key → alle Seiten werden geholt, aber höchstens 20 Seiten (Schutz vor Endlosschleife). Test in Task 1.

Task 1: Konfiguration und Enable-Banking-Client

Files:

  • Modify: server/config.js (neuer Block enableBanking nach stripe)
  • Create: server/services/finance/enableBanking.js
  • Create: test/support/fakeEnableBanking.js
  • Test: test/financeEnableBanking.test.js

Interfaces:

  • Produces:

    • config.enableBanking = { appId, keyPath, apiBase, redirectUrl, aspsp: { name: 'Finom', country: 'DE' }, enabled } – appId aus ENABLE_BANKING_APP_ID, keyPath aus ENABLE_BANKING_KEY_PATH, apiBase = ENABLE_BANKING_API_BASE nur bei 127.0.0.1/localhost sonst 'https://api.enablebanking.com', redirectUrl = ${config.womsBaseUrl}/finanzen/bank-callback, enabled = beide gesetzt.
    • createJwt({ now = Date.now() } = {}) → string
    • startAuth({ state, validUntil }) → Promise<{ url, authorizationId }>
    • createSession(code) → Promise<{ sessionId, validUntil, accounts: Array<{ uid, identificationHash, iban, name, currency }> }>
    • getSession(sessionId) → Promise<{ status, validUntil, accountUids: string[] }>
    • getBalances(accountUid, { psu } = {}) → Promise<Array<{ type, amountCents, currency, date }>>
    • getTransactions(accountUid, { dateFrom, psu } = {}) → Promise<object[]> (Rohobjekte von Enable Banking, alle Seiten)
    • class EnableBankingError extends Error { status, code } – status = HTTP-Status, code = body.code ?? body.error ?? null
    • psu = { ip, userAgent } → Kopfzeilen Psu-Ip-Address, Psu-User-Agent
    • startFakeEnableBanking({ publicKeyPem }) in test/support/fakeEnableBanking.js → { url, calls, sessions: Map, accounts: Map, transactions: Map<uid, object[]>, pageSize, failNext({ path, status, body }), close() }; prüft jedes JWT mit publicKeyPem (crypto.verify('sha256', …)) und Claims, sonst 401.
  • Step 1: Write the failing tests in test/financeEnableBanking.test.js. Schlüsselpaar im Test erzeugen (crypto.generateKeyPairSync('rsa', { modulusLength: 2048 })), PEM in eine Datei im os.tmpdir() schreiben, ENABLE_BANKING_APP_ID='app-test', ENABLE_BANKING_KEY_PATH=<datei>, ENABLE_BANKING_API_BASE=<fake.url> vor dem Import von config.js setzen.

test('createJwt: RS256, kid = App-ID, Claims', () => {
  const [h, p, s] = createJwt({ now: 1_790_000_000_000 }).split('.')
  const header = JSON.parse(Buffer.from(h, 'base64url'))
  const payload = JSON.parse(Buffer.from(p, 'base64url'))
  assert.deepEqual(header, { typ: 'JWT', alg: 'RS256', kid: 'app-test' })
  assert.deepEqual(payload, { iss: 'enablebanking.com', aud: 'api.enablebanking.com', iat: 1_790_000_000, exp: 1_790_003_600 })
  assert.ok(crypto.verify('sha256', Buffer.from(`${h}.${p}`), publicKey, Buffer.from(s, 'base64url')))
})
test('startAuth schickt Finom, business, Redirect und valid_until', async () => {
  const r = await startAuth({ state: 'abc', validUntil: '2027-03-29T00:00:00.000Z' })
  assert.match(r.url, /^https?:\/\//)
  const call = fake.calls.at(-1)
  assert.equal(call.path, '/auth')
  assert.deepEqual(call.body, { access: { valid_until: '2027-03-29T00:00:00.000Z' }, aspsp: { name: 'Finom', country: 'DE' }, state: 'abc', redirect_url: 'https://ticket.webklar.com/finanzen/bank-callback', psu_type: 'business' })
})
test('createSession liefert Sitzung und Konten', async () => { /* fake: code 'ok' → session_id 's1', accounts [{uid:'u1', identification_hash:'h1', account_id:{iban:'DE62…9313'}, name:'Webklar', currency:'EUR'}], access.valid_until */
  const s = await createSession('ok')
  assert.equal(s.sessionId, 's1'); assert.equal(s.accounts[0].uid, 'u1'); assert.equal(s.accounts[0].identificationHash, 'h1'); assert.equal(s.accounts[0].iban, 'DE62…9313')
})
test('getTransactions holt alle Seiten (continuation_key), höchstens 20', async () => { /* 5 Buchungen, pageSize 2 → 3 Aufrufe, 5 Ergebnisse; endlose continuation_key → nach 20 Aufrufen Schluss */ })
test('getBalances rechnet in Cent', async () => { /* "108.93" → 10893, "0.5" → 50 */ })
test('PSU-Kopfzeilen nur wenn psu übergeben', async () => { /* calls[].headers['psu-ip-address'] */ })
test('Fehler → EnableBankingError mit status und code, ohne Sitzungs-ID in der Meldung', async () => { /* failNext({ path: /\/accounts\//, status: 401, body: { code: 'EXPIRED_SESSION' } }) */ })
test('ENABLE_BANKING_API_BASE außerhalb von 127.0.0.1 wird ignoriert', () => { /* reine Funktion resolveApiBase(value) exportieren und prüfen */ })
  • Step 2: Run node --test test/financeEnableBanking.test.js – Expected: FAIL (Modul fehlt).

  • Step 3: Implement test/support/fakeEnableBanking.js (Routen POST /auth, POST /sessions, GET /sessions/:id, GET /accounts/:uid/balances, GET /accounts/:uid/transactions mit continuation_key = Index als String), dann server/config.js und server/services/finance/enableBanking.js. Den Schlüssel bei jedem createJwt lesen (readFileSync(config.enableBanking.keyPath)), damit ein Schlüsseltausch ohne Neustart wirkt. toCents(amountString) als exportierte Hilfsfunktion: Ganzzahl- und Nachkommateil getrennt parsen, keine Gleitkommarechnung.

  • Step 4: Run node --test test/financeEnableBanking.test.js – Expected: PASS. Dann npm test – Expected: alles grün.

  • Step 5: Commit

git add server/config.js server/services/finance/enableBanking.js test/support/fakeEnableBanking.js test/financeEnableBanking.test.js
git commit -m "Finanzen: Enable-Banking-Client (JWT, Freigabe, Sitzung, Salden, Umsaetze)"

Task 2: Collections und Übernahme der bestehenden Sitzung

Files:

  • Create: scripts/setup-finanzen.mjs
  • Modify: test/support/schema.js (zweite Quelldatei)
  • Modify: server/config.js (collections.bankConnections, bankTransactions, financeSettings, financeFixedCosts)
  • Test: test/financeSetup.test.js

Interfaces:

  • Produces: Collections nach Spec „Datenmodell“ mit genau diesen Attributen (Größen):

    • bankConnections: status 16, sessionId 64, accountUid 64, accountIdHash 128, ibanMasked 40, iban 40, validUntil 32, lastSyncAt 32, lastSyncOk boolean, lastError 500, lastManualSyncAt 32, balanceBookedCents integer, balanceAvailableCents integer, balanceDate 10, latestBookingDate 10, authState 64, authStartedAt 32, warnedThresholds 32
    • bankTransactions: accountUid 64, status 8, bookingDate 10, valueDate 10, amountCents integer, currency 3, counterpartyName 255, counterpartyIban 40, remittance 1000, mcc 8, entryReference 128, category 32, matchedInvoiceId 64, matchedInvoiceNumber 64, matchedPayoutId 64, fixedCostId 64, note 500, syncedAt 32; Index idx_booking (key, bookingDate)
    • financeSettings: minBufferCents integer, taxReservePercent integer, previousYearRevenueCents integer, estPrepaymentCents integer, bankDetailsText 500
    • financeFixedCosts: name 120, amountCents integer, interval 16, nextDate 10, counterpartyMatch 120, active boolean
    • Aufruf mit Übernahme: node scripts/setup-finanzen.mjs --session <id> --account <uid> --valid-until <iso> legt/aktualisiert bankConnections/finom mit status: 'aktiv' und legt financeSettings/main mit taxReservePercent: 30, minBufferCents: 0, estPrepaymentCents: 0 an, falls nicht vorhanden. Ohne Argumente nur Collections.
    • schemaKeys(collection) sucht in setup-abo-collections.mjs und setup-finanzen.mjs.
  • Step 1: Write the failing test test/financeSetup.test.js: schemaKeys('bankTransactions') enthält amountCents, category, matchedInvoiceId; schemaKeys('bankConnections') enthält status, warnedThresholds; schemaKeys('financeFixedCosts') enthält counterpartyMatch; schemaKeys('subscriptions') funktioniert weiter.

  • Step 2: Run node --test test/financeSetup.test.js – Expected: FAIL („Collection bankTransactions fehlt im Setup-Skript“).

  • Step 3: Implement scripts/setup-finanzen.mjs nach dem Muster von setup-abo-collections.mjs (gleiche Helfer aw, ensure, waitForAttributes, ensureCollection, Blockformat '<id>', … \n ], damit schemaKeys greift; permissions: []). Argumente mit node:util parseArgs. Sitzungs-ID nie ausgeben (nur „Sitzung übernommen, gültig bis …“).

  • Step 4: Run node --test test/financeSetup.test.js und npm test – Expected: PASS.

  • Step 5: Commit git commit -m "Finanzen: Collections und Setup-Skript mit Sitzungsuebernahme" (Dateien einzeln adden).

Task 3: Buchungen abrufen und speichern

Files:

  • Create: server/services/finance/dates.js
  • Create: server/services/finance/bankStore.js
  • Create: server/services/finance/bankSync.js
  • Test: test/financeDates.test.js
  • Test: test/financeBankSync.test.js

Interfaces:

  • Consumes: Task 1 (getBalances, getTransactions, EnableBankingError), Task 2 (Collections).
  • Produces:
    • dates.js: berlinDate(date = new Date()) → 'YYYY-MM-DD' (Europe/Berlin), addDays(ymd, n) → 'YYYY-MM-DD', daysBetween(fromYmd, toYmd) → number (ganze Tage, toYmd − fromYmd), formatDe(ymd) → 'TT.MM.JJJJ'. Tests: berlinDate(new Date('2026-09-30T22:30:00Z')) === '2026-10-01'; addDays('2026-03-29', 1) === '2026-03-30' (Zeitumstellung); daysBetween('2026-10-01', '2027-03-29') === 179; formatDe('2027-03-29') === '29.03.2027'.
    • transactionDocId(raw) → string (36 Zeichen Hex, Regel aus Spec)
    • toTransactionDoc(raw, { accountUid, syncedAt }) → object – Felder aus Spec; CRDT → debtor/debtor_account, DBIT → creditor/creditor_account; remittance = remittance_information.join(' ') (gekürzt auf 1000); iban aus account.iban ?? account.other?.identification ?? ''.
    • bankStore.getConnection() → Promise<object|null>, saveConnection(patch) → Promise<object> (legt finom an oder aktualisiert), upsertTransaction(doc) → Promise<'neu'|'aktualisiert'> (aktualisiert nur Bankfelder: status, bookingDate, valueDate, amountCents, currency, counterpartyName, counterpartyIban, remittance, mcc, entryReference, accountUid, syncedAt), listTransactions({ limit = 1000 } = {}) → Promise<object[]> (neueste zuerst nach bookingDate), getTransaction(id), updateTransaction(id, patch), deleteTransaction(id).
    • syncBank({ now = new Date(), psu = null } = {}) → Promise<{ ok: boolean, neu: number, aktualisiert: number, geloescht: number, error?: string }>

Ablauf syncBank: Verbindung laden (keine oder status: 'abgelaufen' → { ok: false, error: 'Kein aktiver Bankzugang' } ohne API-Aufruf); dateFrom = latestBookingDate − 7 Tage, mindestens heute − 89 Tage, ohne latestBookingDate heute − 89 Tage; Salden holen (ITBD oder CLBD → balanceBookedCents, ITAV → balanceAvailableCents, Datum des gewählten Saldos → balanceDate); Umsätze holen und upserten; PDNG-Dokumente mit bookingDate >= dateFrom, die in diesem Abruf nicht vorkamen, löschen; Verbindung aktualisieren (lastSyncAt, lastSyncOk: true, lastError: '', latestBookingDate = jüngstes bookingDate der BOOK-Buchungen). Fehler: EnableBankingError mit Status 401/403 → status: 'abgelaufen', lastError: 'Bankzugang abgelaufen oder widerrufen'; andere Fehler → status bleibt, lastSyncOk: false, lastError = Meldung. Nie werfen.

  • Step 1: Write the failing tests (startAppwriteMock({ schema: { bankConnections: schemaKeys('bankConnections'), bankTransactions: schemaKeys('bankTransactions') } }) + startFakeEnableBanking):
    • toTransactionDoc: Eingang „EWV SOFTWARE GMBH“, 168.79, CRDT → amountCents: 16879, counterpartyName: 'EWV SOFTWARE GMBH', remittance: 'Rechnung 0005 vom 28.07.2026'; Kartenzahlung DBIT 4.49 mit merchant_category_code: '4816' → amountCents: -449, mcc: '4816', counterpartyIban: ''.
    • transactionDocId: gleiche entry_reference → gleiche ID; ohne entry_reference und transaction_id → stabil aus Datum|Betrag|Gegenpartei|Zweck; Ergebnis passt auf /^[a-f0-9]{36}$/.
    • Erster Abruf: 9 Buchungen → neu: 9, Saldo balanceBookedCents: 10893, latestBookingDate: '2026-09-29', dateFrom im Aufruf = heute − 89 Tage.
    • Zweiter Abruf mit denselben Buchungen nach updateTransaction(id, { category: 'rechnung-extern', note: 'x' }) → neu: 0, category/note unverändert.
    • Vormerkung PDNG (entry_reference p1) im ersten Abruf, im zweiten stattdessen BOOK (entry_reference b1) → Dokument p1 gelöscht (geloescht: 1), b1 vorhanden.
    • Fake antwortet 401 auf /accounts/…/balances → Ergebnis ok: false, Verbindung status: 'abgelaufen', vorhandene Buchungen bleiben; dritter Aufruf macht keinen API-Aufruf.
    • Fake antwortet 500 → status: 'aktiv', lastSyncOk: false, lastError gesetzt.
  • Step 2: Run node --test test/financeDates.test.js test/financeBankSync.test.js – Expected: FAIL.
  • Step 3: Implement bankStore.js (über listDocuments/getDocument/createDocument/updateDocument/deleteDocument aus appwriteAdmin.js, Query.orderDesc('bookingDate'), Query.limit) und bankSync.js. „Heute“ immer über berlinDate() aus dates.js (Intl.DateTimeFormat('sv-SE', { timeZone: 'Europe/Berlin' }); Tagesrechnung in UTC-Mitternacht, damit die Zeitumstellung nicht stört).
  • Step 4: Run node --test test/financeDates.test.js test/financeBankSync.test.js und npm test – Expected: PASS.
  • Step 5: Commit git commit -m "Finanzen: Bankabruf speichert Buchungen idempotent, Vormerkungen, Ablauf"

Task 4: Bankzugang neu verbinden

Files:

  • Modify: server/services/finance/bankSync.js
  • Test: test/financeBankAuth.test.js

Interfaces:

  • Consumes: Task 1 (startAuth, createSession), Task 3 (bankStore, syncBank).

  • Produces:

    • startBankAuth({ now = new Date() } = {}) → Promise<{ url }> – state = randomBytes(12).toString('hex'), speichert authState, authStartedAt.
    • completeBankAuth({ code, state, error }, { now = new Date() } = {}) → Promise<{ ok: true, validUntil, sync }>; Fehler als Error mit status = 400 und Meldung:
      • state leer/abweichend → „Diese Rückkehr passt zu keiner gestarteten Freigabe.“
      • authStartedAt älter als 30 Minuten → „Die Freigabe ist abgelaufen. Bitte neu starten.“
      • error gesetzt → „Finom hat die Freigabe nicht erteilt ().“
    • Kontowahl: gleiches identificationHash wie gespeichert, sonst gleiche IBAN, sonst erstes EUR-Konto; keins → 400 „Kein Euro-Konto in der Freigabe.“
    • Erfolg setzt sessionId, accountUid, accountIdHash, iban, ibanMasked (erste 4 + „…“ + letzte 4), validUntil, status: 'aktiv', authState: '', warnedThresholds: '', lastError: '' und startet syncBank.
  • Step 1: Write the failing tests: Start speichert authState und liefert URL; Rückkehr mit richtigem state → neue sessionId, status: 'aktiv', warnedThresholds: '', Sync gelaufen; falscher state → 400 und alte sessionId unverändert; 31 Minuten später → 400; error: 'access_denied' → 400 mit „access_denied“ in der Meldung; zwei Konten, gespeicherter Hash passt aufs zweite → zweites gewählt.

  • Step 2: Run node --test test/financeBankAuth.test.js – Expected: FAIL.

  • Step 3: Implement in bankSync.js.

  • Step 4: Run die Testdatei und npm test – Expected: PASS.

  • Step 5: Commit git commit -m "Finanzen: Bankzugang aus WOMS neu verbinden (Freigabe, Rueckkehr, Kontowahl)"

Task 5: Ablaufwarnung (Vorlage T18) und Jobs

Files:

  • Create: server/mail-templates/T18 Bankzugang erneuern.html
  • Modify: server/services/finance/bankSync.js
  • Modify: server/index.js (Jobs im app.listen-Callback)
  • Test: test/financeBankConsent.test.js

Interfaces:

  • Produces: checkBankConsent({ now = new Date() } = {}) → Promise<{ sent: number|null }> – tage = volle Tage bis validUntil (aufgerundet); Schwellen [30, 14, 3, 0]; die kleinste erreichte, noch nicht in warnedThresholds stehende Schwelle löst sendAdminNotice({ template: 'T18', values, blocks }) aus und wird angehängt; sent = Schwelle oder null.

  • T18: Betreff Bankzugang Finom läuft {{ablauf_text}} mit ablauf_text = „in {{tage}} Tagen ab“ bzw. „heute ab“/„ist abgelaufen“; Platzhalter {{ablauf_text}}, {{gueltig_bis}} (29.03.2027), {{link}} (${config.womsBaseUrl}/finance/einstellungen); optionaler Block abgelaufen. Kopfkommentar und Aufbau wie T17 Repo zuordnen.html.

  • Jobs (nur wenn config.enableBanking.enabled): syncBank() 2 Minuten nach Start und alle 6 Stunden; checkBankConsent() 3 Minuten nach Start und alle 24 Stunden; beide .unref(), Fehler nur console.warn mit Präfix [finanzen].

  • Step 1: Write the failing tests (mailSink wie in aboRoutes.test.js): validUntil in 29 Tagen → T18 einmal mit gueltig_bis, zweiter Aufruf am selben Tag → keine Mail; 13 Tage → Schwelle 14; abgelaufen → Block abgelaufen enthalten; test/mailTemplates.test.js bleibt grün (T18 hat Kopfkommentar mit Betreff und Platzhaltern).

  • Step 2: Run node --test test/financeBankConsent.test.js – Expected: FAIL.

  • Step 3: Implement Vorlage, checkBankConsent, Jobs in server/index.js.

  • Step 4: Run node --test test/financeBankConsent.test.js test/mailTemplates.test.js und npm test – Expected: PASS.

  • Step 5: Commit git commit -m "Finanzen: Warnung vor Ablauf des Bankzugangs (T18) und Abruf-Jobs"

Task 6: Admin-Router für Bank und Buchungen

Files:

  • Create: server/routes/admin/finanzen.js
  • Modify: server/index.js (app.use('/api/admin/finanzen', adminFinanzenRoutes) bei den übrigen /api/admin/*)
  • Modify: server/index.js CORS-Methoden bleiben GET, POST, PATCH, DELETE, OPTIONS (kein PUT verwenden)
  • Test: test/financeRoutesBank.test.js

Interfaces:

  • Consumes: Tasks 3–4.

  • Produces (alle hinter requireAppwriteStaff):

    • GET /api/admin/finanzen/bank → { verbindung: { status, ibanMasked, validUntil, lastSyncAt, lastSyncOk, lastError, balanceBookedCents, balanceAvailableCents, balanceDate, latestBookingDate } | null } (ohne sessionId, iban, authState)
    • POST /api/admin/finanzen/bank/aktualisieren → Ergebnis von syncBank({ psu: { ip: req.ip, userAgent: req.get('User-Agent') } }); war lastManualSyncAt vor < 10 Minuten → 429 { error: 'Bitte in N Minuten erneut versuchen.' }; setzt lastManualSyncAt.
    • POST /api/admin/finanzen/bank/verbinden → { url }
    • POST /api/admin/finanzen/bank/rueckkehr Body { code, state, error } → { ok: true, validUntil } oder 400 { error }
    • GET /api/admin/finanzen/buchungen → { buchungen: object[] } (Felder aus bankTransactions, ohne $-Metafelder außer $id)
    • Router exportiert zusätzlich nichts; Plan 2 ergänzt Routen in derselben Datei.
  • Step 1: Write the failing tests mit startTestApp({ '/api/admin/finanzen': routes }) und Header Authorization: Bearer jwt-admin: ohne JWT → 401; jwt-staff → 403; GET /bank enthält kein sessionId; zweimal aktualisieren hintereinander → 200 dann 429; rueckkehr mit falschem state → 400 mit Meldung; GET /buchungen sortiert neueste zuerst.

  • Step 2: Run node --test test/financeRoutesBank.test.js – Expected: FAIL.

  • Step 3: Implement Router und Mount.

  • Step 4: Run die Testdatei und npm test – Expected: PASS.

  • Step 5: Commit git commit -m "Finanzen: Admin-Routen fuer Bankstatus, Abruf, Neuverbindung und Buchungen"