SaaS-Billing-Edge-Cases: Proration, Gutschriften und fehlgeschlagene Zahlungen, die dein Umsatzmodell brechen

#SaaS-Billing-Edge-Cases
Sandor Farkas - Founder & Lead Developer at Wolf-Tech

Sandor Farkas

Gründer & Lead Developer

Experte für Softwareentwicklung und Legacy-Code-Optimierung

Die meisten Stripe-Tutorials enden dort, wo Billing-Probleme anfangen. Eine Subscription anlegen, einen Webhook empfangen und eine Datenbankzeile aktualisieren ist ein Wochenendprojekt. Die SaaS-Billing-Edge-Cases, die einem Geschäft wirklich schaden, tauchen später auf: Ein Kunde upgradet mitten im Monat und die Rechnung sieht falsch aus, eine Karte schlägt still fehl und der Account bleibt drei Monate aktiv, eine Erstattung landet in einem anderen Quartal als der Umsatz, den sie rückgängig macht. Nichts davon ist exotisch. Jedes Subscription-Geschäft trifft alle diese Fälle im ersten Jahr, und wer nicht vorbereitet ist, rekonstruiert am Ende seine Umsatzhistorie aus Stripe-Exporten und Support-Tickets.

Dieser Beitrag geht die Edge Cases durch, die wir in Audits von Billing-Code am häufigsten sehen, und zeigt, wie korrekte Behandlung für jeden einzelnen aussieht.

Warum SaaS-Billing-Edge-Cases einen eigenen Design-Durchgang verdienen

Billing-Bugs unterscheiden sich von gewöhnlichen Bugs in einem wichtigen Punkt: Sie sind für deine Kunden und deine Buchhaltung sichtbar, bevor sie für dich sichtbar sind. Ein kaputter API-Endpoint wirft Fehler in dein Monitoring. Eine falsche Proration-Berechnung erzeugt eine syntaktisch gültige Rechnung, die ein Kunde ohne Beschwerde bezahlt, und der Fehler fällt erst auf, wenn jemand Stripe-Auszahlungen gegen realisierten Umsatz abstimmt. Bis dahin sind die falschen Daten in Finanzberichte durchgesickert.

Der rote Faden durch alles Folgende: Der Subscription-Zustand in deiner Datenbank ist eine Projektion, und Stripe ist die Source of Truth für Geld. Wenn beide nicht übereinstimmen, ist deine Projektion falsch. Entwirf jeden Edge-Case-Handler um diese Annahme herum.

Proration bei Upgrades und Downgrades mitten in der Periode

Wenn ein Kunde auf einem 50-Euro-Monatsplan an Tag 15 auf einen 100-Euro-Plan upgradet, berechnet Stripe zwei Positionen: eine Gutschrift für die ungenutzte Hälfte des alten Plans (ungefähr 25 Euro) und eine Belastung für die verbleibende Hälfte des neuen Plans (ungefähr 50 Euro). Das Wort "ungefähr" leistet dort echte Arbeit. Stripe proratiert sekundengenau, die exakten Beträge hängen also vom Zeitstempel der Änderung ab, und deine eigene Überschlagsrechnung wird um ein paar Cent daneben liegen. Berechne Proration nicht selbst nach, um sie mit strikter Gleichheit gegen Stripes Zahlen zu vergleichen. Vertraue entweder den Rechnungspositionen oder vergleiche mit einer Toleranz.

Die folgenreichere Entscheidung ist, wann die Proration eingezogen wird. Mit dem Default-Verhalten liegen Proration-Positionen auf der kommenden Rechnung und werden bei der nächsten Verlängerung abgebucht. Mit proration_behavior kombiniert mit einer sofortigen Rechnung zahlt der Kunde die Differenz sofort. Sofortige Zahlung ist bei Upgrades meist richtig, weil der Kunde ab jetzt mehr Wert bekommt. Bei Downgrades planen die meisten Teams die Änderung stattdessen auf das Periodenende, was Gutschriften vermeidet und die Buchhaltung einfach hält. Was auch immer du wählst, wähle es explizit. Verbrannt werden die Teams, die die Entscheidung nie getroffen und den Default ausgeliefert haben, ohne zu wissen, was er war.

Trial-Konversion und die erste Rechnung

