Finanzen: Warnung vor Ablauf des Bankzugangs (T18) und Abruf-Jobs

checkBankConsent mailt bei 30, 14, 3 Tagen und am Ablauftag (Berliner Kalendertage),
merkt sich die erreichten Stufen in warnedThresholds und holt eine nicht versandte
Mail beim naechsten Lauf nach. Jobs nur mit Enable Banking: Abruf nach 2 Minuten und
alle 6 Stunden, Pruefung nach 3 Minuten und taeglich.

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:01 +00:00
1 parent cba14ed0f0
commit 7b35ed67a5
5 files changed
+375 -5

No files matched your search

+69 -2
View File
@@ -1,7 +1,9 @@
import { createHash, randomBytes, timingSafeEqual } from 'node:crypto'
import { config } from '../../config.js'
import { withLocks } from '../keyedLock.js'
import { sendAdminNotice } from '../mailer.js'
import { createSession, EnableBankingError, getBalances, getTransactions, startAuth, toCents } from './enableBanking.js'
import { addDays, berlinDate } from './dates.js'
import { addDays, berlinDate, daysBetween, formatDe } from './dates.js'
import { deleteTransaction, getConnection, listPendingTransactions, saveConnection, upsertTransaction } from './bankStore.js'
/**
@@ -11,7 +13,8 @@ import { deleteTransaction, getConnection, listPendingTransactions, saveConnecti
* Bank nicht mehr nennt, werden geloescht. syncBank wirft nie: jeder Fehler landet in der Verbindung (lastError).
*
* Ausserdem der Weg zum neuen Bankzugang (startBankAuth, completeBankAuth): Freigabe bei Finom starten, nach der
* Rueckkehr die Sitzung speichern und sofort einmal abrufen.
* Rueckkehr die Sitzung speichern und sofort einmal abrufen, und die Warnung vor dem Ablauf des Zugangs
* (checkBankConsent, Mail T18).
*/
// Die Bank liefert hoechstens ~90 Tage: ein Abruf beginnt spaetestens heute - 89 Tage, sonst eine Woche vor der
@@ -271,3 +274,67 @@ export async function completeBankAuth({ code, state, error } = {}, { now = new
const sync = await syncBank({ now, psu })
return { ok: true, validUntil, sync }
}
// ---------- Warnung vor Ablauf des Bankzugangs ----------
// Warnstufen in Tagen bis zum Ablauf; 0 = am Ablauftag (und danach, falls die Mail zuvor nicht ankam)
const WARN_THRESHOLDS = [30, 14, 3, 0]
const parseWarned = (value) =>
String(value || '')
.split(',')
.map((part) => Number.parseInt(part, 10))
.filter((n) => Number.isInteger(n))
/** Der Satzteil im Betreff: "läuft in 14 Tagen ab", "läuft morgen ab", "läuft heute ab", "ist abgelaufen". */
function ablaufText({ expired, tage }) {
if (expired) return 'ist abgelaufen'
if (tage <= 0) return 'läuft heute ab'
if (tage === 1) return 'läuft morgen ab'
return `läuft in ${tage} Tagen ab`
}
/**
* Warnt das Team per Mail (T18), wenn der Bankzugang bald ablaeuft oder abgelaufen ist. tage = Berliner
* Kalendertage von heute bis zum Ablauftag (0 am Ablauftag, solange der Zugang noch gilt; abgelaufen, sobald
* validUntil vorbei ist). Warnstufen 30, 14, 3 und 0: erreicht ist eine Stufe, wenn tage <= Stufe; gemailt wird
* die kleinste erreichte, falls sie noch nicht in warnedThresholds steht. Gespeichert werden danach alle
* erreichten Stufen (sonst kaeme nach "14" am naechsten Tag noch "30"). Die Stufen stehen in der Verbindung
* und werden bei einer Neuverbindung geleert (completeBankAuth). Kommt die Mail nicht raus, bleibt die Stufe
* offen und der naechste Lauf versucht es erneut. Liefert { sent: Stufe } oder { sent: null }; wirft nur, wenn
* Appwrite nicht erreichbar ist (der Job faengt das ab).
*/
export async function checkBankConsent({ now = new Date() } = {}) {
const connection = await getConnection()
const validUntilMs = Date.parse(connection?.validUntil ?? '')
if (Number.isNaN(validUntilMs)) return { sent: null }
const ablaufTag = berlinDate(new Date(validUntilMs))
const tage = daysBetween(berlinDate(now), ablaufTag)
const expired = validUntilMs <= now.getTime()
const reached = WARN_THRESHOLDS.filter((threshold) => tage <= threshold)
if (!reached.length) return { sent: null }
const threshold = Math.min(...reached)
if (parseWarned(connection.warnedThresholds).includes(threshold)) return { sent: null }
const result = await sendAdminNotice({
template: 'T18',
values: {
ablauf_text: ablaufText({ expired, tage }),
gueltig_bis: formatDe(ablaufTag),
link: `${config.womsBaseUrl}/finance/einstellungen`,
},
blocks: { abgelaufen: expired },
})
if (!result.sent) return { sent: null }
// Speichern unter der Sperre der Abrufe: eine Neuverbindung, die waehrend des Versands ihre Warnstufen leert
// (completeBankAuth), wird nicht mit den Stufen der alten Frist ueberschrieben
await withLocks(SYNC_LOCK, async () => {
const current = await getConnection()
if (!current || current.validUntil !== connection.validUntil) return
const warned = new Set([...parseWarned(current.warnedThresholds), ...reached])
await saveConnection({ warnedThresholds: [...warned].sort((a, b) => b - a).join(',') })
})
return { sent: threshold }
}