4 axioms for any payment system
We just went through a major refactor in our payments system to better our integration with three providers: Stripe, Google Play, and Apple’s App Store. We had a working integration for years, however we were lacking some visibility in our full user journey due to a combination of offline/online interactions with the providers after the payment hands-off.
We share here four axioms that have proven key during the development.
Be server based
Any client logic (web, mobile, desktop, etc) will eventually be tempered with; intentionally or not. For example, a user could clear their local storage half-way through a payment, lose their internet connection, have a months old app version, intentionally change your beloved js, create & distribute a fake version of your app, etc. Therefore, relying on any client sensitive logic is detrimental for your monitoring, auditing, security, & business needs.
Strive for server-based solutions that are resilient to unexpected client behaviour whenever possible. It’s ok for a payment solution to rely heavily on the server. In turn, this has other benefits for our users, for example, seamless experience when moving between platforms or devices.
It’s not about efficiency, it’s about security for you and your user.
Be explicit
Your source of truth is always the bank account, i.e. the providers. Any other value should be considered an “educated guess” until explicitly matched with a final invoice… and even in those cases the value could still be inaccurate, e.g. a purchase could be refunded offline months later. Thus, we explicitly state at which degree we are estimating all values.
This further expands to complex topics like: currency exchange rates, tax regulations, delayed payments, etc.
Empower your finance team, they have the final call.
Store all results
Let’s say we have a record in our DB indicating a user purchased product Y at $100 with 21% value added tax (VAT). How much was the VAT amount?… If you said $17.3553719008, you are right! See how confusing it could be? That $100 is actually 121% of the tag price. How would you round it? Are you considering your provider fees?
Yes, there are industry standards on how to calculate VAT, how to round, and how to handle invoices with several items that avoid these issues. The question I pose is: how reliable of a system are you building if you assume everyone in your organisation is familiar with those standards?
Finally, another very common mistake in computer science is to assume that math is perfect; it’s not. Numerical errors cost money. I propose two rules aimed to reduce those:
- Store every calculation result (including intermediate results).
- Use integers instead of floats (floats/doubles could be affordable).
The example above will look like this: we actually store those five values separately [21%,1736, 8264, 10000, USD]. This allows us to track and correct errors (rounding, wrong VAT applied, wrong exchange rate, etc.) and debug old purchases, even when the faulty code is long gone.
Audit everything
The history of a purchase is as important as the purchase itself when improving a users’ journey. For example, we need to:
- Store the historic tier payments (Apple’s App Store) whenever they change, in a very lean manner. We use tier payments in Apple’s App Store.
- Know how long it took a user to go from “start pay” to “ received good”, or “ refund payment”.
- Track who introduced changes to our system, authorised exceptions, change prices, etc.
Logging is not enough. We need to be able to go back years, store transactions synchronously, ensure we record all, etc. Bear in mind, this also could provide information to reconstruct a whole user’s journey when necessary.
As you may see, these principles could be applied broadly. My advice, you should only enforce it when it pays off ;)