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.
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.
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
.
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.
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:
Validating that the requestId
is unique for the cart.
Validating that the current cart status is IN_PROCESS
or CSR_OWNED
.
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.
Recording the requestId
in the Cart#checkoutSubmissions
map.
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…
The cart is finalized by setting its status to SUBMITTED
, setting the submitDate
, and declaring an order number for the cart.
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.
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 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 |
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.
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:
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…
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
For each cart:
Gather cart’s payments
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.
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.
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:
Checkout completion: /carts/{cartId}/resend-checkout-completion-event
Guarded by SEND_CHECKOUT_COMPLETION_EVENT
permission root
Requires that the cart is already finalized
Checkout rollback: /carts/{cartId}/resend-checkout-rollback-event?requestId={checkout_request_id}
Guarded by SEND_ROLLBACK_EVENT
permission root
Requires that the cart has a checkout workflow error with the provided checkout request id
Cart pending payment failed: /carts/{cartId}/resend-pending-payment-failed-event
Guarded by SEND_CART_PENDING_PAYMENT_FAILED_EVENT
permission root
Requires that the cart is in the PENDING_PAYMENT_FAILED
status
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.
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!
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.
For more information on how to best handle these scenarios, see our doc on Externally-Executed Transactions.