Finanzen: Zeitgrenzen fuer Stripe-Lesen, Schutz vor doppelt angelegten Rechnungen

I2: Leseanfragen an Stripe mit timeout 15 s und einer Wiederholung (auch Folgeseiten), die Uebersicht
wartet hoechstens 20 s auf Stripe und rechnet sonst ohne Stripe weiter.
I3: idempotencyKey vom Aufrufer an stripe.invoices.create, Schutz im Prozess gegen gleichzeitiges und
30 s spaeteres Doppelanlegen (409), 502 mit invoiceId/Nummer wenn die fertige Rechnung nicht lesbar ist.
M9: Warnung, wenn die Rechnungsliste bei 1000 abgeschnitten wird.
M11: Ursache eines 502 an einer bestehenden Rechnung (Art, Status, Code) im Protokoll.

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 ef2ce49669
commit daa451094a
8 files changed
+476 -48

No files matched your search

+26 -6
View File
@@ -39,9 +39,28 @@ function berlinDayOf(iso) {
// --- Stripe -------------------------------------------------------------------------------------------------
/** Rechnungen und Auszahlungen; was nicht kommt, ist eine leere Liste und macht ok false. */
async function loadStripe(now) {
const [invoices, payouts] = await Promise.allSettled([listInvoices(), listPayouts()])
// So lange wartet die Uebersicht hoechstens auf Stripe (die Anfragen selbst haben eigene Zeitgrenzen, siehe stripeFinance)
const STRIPE_WAIT_MS = 20_000
const failedStatus = () => ({ ok: false, fehler: STRIPE_UNAVAILABLE_MESSAGE, abgerufenUm: null })
/**
* Rechnungen und Auszahlungen; was nicht kommt, ist eine leere Liste und macht ok false. Antwortet Stripe nicht binnen
* waitMs, rechnet die Uebersicht ohne Stripe weiter (wie bei einem Fehler); die Anfragen laufen im Hintergrund aus.
*/
async function loadStripe(now, waitMs) {
let timer
const timedOut = new Promise((resolve) => {
timer = setTimeout(resolve, waitMs, null)
timer.unref?.()
})
const settled = await Promise.race([Promise.allSettled([listInvoices(), listPayouts()]), timedOut])
clearTimeout(timer)
if (!settled) {
console.error(`[finanzen] Stripe laden fehlgeschlagen: keine Antwort nach ${waitMs} ms`)
return { ok: false, invoices: [], payouts: [], status: failedStatus() }
}
const [invoices, payouts] = settled
if (invoices.status === 'rejected') logFailure('Rechnungen laden', invoices.reason)
if (payouts.status === 'rejected') logFailure('Auszahlungen laden', payouts.reason)
const ok = invoices.status === 'fulfilled' && payouts.status === 'fulfilled'
@@ -50,7 +69,7 @@ async function loadStripe(now) {
invoices: invoices.status === 'fulfilled' ? invoices.value : [],
payouts: payouts.status === 'fulfilled' ? payouts.value : [],
// abgerufenUm: wann der Stand geholt wurde; bei einem Fehler gibt es keinen
status: ok ? { ok: true, abgerufenUm: now.toISOString() } : { ok: false, fehler: STRIPE_UNAVAILABLE_MESSAGE, abgerufenUm: null },
status: ok ? { ok: true, abgerufenUm: now.toISOString() } : failedStatus(),
}
}
@@ -151,9 +170,10 @@ const isOpenIncoming = (tx) => tx.status === 'BOOK' && isCents(tx.amountCents) &
/**
* @param {object} [args]
* @param {Date} [args.now] Zeitpunkt; "heute" ist der Berliner Tag dazu
* @param {number} [args.stripeWaitMs] so lange wartet die Uebersicht hoechstens auf Stripe (Voreinstellung 20 s)
* @returns {Promise<{ stand, kopf, aufgaben, vorschau, steuer, abos, rechnungen }>}
*/
export async function buildOverview({ now = new Date() } = {}) {
export async function buildOverview({ now = new Date(), stripeWaitMs = STRIPE_WAIT_MS } = {}) {
const today = berlinDate(now)
const [connection, transactions, settings, fixedCosts, subscriptions, stripe] = await Promise.all([
getConnection(),
@@ -161,7 +181,7 @@ export async function buildOverview({ now = new Date() } = {}) {
getSettings(),
listFixedCosts(),
listDocuments(config.collections.subscriptions, [Query.limit(SUBSCRIPTION_LIMIT)]),
loadStripe(now),
loadStripe(now, stripeWaitMs),
])
// Kundennamen nur für die Abos der Liste