Payments
Come progetterei un sistema di pagamenti da junior .NET developer
Se domani mi affidassero una feature di pagamento, le parti difficili non sarebbero il design dell'API. Sarebbero l'idempotency, il fatto che i problemi con i soldi non emergono nel testing, e sapere cosa non costruire.
Se qualcuno mi affidasse domani una feature di pagamento, il design dell'API sarebbe la parte facile. Quello di cui mi preoccuperei davvero è una lista più corta e meno ovvia di quanto avrei immaginato un anno fa.
Non costruirei la parte di pagamento
La prima decisione è cosa non costruire. I dati di carta hanno obblighi di compliance legati a loro, e uno sviluppatore junior che decide di gestire numeri di carta grezzi per risparmiare una fee di integrazione sta prendendo una decisione che non è sua da prendere. Usa un provider, lascia che i dettagli della carta vadano a loro, e salva il reference che ti restituiscono.
Sembra evitare il problema. In realtà è la risposta corretta, e saperlo vale più che essere in grado di implementare la cosa che non dovresti implementare.
Presumi che ogni request arrivi due volte
Questa è quella che sbaglierei se non l'avessi già imparata nel modo difficile. Le reti vanno in timeout, gli utenti fanno doppio clic, i client mobile ritentano automaticamente, e niente di tutto ciò è insolito. Un endpoint di pagamento che addebita due volte quando riceve lo stesso request due volte è rotto, anche se supera ogni test che hai scritto per lui.
La soluzione è una idempotency key dal client e una constraint univoca su di essa nel database. Controllare se la key esiste già prima di inserire sembra funzionare, ma c'è una finestra tra la lettura e la scrittura dove due request concorrenti possono entrambi superare il controllo. Lascia che sia il database a farlo rispettare, cattura il fallimento, e restituisci il risultato che hai già salvato.
Il denaro non dovrebbe mai essere un float
decimal, sempre. L'aritmetica in virgola mobile perde precisione in modi che vanno bene per una simulazione fisica e sono inaccettabili per un saldo. Questo è facile da fare bene e genuinamente grave da sbagliare.
Salva anche la valuta insieme all'importo. Un importo senza valuta è un numero, non denaro, e nel momento in cui una seconda valuta compare nel sistema, ogni importo senza una diventa ambiguo.
Progetta per la domanda "cosa è successo?"
La cosa che ho sottovalutato di più: il compito di un sistema di pagamenti non è solo spostare denaro, è essere in grado di spiegare dopo cosa ha fatto e perché. Quando un cliente contesta un addebito o un acquirer chiede di una transazione, la risposta deve venire da record salvati, non dal leggere il codice e ragionare su cosa probabilmente ha fatto.
Questo significa che ogni passo significativo viene persistito, incluse le failure. Significa anche che i record di pagamento non vengono cancellati, perché un record di pagamento cancellato è prova distrutta, non dati messi in ordine.
Rendi gli stati espliciti e le transizioni protette
Un pagamento è una state machine che tu lo modelli come tale o no. È pending, poi authorised o declined, poi forse captured, poi forse refunded. Se questi stati sono solo un campo stringa su cui chiunque può scrivere, prima o poi qualcosa scrive una combinazione impossibile e non c'è nessun record di come sia successo.
Mettere le transizioni dentro l'entity, così che una non valida lanci un errore invece di salvare silenziosamente, costa pochissimo e trasforma una classe di bug in un'exception che puoi davvero vedere.
Le failure non sono tutte uguali
Fondi insufficienti e una carta segnalata come rubata sono entrambi decline, e trattarli allo stesso modo è un errore. Uno è temporaneo e vale la pena riprovare, l'altro è definitivo e riprovarlo altrove è solo frode con passaggi in più. Qualsiasi retry logic deve sapere con che tipo di failure ha a che fare prima di decidere di riprovare.
Su cosa chiederei aiuto
La reconciliation, il processo di confermare che quello che il tuo sistema pensa sia successo corrisponda a quello che dice l'acquirer. Capisco che conta e non ho esperienza reale con essa, ed è il genere di cosa dove sbagliare è costoso e silenzioso.
Anche qualsiasi cosa che coinvolga più valute con conversione reale, e qualsiasi cosa che tocchi i payout invece delle collection. Ne so abbastanza da sapere che sono più profonde di quanto sembrino.
La forma generale della cosa
Usa un provider per i dati di carta. Presumi request duplicati. Usa decimal con una valuta esplicita. Persisti abbastanza da poter spiegare qualsiasi transazione più tardi. Modella gli stati esplicitamente. Distingui le failure su cui vale la pena ritentare da quelle definitive. E sii onesto su quali parti vorresti che un senior rivedesse prima che si avvicinino a soldi veri.