Die Konversion von Trial zu Paid sieht trivial aus, bis Steuern ins Bild kommen. Während eines Trials gibt es keine Rechnung, das heißt, du hast die steuerliche Lokation des Kunden womöglich noch nie validiert. Die erste bezahlte Rechnung ist der Ort, an dem Umsatzsteuer oder Sales Tax berechnet wird, und wenn Rechnungsadresse oder Steuernummer nie erhoben wurden, kann diese Rechnung auf eine Art falsch sein, die mühsam zu korrigieren ist, denn Steuerbehörden akzeptieren "wir haben es in der nächsten Rechnung behoben" nicht als Korrekturverfahren. Erhebe Rechnungsadresse und eine etwaige USt-IdNr., bevor der Trial startet, auch wenn das Reibung erzeugt, oder blockiere mindestens die Konversion, bis die Daten existieren.

Trial-Verlängerungen sind die andere Falle. Einen Trial durch Verschieben von trial_end zu verlängern ist einmal in Ordnung. Wiederholt über Support-Anfragen erzeugt es Subscriptions, deren Trial fünfmal verlängert wurde, und deine Analytics zählen eine neun Monate alte Anmeldung jetzt als frischen Trial. Erfasse Verlängerungen als eigenständige Events, damit deine Konversionsmetriken ehrlich bleiben.

Fehlgeschlagene Zahlungen und Dunning

Behandle eine fehlgeschlagene Verlängerungszahlung als Beginn eines Prozesses, und gib dem Prozess einen Owner. Stripes Smart Retries versuchen die Abbuchung in maschinell gewählten Intervallen über ein konfigurierbares Fenster erneut, was jeden festen Zeitplan schlägt, den du selbst schreiben würdest. Was Stripe nicht für dich entscheidet, ist, was in der Zwischenzeit mit dem Account passiert.

Das Muster, das funktioniert: voller Service durch das erste Retry-Fenster, Herabstufung auf Read-only-Zugriff nach einer definierten Zahl von Tagen im Verzug, und Zugriff erst kappen, wenn die Subscription ihren finalen Zustand erreicht hat. Sofortiges Kappen beim ersten Fehlschlag ist ein Fehler, denn ein großer Teil der Fehlschläge ist temporär (abgelaufene Karte, Monatslimit erreicht, Ablehnung auf Bankebene) und erholt sich beim Retry, ohne dass der Kunde je etwas mitbekommt. Stille unbegrenzte Kulanz ist der umgekehrte Fehler, und der teurere: Wir haben Systeme auditiert, in denen Accounts Monate nach der letzten erfolgreichen Abbuchung weiterliefen, weil niemand den invoice.payment_failed-Webhook mit irgendetwas verdrahtet hatte.

Versende eigene Dunning-Mails, statt dich nur auf die von Stripe zu verlassen, denn deine Mails können auf dein Billing-Portal verlinken, benennen, was der Kunde verliert, und zur Stimme deines Produkts passen. Und protokolliere jeden Zustandsübergang des Dunning-Prozesses in deiner eigenen Datenbank. Wenn ein Kunde eine Sperrung anficht, ist "der Webhook kam am 14., und wir haben den Zugriff am 21. gemäß Policy herabgestuft" eine verteidigbare Antwort. "Stripe hat das gemacht" ist keine.

Gutschriften und Erstattungen

Gutschriften klingen einfach und sind es nicht. Hält ein Kunde sowohl ein Promo-Guthaben als auch eine Proration-Gutschrift, bestimmt die Anwendungsreihenfolge, wie viel echtes Geld fließt, und damit, wie viel Umsatz du realisierst. Stripe wendet Guthaben auf dem Kundenkonto an, bevor die Zahlungsmethode belastet wird. Wenn du zusätzlich Coupons darüberlegst, rechne eine konkrete Rechnung von Hand durch, bevor du auslieferst, und gieße das erwartete Ergebnis in einen Test.

Erstattungen haben eine Steuerdimension, die Engineers regelmäßig übersehen. Eine Teilerstattung einer Rechnung, die Umsatzsteuer enthielt, muss die anteilige Umsatzsteuer mit erstatten, und die Stornorechnung, die das dokumentiert, ist in weiten Teilen der EU ein rechtlich relevantes Dokument. Wenn du Erstattungen über dein Admin-Panel per rohem Refund-API ausführst, ohne Credit Notes zu erzeugen, hat deine Buchhaltung irgendwann eine sehr schlechte Woche. Nutze Stripes Credit-Note-Mechanismus statt nackter Refunds für alles, was eine Rechnung mit ausgewiesener Steuer berührt hat.

Promo-Guthaben sollten ablaufen. Guthaben ohne Ablaufdatum akkumulieren als Verbindlichkeit in deinen Büchern, die ewig wächst, und Finance wird Engineering irgendwann bitten, den offenen Saldo zu rekonstruieren. Speichere ein Ablaufdatum vom ersten Tag an, auch wenn die erste Version es nie durchsetzt.

