Broadleaf Microservices
  • v1.0.0-latest-prod

Checkout Payment Architecture

The Ideal Payment 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 PaymentTransactionExecutionActivity is responsible for communicating with PaymentTransactionServices to execute 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 multiple and different types of payment methods can be used for a cart, we’ll primarily focus on the scenario of using a single credit card, to keep things simple.

Creating a PaymentTransactionServices Payment

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). Depending on the payment gateway being used, the credit card form may be rendered by the gateway (typically in an iframe), or it may be produced by the front-end application.

In this stage, we ultimately need to create a Payment in PaymentTransactionServices, 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 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.

Note
You can also use a saved payment method when creating a Payment, which will automatically populate the Payment from the saved payment method. Please refer to Using a Saved Payment Method for more details

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 or form submission from the browser. From there, the gateway will give us a token in return. You can think of this token as a simple reference 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 in PaymentTransactionServices 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 ownerType and ownerId identify the Payment’s owning entity. In the context of a checkout, we’d expect the following values to be declared: ownerType = "BLC_CART" and ownerId = {cart.id}

  • The Payment must declare the gateway type. This lets us know which payment gateway tokenized the credit card, and where we’ll need to execute transactions for the Payment.

  • The paymentMethodProperties map is used to store whatever properties are needed to represent the payment method when executing transactions. In the case of a credit card, that’s typically the tokenized form of the card. Review the payment gateway integration library’s documentation to identify the required map key for this property.

  • Make sure to declare whether the token is a single-use or multi-use token via the isSingleUsePaymentMethod property. 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.

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

  • Feel free to populate the Payment#attributes map with an additional data points that you store 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#shouldSavePaymentForFutureUse property. This flag will cause the payment to be automatically stored in the PaymentTransactionServices BLC_SAVED_PAYMENT_METHOD table.

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 PaymentTransaction includes the requestId to identify the request that caused the transaction to be executed.

Additionally, when using a saved payment method, some gateways require that the credit card’s CVV/CVC 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/CVCs. 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 validating the checkout request & preparing the cart for checkout. This includes:

  1. Validating that the requestId is unique for the cart.

  2. Validating that the current cart status is IN_PROCESS or CSR_OWNED.

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

  4. Recording the requestId in the Cart#checkoutSubmissions map.

  5. Locking the cart’s Payments in PaymentTransactionServices

    • This ensures that while the checkout is being processed, no other process can modify the cart’s payments.

From there, the CheckoutService delegates to the CheckoutWorkflow to do most of the heavy lifting. If the workflow processing is successful, then…​

  1. The cart is finalized by setting its status to SUBMITTED, setting the submitDate, and declaring an order number for the cart.

  2. The cart’s payments are finalized by adding the CUSTOMER_MUTABILITY_BLOCKED customer access restriction and setting managementState to AUTOMATIC_REVERSAL_NOT_ALLOWED on each payment’s successful transactions.

  3. A message (the CheckoutCompletionEvent) is globally sent out to notify other services of the completed checkout.

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 managed by the customer once again.

If the workflow processing is successful but one or more cart’s payments are awaiting results, then the cart status is set to AWAITING_PAYMENT_RESULT, and customer access restriction are added for those payments, to prevent customers from modifying the payments while waiting for the transaction results. For more details, see Handling Cart with Payments Awaiting Results.

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 PaymentTransactionExecutionActivity which is responsible for communicating with PaymentTransactionServices to execute 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 PaymentTransactionExecutionActivity last in your workflow. Therefore, there are no activities after the PaymentTransactionExecutionActivity, that could require us to roll back payment transactions.

Handling Cart with Payments Awaiting Asynchronous Results

When executing Authorize or AuthorizeAndCapture transactions during checkout, payment gateways will sometimes return a response letting us know that transaction results will be provided asynchronously. In other words, the API response doesn’t immediately tell us whether the transaction succeeded or failed, and instead, we must wait to be notified of the outcome via a webhook notification. This can happen due to the transaction being flagged by fraud checks for manual review, or in some cases, gateways may be able to communicate the Authorize result of an AuthorizeAndCapture transaction synchronously, but must provide the Capture result asynchronously.

When this occurs, the TransactionExecutionResponse coming from PaymentTransactionService contains the AWAITING_ASYNC_RESULTS transaction status. Since Broadleaf 1.7.4-GA, carts that have one or more payments awaiting async results will have their status set to AWAITING_PAYMENT_RESULT. Additionally, the CheckoutResponse returned from CartOperationServices will contain awaitingPaymentResult = true, allowing the checkout UI to communicate the pending state to the customer. In our demo site, we leverage this flag to show the following messaging, in a similar way to the order confirmation page:

Checkout Awaiting Payment Results

