Broadleaf Microservices

Checkout Payment Architecture

The Ideal integration

Checkout Payment Flow

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.

Tokenizing the Payment Method

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.

Note
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 or 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 CheckoutCompletionEvent.

Handling the Checkout Submission

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, CheckoutService, and CheckoutWorkflow.

The CheckoutProcessRequest

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 CheckoutService and 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.

The CheckoutService

The 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

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.

Note

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 CheckoutCompletionEvent. For example, the order confirmation email should be triggered by the CheckoutCompletionEvent. While this communication to the customer is important, it’s not required to complete the checkout. If this email were sent within the workflow, then a failure to send the email would block the customer from completing their order. That’s bad news all around! On the other hand, if the email is triggered via the CheckoutCompletionEvent and the email fails to send, then we’ve already completed the order (yay business!) and can retry sending the email in an offline process.

Checkout Workflow Error Handling

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.

Payment Activities

In the out-of-box checkout workflow, there are two activities that deal with cart payments: the CartPaymentMethodValidationActivity and the PaymentConfirmationActivity.

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.

The 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.

Note
For more information on how to create your own payment gateway library, see the Creating a Payment Gateway Module guide.
Note
While the PaymentConfirmationService supports the ability to execute AUTHORIZE or AUTHORIZE_AND_CAPTURE transactions, we suggest using AUTHORIZE transactions during checkout, followed by CAPTURE transactions after the checkout is finalized (usually when the items are fulfilled). Since authorize transactions represent a temporary reservation of funds and those reservations expire on their own, the impact of something going wrong is significantly less damaging to customers.

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!

Variations on the Ideal Integration Pattern

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 CheckoutService and 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:

  1. 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.

  2. PaymentConfirmationActivity will recognize that the payment transactions have already been executed. Therefore, it won’t attempt to execute a transaction for that payment.