Salta al contenuto

Blog ·

Stripe per l’hosting: quando un pagamento è suo

Riuscito non è incassato, una contestazione non è un rimborso e il recupero crediti è un’impostazione. Che cosa comporta la fatturazione ricorrente su Stripe.

Di Provibr

Fatturare l’hosting non è commercio elettronico. Un negozio incassa una volta e spedisce una scatola; lei incassa ogni mese e nel frattempo tiene in funzione una macchina. Quella differenza si manifesta in un punto solo, ed è il punto che ogni integrazione sbaglia per prima: decidere quando un pagamento sia abbastanza reale da continuare a erogare.

Questo è ciò in cui ci siamo imbattuti costruendo la fatturazione ricorrente su Stripe. Nulla di esotico, e quasi tutto resta invisibile fino al mese in cui un pagamento si inverte.

«Riuscito» non significa «incassato»

Un checkout riuscito non è denaro. La carta e iDEAL sono abbastanza vicini all’istantaneo da poter fingere il contrario. L’addebito diretto SEPA non lo è, e nemmeno un bonifico bancario.

Un pagamento SEPA resta in processing dopo il checkout, senza che sia arrivato nulla, per giorni. Di solito si conclude. A volte no. Se la sua integrazione tratta il checkout come il momento della verità, ha appena effettuato il provisioning di un server sulla forza di un’intenzione.

Perciò il modello degli stati non è un booleano. Siamo arrivati a sei fasi, ed esattamente una di esse significa «erogare»:

FaseChe cosa significaErogare?
pendingAvviato, nulla di ricevuto finoraNo
settledDenaro ricevuto e nostroSì
failedNon succederàNo
disputedIl cliente lo sta contestandoNo, ma può tornare
reversedStornato o rimborsatoNo, in modo permanente
unknownNon siamo riusciti a stabilirloNo

Far sì che «erogabile» significhi precisamente e soltanto settled non costa nulla da scrivere e le compra la proprietà per cui nessun’altra parte della base di codice ha il diritto di avere un’opinione in merito. Ovunque altrove si pone una sola domanda.

L’ordine degli eventi non è garantito, quindi renda illegali le transizioni illegali

I webhook arrivano fuori ordine. Non spesso, ma succede, e il guasto è silenzioso: un evento vecchio sovrascrive uno stato più recente, e il servizio di un cliente o si ferma senza motivo o continua a funzionare dopo uno storno.

La difesa economica è mettere per iscritto quali transizioni siano impossibili e rifiutarle. Da settled non si torna a pending, settled non può diventare failed, pending non può saltare direttamente a disputed, e da reversed o failed non si esce mai. Un evento in ritardo che provocherebbe una di queste viene scartato anziché applicato.

La seconda difesa è architetturale, ed è la decisione che terremmo in qualunque integrazione di pagamento.

Il webhook è un acceleratore, non una fonte di verità. Il nostro verifica la firma, deduplica e mette un solo identificatore di organizzazione in una coda. Non fa altro. Un poller recupera poi lo stato corrente da Stripe prima che cambi qualunque cosa. La conseguenza vale l’impianto: un webhook perduto non è mai un pagamento perduto, è un pagamento più lento. L’erogazione non dipende da un meccanismo di consegna che lei non controlla.

La verifica della firma ha tre dettagli facili da sbagliare in modo sottile:

  • Confrontare a tempo costante, ma controllare prima le lunghezze, perché il confronto a tempo costante solleva un’eccezione sulle lunghezze diverse anziché restituire falso.
  • Accettare firme multiple nell’intestazione. È così che funziona la rotazione del segreto, e un’implementazione che legge soltanto la prima si rompe il giorno in cui lei ruota il segreto.
  • Rifiutare una firma non esadecimale prima di interpretarla come esadecimale, perché la lettura esadecimale tronca in silenzio al primo carattere non valido e una firma di due caratteri si confronterà allegramente con il nulla.

