# 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/.test.js`. - Keine neuen npm-Abhängigkeiten. - Enable Banking: API `https://api.enablebanking.com`, JWT-Header `{ typ: 'JWT', alg: 'RS256', kid: }`, 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>` - `getTransactions(accountUid, { dateFrom, psu } = {}) → Promise` (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, 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=`, `ENABLE_BANKING_API_BASE=` vor dem Import von `config.js` setzen. ```js 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** ```bash 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 --account --valid-until ` 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 `'',` … `\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`, `saveConnection(patch) → Promise` (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` (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"`