Implement the checkout process

Advanced license required

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:

  1. Store products in the shopping cart
  2. Retrieve the current shopping cart
  3. Create a new order from the shopping cart

Commerce object types

Object types used by the commerce feature (Shopping cart, Order, Order address, Order item, Customer, Customer address …):

  • are considered live-site data and are not supported by the CI/CD feature
  • can be extended by adding custom fields

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:

C#
Example - ShoppingCartDataItem.cs

// 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; }
}
C#
Example - ShoppingCartDataModel.cs

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

C#
Example - ShoppingCartDataModelExtensions.cs

// 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:

C#
Example - DancingGoatShoppingCartController.cs (excerpt)

// 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 the Get method to retrieve the ShoppingCartInfo object for the current user. Returns null if no cart exists.
  • ICurrentShoppingCartCreator – use the Create method to create a new ShoppingCartInfo object and store its identifier (in a cookie for anonymous users or linked to the member for signed-in users).
  • ICurrentShoppingCartDiscardHandler – use the Discard method to delete the current user’s shopping cart from the database and remove the identifier.
  • ICurrentShoppingCartMemberSignInHandler – use the MemberSignIn 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.

C#
Managing the current shopping cart

// 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.
  1. Create or retrieve a CustomerInfo object for the current user.

    • For members, you can use the CustomerMemberID property of the CustomerInfo 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.

  2. Retrieve current shopping cart and data about products contained in the shopping cart.

  3. 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;
     
  4. 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 the CustomerInfo 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.

  5. Create the OrderInfo object and add OrderItemInfo 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);
     }
     
  6. 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.

C#
Change status of an order

// 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);