Deduplichiamo anche rivendicando l’ID dell’evento prima di pubblicare la spinta. Questo ha uno svantaggio noto: si rivendica, poi non si riesce a pubblicare, e il suggerimento va perduto. È accettabile proprio perché la fonte di verità è l’interrogazione, il che illustra bene come una decisione architetturale paghi un compromesso altrove.

Il recupero crediti è un’impostazione del pannello, non un parametro di API

Se un pagamento mensile fallisce, qualcosa deve riprovare e qualcosa deve decidere quando il servizio si ferma. Quella logica l’abbiamo scritta per prima e poi ne abbiamo cancellata gran parte.

Gli Smart Retries di Stripe scelgono i tempi dei tentativi in base ai propri dati, e ciò che un abbonamento diventa dopo che ogni tentativo è fallito si configura nel pannello. È un’impostazione valida per l’intero account anziché qualcosa che si passa per singolo abbonamento, e la modalità di test e quella reale si configurano separatamente, che è un ottimo modo per farsi sorprendere in produzione dopo un test andato a buon fine.

Questo lascia al suo lato un solo compito: tradurre lo stato dell’abbonamento in ciò che il cliente può usare.

Stato dell’abbonamentoChe cosa concediamoPerché
active, trialingAccesso completo
past_dueTolleranza: un avviso, nulla di fermatoStripe sta ancora riprovando
unpaidSospesoI tentativi sono esauriti
canceledSospeso
incompleteAccesso completo, nessuna restrizioneIl primo pagamento non è mai riuscito
pausedAccesso completo

La riga past_due è guidata dallo stato anziché dall’orologio, e questa è la semplificazione che vale la pena prendere. Una volta che il processore possiede il calendario dei tentativi, «da quanti giorni sta fallendo» smette di essere una sua domanda. Porla comunque significa far girare un secondo cronometro contro un calendario che lei non può vedere.

La riga incomplete è quella che morde. Indica un abbonamento il cui primissimo pagamento non è mai andato a buon fine, che non è la stessa cosa di un cliente pagante che ha smesso di pagare. Trattarli allo stesso modo sospende retroattivamente un cliente in attività sulla forza di un evento riguardante un abbonamento che non è mai partito.

E la sospensione è una pausa, mai una cancellazione. Tornare indietro è un’azione compiuta da una persona. Nulla in una macchina a stati di fatturazione dovrebbe poter distruggere i dati di un cliente, per quanto ne sia sicura.

Il bug che ha restituito i soldi e tenuto in piedi il servizio

Ecco il guasto che ci ha insegnato di più, e non aveva nulla a che fare con il fornitore di pagamenti.

Uno storno impostava correttamente lo stato di licenza del cliente su bloccato. Nel giro di cinque minuti tornava attivo. Il cliente aveva i suoi soldi e i suoi server.

La causa era che un lavoro in background che ricalcolava una cosa diversa, l’eccedenza di risorse, emetteva un UPDATE sulla stessa colonna ogni cinque minuti senza una clausola WHERE. Tre pezzi di codice distinti componevano lo stesso campo, ciascuno corretto per conto proprio, e vinceva quello che girava più spesso.

La correzione è stata strutturale anziché una toppa: una sola funzione compone quella colonna, prendendo in ingresso tre limiti inferiori, uno dalla fatturazione, uno dall’eccedenza, uno dagli storni, e nient’altro la scrive. La regola generale merita di essere enunciata in astratto, perché si applica ben oltre la fatturazione. Se più di un punto di scrittura calcola la stessa colonna, quella colonna non è una decisione. È una corsa, e si risolverà a favore del lavoro che ha l’intervallo più breve.

Una contestazione non è un rimborso, e un rimborso parziale non è mezza erogazione

Sono due distinzioni di cui la fatturazione dell’hosting ha bisogno e che una guida generica ai pagamenti non farà per lei.

Una contestazione aperta non è una perdita. Si può vincere. Il pagamento smette di contare come erogato, ma questa è un’affermazione sul pagamento e non sulla licenza: una contestazione aperta corrisponde alla tolleranza, quindi il cliente mantiene il servizio e lei riceve una notifica. Sospendere qualcuno che vincerà la propria contestazione è una conversazione con l’assistenza che non desidera. Uno storno definitivo è una perdita, e quello corrisponde alla sospensione.

