The diagram above depicts the ideal payment gateway integration, where the frontend application is responsible for tokenizing the payment method and the checkout workflow’s
PaymentConfirmationActivity is responsible for executing the transactions needed to complete checkout. In the following sections, we’ll explore each component of this integration and discuss why we consider this the ideal integration pattern. While a cart can handle multiple and different types of payment methods, we’ll primarily focus on the scenario of using a single credit card to keep things simple.
The first stage of this integration starts with the billing section of the checkout flow. At this point in the checkout flow, the customer is presented with a form to gather their payment method data. Typically, this includes a credit card and billing address form (or option to use the shipping address).
In this stage, we ultimately need to persist a Payment object for the cart, which holds details of the payment method and declares the monetary amount that this payment method will be responsible for. (Note: if using a single payment method, then the Payment amount should be equal to the cart total.) To avoid PCI risk, we don’t want to simply store the raw credit card data to the Payment. Instead, it’s best to tokenize the credit card and persist that representation to the Payment.
If you’re not familiar with credit card tokenization, here’s a quick summary…
The credit card form should be submitted directly to the payment gateway via an API call from the browser. From there, the gateway will give us a token in return. You can think of this token as a simple reference id to the credit card that the gateway has knowledge of. The token itself contains no sensitive customer or card data, so it’s safe to pass to our backend services without concern about increasing PCI risk.
|Each payment gateway’s integration pattern is slightly different, but the key is that the raw credit card data is only passed to the gateway, and it never hits our backend servers.|
Once the token is gathered from the gateway, you should create a Payment to store the token and declare the amount that should be charged to this payment method.
A few additional notes about saving the Payment:
The Payment must declare the gateway type. This lets us know which gateway tokenized the credit card, and where we’ll need to execute transactions for that Payment.
Make sure to declare whether the token is a single-use or multi-use token. Typically, single-use tokens can only be used for a single successful
authorize and capture transaction. This becomes very important for managing the Payment once we start executing transactions against the payment method. For example, we can’t re-authorize a single-use payment, so we’d need to re-tokenize the credit card or request a new form of payment from the customer.
Feel free to populate the
Payment#displayAttributes map with things like the card type (Visa, Mastercard, etc.) and the credit card number’s last four digits or a masked card number. Just make sure to never store the raw credit card number or CVV on the Payment.
If it’s supported by the gateway, you can record a logged-in customer’s request to save the payment method for future use via the
Payment#shouldSavePaymentToCustomer property. This flag will cause the payment to be automatically stored in the CustomerServices
BLC_PAYMENT_ACCOUNT table. Note: this is done via the CustomerService’s consumption of the
The majority of a checkout submission is handled by the Checkout Workflow. Before we discuss the payment-specific portions of the checkout, it’s important that we first explore the foundational pieces of the
CheckoutProcessRequest is actually quite simple, but it contains a very important piece of data: the
requestId. This id represents a unique request to initiate a checkout. Throughout the
CheckoutWorkflow, we use this id to identify work that was done as a result of a specific checkout submission. For example, each persisted payment transaction includes the
requestId to identify the request that caused the transaction to be executed.
Additionally, some gateways require that the credit card’s CVV is provided with the payment token in order to execute a transaction. To support these cases, the request payload includes a
securityCodes map, mapping payment ids to CVVs. Note: it’s very important that this data is only used in this very limited scope, and that it’s never persisted or logged.
CheckoutService coordinates the checkout submission processing. In
CheckoutService#processCheckout(…), we start by updating the cart status to
SUBMISSION_IN_PROGRESS which locks the cart. In doing so, we enforce that only one checkout submission for that cart can be processed at a given time, and that the cart cannot be edited while the checkout is in progress. From there, the
CheckoutService delegates to the
CheckoutWorkflow to do most of the heavy lifting (Action 2.A in the diagram above).
If the workflow processing is successful, then the cart is finalized by setting its status to
SUBMITTED, setting the
submitDate, and declaring an order number for the cart. Additionally, a message (the
CheckoutCompletionEvent) is globally sent out to notify other services of the completed checkout (Action 2.B in the diagram above).
If the workflow processing is not successful, then the failure is recorded on the cart, and the cart status is returned to
IN_PROCESS, allowing the cart to be edited once again.
The checkout workflow consists of any number of ordered activities that must be completed to validate the cart and process the checkout. Typically, the workflow is front-loaded with validation activities to verify that everything is in order, before starting processing activities. Out of box, these validation activities include:
Cart item validation (
CartItemValidationActivity) - Does the cart clearly declare what needs to be fulfilled?
Cart fulfillment validation (
CartFulfillmentValidationActivity) - Does the cart clearly declare how to fulfill the items?
Cart pricing validation (
CartPricingValidationActivity) - Is the cart priced?
Cart stale price validation (
CartStalePricingValidationActivity) - Are any of the cart’s prices out-of-date?
Cart payment method validation (
CartPaymentMethodValidationActivity) - Do the cart’s payment(s) sufficiently cover the cart’s total price?
Inventory validation (
InventoryAvailabilityValidationCheckoutWorkflowActivity) - Is there sufficient inventory to fulfill this order?
Cart offer validation (
CartOfferValidationActivity) - Are the cart and its items still eligible for the cart’s discounts?
The primary processing activity is the
PaymentConfirmationActivity which is responsible for executing payment transactions for each of the cart’s payments. We’ll go into more detail on this activity later.
When thinking about adding a new activity, or modifying an existing activity, it’s very important to consider: is this action required to complete the checkout?
Overall, it’s critically important to minimize the potential points of failure in the checkout workflow, so it should only include activities that are absolutely required. In many cases, it’s actually much better to handle checkout-related actions via the
One of the most important things to consider with the checkout workflow is error handling. If an unrecoverable failure is encountered, then the workflow must be terminated, and all previously completed work must be rolled back.
There are two potential paths for rolling back this previously completed work: 1. Using a real-time rollback - i.e. rolling back the work, before handing the cart back to the customer 2. Using an offline process to roll back
While a real-time rollback is conceptually simple, we strongly suggest avoiding this pattern. In our experience, real-time rollbacks make the workflow more brittle by creating more points of failure. Additionally, if the real-time rollback fails, then you often can’t recover. This is why we strongly suggest finding a way to handle any required rollbacks as an offline process.
Another thing to consider when working to mitigate workflow error scenarios is the order of your activities. To avoid tricky rollbacks or lessen the likelihood of a rollback, place those activities toward the end of the workflow. This is exactly why we strongly suggest placing the
PaymentConfirmationActivity last in your workflow. Therefore, there are no activities after the
PaymentConfirmationActivity, that could require us to roll back payment transactions.
In the out-of-box checkout workflow, there are two activities that deal with cart payments: the
CartPaymentMethodValidationActivity and the
As mentioned above, the
CartPaymentMethodValidationActivity is responsible for ensuring that the payments are valid relative to the cart. The main portion of this validation is checking that the sum of cart’s payment amounts matches the cart total.
PaymentConfirmationActivity is responsible for executing payment transactions for each of the cart’s payments. This activity leverages the
PaymentConfirmationService and a payment gateway module (i.e. a PaymentGatewayCommon implementation) to execute transactions against the gateway. To register a specific gateway with your project, the gateway module’s dependency should be included in your CartOperationServices and OrderOperationServices projects.
|For more information on how to create your own payment gateway library, see the Creating a Payment Gateway Module guide.|
The primary reason that we strongly prefer executing transactions in this context is our ability to know when payment transactions have been attempted/executed. Before sending a transaction to the payment gateway, the
PaymentConfirmationService first records a
PaymentTransaction with status
SENDING_TO_PROCESSOR and a
transactionReferenceId. That way, if anything happens where we don’t receive transaction results, we always know that we attempted to execute a transaction. Additionally, since the
transactionReferenceId is always unique and is sent to the gateway in the transaction request, we can always check with the gateway to see if they have knowledge of the transaction results.
If transactions are executed without using this pattern, then there’s a chance that the gateway has successful transactions, that we have no knowledge of. In other words, the customer could have been charged, without us giving them anything in return. This is a situation that should absolutely be avoided!
While the integration pattern described above is considered the ideal pattern, we know that it won’t work for all payment gateway integrations. In some cases, it’s not realistic to execute transactions within the checkout workflow. Often these transactions are triggered via the frontend gateway integration.
In these cases, it’s especially important to record transaction results as soon as they’re known. If transaction results are recorded prior to submitting the checkout and entering the checkout workflow, then the
CheckoutWorkflow can still be used to finalize the checkout. In that case, the
CartPaymentMethodValidationActivity and the
PaymentConfirmationActivity will need to do a few things differently:
CartPaymentMethodValidationActivity should validate that the payment contains successful payment transactions covering the full payment amount. Note: this check is currently not done automatically out-of-box.
PaymentConfirmationActivity will recognize that the payment transactions have already been executed. Therefore, it won’t attempt to execute a transaction for that payment.