Jahrespläne, Kündigungen und Multi-Currency

Jahres-Subscriptions konzentrieren ein Jahr Umsatz in einer Zahlung, was ihre Kündigungsregelung zu einer finanziellen Entscheidung macht statt zu einer UX-Präferenz. Entscheide vorab, ob eine Kündigung mitten im Jahr die verbleibenden Monate erstattet, in Guthaben umwandelt oder die Laufzeit einfach auslaufen lässt, und schreibe es in deine AGB. Aus Buchhaltungssicht ist die Jahreszahlung Deferred Revenue, das monatlich realisiert wird, eine Erstattung in Monat sieben macht also fünf Monate nicht realisierten Umsatz rückgängig. Dein Event-Log (nächster Abschnitt) sollte diese Rechnung mechanisch machen.

Multi-Currency ergänzt eine leisere Klasse von Problemen. Wenn du in EUR und USD bepreist, existieren die Rechnungen eines Kunden in seiner Währung, während dein Reporting in deiner Basiswährung existiert, und der Wechselkurs zum Rechnungszeitpunkt unterscheidet sich vom Kurs zum Auszahlungszeitpunkt. Speichere pro Transaktion den Betrag in Originalwährung, den Settlement-Betrag und den verwendeten Kurs. Historische Wechselkurse später zu rekonstruieren, für eine Abstimmung, die jemand dringend braucht, ist mühsam und unpräzise.

Ein Event-Log, das Audits übersteht

Alles oben wird handhabbar mit einer Architekturentscheidung: Erfasse jede Billing-Zustandsänderung als unveränderliches Event in deiner eigenen Datenbank, statt nur den aktuellen Zustand zu speichern.

CREATE TABLE billing_events (
    id              BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    occurred_at     TIMESTAMPTZ NOT NULL,
    recorded_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
    customer_id     UUID        NOT NULL,
    subscription_id TEXT,
    event_type      TEXT        NOT NULL,
    amount_cents    BIGINT,
    currency        CHAR(3),
    base_amount_cents BIGINT,
    exchange_rate   NUMERIC(18, 8),
    stripe_event_id TEXT UNIQUE,
    stripe_object_id TEXT,
    payload         JSONB       NOT NULL,
    UNIQUE (stripe_event_id)
);

CREATE INDEX idx_billing_events_customer
    ON billing_events (customer_id, occurred_at);

Ein paar bewusste Entscheidungen in diesem Schema. Der Unique-Constraint auf stripe_event_id macht die Webhook-Ingestion idempotent, ein erneut zugestelltes Event fügt also nichts ein. Die Trennung von occurred_at und recorded_at lässt dich unterscheiden, wann etwas passiert ist und wann du davon erfahren hast, was bei spät eintreffenden Webhooks zählt. Die rohe payload-Spalte bedeutet, dass du Fragen beantworten kannst, die du zur Designzeit nicht vorhergesehen hast. Und weil Zeilen nie aktualisiert oder gelöscht werden, bleibt die Tabelle vertrauenswürdig während einer strittigen Abbuchung, eines Steueraudits oder einer Due Diligence, alles Situationen, in denen "der aktuelle Wert einer veränderbaren Spalte" niemanden überzeugt.

Baue deinen Subscription-Zustand in einem nächtlichen Reconciliation-Job aus diesem Log neu auf und vergleiche ihn sowohl gegen deine Anwendungstabellen als auch gegen die Stripe-API. Abweichungen werden zu Alerts statt zu Überraschungen.

Wo du anfängst, wenn dein Billing schon existiert

Wenn du Billing von Grund auf baust, zieh das Event-Log vor dem Launch ein und triff die Proration- und Dunning-Entscheidungen explizit. Wenn du bereits ein Subscription-System betreibst, ist der wertvollste erste Schritt ein Reconciliation-Skript, das deine Datenbank für jeden aktiven Kunden gegen Stripe vergleicht. In unseren Code-Audits findet dieses Skript fast immer Drift, und die Größe des Drifts sagt dir, wie dringend der Rest dieser Liste ist. Für Teams, die die Billing-Schicht sauber neu gebaut haben wollen, ist das die Art von Arbeit, die wir als Projekte der individuellen Softwareentwicklung übernehmen.

Fragen zu einem konkreten Edge Case, den dein Billing schon getroffen hat? Schreib an hello@wolf-tech.io oder melde dich über wolf-tech.io/contact. Wir haben diesen Fehlermodus vermutlich schon gesehen.