import { getStripe, subscriptionIdOfInvoice } from '../stripeBilling.js' import { addDays, berlinDate, daysBetween } from './dates.js' /** * Stripe-Rechnungen und -Auszahlungen fuer die Finanzen (nur lesen). Rechnungen und Auszahlungen werden nicht * gespiegelt: sie kommen bei jeder Anfrage von Stripe, hoechstens alle 60 Sekunden (Zwischenspeicher je Art). * Die Rohdaten liegen im Zwischenspeicher, gemappt wird bei jedem Aufruf neu: so stimmt "ueberfaellig" auch, * wenn um Mitternacht ein neuer Tag beginnt, und ein Aufrufer kann den Zwischenspeicher nicht veraendern. */ const CACHE_MS = 60_000 // Rueckblick: Rechnungen der letzten 400 Tage (reicht fuer ein volles Jahr samt Nachzuegler) const LOOKBACK_DAYS = 400 const MAX_INVOICES = 1000 // Stripe-Status -> Status nach aussen; "open" wird je nach Faelligkeit zu "offen" oder "ueberfaellig" const STATUS_DE = { draft: 'entwurf', open: 'offen', paid: 'bezahlt', void: 'storniert', uncollectible: 'uneinbringlich' } // Je Art ein Eintrag { at, promise }; die Zusage (statt der Daten) wird gemerkt, damit gleichzeitige Aufrufe // dieselbe Stripe-Anfrage teilen. Ein Fehler wird nicht zwischengespeichert. const caches = { invoices: null, payouts: null } function cached(kind, force, load) { const entry = caches[kind] if (!force && entry && Date.now() - entry.at < CACHE_MS) return entry.promise const fresh = { at: Date.now(), promise: null } fresh.promise = load().catch((err) => { if (caches[kind] === fresh) caches[kind] = null throw err }) caches[kind] = fresh return fresh.promise } /** Zwischenspeicher leeren (nach jeder Aenderung an Rechnungen oder Auszahlungen, damit die Ansicht stimmt). */ export function invalidateFinanceCache() { caches.invoices = null caches.payouts = null } // Stripe liefert Zeitpunkte als Unix-Sekunden; die Finanzen rechnen mit Berliner Kalendertagen const ymdOfUnix = (seconds) => (Number.isFinite(seconds) ? berlinDate(new Date(seconds * 1000)) : null) /** Eine rohe Stripe-Rechnung (Kunde moeglichst expandiert) in die deutsche Form der Finanzen; today = YYYY-MM-DD. */ export function mapInvoice(raw, { today }) { const customer = raw.customer && typeof raw.customer === 'object' ? raw.customer : null const metadata = raw.metadata || {} const dueDate = ymdOfUnix(raw.due_date) let status = STATUS_DE[raw.status] || raw.status if (raw.status === 'open' && dueDate && dueDate < today) status = 'ueberfaellig' return { id: raw.id, number: raw.number || '', status, customerName: raw.customer_name || customer?.name || '', customerEmail: raw.customer_email || customer?.email || '', webklarCustomerId: metadata.webklarCustomerId || customer?.metadata?.webklarCustomerId || '', createdDate: ymdOfUnix(raw.created), dueDate, totalCents: raw.total ?? 0, remainingCents: raw.amount_remaining ?? 0, paidCents: raw.amount_paid ?? 0, // Bei einer Bankzuordnung zaehlt der Tag, an dem das Geld auf dem Konto ankam, nicht der der Stripe-Buchung paidDate: metadata.bankBookingDate || ymdOfUnix(raw.status_transitions?.paid_at), overdueDays: status === 'ueberfaellig' ? daysBetween(dueDate, today) : 0, source: subscriptionIdOfInvoice(raw) ? 'abo' : 'projekt', hostedUrl: raw.hosted_invoice_url || null, pdfUrl: raw.invoice_pdf || null, outOfBand: Boolean(metadata.bankTransactionId), bankTransactionId: metadata.bankTransactionId || '', } } /** Rechnungen der letzten 400 Tage (hoechstens 1000, neueste zuerst), mit Kunde, auf Deutsch gemappt. */ export async function listInvoices({ force = false } = {}) { const raw = await cached('invoices', force, () => getStripe() .invoices.list({ limit: 100, created: { gte: Math.floor(Date.now() / 1000) - LOOKBACK_DAYS * 86_400 }, expand: ['data.customer'], }) .autoPagingToArray({ limit: MAX_INVOICES }) ) const today = berlinDate() return raw.map((invoice) => mapInvoice(invoice, { today })) } /** Die letzten 50 Stripe-Auszahlungen: { id, amountCents, arrivalDate, status }. */ export async function listPayouts({ force = false } = {}) { const raw = await cached('payouts', force, () => getStripe().payouts.list({ limit: 50 }).then((page) => page.data)) return raw.map((payout) => ({ id: payout.id, amountCents: payout.amount, arrivalDate: ymdOfUnix(payout.arrival_date), status: payout.status, })) }