Blog ·
Stripe pentru găzduire: când o plată este reală
Reușit nu înseamnă decontat, un litigiu nu este o rambursare, iar reîncercările sunt o setare. Ce presupune facturarea recurentă pe Stripe.
De Provibr
Facturarea pentru găzduire nu este comerț electronic. Un magazin ia banii o dată și expediază o cutie; dumneavoastră luați bani în fiecare lună și țineți o mașină pornită între timp. Diferența aceea se arată într-un singur loc, iar acela este locul pe care fiecare integrare îl greșește prima dată: momentul în care hotărâți că o plată este destul de reală ca să continuați livrarea.
Iată peste ce am dat construind facturarea recurentă pe Stripe. Nimic din toate acestea nu este exotic, iar cea mai mare parte este invizibilă până în luna în care o plată se întoarce.
„Reușit” nu înseamnă „decontat”
O finalizare reușită a plății nu înseamnă bani. Cardul și iDEAL sunt suficient de aproape de instantaneu încât să vă puteți preface că da. Debitul direct SEPA nu este, și nici transferul bancar.
O plată SEPA stă în processing după finalizare, fără să fi sosit nimic, zile în șir. De obicei se decontează. Uneori nu. Dacă integrarea dumneavoastră tratează finalizarea drept momentul adevărului, atunci ați livrat un server pe temeiul unei intenții.
Așa că modelul de stări nu este o valoare de tip da sau nu. Am ajuns la șase faze, iar exact una dintre ele înseamnă „livrează”:
| Faza | Ce înseamnă | Livrăm? |
|---|---|---|
| în așteptare | Pornită, nu a sosit încă nimic | Nu |
| decontată | Banii au sosit și sunt ai noștri | Da |
| eșuată | Nu se va întâmpla | Nu |
| în litigiu | Clientul o contestă | Nu, dar se poate întoarce |
| întoarsă | Stornată sau rambursată | Nu, definitiv |
| necunoscută | Nu am putut stabili | Nu |
Ca „livrabil” să însemne precis și numai settled nu vă costă nimic de scris și vă cumpără proprietatea că nicio altă parte a codului nu mai are voie să aibă o părere despre asta. Peste tot în rest se pune o singură întrebare.
Ordinea evenimentelor nu este garantată, deci faceți ilegale tranzițiile ilegale
Notificările web sosesc în dezordine. Nu des, dar se întâmplă, iar eșecul este tăcut: un eveniment vechi suprascrie o stare mai nouă, iar serviciul unui client fie se oprește fără motiv, fie continuă să meargă după o stornare.
Apărarea ieftină este să scrieți care tranziții sunt imposibile și să le refuzați. Decontată nu se poate întoarce la în așteptare, decontată nu poate deveni eșuată, în așteptare nu poate sări direct la în litigiu, iar din întoarsă sau eșuată nu se mai iese niciodată. Un eveniment întârziat care ar provoca una dintre acestea este aruncat în loc să fie aplicat.
A doua apărare este arhitecturală și este decizia pe care am păstra-o în orice integrare de plăți.
Notificarea web este un accelerator, nu o sursă a adevărului. A noastră verifică semnătura, elimină duplicatele și pune un singur identificator de organizație într-o coadă. Atât face. Apoi un interogator aduce starea curentă de la Stripe înainte să se schimbe ceva. Consecința merită instalația: o notificare pierdută nu este niciodată o plată pierdută, ci o plată mai lentă. Livrarea nu depinde de un mecanism de livrare pe care nu îl controlați.
Verificarea semnăturii are trei amănunte ușor de greșit subtil:
- Comparați în timp constant, dar verificați întâi lungimile, pentru că la lungimi diferite comparația în timp constant aruncă o excepție în loc să întoarcă fals.
- Acceptați mai multe semnături în antet. Așa funcționează rotirea secretului, iar o implementare care citește doar prima se strică în ziua în care rotiți.
- Respingeți o semnătură care nu este hexazecimală înainte să o analizați drept hexazecimală, pentru că analiza hexazecimală trunchiază în tăcere la primul caracter nevalid, iar o semnătură de două caractere se va compara voios cu nimic.
Eliminăm duplicatele revendicând identificatorul evenimentului înainte de a publica ghiontul. Asta are un dezavantaj cunoscut: revendicați, apoi nu reușiți să publicați, iar indiciul este pierdut. Este acceptabil tocmai pentru că interogarea este sursa adevărului, ceea ce ilustrează bine cum o decizie arhitecturală plătește pentru un compromis din altă parte.
Reîncercările sunt o setare din tabloul de bord, nu un parametru de API
Dacă o plată lunară eșuează, ceva trebuie să o reîncerce și ceva trebuie să hotărască când se oprește serviciul. Am scris mai întâi acea logică și apoi am șters cea mai mare parte din ea.
Smart Retries de la Stripe alege momentul reîncercării din datele proprii, iar ce devine un abonament după ce toate reîncercările au eșuat se configurează în tabloul de bord. Este o setare valabilă pentru tot contul, nu ceva ce transmiteți per abonament, iar modul de test și modul real se configurează separat, ceea ce este o bună cale de a fi surprins în producție după o rundă de teste verde.
Asta lasă părții dumneavoastră o singură treabă: să traducă starea abonamentului în ceea ce are voie clientul să folosească.
| Starea abonamentului | Ce acordăm | De ce |
|---|---|---|
| active, trialing | Acces deplin | |
| past_due | Răgaz: un banner, nimic oprit | Stripe încă reîncearcă |
| unpaid | Suspendat | Reîncercările s-au epuizat |
| canceled | Suspendat | |
| incomplete | Acces deplin, fără restricție | Prima plată nu a reușit niciodată |
| paused | Acces deplin |
Rândul past_due este condus de stare și nu de ceas, iar aceea este simplificarea care merită luată. Odată ce procesatorul deține programul de reîncercare, „de câte zile eșuează asta” încetează să mai fie întrebarea dumneavoastră. Punând-o oricum, ajungeți să țineți un al doilea cronometru față de un program pe care nu îl puteți vedea.
Rândul incomplete este cel care mușcă. El înseamnă un abonament a cărui primă plată nu a trecut niciodată, ceea ce nu este același lucru cu un client plătitor care a încetat să plătească. Tratându-le la fel, suspendați retroactiv un client activ pe temeiul unui eveniment despre un abonament care nu a început niciodată.
Iar suspendarea este o pauză, niciodată o ștergere. Întoarcerea este o acțiune făcută de un om. Nimic dintr-o mașină de stări de facturare nu ar trebui să poată distruge datele unui client, oricât de sigură ar fi pe ea.
Defectul care a dat banii înapoi și a ținut serviciul pornit
Iată eșecul din care am învățat cel mai mult, iar el nu avea nicio legătură cu furnizorul de plăți.
O stornare a pus corect starea de licență a clientului pe blocat. În cinci minute era din nou activă. Clientul avea și banii, și serverele.
Cauza era că o sarcină de fundal care recalcula cu totul altceva, depășirea de resurse, emitea un UPDATE pe aceeași coloană la fiecare cinci minute, fără clauză WHERE. Trei bucăți separate de cod compuneau același câmp, fiecare corectă de una singură, iar cea care rula cel mai des câștiga.
Reparația a fost structurală și nu un petic: o singură funcție compune acea coloană, luând drept intrare trei praguri, unul din facturare, unul din depășire, unul din întoarceri, și nimic altceva nu o scrie. Regula generală merită enunțată în abstract, pentru că se aplică mult dincolo de facturare. Dacă mai mult de un scriitor calculează aceeași coloană, acea coloană nu este o decizie. Este o cursă, iar ea se va rezolva în favoarea sarcinii cu intervalul cel mai scurt.
Un litigiu nu este o rambursare, iar o rambursare parțială nu este o jumătate de livrare
Acestea sunt două deosebiri de care facturarea pentru găzduire are nevoie și pe care un ghid general despre plăți nu le va face în locul dumneavoastră.
Un litigiu deschis nu este o pierdere. Poate fi câștigat. Plata încetează să conteze drept livrată, dar aceasta este o afirmație despre plată și nu despre licență: un litigiu deschis se transformă în răgaz, așa că clientul își păstrează serviciul, iar dumneavoastră primiți o notificare. Suspendarea cuiva care își va câștiga litigiul este o discuție cu asistența pe care nu o doriți. O stornare definitivă este o pierdere, iar aceasta se transformă în suspendare.
O rambursare parțială este cea incomodă. Este tentant să o tratați ca pe o livrare parțială, iar așa ceva nu există. O lună de abonament fie este livrată, fie nu. Așa că o rambursare parțială scoate plata cu totul din starea decontată, iar livrarea se oprește. Aceasta este citirea sinceră, chiar dacă pare aspră, pentru că alternativa este să inventați o fracțiune de serviciu pe care produsul dumneavoastră nu o poate oferi în realitate.
Dacă împingeți note de credit într-un sistem de contabilitate, încă un avertisment. La noi o notă de credit nu este o resursă separată: este o factură cu sume negative, iar apelul duplicat copiază câmpul de referință din originalul ei. Acea referință este exact cheia pe care o caută o verificare de idempotență, așa că o implementare naivă găsește originalul la rularea următoare, trage concluzia că nu s-a creditat nimic și emite o a doua notă de credit. Bani înapoi de două ori. Fiecare document generat are nevoie de propria lui ancoră, derivată din evenimentul care l-a provocat.
Ce este aici o proformă și de ce contorul are două prefixe
O proformă este documentul pe care îl produce platforma însăși, cu numărul ei propriu, și este ceea ce vede clientul în panou. Factura legală se creează în sistemul de contabilitate, cu seria proprie a acestuia. Fără acel sistem de contabilitate conectat, proforma este singurul document care există, motiv pentru care trebuie numerotată ca lumea în loc să fie tratată ca o chitanță.
Două amănunte ieșite din practică:
Numărul este completat la patru cifre ca minimum, nu ca lățime fixă. Documentul cu numărul zece mii primește cinci cifre. Trunchierea de dragul unui format ordonat ar da două documente cu același număr, iar un indice unic vă va lăsa să aflați asta în cel mai prost moment cu putință.
Un contor, două prefixe. Când prefixul s-a schimbat, secvența a continuat în loc să o ia de la capăt. Două contoare ar fi produs un document nou al cărui număr diferă de unul vechi numai printr-un prefix de două litere, ceea ce o bază de date vede ca unic, iar un om care citește un extras de cont nu.
Iar o alimentare de credit preplătit nu poartă TVA pe proforma ei, pentru că o alimentare este o plată în avans și nu un serviciu. TVA-ul își are locul pe factura pentru lucrul pe care se cheltuiește până la urmă creditul. Făcând asta invers, taxați de două ori aceiași bani.
Ce am putut măsura și ce nu
Cel mai folositor lucru din toate acestea este ceasul de test. Un ciclu lunar de facturare poate fi condus de la un capăt la altul în câteva minute: creați o plată SEPA, confirmați că processing nu activează absolut nimic, avansați ceasul dincolo de fereastra de decontare, priviți cum se decontează și verificați că activarea se întâmplă exact o dată și că este idempotentă la o a doua rulare. Fără el, ciclul lunar este singurul lucru pe care nu îl puteți repeta, iar prima lui rulare adevărată se întâmplă în producție.
Două limite sincere pentru tot ce este mai sus. Toate acestea au fost măsurate față de mediul de testare al Stripe și față de o notificare web livrată real prin rețea, nu față de o bază mare de clienți activi. Și o lecție care se generalizează dincolo de plăți: un comutator de funcționalitate pe un lucrător și pe un buton nu pune o poartă pe schema bazei de date. O livrare a dus odată în producție cod care citea coloane a căror migrare nu rulase, iar o pagină de administrare fără legătură a întors o eroare de server pentru toată lumea, pentru că indicatorul era stins, dar interogarea nu era.
Asta a ieșit din construirea părții de facturare a Provibr, unde furnizorii de găzduire își administrează propria infrastructură dintr-un singur panou, iar agentul se conectează numai spre exterior.