Once transaction results are communicated to PaymentTransactionServices via webhook notifications, the FinalizeCartAwaitingPaymentResult scheduled job will attempt to finalize the checkout. In CartOperationServices, this job is handled by the FinalizeCartAwaitingPaymentResultJobListener.

This process first gathers the carts with the AWAITING_PAYMENT_RESULT status, then…​

  1. Sets the cart’s status to PAYMENT_RESULT_CHECK_IN_PROGRESS to prevent any potential simultaneous processing of the same carts by the scheduled job

  2. For each cart:

    1. Gather cart’s payments

    2. If all of cart’s payments are successful, then finalize the cart. This will send out the CheckoutCompletionEvent message to notify other services of the completed checkout.

    3. If any one of the cart’s payments failed, then rollback checkout using the same rollback process specified in Checkout Workflow Error Handling. This also sends out the CartPendingPaymentFailedEvent message, which can be consumed to notify the customers to encourage them to resubmit their orders.

    4. If any of the cart’s payments are still awaiting results, then the cart status is set back to AWAITING_PAYMENT_RESULT, so that it’s processed by a future iteration of this job.

Note
Out of the box, this scheduled job is triggered every 5 minutes.

In the event that a checkout-related message failed to send, the following endpoints (in CheckoutEventsEndpoint) can now be used to re-send the message as of 1.7.4-GA:

  1. Checkout completion: /carts/{cartId}/resend-checkout-completion-event

    1. Guarded by SEND_CHECKOUT_COMPLETION_EVENT permission root

    2. Requires that the cart is already finalized

  2. Checkout rollback: /carts/{cartId}/resend-checkout-rollback-event?requestId={checkout_request_id}

    1. Guarded by SEND_ROLLBACK_EVENT permission root

    2. Requires that the cart has a checkout workflow error with the provided checkout request id

  3. Cart pending payment failed: /carts/{cartId}/resend-pending-payment-failed-event

    1. Guarded by SEND_CART_PENDING_PAYMENT_FAILED_EVENT permission root

    2. Requires that the cart is in the PENDING_PAYMENT_FAILED status

How to add authorized client with required permissions so that you can successfully call the endpoints

These endpoints are guarded by special permissions that should only be used by a limited number of people/processes. For example, you could have a process to manually hit those endpoints with cUrl or Postman, which you’d need to be authenticated with specific scopes. You can create an AuthorizedClient with the correct scopes and permissions assigned, you can then authenticate with those client_credentials for your process. To create an authorized client with the correct permissions and scopes, you can use the following sql:

INSERT INTO "auth"."blc_client" ("id", "application_id", "attributes", "client_id", "client_secret",
"friendly_name", "is_admin", "auth_server_id",
"token_timeout_seconds", "default_redirect_uri",
"refresh_token_rot_intrvl_scnds", "refresh_token_timeout_seconds")
VALUES ('MyProcessClient', NULL, '{}', 'MyProcessClient', 'some_hashed_secret', 'MyProcessClient',
'N', '2', 300, NULL, 60, 7200);

INSERT INTO "auth"."blc_client_scopes" ("id", "scope")
VALUES ('MyProcessClient', 'SEND_CHECKOUT_COMPLETION_EVENT'),
('MyProcessClient', 'SEND_CART_PENDING_PAYMENT_FAILED_EVENT'),
('MyProcessClient', 'SEND_ROLLBACK_EVENT');

INSERT INTO "auth"."blc_client_permissions" ("id", "permission")
VALUES ('MyProcessClient', 'UPDATE_SEND_CHECKOUT_COMPLETION_EVENT'),
('MyProcessClient', 'UPDATE_SEND_CART_PENDING_PAYMENT_FAILED_EVENT'),
('MyProcessClient', 'UPDATE_SEND_ROLLBACK_EVENT');

From there, your process can authenticate as MyProcessClient, and successfully hit the endpoints in CartOperationServices.

Payment Activities

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

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 PaymentTransactionExecutionActivity is responsible for communicating with PaymentTransactionServices to execute payment transactions for each of the cart’s payments.

Note
This requires that the relevant payment gateway library is registered with PaymentTransactionServices. For more information on how to create your own payment gateway library, see the Creating a Payment Gateway Module guide.
Note
When the execution of an AUTHORIZE or AUTHORIZE_AND_CAPTURE payment transaction fails, PaymentTransactionServices archives the Payment by default. This is meant to simplify the calling service’s decision about how to handle the failed transaction. In this context, the customer must provide a new payment method.
Note
While the PaymentTransactionExecutionActivity 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 PaymentTransactionService 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 PaymentTransactionExecutionActivity will need to do a few things differently:

  1. CartPaymentMethodValidationActivity should validate that the payment contains successful payment transactions covering the full payment amount.

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