Un rimborso parziale è quello scomodo. Viene la tentazione di trattarlo come un’erogazione parziale, e una cosa simile non esiste. Un mese di abbonamento o è erogato o non lo è. Perciò un rimborso parziale fa uscire del tutto il pagamento da settled, e l’erogazione si ferma. È la lettura onesta, anche se sembra dura, perché l’alternativa è inventare una frazione di servizio che il suo prodotto non è realmente in grado di fornire.

Se spinge note di credito dentro un sistema contabile, un’altra avvertenza. Nel nostro una nota di credito non è una risorsa separata: è una fattura con importi negativi, e la chiamata di duplicazione copia il campo di riferimento dall’originale. Quel riferimento è esattamente la chiave su cui cerca un controllo di idempotenza, quindi un’implementazione ingenua trova l’originale al giro successivo, conclude che non è stato accreditato nulla ed emette una seconda nota di credito. Soldi restituiti due volte. Ogni documento generato ha bisogno di un proprio ancoraggio, derivato dall’evento che lo ha causato.

Che cosa è qui una proforma, e perché il contatore ha due prefissi

Una proforma è il documento che la piattaforma stessa produce, con un proprio numero, ed è ciò che il cliente vede nel pannello. La fattura legale viene creata nel sistema contabile con una propria serie. Senza quel sistema contabile collegato, la proforma è l’unico documento esistente, motivo per cui va numerata come si deve anziché trattata come una ricevuta.

Due dettagli emersi dal farlo:

Il numero è riempito a quattro cifre come minimo, non a larghezza fissa. Il documento diecimila prende cinque cifre. Troncare per tenere ordinato il formato darebbe lo stesso numero a due documenti, e un indice unico glielo farà scoprire nel momento peggiore possibile.

Un contatore, due prefissi. Quando il prefisso è cambiato, la sequenza è proseguita anziché ripartire. Due contatori avrebbero prodotto un documento nuovo il cui numero differisce da uno vecchio soltanto per un prefisso di due lettere, cosa che un database considera unica e una persona che legge un estratto conto no.

E una ricarica di credito prepagato non porta IVA sulla propria proforma, perché una ricarica è un pagamento anticipato anziché un servizio. L’IVA appartiene alla fattura di ciò per cui il credito viene infine speso. Invertire le due cose significa applicare l’imposta due volte sullo stesso denaro.

Che cosa abbiamo potuto misurare, e che cosa no

La cosa più utile in tutto questo è l’orologio di test. Un ciclo di fatturazione mensile si può percorrere da capo a fondo in pochi minuti: creare un pagamento SEPA, verificare che processing non attivi proprio nulla, spostare l’orologio oltre la finestra di incasso, guardarlo concludersi e controllare che l’attivazione avvenga esattamente una volta e sia idempotente a una seconda esecuzione. Senza di esso, il ciclo mensile è l’unica cosa che non si può provare, e la sua prima esecuzione reale avviene in produzione.

Due limiti onesti su tutto quanto sopra. Questo è stato misurato contro la sandbox di Stripe e un webhook reale consegnato attraverso la rete, non contro una grande base di clienti in produzione. E una lezione che si generalizza oltre i pagamenti: un interruttore di funzionalità su un lavoratore e su un pulsante non protegge uno schema di database. Una volta un rilascio ha messo in produzione codice che leggeva colonne la cui migrazione non era stata eseguita, e una pagina di amministrazione senza alcun rapporto restituiva un errore del server per chiunque, perché l’interruttore era spento ma la query no.

Questo è emerso costruendo il lato della fatturazione di Provibr, dove i fornitori di hosting gestiscono la propria infrastruttura da un unico pannello e l’agente si connette solo verso l’esterno.

Inizi con un host e un agente

Crei un account, installi l’agente con un solo comando e colleghi il suo primo host Proxmox. Il periodo di prova dura 14 giorni e non chiede alcun dato di pagamento.