Naar de inhoud

Blog ·

Stripe voor hosting: wanneer geld van jou is

Geslaagd is niet afgewikkeld, een geschil is geen terugbetaling, en dunning is een instelling. Wat terugkerende hostingfacturatie op Stripe echt inhoudt.

Door Provibr

Factureren voor hosting is geen webwinkel. Een winkel int één keer geld en verstuurt een doos; je int elke maand geld en houdt er tussendoor een machine mee draaiend. Dat verschil komt op één plek naar boven, en het is de plek waar elke koppeling het als eerste verkeerd doet: beslissen wanneer een betaling echt genoeg is om te blijven leveren.

Dit is waar we tegenaan liepen bij het bouwen van terugkerende facturatie op Stripe. Niets ervan is exotisch, en het meeste is onzichtbaar tot de maand waarin een betaling wordt teruggedraaid.

"Geslaagd" is niet "afgewikkeld"

Een geslaagde afrekening is geen geld. Kaart en iDEAL zijn zo dicht bij ogenblikkelijk dat je kunt doen alsof. SEPA-incasso is dat niet, en een bankoverschrijving evenmin.

Een SEPA-betaling blijft na de afrekening dagenlang in processing staan, zonder dat er iets ontvangen is. Meestal wikkelt hij af. Soms niet. Behandelt je koppeling de afrekening als het moment van waarheid, dan heb je een server uitgerold op basis van een voornemen.

Het toestandsmodel is dus geen booleaan. We zijn uitgekomen op zes fasen, en precies één ervan betekent "leveren":

FaseWat het betekentLeveren?
pendingGestart, nog niets ontvangenNee
settledGeld ontvangen en van onsJa
failedGaat niet meer gebeurenNee
disputedDe klant betwist hetNee, maar het kan terugkomen
reversedTeruggeboekt of terugbetaaldNee, blijvend
unknownWe konden het niet vaststellenNee

"Leverbaar" precies en uitsluitend settled laten betekenen kost niets om op te schrijven en levert je de eigenschap op dat geen enkel ander deel van de codebase er een mening over mag hebben. Overal elders wordt één vraag gesteld.

De volgorde van gebeurtenissen ligt niet vast, dus maak onmogelijke overgangen onmogelijk

Webhooks komen door elkaar aan. Niet vaak, maar het gebeurt, en het faalt stil: een verouderde gebeurtenis overschrijft een nieuwere toestand, en de dienst van een klant stopt zonder reden of blijft doordraaien na een terugboeking.

De goedkope verdediging is opschrijven welke overgangen onmogelijk zijn en ze weigeren. Afgewikkeld kan niet terug naar in behandeling, afgewikkeld kan niet mislukt worden, in behandeling kan niet rechtstreeks naar betwist springen, en niets verlaat ooit teruggedraaid of mislukt. Een late gebeurtenis die er een van die overgangen doorheen zou duwen wordt weggegooid in plaats van toegepast.

De tweede verdediging is architecturaal, en het is de beslissing die we in elke betaalkoppeling zouden aanhouden.

De webhook is een versneller, geen bron van waarheid. De onze controleert de handtekening, ontdubbelt, en zet één organisatieaanduiding op een wachtrij. Meer doet hij niet. Vervolgens haalt een poller de actuele toestand bij Stripe op voordat er iets verandert. Het gevolg is al dat leidingwerk waard: een gemiste webhook is nooit een gemiste betaling, hij is een tragere betaling. Levering hangt niet af van een afleveringsmechanisme dat je niet in de hand hebt.

Bij het controleren van de handtekening zijn er drie details die subtiel mis kunnen gaan:

  • Vergelijk in constante tijd, maar controleer eerst de lengtes, want de vergelijking in constante tijd gooit een fout bij ongelijke lengtes in plaats van onwaar terug te geven.
  • Accepteer meerdere handtekeningen in de header. Zo werkt het roteren van een geheim, en een implementatie die alleen de eerste leest breekt op de dag dat je roteert.
  • Weiger een niet-hexadecimale handtekening voordat je hem als hex ontleedt, want het ontleden van hex kapt stilletjes af bij het eerste ongeldige teken, en een handtekening van twee tekens vergelijkt zich vrolijk met niets.

