Finanzen: Start-Abruf nur bei altem Stand, unlesbare Buchungen ueberspringen, Mail bei Widerruf des Bankzugangs

I1: shouldRunStartupSync; der Abruf 2 Minuten nach dem Start entfaellt, wenn der letzte erfolgreiche
Abruf weniger als 5,5 Stunden her ist (Deployments verbrauchten das Tageslimit der Bank).
M4: eine unlesbare Buchung wird uebersprungen und gezaehlt, der Abruf endet ok.
M10: 401/403 beim Abruf sendet T18 einmal (Stufe 0 in warnedThresholds).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-01 15:42:06 +00:00
1 parent 3c5e06723f
commit ef2ce49669
3 files changed
+168 -25

No files matched your search

+16 -5
View File
@@ -6,7 +6,8 @@ import { config, assertServerConfig, WOMS_DATABASE_ID } from './config.js'
import { verifyDatabaseAccess } from './services/appwriteAdmin.js'
import { syncPreviewTlsOnStartup } from './services/previewDeploy.js'
import { runReconcile } from './services/previewReconcile.js'
import { checkBankConsent, syncBank } from './services/finance/bankSync.js'
import { checkBankConsent, shouldRunStartupSync, syncBank } from './services/finance/bankSync.js'
import { getConnection } from './services/finance/bankStore.js'
import { requireSession, sessionMiddleware } from './middleware/session.js'
import authRoutes from './routes/auth.js'
import projectsRoutes from './routes/projects.js'
@@ -189,15 +190,25 @@ const server = app.listen(config.port, '0.0.0.0', () => {
setTimeout(repoAssign, 60 * 1000).unref()
setInterval(repoAssign, 10 * 60 * 1000).unref()
// Finanzen (nur wenn Enable Banking eingerichtet ist): Bankabruf 2 Minuten nach dem Start und alle 6 Stunden,
// Warnung vor dem Ablauf des Bankzugangs 3 Minuten nach dem Start und taeglich. syncBank wirft nie, meldet einen
// Fehlschlag aber im Ergebnis; die Meldung enthaelt keine Sitzungs- oder Schluesselangaben.
// Finanzen (nur wenn Enable Banking eingerichtet ist): Bankabruf 2 Minuten nach dem Start (nur wenn der letzte
// nicht frisch war) und alle 6 Stunden, Warnung vor dem Ablauf des Bankzugangs 3 Minuten nach dem Start und
// taeglich. syncBank wirft nie, meldet einen Fehlschlag aber im Ergebnis; die Meldung enthaelt keine Sitzungs-
// oder Schluesselangaben.
if (config.enableBanking.enabled) {
const bankSync = () =>
syncBank()
.then((r) => !r.ok && console.warn('[finanzen] Bankabruf fehlgeschlagen:', r.error))
.catch((err) => console.warn('[finanzen] Bankabruf fehlgeschlagen:', err.message))
setTimeout(bankSync, 2 * 60 * 1000).unref()
// Beim Start nur abrufen, wenn der letzte Abruf nicht frisch war: Deployments starten den Server oft neu und
// wuerden sonst das Tageslimit der Bank aufbrauchen
const startupBankSync = () =>
getConnection()
.then((connection) => {
if (shouldRunStartupSync(connection, new Date())) return bankSync()
console.log('[finanzen] Bankabruf beim Start übersprungen (letzter Abruf ist frisch)')
})
.catch((err) => console.warn('[finanzen] Bankabruf fehlgeschlagen:', err.message))
setTimeout(startupBankSync, 2 * 60 * 1000).unref()
setInterval(bankSync, 6 * 60 * 60 * 1000).unref()
const bankConsent = () =>
+45 -5
View File
@@ -119,12 +119,18 @@ async function runSync({ now, psu }) {
const balances = await getBalances(accountUid, { psu })
const rawTransactions = await getTransactions(accountUid, { dateFrom, psu })
// Erst alles umrechnen, dann schreiben: eine kaputte Buchung der Bank bricht ab, bevor etwas gespeichert ist
// Erst alles umrechnen, dann schreiben. Eine unlesbare Buchung der Bank (kaputter Betrag, unbekannte Richtung)
// wird uebersprungen und gezaehlt, sie blockiert nicht jeden weiteren Abruf
const syncedAt = now.toISOString()
const docs = new Map()
let skipped = 0
for (const raw of rawTransactions) {
const doc = toTransactionDoc(raw, { accountUid, syncedAt })
docs.set(doc.$id, doc)
try {
const doc = toTransactionDoc(raw, { accountUid, syncedAt })
docs.set(doc.$id, doc)
} catch {
skipped += 1
}
}
let latestBookingDate = latest || ''
@@ -147,7 +153,7 @@ async function runSync({ now, psu }) {
latestBookingDate,
lastSyncAt: syncedAt,
lastSyncOk: true,
lastError: '',
lastError: skipped ? `${skipped} Buchung(en) übersprungen (unlesbar).` : '',
})
return { ...result, ok: true }
} catch (err) {
@@ -155,7 +161,8 @@ async function runSync({ now, psu }) {
const expired = err instanceof EnableBankingError && (err.status === 401 || err.status === 403)
const error = expired ? EXPIRED_MESSAGE : errorText(err)
try {
await saveConnection({ ...(expired ? { status: 'abgelaufen' } : {}), lastSyncOk: false, lastError: error })
const saved = await saveConnection({ ...(expired ? { status: 'abgelaufen' } : {}), lastSyncOk: false, lastError: error })
if (expired) await notifyRevoked(saved)
} catch {
// Appwrite selbst gestoert: der Fehler geht trotzdem an den Aufrufer
}
@@ -163,6 +170,39 @@ async function runSync({ now, psu }) {
}
}
/**
* Ablauf oder Widerruf per Mail (T18) melden, hoechstens einmal je Freigabe: Stufe 0 steht danach in
* warnedThresholds, dann mailt auch checkBankConsent nicht mehr. War Stufe 0 schon gemeldet (am Ablauftag), bleibt es
* bei dieser Mail. Kommt die Mail nicht raus, bleibt die Stufe offen (checkBankConsent versucht es spaeter).
*/
async function notifyRevoked(connection) {
const warned = parseWarned(connection?.warnedThresholds)
if (warned.includes(0)) return
const validUntilMs = Date.parse(connection?.validUntil ?? '')
const result = await sendAdminNotice({
template: 'T18',
values: {
ablauf_text: 'ist abgelaufen oder wurde widerrufen',
gueltig_bis: Number.isNaN(validUntilMs) ? 'unbekannt' : formatDe(berlinDate(new Date(validUntilMs))),
link: `${config.womsBaseUrl}/finance/einstellungen`,
},
blocks: { abgelaufen: true },
})
if (result.sent) await saveConnection({ warnedThresholds: [...new Set([...warned, 0])].sort((a, b) => b - a).join(',') })
}
// Ein Abruf beim Start des Servers wird uebersprungen, wenn der letzte erfolgreiche weniger als 5,5 Stunden her ist:
// jedes Deployment startet den Server neu, und die Bank erlaubt nur etwa vier Abrufe ohne Nutzer pro Tag
const STARTUP_SYNC_MIN_AGE_MS = 5.5 * 3_600_000
/** Soll der Abruf nach dem Start des Servers laufen? Nein, wenn der letzte Abruf gelang und noch frisch ist. */
export function shouldRunStartupSync(connection, now = new Date()) {
if (connection?.lastSyncOk !== true) return true
const lastMs = Date.parse(connection.lastSyncAt ?? '')
if (Number.isNaN(lastMs)) return true
return now.getTime() - lastMs >= STARTUP_SYNC_MIN_AGE_MS
}
/**
* Salden und Buchungen abrufen und speichern. psu = { ip, userAgent } nur, wenn ein Mensch den Abruf ausgeloest
* hat (zaehlt dann nicht auf das Tageslimit der Bank). Liefert { ok, neu, aktualisiert, geloescht, error? } und