Once you know how much you want to charge and which taxes apply, and have all the information you are required by law to keep, it’s finally time to perform the payment and fulfill the order. And, of course, you want both to happen at once. Fulfillment without payment is bad for obvious reasons, and because moreover you become liable for tax over something you were not paid for. Payment without fulfillment is even worse: there’s no easier way to lose a customer than charging without delivering (and you’ll get a chargeback with a penalty sometime later).
This post explains how we perform (credit/debit) card payment via Stripe and order fulfillment transactionally.
This entry belongs to a series of posts on EU VAT + invoicing requirements and (credit/debit) card payment + fulfillment using Stripe:
- legalities
- initial pricing and data capture
- computing the location of supply
- transactional payment and fulfillment
The standard example used to teach about ACID transactions is the transfer between 2 different accounts. Students are taught the money is debited in one and taken from the other atomically. Afterwards, when they come in contact with real life, they learn the truth that financial systems do not work at all this way (with atomicity) and are instead based on the notion of having an accurate paper trail and being able to undo things. (The author knows several people who saw literally millions appear in their accounts and disappear later.)
Stripe provides two such mechanisms to build transactional fulfillment and payment upon:
you create a charge (authorize it) and capture it later. The funds are released automatically after one week if the charge is not captured
you can refund captured charges, entire- or partially
Given these, and our own database’s ACID functionality, we can build what amounts to a 2-phase commit:
- create a charge with
capture = false
to authorize the required amount - start a local transaction
- save the order and invoicing details in our DB, perform fulfillment (DB-only, no external effects yet)
- for good measure, save the encrypted order and invoicing data in the charge’s metadata
- capture the charge
- commit local transaction
In case of commit error, the charge is refunded.
If you squint just a little, this can be seen as a 2-phase commit where the coordinator happens to be co-located with one of the workers, and the voting corresponds to Stripe authorizing the charge and the fulfillment work (in our case, saving order and invoice info and generating the chosen Sync Appliance license) being performed in a local transaction.
All errors at any step before (5) are dealt with automatically, since uncaptured charges will be released in 1 week.
Release can be sped up by periodically listing recent charges and releasing those that do not correspond to any order. This also deals with crashes in the coordinator (which is the same node doing fulfillment in this particular case), i.e., it’s the way rollbacks are performed.
Steps (5) and (6) can be reverted, leading to a different recovery strategy: a crash after committing (and thus fulfilling the order) would have to be handled by capturing charges whose corresponding order has been fulfilled – the charge metadata allows this. This is more work than implementing a single mechanism to deal with uncaptured and captured but unfulfilled ones at once.
Since the order, invoicing and fulfillment data is associated to the charge in Stripe’s database, instead of using rollbacks, it would be possible to consider the transaction committed the moment the metadata is updated, and recover from crashes past that point by moving forward and fulfilling the order using the metadata. Whether that is possible or not is application-dependent: it only works when the order/fulfillment system can vote and commit to that vote.
What about Strong Customer Authentication (SCA)
As of 14 September 2019, the Revised Directive on Payment Services (PSD2) will require that most payments be authenticated, mainly using 3D Secure 2. Stripe has created a new API named PaymentIntents, currently in beta phase, to allow this. The API is not stable yet so the above procedure should be followed for the time being, all while keeping an eye on how the new one is shaping up.
PaymentIntents “inverts control” of the payment process, which now works as follows:
- the server creates a PaymentIntent and obtains a token (“client secret”)
- the token is given to the client-side Stripe.js which collects payment information, performs authentication and completes payment at once
- the server is told the payment completed either synchronously or via an asynchronous webhook (the latter must be supported anyway to deal with crashes or the user closing the window right after paying)
This seems initially at odds with the above procedure. On further inspection, though, we discover the new API does have the primitives we need:
- it is possible to create PaymentIntents backed by an existing source (credit/debit card): this means that card info such as country bank will be known beforehand and usable for VAT purposes
- you can perform two-step payments with separate authorization and capture
- lastly, it is possible to refund PaymentIntents charges
These are the base capabilities required to perform transactional payment and fulfilling as described above.
This post will be updated when the API is stable to document the server-side API calls and client-side Stripe.js invocations to perform transactional payment and fulfillment with PaymentIntents.