We ontdubbelen bovendien door het gebeurtenis-ID te claimen voordat we het duwtje publiceren. Dat heeft een bekend nadeel: claimen, dan mislukken bij het publiceren, en de hint is weg. Het is juist aanvaardbaar omdat de bevraging de bron van waarheid is, en dat is een mooie illustratie van één architectuurbeslissing die elders een compromis betaalt.

Dunning is een instelling in het dashboard, geen API-parameter

Mislukt een maandelijkse betaling, dan moet iets hem opnieuw proberen en moet iets beslissen wanneer de dienst stopt. Die logica hebben we eerst geschreven en daarna grotendeels weggegooid.

Stripe's Smart Retries kiest de timing van nieuwe pogingen op basis van eigen gegevens, en wat een abonnement wordt nadat elke poging is mislukt wordt in het dashboard ingesteld. Het is een instelling voor het hele account in plaats van iets dat je per abonnement meegeeft, en de testmodus en de live modus worden apart ingesteld, wat een uitstekende manier is om in productie verrast te worden na een groene testronde.

Daarmee blijft er aan je kant één taak over: de abonnementsstatus vertalen naar wat de klant mag gebruiken.

AbonnementsstatusWat we toekennenWaarom
active, trialingVolledige toegang
past_dueRespijt: een banner, niets gestoptStripe probeert het nog
unpaidOpgeschortDe pogingen zijn op
canceledOpgeschort
incompleteVolledige toegang, geen beperkingDe eerste betaling slaagde nooit
pausedVolledige toegang

De rij past_due wordt door de status gestuurd in plaats van door de klok, en dat is de vereenvoudiging die het nemen waard is. Zodra de verwerker het schema van nieuwe pogingen bezit, houdt "hoeveel dagen mislukt dit nu al" op je vraag te zijn. Hem toch stellen betekent een tweede klok laten lopen tegen een schema dat je niet kunt zien.

De rij incomplete is degene die bijt. Hij betekent een abonnement waarvan de allereerste betaling nooit is doorgekomen, en dat is iets anders dan een betalende klant die is gestopt met betalen. Ze gelijk behandelen schort met terugwerkende kracht een draaiende klant op, op grond van een gebeurtenis over een abonnement dat nooit is begonnen.

En opschorten is een pauze, nooit een verwijdering. Terugkomen is een handeling die een mens verricht. Niets in een facturatiemachine hoort de gegevens van een klant te kunnen vernietigen, hoe zeker het ook van zijn zaak is.

De fout die het geld teruggaf en de dienst liet doordraaien

Hier is de storing waar we het meest van geleerd hebben, en hij had niets met de betaalprovider te maken.

Een terugboeking zette de licentiestand van de klant terecht op geblokkeerd. Binnen vijf minuten stond hij weer op actief. De klant had zijn geld en zijn servers.

De oorzaak was dat een achtergrondtaak die iets anders herberekende, het overschrijden van hulpbronnen, elke vijf minuten een UPDATE op dezelfde kolom deed zonder WHERE-voorwaarde. Drie losse stukken code stelden hetzelfde veld samen, elk op zichzelf correct, en degene die het vaakst draaide won.

De reparatie was structureel in plaats van een pleister: één functie stelt die kolom samen, met drie ondergrenzen als invoer, één uit de facturatie, één uit het overschrijden, één uit de terugdraaiingen, en verder schrijft niets erin. De algemene regel is het waard om in het abstracte te formuleren, want hij geldt ver buiten de facturatie. Berekenen meer schrijvers dezelfde kolom, dan is die kolom geen beslissing. Het is een wedloop, en hij wordt beslecht in het voordeel van de taak met de kortste tussenpoos.

Een geschil is geen terugbetaling, en een deelterugbetaling is geen halve levering

Dit zijn twee onderscheidingen die hostingfacturatie nodig heeft en die een algemene handleiding over betalen niet voor je maakt.

Een lopend geschil is geen verlies. Het kan gewonnen worden. De betaling telt niet langer als geleverd, maar dat is een uitspraak over de betaling en niet over de licentie: een lopend geschil valt onder respijt, dus de klant houdt zijn dienst en je krijgt een melding. Iemand opschorten die zijn geschil gaat winnen is een gesprek met support waar je niet op zit te wachten. Een definitieve terugboeking is een verlies, en die valt onder opschorting.

