Blog ·
Stripe im Hosting: wann eine Zahlung Ihnen gehört
Erfolgreich ist nicht eingegangen, eine Anfechtung ist keine Rückerstattung, und das Mahnwesen ist eine Einstellung im Dashboard. Was Stripe wirklich verlangt.
Von Provibr
Hosting abzurechnen ist kein Onlinehandel. Ein Laden nimmt einmal Geld und verschickt eine Kiste; Sie nehmen jeden Monat Geld und halten dazwischen eine Maschine am Laufen. Dieser Unterschied zeigt sich an einer Stelle, und es ist die Stelle, an der jede Anbindung als Erstes danebengreift: bei der Entscheidung, wann eine Zahlung echt genug ist, um weiter zu liefern.
Das hier ist uns beim Bau der wiederkehrenden Abrechnung auf Stripe begegnet. Nichts davon ist exotisch, und das meiste ist unsichtbar, bis zu dem Monat, in dem eine Zahlung rückabgewickelt wird.
„Erfolgreich“ ist nicht „eingegangen“
Ein erfolgreicher Bezahlvorgang ist kein Geld. Karte und iDEAL sind nah genug an sofort, dass man so tun kann. Das SEPA-Lastschriftverfahren ist es nicht, und eine Überweisung auch nicht.
Eine SEPA-Zahlung liegt nach dem Bezahlvorgang tagelang in processing, ohne dass etwas eingegangen wäre. Meistens wird sie ausgeführt. Manchmal nicht. Behandelt Ihre Anbindung den Bezahlvorgang als Augenblick der Wahrheit, haben Sie einen Server auf der Grundlage einer Absicht bereitgestellt.
Das Zustandsmodell ist deshalb kein Wahrheitswert. Wir sind bei sechs Phasen gelandet, und genau eine davon bedeutet „liefern“:
| Phase | Was sie bedeutet | Liefern? |
|---|---|---|
| pending | Begonnen, noch nichts eingegangen | Nein |
| settled | Geld eingegangen und uns gehörend | Ja |
| failed | Wird nicht mehr passieren | Nein |
| disputed | Der Kunde ficht sie an | Nein, aber sie kann zurückkommen |
| reversed | Zurückgebucht oder erstattet | Nein, dauerhaft |
| unknown | Wir konnten es nicht feststellen | Nein |
„Lieferbar“ genau und ausschließlich settled bedeuten zu lassen kostet Sie beim Schreiben nichts und verschafft Ihnen die Eigenschaft, dass kein anderer Teil der Codebasis dazu eine Meinung haben darf. Überall sonst wird eine einzige Frage gestellt.
Die Ereignisreihenfolge ist nicht garantiert, also machen Sie die unerlaubten Übergänge unerlaubt
Webhooks kommen in falscher Reihenfolge an. Nicht oft, aber sie tun es, und der Fehler ist still: Ein veraltetes Ereignis überschreibt einen neueren Zustand, und der Dienst eines Kunden hört entweder grundlos auf oder läuft nach einer Rückbuchung weiter.
Die billige Abwehr besteht darin, aufzuschreiben, welche Übergänge unmöglich sind, und sie abzulehnen. Settled kann nicht zurück nach pending, settled kann nicht failed werden, pending kann nicht direkt nach disputed springen, und aus reversed oder failed kommt nie wieder etwas heraus. Ein spätes Ereignis, das einen davon auslösen würde, wird verworfen statt angewandt.
Die zweite Abwehr ist eine des Aufbaus, und es ist die Entscheidung, die wir in jeder Zahlungsanbindung beibehalten würden.
Der Webhook ist ein Beschleuniger, keine Quelle der Wahrheit. Unserer prüft die Signatur, entdoppelt und legt eine einzige Organisationskennung in eine Warteschlange. Mehr macht er nicht. Ein Abfrager holt danach den aktuellen Zustand von Stripe, bevor sich irgendetwas ändert. Die Folge ist den Aufwand wert: Ein verpasster Webhook ist nie eine verpasste Zahlung, er ist eine langsamere Zahlung. Die Lieferung hängt nicht an einem Zustellmechanismus, den Sie nicht kontrollieren.
Die Signaturprüfung hat drei Einzelheiten, die man leicht auf feine Weise falsch macht:
- In konstanter Zeit vergleichen, aber vorher die Längen prüfen, denn der Vergleich in konstanter Zeit wirft bei ungleichen Längen einen Fehler, statt falsch zurückzugeben.
- Mehrere Signaturen im Kopffeld akzeptieren. So funktioniert der Wechsel eines Geheimnisses, und eine Umsetzung, die nur die erste liest, geht an dem Tag kaputt, an dem Sie wechseln.
- Eine nicht hexadezimale Signatur ablehnen, bevor sie als Hex gelesen wird, denn das Hexlesen schneidet beim ersten ungültigen Zeichen still ab, und eine Signatur aus zwei Zeichen vergleicht sich fröhlich gegen nichts.
Wir entdoppeln außerdem, indem wir die Ereigniskennung beanspruchen, bevor wir den Anstoß veröffentlichen. Das hat einen bekannten Nachteil: beanspruchen, dann beim Veröffentlichen scheitern, und der Hinweis ist verloren. Hinnehmbar ist das genau deshalb, weil die Abfrage die Quelle der Wahrheit ist, was schön zeigt, wie eine Entscheidung im Aufbau anderswo einen Kompromiss bezahlt.
Mahnwesen ist eine Einstellung im Dashboard, kein API-Parameter
Schlägt eine monatliche Zahlung fehl, muss etwas sie wiederholen und etwas entscheiden, wann der Dienst aufhört. Wir haben diese Logik zuerst geschrieben und dann das meiste davon gelöscht.
Stripes Smart Retries wählt den Zeitpunkt der Wiederholung anhand eigener Daten, und was aus einem Abonnement wird, nachdem jede Wiederholung fehlgeschlagen ist, wird im Dashboard eingestellt. Es ist eine kontoweite Einstellung und nichts, was Sie je Abonnement übergeben, und Testmodus und Livemodus werden getrennt eingestellt, was eine gute Art ist, nach einem grünen Testlauf in der Produktion überrascht zu werden.
Damit bleibt Ihrer Seite eine Aufgabe: den Abonnementstatus in das zu übersetzen, was der Kunde benutzen darf.
| Abonnementstatus | Was wir gewähren | Warum |
|---|---|---|
| active, trialing | Voller Zugriff | |
| past_due | Schonfrist: ein Hinweisband, nichts wird gestoppt | Stripe wiederholt noch |
| unpaid | Gesperrt | Die Wiederholungen sind erschöpft |
| canceled | Gesperrt | |
| incomplete | Voller Zugriff, keine Einschränkung | Die erste Zahlung ging nie durch |
| paused | Voller Zugriff |
Die Zeile past_due folgt dem Status und nicht der Uhr, und das ist die Vereinfachung, die man mitnehmen sollte. Sobald der Zahlungsdienstleister den Wiederholungsplan besitzt, ist „wie viele Tage schlägt das nun schon fehl“ nicht mehr Ihre Frage. Sie trotzdem zu stellen heißt, einen zweiten Zeitgeber gegen einen Plan laufen zu lassen, den Sie nicht sehen können.
Die Zeile incomplete ist die, die beißt. Sie bedeutet ein Abonnement, dessen allererste Zahlung nie durchging, und das ist nicht dasselbe wie ein zahlender Kunde, der aufgehört hat zu zahlen. Beide gleich zu behandeln sperrt einen laufenden Kunden rückwirkend, gestützt auf ein Ereignis über ein Abonnement, das nie begonnen hat.
Und eine Sperre ist eine Pause, niemals eine Löschung. Die Rückkehr ist eine Handlung, die ein Mensch ausführt. Nichts in einem Abrechnungsautomaten sollte die Daten eines Kunden zerstören können, wie überzeugt er auch sein mag.
Der Fehler, der das Geld zurückgab und den Dienst weiterlaufen ließ
Hier ist der Fehlschlag, der uns am meisten beigebracht hat, und er hatte nichts mit dem Zahlungsdienstleister zu tun.
Eine Rückbuchung setzte den Lizenzzustand des Kunden korrekt auf gesperrt. Binnen fünf Minuten stand er wieder auf aktiv. Der Kunde hatte sein Geld und seine Server.
Die Ursache war, dass ein Hintergrundauftrag, der etwas anderes neu berechnete, nämlich die Mehrnutzung von Ressourcen, alle fünf Minuten ein UPDATE auf dieselbe Spalte absetzte, ohne WHERE. Drei getrennte Codestellen setzten dasselbe Feld zusammen, jede für sich korrekt, und diejenige, die am häufigsten lief, gewann.
Die Behebung war strukturell statt ein Pflaster: Eine einzige Funktion setzt diese Spalte zusammen und nimmt dafür drei Untergrenzen entgegen, eine aus der Abrechnung, eine aus der Mehrnutzung, eine aus den Rückabwicklungen, und sonst schreibt nichts hinein. Die allgemeine Regel gehört im Abstrakten ausgesprochen, denn sie gilt weit über die Abrechnung hinaus. Berechnet mehr als ein Schreiber dieselbe Spalte, dann ist diese Spalte keine Entscheidung. Sie ist ein Wettlauf, und er geht zugunsten des Auftrags mit dem kürzesten Abstand aus.
Eine Anfechtung ist keine Rückerstattung, und eine Teilerstattung ist keine halbe Lieferung
Das sind zwei Unterscheidungen, die eine Hostingabrechnung braucht und die ein allgemeiner Zahlungsleitfaden nicht für Sie trifft.
Eine offene Anfechtung ist kein Verlust. Sie kann gewonnen werden. Die Zahlung zählt nicht mehr als geliefert, aber das ist eine Aussage über die Zahlung und nicht über die Lizenz: Eine offene Anfechtung bildet sich auf die Schonfrist ab, der Kunde behält also seinen Dienst und Sie bekommen eine Benachrichtigung. Jemanden zu sperren, der seine Anfechtung gewinnen wird, ist ein Supportgespräch, das Sie nicht wollen. Eine endgültige Rückbuchung ist ein Verlust, und diese bildet sich auf die Sperre ab.
Eine Teilerstattung ist der unangenehme Fall. Es ist verlockend, sie als Teillieferung zu behandeln, und so etwas gibt es nicht. Ein Abonnementmonat ist geliefert oder eben nicht. Eine Teilerstattung wirft die Zahlung also ganz aus settled heraus, und die Lieferung hört auf. Das ist die ehrliche Lesart, auch wenn sie sich hart anfühlt, denn die Alternative besteht darin, einen Bruchteil eines Dienstes zu erfinden, den Ihr Produkt gar nicht erbringen kann.
Wenn Sie Gutschriften in ein Buchhaltungssystem schieben, noch eine Warnung. In unserem ist eine Gutschrift keine eigene Ressource: Sie ist eine Rechnung mit negativen Beträgen, und der doppelte Aufruf kopiert das Referenzfeld vom Original. Genau auf diese Referenz sucht eine Prüfung auf Einmaligkeit, eine naive Umsetzung findet also im nächsten Lauf das Original, schließt daraus, dass nichts gutgeschrieben wurde, und stellt eine zweite Gutschrift aus. Geld zweimal zurück. Jedes erzeugte Dokument braucht einen eigenen Anker, abgeleitet aus dem Ereignis, das es verursacht hat.
Was eine Proforma hier ist, und warum der Zähler zwei Präfixe hat
Eine Proforma ist das Dokument, das die Plattform selbst erzeugt, mit einer eigenen Nummer, und sie ist das, was der Kunde im Panel sieht. Die rechtsgültige Rechnung entsteht im Buchhaltungssystem mit einer eigenen Reihe. Ohne angebundenes Buchhaltungssystem ist die Proforma das einzige Dokument, das es gibt, und deshalb muss sie ordentlich nummeriert und nicht wie ein Kassenbon behandelt werden.
Zwei Einzelheiten, die beim Bauen herauskamen:
Die Nummer wird auf mindestens vier Stellen aufgefüllt, nicht auf eine feste Breite. Dokument zehntausend bekommt fünf Stellen. Abzuschneiden, um das Format ordentlich zu halten, gäbe zwei Dokumenten dieselbe Nummer, und ein eindeutiger Index lässt Sie das im denkbar ungünstigsten Moment herausfinden.
Ein Zähler, zwei Präfixe. Als das Präfix wechselte, lief die Folge weiter, statt neu zu beginnen. Zwei Zähler hätten ein neues Dokument erzeugt, dessen Nummer sich von einer alten nur in einem Präfix aus zwei Buchstaben unterscheidet, was eine Datenbank als eindeutig ansieht und ein Mensch beim Lesen eines Kontoauszugs nicht.
Und eine Aufladung von Guthaben trägt auf ihrer Proforma keine Umsatzsteuer, denn eine Aufladung ist eine Vorauszahlung und keine Leistung. Die Steuer gehört auf die Rechnung für das, wofür das Guthaben am Ende ausgegeben wird. Das andersherum zu machen heißt, auf dasselbe Geld zweimal Steuer zu erheben.
Was wir messen konnten und was nicht
Das Nützlichste an all dem ist die Testuhr. Ein monatlicher Abrechnungszyklus lässt sich in Minuten von Anfang bis Ende durchspielen: eine SEPA-Zahlung anlegen, bestätigen, dass processing überhaupt nichts freischaltet, die Uhr über das Ausführungsfenster hinaus vorstellen, zusehen, wie sie eingeht, und prüfen, dass die Freischaltung genau einmal geschieht und bei einem zweiten Lauf unverändert bleibt. Ohne sie ist der Monatszyklus das eine, was sich nicht proben lässt, und sein erster echter Durchlauf findet in der Produktion statt.
Zwei ehrliche Grenzen für all das. Gemessen wurde gegen Stripes Sandkasten und einen echten, über das Netz zugestellten Webhook, nicht gegen einen großen Bestand echter Kunden. Und eine Lehre, die über Zahlungen hinaus gilt: Ein Funktionsschalter auf einem Worker und einer Schaltfläche schaltet kein Datenbankschema ab. Ein Deploy lieferte einmal Code aus, der Spalten las, deren Migration nicht gelaufen war, und eine völlig andere Verwaltungsseite gab für jeden einen Serverfehler zurück, denn der Schalter war aus und die Abfrage nicht.
Das entstand beim Bau der Abrechnungsseite von Provibr, wo Hostinganbieter ihre eigene Infrastruktur über ein Panel verwalten und der Agent sich ausschließlich nach außen verbindet.