online-checkouts
From an high level perspective, the online payments integrations are the ones that allow us to receive payments from our customers (both B2B and B2C). In our implementation we have 3 main concepts involved:
- Payment providers
- Payable items
- Payer actors
- Online Checkouts
Payment providers
A payment provider is essentially an external entity that is able to process payments on our behalf. It can be a bank, a payment gateway, a payment processor, etc. In our case, we have 4 main payment providers:
The main goal of a payment provider is to abstract the interaction with a customer in order to make him able to securely provide a payment method (e.g. a credit card) and to process a payment. In order to do that, a payment provider exposes a set of APIs that we can use to interact with it. In our case, we have a set of PaymentProviders that are responsible to interact with the payment providers APIs in a a standard way by designing a common interface that can be used by the rest of the application.
Our payment providers live in the Payments::OnlinePayment namespace and are stored in the services/payments/online_payment folder. Since in ruby there's no concept of strict interface each payment provider is a class that implements a well known set of methods to declare what is supported and what is not.
:::code source="../../uala-main/spec/services/payments/online_payment/payment_provider_interface_spec.rb" :::
Essentially a Payment Provider must support a generic execute_transaction method and some query method to see if a payment went fine. Another big topic in the payment provider context is if a provider supports the so called authorize then capture flow (more on this later). Finally a provider may or may not support refunds.
Any payment gateway will need a set of credentials to interact with their API and we have different accounts for different country. For this reason any credential needed to interact with a payment provider is stored in a specific CountryProvider model, in this way we support multiple instances of any provider and we retrieve the right one at runtime according to the type of payment we're about to perform.
Stripe
This is the provider for which we have the most features implemented. It supports the authorize then capture flow and refunds. It also supports the concept of payment intents that is a way to handle 3D secure payments. It also supports the concept of payment methods that is a way to handle multiple payment methods for a single customer that can be stored and reused in the future. A payment method is essentially a way to store a credit card for a customer while not storing any sensitive data in our database. We map Stripe's payment methods to our CreditCard model and Stripe's Customer in our ExternalProviderCustomer model.
It is implemented in the Payments::OnlinePayment::StripeProvider class.
For this provider we also have some additional features that are specific to it and are implemented outside the interface we discussed above. The most notable feature is the usage of their Setup Intents API that is a way to collect a payment method for a customer and store it to be used at a later time. We use this feature to enable the self provisioning feature for our SaaS and also to implement the CancellationProtection feature to protect appointments in the future. This data collection is implemented in Payments::CreateCreditCardService class.
Communication between us and Stripe happen in bidirectional way since they have a rich set of event types delivered via webhooks. We listen for webhooks using a single StripeController and we parameterize the webhook configuration by referring the CountryProvider in use in the webhook URL. Incoming Stripe calls are of course authenticated using a HMAC signature that we verify using the webhook secret stored in the CountryProvider. Once an event has been authenticated we dispatch it to the right handler using the ProcessStripeEventJob background job class that will route the event to the proper part of code.
:::code source="../../uala-main/app/controllers/stripe_controller.rb" :::
PayPal
We implemented for Paypal full set of features of the Payment Provider interface. It supports the authorize then capture flow and refunds.
It is implemented in the Payments::OnlinePayment::PaypalProvider class.
Communication between us and Paypal happen in bidirectional way since they have a reduced set of event types delivered via webhooks. We listen for webhooks using a single PaypalController and we parameterize the webhook configuration by referring the CountryProvider in use in the webhook URL. Incoming Paypal calls are of course authenticated using a signature that we verify using the webhook secret stored in the CountryProvider. Once an event has been authenticated we dispatch it to the right handler using the ProcessPaypalEventJob background job class that will route the event to the proper part of code. (Relevant code is stored in app/controllers/paypal_controller.rb)
DojoPay
It was our initial implementation of self provisioned payments, it's implemented using the same pattern of the other payment providers using the Dojo platform. It's currently disabled on our platform.
BrainTree
Our previous payment gateway before the introduction of OnlineCheckout in our system, it was then acquired by Paypal and then replaced in our system by it.
Payable items
A payable item is essentially any object that can be paid. It is described by a weak interface that abstract the details that we need in order to create an OnlineCheckout and a payment for it. Also the interface itself exposes some callbacks that are used to notify the object about the payment status (more on this in payment flow).
:::code source="../../uala-main/spec/services/payments/online_payment/payable_interface_spec.rb" :::
Once an object implements this interface we will be able to extract payment details (e.g. unit price, total price, quantity, description, currency, etc) and create the OnlineCheckout that will be used to process the payment. If the item to pay has some specific needs (e.g. a specific payment provider, a specific payment method, etc) it can specify it using payment_provider_requirements.
Also any payable items can interrupt a payment flow by failing some preconditions (passes_integrity_checks_for?) or at a later stage (after_charge_logic).
Currently we do have lot of payable items all of them processed by our system in a unified way. The most notable are:
- Appointment paid by B2C users
- BillingCompanyInvoice paid by Billing Company in B2B SaaS
- BillingCompanyProduct (a.k.a. Addons) paid by Billing Company in B2B
- CancellationProtection paid by end user when they miss appointments
Payer actors
A payer models the entity that is going to pay for something, is a smaller interface in respect to the other discussed so far and it just need to expose some ID, an email and possibly a human friendly name. It is linked to OnlineCheckout model as a payer polymorphic association and it is used to create (or lookup) a Stripe Customer entity whenever needed.
Online Checkouts
This is our entity that models a payment in our system. It is implemented as a state machine and its state follows the actual state of a payment in the payment provider.
It has the following association with other entities in our system.
State machine implementation
The state machine implemented by OnlineCheckout is modeled in the following diagram. Here are the most relevant states in the diagram:
- pending: Is the initial state of any OnlineCheckout after its creation
- requires_action: Used for 3D secure payment processing (i.e. when user payment method requires further authentication like a OTP)
- authorized: Is the state of a payment after it has been authorized by the payment provider but funds are not already withdrawn.
- success: Is the state of a payment after it has been successfully processed by the payment provider and the money has been captured.
- failed: There's something wrong with the payment method (e.g. expired card, insufficient funds, etc). Payment can be retried by the user (maybe using a different card).
- errored: Something went wrong during the payment processing. Process completed with some error in our system but money are not captured.
- errored_paid: Something went wrong during the payment processing. Process completed with some error in our system. We must issue a refund to the user.
!!!
When we introduced the authorize then capture flow in our system we have effectively resolved the errored_paid checkouts. Nowadays we have a zero rate of errored_paid checkouts (last one is form September 23, 2020).
!!!
Full payment flow
The full payment flow is a complex process that involves many actors and many steps. Here we are going to describe the full flow from the user perspective and from the system perspective.
We start within the assumption that some UI actions added some payable items to a virtual cart (identified by a token) and the user is ready to pay for them.
Backend internals for payment processing
Here'e the full flow from the backend perspective when a paymente is authorized. As you can see lot of things can go wrong and we have to handle all of them. A common failure case was when a given availability for an appointment being paid is not available anymore (remember that salon owners can always change their availability). In this case we have to stop the actual payment and return a failed checkout.
With simple capture flows there are lot of possible cases that lead to errored paid checkouts. Any action performed in the after_charge_logic can fail for any possible reason (e.g. a network timeout), so is not reliable as a payment flow.
We moved all current payment processing to work with authorize then capture flow to avoid this situation.
Cancellation protection
This particular use case doesn't follow strictly the payment flow because a card is given before the appointment and is used to guarantee the appointment. The payment is done in a later step if someone misses an appointment and salon owner wants to charge the cancellation fee.
In that case we only support Stripe using their Setup Intents API and we store the customer credit card.
When we need to charge the user we inititate a Payment Flow without user intervention by creating an online checkout in a server side call using the same steps outlined in Full payment flow but without starting from OnlineCheckout creation passing a confirm_with_card flag with the user card.
The rest of the flow is identical.
Self provisioning
A similar flow is used to allow users to self register to our SaaS via our wizard. Again we use Stripe to collect the payment and we use the same flow as Cancellation Protection when we need to charge the user.
The need for authorize then capture flow
We introduced this flow because in some cases it's impossible to know if something can be paid until the very last moment when we try to confirm it. For example if a user try to book an appointment for a given time and the salon owner changes the availability for that time, the appointment is not valid anymore and we have to cancel the payment. At the same time we don't want to lock the slot for the user until we get the payment confirmation.
With two phases we can actually reserve the slot only after we get confirmation from Stripe about the payment and we can cancel the payment if something goes wrong without the need of refunding the user.
Another case for this flow is for intermittent failures. For example if we have a network timeout or a database deadlock when we try to write the payment status or when we send a notification, if something goes wrong we don't need to "rollback" the payment because we didn't capture the funds yet.
In conclusion the ATC flow provides a more robust way to handle payments but it's only strictly mandatory for Appointment (marketplace) bookings where a slot can be taken by someone else at any time between the initial query and the actual payment. For all other use cases is a nice to have and its usage can be replaced with a retry mechanism in case of failures.
If in the future we get rid of it we should also provide a way to cancel a payment and refund the user in future payment gateways.
We also need a way to be notified about refunds and cancellations from the payment gateway to update our internal state.
Authorizing future payments
Any payment gateway that must be used for future payments must support an API similar to the Setup Intents API and we must store the customer credit card (in a tokenized form to not being subject to PCI compliance).
Essentially we need a set of APIs to:
- Create a payment method (e.g. a credit card) and store it in a tokenized form
- Create a payment for a given amount and currency using a previously stored payment method (with or without end user intervention).
- Get feedback about the payment status
Payment methods can be used just once or multiple times depending on the item being paid. Also since payment methods can expire we possibly need to support a way to update them and a couple of webhooks for expiration and renewals.