Een deelterugbetaling is de lastige. Het is verleidelijk om die als deellevering te behandelen, en zoiets bestaat niet. Een abonnementsmaand wordt geleverd of hij wordt niet geleverd. Een deelterugbetaling haalt de betaling dus volledig uit afgewikkeld, en de levering stopt. Dat is de eerlijke lezing, ook al voelt hij hard, want het alternatief is een fractie van een dienst verzinnen die je product helemaal niet kan leveren.

Duw je creditnota's een boekhoudsysteem in, dan is er nog één waarschuwing. Bij ons is een creditnota geen aparte bron: het is een factuur met negatieve bedragen, en de dubbele aanroep kopieert het referentieveld van het origineel. Die referentie is precies de sleutel waar een idempotentiecontrole op zoekt, dus een naïeve uitvoering vindt bij de volgende ronde het origineel, concludeert dat er niets gecrediteerd is, en geeft een tweede creditnota uit. Twee keer geld terug. Elk gegenereerd document heeft zijn eigen anker nodig, afgeleid uit de gebeurtenis die hem veroorzaakte.

Wat een proforma hier is, en waarom de teller twee prefixen heeft

Een proforma is het document dat het platform zelf maakt, met een eigen nummer, en het is wat de klant in het paneel ziet. De juridische factuur wordt in het boekhoudsysteem gemaakt, met een eigen reeks. Zonder dat boekhoudsysteem gekoppeld is de proforma het enige document dat er is, en daarom moet hij fatsoenlijk genummerd worden in plaats van als een kassabon behandeld te worden.

Twee details die bij het bouwen naar boven kwamen:

Het nummer wordt aangevuld tot minimaal vier cijfers, geen vaste breedte. Document tienduizend krijgt vijf cijfers. Afkappen om het formaat netjes te houden zou twee documenten hetzelfde nummer geven, en een unieke index laat je dat op het slechtst denkbare moment ontdekken.

Eén teller, twee prefixen. Toen de prefix veranderde, liep de reeks door in plaats van opnieuw te beginnen. Twee tellers zouden een nieuw document hebben opgeleverd waarvan het nummer alleen in een prefix van twee letters van een oud document verschilt, wat een database als uniek ziet en een mens die een bankafschrift leest niet.

En het opwaarderen van vooruitbetaald tegoed draagt geen btw op zijn proforma, want opwaarderen is een vooruitbetaling en geen dienst. De btw hoort op de factuur voor datgene waar het tegoed uiteindelijk aan besteed wordt. Dat omdraaien betekent twee keer belasting heffen over hetzelfde geld.

Wat we konden meten, en wat niet

Het nuttigste van dit alles is de testklok. Een maandelijkse factuurcyclus is in minuten van begin tot eind door te lopen: maak een SEPA-betaling aan, stel vast dat processing helemaal niets activeert, zet de klok voorbij het afwikkelvenster, kijk hem afwikkelen, en controleer dat de activering precies één keer plaatsvindt en idempotent is bij een tweede ronde. Zonder zo'n klok is de maandcyclus het enige dat je niet kunt repeteren, en gebeurt de eerste echte uitvoering ervan in productie.

Twee eerlijke beperkingen op het bovenstaande. Dit is gemeten tegen de zandbak van Stripe en een echte webhook die over het netwerk werd afgeleverd, niet tegen een groot klantenbestand in productie. En een les die verder reikt dan betalen: een functievlag op een worker en een knop schermt geen databaseschema af. Een deploy bracht ooit code live die kolommen las waarvan de migratie niet gedraaid had, en een niet-verwante beheerpagina gaf voor iedereen een serverfout, want de vlag stond uit maar de query niet.

Dit kwam voort uit het bouwen van de facturatiekant van Provibr, waar hostingaanbieders hun eigen infrastructuur via één paneel beheren en de agent uitsluitend uitgaand verbindt.

Begin met één host en één agent

Maak een account aan, installeer de agent met één commando en koppel je eerste Proxmox-host. De proefperiode loopt 14 dagen en vraagt geen betaalgegevens.