Payments
Comment je concevrais un système de paiement en tant que junior .NET developer
Si on me confiait demain une feature de paiement, les parties difficiles ne seraient pas le design de l'API. Ce seraient l'idempotency, le fait que les problèmes d'argent n'apparaissent pas dans le testing, et savoir quoi ne pas construire.
Si quelqu'un me confiait demain une feature de paiement, le design de l'API serait la partie facile. Ce dont je m'inquiéterais vraiment est une liste plus courte et moins évidente que ce que j'aurais imaginé il y a un an.
Je ne construirais pas la partie paiement
La première décision, c'est quoi ne pas construire. Les données de carte ont des obligations de compliance attachées, et un développeur junior qui décide de manipuler des numéros de carte bruts pour économiser des frais d'intégration prend une décision qui n'est pas la sienne à prendre. Utilisez un provider, laissez les détails de la carte lui aller, et stockez la référence qu'il vous renvoie.
Ça a l'air d'éviter le problème. C'est en fait la bonne réponse, et le savoir vaut plus que d'être capable d'implémenter la chose que vous ne devriez pas implémenter.
Supposez que chaque request arrive deux fois
C'est celle que je raterais si je ne l'avais pas déjà apprise à la dure. Les réseaux font des timeouts, les utilisateurs double-cliquent, les clients mobiles réessaient automatiquement, et rien de tout ça n'est inhabituel. Un endpoint de paiement qui facture deux fois quand il reçoit deux fois le même request est cassé, même s'il passe tous les tests que vous avez écrits pour lui.
La solution est une idempotency key venant du client et une constraint unique dessus dans le database. Vérifier si la key existe déjà avant d'insérer a l'air de marcher, mais il y a une fenêtre entre la lecture et l'écriture où deux requests concurrents peuvent tous les deux passer la vérification. Laissez le database l'appliquer, capturez l'échec, et renvoyez le résultat déjà stocké.
L'argent ne devrait jamais être un float
decimal, toujours. L'arithmétique en virgule flottante perd de la précision d'une façon qui convient à une simulation physique et qui est inacceptable pour un solde. C'est facile à bien faire et vraiment mauvais à rater.
Stockez aussi la devise à côté du montant. Un montant sans devise est un nombre, pas de l'argent, et au moment où une deuxième devise apparaît dans le système, tout montant sans devise devient ambigu.
Concevez pour la question « qu'est-ce qui s'est passé ? »
La chose que j'ai le plus sous-estimée : le travail d'un système de paiement n'est pas seulement de déplacer de l'argent, c'est de pouvoir expliquer après coup ce qu'il a fait et pourquoi. Quand un client conteste un prélèvement ou qu'un acquirer pose une question sur une transaction, la réponse doit venir de records stockés, pas de la lecture du code en essayant de deviner ce qu'il a probablement fait.
Ça veut dire que chaque étape significative est persistée, y compris les échecs. Ça veut aussi dire que les records de paiement ne sont jamais supprimés, parce qu'un record de paiement supprimé est une preuve détruite, pas des données rangées.
Rendez les états explicites et les transitions gardées
Un paiement est une state machine, que vous le modélisiez comme telle ou non. Il est pending, puis authorised ou declined, puis peut-être captured, puis peut-être refunded. Si ces états ne sont qu'un champ string sur lequel n'importe quoi peut écrire, un jour quelque chose écrit une combinaison impossible et il n'y a aucune trace de comment.
Mettre les transitions à l'intérieur de l'entity, pour qu'une transition invalide lève une erreur au lieu d'enregistrer silencieusement, coûte très peu et transforme une classe de bug en une exception que vous pouvez vraiment voir.
Les échecs ne sont pas tous les mêmes
Des fonds insuffisants et une carte signalée volée sont tous les deux des declines, et les traiter de la même façon est une erreur. L'un est temporaire et ça vaut le coup de réessayer, l'autre est définitif et réessayer ailleurs n'est que de la fraude avec des étapes en plus. Toute retry logic doit savoir à quel type d'échec elle a affaire avant de décider de réessayer.
Ce pour quoi je demanderais de l'aide
La reconciliation, le processus qui consiste à confirmer que ce que votre système pense qui s'est passé correspond à ce que l'acquirer dit qui s'est passé. Je comprends que ça compte et je n'ai pas d'expérience réelle avec ça, et c'est le genre de chose où se tromper coûte cher et en silence.
Aussi tout ce qui implique plusieurs devises avec une vraie conversion, et tout ce qui touche aux payouts plutôt qu'aux collections. J'en sais assez pour savoir que c'est plus profond que ça n'en a l'air.
La forme générale de la chose
Utilisez un provider pour les données de carte. Supposez des requests dupliqués. Utilisez decimal avec une devise explicite. Persistez assez pour pouvoir expliquer n'importe quelle transaction plus tard. Modélisez les états explicitement. Distinguez les échecs qu'on peut réessayer de ceux qui sont définitifs. Et soyez honnête sur les parties que vous voudriez qu'un senior relise avant que ça n'approche du vrai argent.