Implement the checkout process
Features described on this page require the Xperience by Kentico Advanced license tier.
Most of the checkout process for digital commerce needs to be implemented by developers. This is because checkout requirements (e.g., payment methods, shipping options, tax handling, order confirmation) can vary greatly between projects. The commerce feature provides the core building blocks, but it is up to the development team to customize the checkout process to match the specific needs of the shop.
To implement a checkout process in your commerce solution, you need to handle the following steps:
- Store products in the shopping cart
- Retrieve the current shopping cart
- Create a new order from the shopping cart
Manage shopping cart data
Shopping carts are represented by ShoppingCartInfo
objects in the API. The actual content of a shopping cart is stored in the ShoppingCartData
string property. You are responsible for implementing the data model and serialization logic for the shopping cart content.
A typical implementation (as in the Dancing Goat project template) uses a ShoppingCartDataModel
to represent the cart, containing a collection of ShoppingCartDataItem
objects for each product in the cart. This example uses three properties to represent the item ID, quantity, and variant ID, but your actual implementation may need to reflect the content model of your product catalog:
// Represents a single item in the shopping cart
public class ShoppingCartDataItem
{
public int ContentItemId { get; set; }
public int Quantity { get; set; }
public int? VariantId { get; set; }
}
// Represents the shopping cart data model containing all items
public class ShoppingCartDataModel
{
public ICollection<ShoppingCartDataItem> Items { get; init; } = new List<ShoppingCartDataItem>();
}
To store and retrieve the shopping cart data model from the ShoppingCartInfo
object, you can implement custom extension methods. You can use a data model and persistence method that fits your requirements; the following example uses JSON serialization for simplicity, but you can use any approach that suits your project.
// Provides extension methods for serializing and deserializing the shopping cart data
public static class ShoppingCartDataModelExtensions
{
public static ShoppingCartDataModel GetShoppingCartDataModel(this ShoppingCartInfo shoppingCart)
{
// Deserializes the ShoppingCartData string into a ShoppingCartDataModel instance
return (string.IsNullOrEmpty(shoppingCart?.ShoppingCartData) ? null : JsonSerializer.Deserialize<ShoppingCartDataModel>(shoppingCart.ShoppingCartData))
?? new ShoppingCartDataModel();
}
public static void StoreShoppingCartDataModel(this ShoppingCartInfo shoppingCart, ShoppingCartDataModel shoppingCartData)
{
// Serializes the ShoppingCartDataModel instance into a JSON string and stores it in ShoppingCartData
shoppingCart.ShoppingCartData = JsonSerializer.Serialize(shoppingCartData);
}
}
To update the shopping cart (add, update, or remove items), you can use logic similar to the following, as seen in DancingGoatShoppingCartController.cs
:
// An instance of IInfoProvider<ShoppingCartInfo> (e.g., obtained via dependency injection)
private readonly IInfoProvider<ShoppingCartInfo> shoppingCartInfoProvider;
// Updates the quantity of a product in the shopping cart, or adds/removes the item as needed
private void UpdateQuantity(ShoppingCartInfo shoppingCart,
int contentItemId,
int quantity,
int? variantId,
bool setAbsoluteValue = false)
{
// Retrieves the current shopping cart data model
var shoppingCartData = shoppingCart.GetShoppingCartDataModel();
// Finds the product item in the cart by content item ID and variant ID
var productItem = shoppingCartData.Items.FirstOrDefault(
x => x.ContentItemId == contentItemId && x.VariantId == variantId
);
if (productItem != null)
{
// Updates the quantity (absolute or relative)
productItem.Quantity = setAbsoluteValue ? quantity : Math.Max(0, productItem.Quantity + quantity);
// Removes the item if the quantity is zero
if (productItem.Quantity == 0)
{
shoppingCartData.Items.Remove(productItem);
}
}
else if (quantity > 0)
{
// Adds a new item to the cart if it does not exist and quantity is positive
shoppingCartData.Items.Add(new ShoppingCartDataItem {
ContentItemId = contentItemId,
Quantity = quantity,
VariantId = variantId
});
}
// Serializes and stores the updated shopping cart data model
shoppingCart.StoreShoppingCartDataModel(shoppingCartData);
// Stores the updated shopping cart object to the database
await shoppingCartInfoProvider.SetAsync(shoppingCart);
}
This approach allows you to flexibly manage the shopping cart’s contents and persist them as a serialized string in the database.
For a complete example, see the Dancing Goat project template’s shopping cart controller and related models.
Manage the current shopping cart
The current shopping cart is managed using the ICurrentShoppingCart*
services, available in the Kentico.Commerce.Web.Mvc
namespace. These services provide methods to retrieve, create, and delete shopping carts for the current user session.
- For anonymous users, the shopping cart identifier is stored in a browser cookie.
- For signed-in members, the shopping cart is associated with the member account, ensuring that the cart content is consistent across devices and sessions.
The actual shopping cart data is persisted in the database. This allows members to access their cart from any device after signing in. You can also configure the expiration period after which abandoned shopping carts are deleted.
To work with the current user’s shopping cart, use the following services:
ICurrentShoppingCartRetriever
– use theGet
method to retrieve theShoppingCartInfo
object for the current user. Returnsnull
if no cart exists.ICurrentShoppingCartCreator
– use theCreate
method to create a newShoppingCartInfo
object and store its identifier (in a cookie for anonymous users or linked to the member for signed-in users).ICurrentShoppingCartDiscardHandler
– use theDiscard
method to delete the current user’s shopping cart from the database and remove the identifier.ICurrentShoppingCartMemberSignInHandler
– use theMemberSignIn
method to handle the transition from anonymous to member cart during sign-in. If both an anonymous and a member cart exist, the member cart is deleted and the anonymous cart is assigned to the member.
The default behavior of these services can be customized by overriding the respective service implementations in your project.
// An instance of ICurrentShoppingCartService (e.g., obtained via dependency injection)
private readonly ICurrentShoppingCartService currentShoppingCartService;
// Retrieves the shopping cart for the current user
ShoppingCartInfo shoppingCart = await currentShoppingCartService.Get();
// If there is no shopping cart for the current user, creates a new shopping cart
shoppingCart ??= await currentShoppingCartService.Create(null);
// Accesses the shopping cart content (e.g., products in the cart) using
// the extensions methods from the previous section.
var cartData = shoppingCart.GetShoppingCartDataModel();
// Deletes the shopping cart for the current user if needed
await currentShoppingCartService.Delete();
Create orders from shopping carts
Orders are represented as OrderInfo
objects in the API.
Notes
- It is recommended to implement the checkout process as a separate service, such as
OrderService
, which handles the logic of creating orders from shopping carts. This service can be injected into your controllers or other components where you need to create orders. - It is recommended to perform the checkout process in a transaction scope (using the
CMSTransactionScope
class) to ensure that all operations succeed or fail together. This prevents partial updates in case of errors during order creation. - This scenario uses the architecture of the commerce solution in the Dancing Goat project template. You can view a complete example of the order creation process in the Dancing Goat project template’s
OrderService.cs
file.
Create or retrieve a
CustomerInfo
object for the current user.For members, you can use the
CustomerMemberID
property of theCustomerInfo
object to retrieve the respective customer object and ensure that order history is maintained.C#Retrieving customer information for existing members// An instance of IInfoProvider<CustomerInfo> (e.g., obtained via dependency injection) private readonly IInfoProvider<CustomerInfo> customerInfoProvider; // The member ID of the current user int memberId; // Retrieves the customer object for the current member CustomerInfo customer = (await customerInfoProvider .Get() .WhereEquals(nameof(CustomerInfo.CustomerMemberID), memberId) .TopN(1) .GetEnumerableTypedResultAsync(cancellationToken: cancellationToken)) .FirstOrDefault();
For anonymous users, you need to create a new
CustomerInfo
object with each order, as they do not have a persistent account.
Retrieve current shopping cart and data about products contained in the shopping cart.
Calculate the total price and any additional charges (e.g., shipping, taxes).
C#Example - Calculating total price of items in the shopping cart// Shopping cart content retrieved from the current shopping cart ShoppingCartDataModel shoppingCartDataModel; // Calculates the total price of items in the shopping cart decimal itemsPrice = shoppingCartDataModel.Items .Sum(item => CalculateItemPrice( item.Quantity, products.First(product => (product as IContentItemFieldsSource).SystemFields.ContentItemID == item.ContentItemId ).ProductFieldPrice )); // Calculation of shipping and tax prices depends on your business logic decimal shippingPrice = 0; decimal taxPrice = 0; decimal totalPrice = itemsPrice + shippingPrice + taxPrice;
Create
OrderAddressInfo
objects for billing address (and shipping address, if necessary).C#Example - Create an order address from customer data// An instance of IInfoProvider<OrderAddressInfo> (e.g., obtained via dependency injection) private readonly IInfoProvider<OrderAddressInfo> orderAddressInfoProvider; // The customer data from the checkout form CustomerDto customerDto; // Creates a billing address object var billingAddress = new OrderAddressInfo() { OrderAddressFirstName = customerDto.FirstName, OrderAddressLastName = customerDto.LastName, OrderAddressLine1 = customerDto.AddressLine1, // ... AddressType = "Billing" }; // Saves the billing address object await orderAddressInfoProvider.SetAsync(billingAddress);
For members, you can store addresses in the
CustomerAddressInfo
object linked to theCustomerInfo
object. The stored address can be used to pre-fill the checkout form.C#Retrieve customer address// An instance of IInfoProvider<CustomerAddressInfo> (e.g., obtained via dependency injection) private readonly IInfoProvider<CustomerAddressInfo> customerAddressInfoProvider; // The customer ID of the current customer int customerId; // Retrieves the address for the current customer CustomerAddressInfo customerAddress = (await customerAddressInfoProvider .Get() .WhereEquals(nameof(CustomerAddressInfo.CustomerID), customerId) .TopN(1) .GetEnumerableTypedResultAsync(cancellationToken: cancellationToken)) .FirstOrDefault();
For anonymous users, you need to create a new
OrderAddressInfo
object from the checkout form data for each order.
Create the
OrderInfo
object and addOrderItemInfo
entries for each product.C#Create OrderInfo object// Instances of required services (e.g., obtained via dependency injection) private readonly IInfoProvider<OrderInfo> orderInfoProvider; private readonly IInfoProvider<OrderItemInfo> orderItemInfoProvider; private readonly IInfoProvider<OrderStatusInfo> orderStatusInfoProvider; // Retrieves the ID of the initial order status, in this case the first order status in the system var initialOrderStatusId = await orderStatusInfoProvider .Get() .OrderByAscending(nameof(OrderStatusInfo.OrderStatusOrder)) .TopN(1) .Column(nameof(OrderStatusInfo.OrderStatusID)) .GetScalarResultAsync<int>(cancellationToken: cancellationToken); var order = new OrderInfo() { OrderCreatedWhen = DateTime.Now, OrderNumber = orderNumber, // Generated order number OrderCustomerID = customerId, OrderTotalPrice = totalPrice, OrderShippingAddress = shippingAddress, OrderBillingAddress = billingAddress, OrderOrderStatusID = initialOrderStatusId, }; await orderInfoProvider.SetAsync(order); // Retrieves the shopping cart data model var shoppingCartData = shoppingCart.GetShoppingCartDataModel(); // foreach (var item in shoppingCartData.Items) { // Retrieves the product by its content item ID var product = products.First(product => (product as IContentItemFieldsSource).SystemFields.ContentItemID == item.ContentItemId); // Handles product variants if applicable // Handling of product variants depends on your product catalog implementation. This examples uses methods from the Dancing Goat project template. var variantSKUs = product == null ? null : productVariantsExtractor.ExtractVariantsSKUCode(product); var variantSKU = variantSKUs == null || !item.VariantId.HasValue ? null : variantSKUs[item.VariantId.Value]; // Creates the order item object var orderItem = new OrderItemInfo() { // Ensures that the order item is linked to the order OrderItemOrderID = order.OrderID, OrderItemUnitCount = item.Quantity, OrderItemUnitPrice = product.ProductFieldPrice, OrderItemSKU = variantSKU ?? (product as IProductSKU).ProductSKUCode, OrderItemTotalPrice = CalculationService.CalculateItemPrice(item.Quantity, product.ProductFieldPrice), // ... }; await orderItemInfoProvider.SetAsync(orderItem); }
Trigger a notification to the customer and internal users about the order creation. See Send notifications for new orders for more information.
Automatically change status of orders
If you have an external system that performs actions that can affect the status of your orders (e.g., shipping company, payment provider), you can automatically update the order status by changing the OrderOrderStatusID
property of the OrderInfo
object.
The following example showcases a sample code that receives a confirmation of payment from a payment provider and automatically updates the respective order to reflect this change. You would typically call this code in the callback handler of your third-party payment provider integration. When changing order statuses via the API, the notifications are sent as usual.
// Instance of provider services (e.g., obtained via dependency injection)
private readonly IInfoProvider<OrderInfo> orderInfoProvider;
private readonly IInfoProvider<OrderStatusInfo> orderStatusInfoProvider;
// Order ID received from a payment provider
int orderId;
// Retrieves the order data
OrderInfo order = await orderInfoProvider.GetAsync(orderId, cancellationToken);
// Retrieves data of the new order status
OrderStatusInfo newOrderStatus = await orderStatusInfoProvider.GetAsync("PaymentReceived", cancellationToken);
// Sets the new status to the order data
order.OrderOrderStatusID = newOrderStatus.OrderStatusID;
// Saves the changes of the order data
await orderInfoProvider.SetAsync(order, cancellationToken);