Gathering customer details during checkout

This page is part two of the Implementing a checkout process series.

Building upon the basic shopping cart interactions created in the previous part of this series, this page covers the implementation of the next step in the typical checkout process. This step prompts customers to enter details essential for a successful completion of their order. The required details typically include at least the customer’s personal information, shipping address, and their preferred payment method.

In this part of the series, we extend the basic shopping cart functionality with new actions, models, and views enabling customers to:

  • Enter their personal and business information.
  • Enter their billing address.
  • Select a shipping option for their order.

Tip: To view the full code of a functional example, you can inspect and download the LearningKit project on GitHub. You can also run the LearningKit website by connecting the project to a Kentico database.

To extend the checkout process to gather customer details:

  1. Open the CheckoutController class used to manage the checkout process in your MVC application.
  2. Add new view models for:
  • Customer details. This model will store select properties of the CustomerInfo type (available in the CMS.Ecommerce namespace).

    
    
    
          public class CustomerViewModel
          {
              [Required]
              [DisplayName("First Name")]
              [MaxLength(100, ErrorMessage = "The maximum length allowed for the field has been exceeded.")]
              public string FirstName { get; set; }
    
              [Required]
              [DisplayName("Last Name")]
              [MaxLength(100, ErrorMessage = "The maximum length allowed for the field has been exceeded.")]
              public string LastName { get; set; }
    
              [DataType(DataType.EmailAddress, ErrorMessage = "Please enter a valid email address.")]
              public string Email { get; set; }
    
              [DisplayName("Phone Number")]
              [MaxLength(20, ErrorMessage = "The maximum length allowed for the field has been exceeded.")]
              [DataType(DataType.PhoneNumber, ErrorMessage = "Please enter a valid phone number.")]
              public string PhoneNumber { get; set; }
    
              public string Company { get; set; }
    
              public string OrganizationID { get; set; }
    
              public string TaxRegistrationID { get; set; }
    
              [DisplayName("Are you ordering from a business account?")]
              public bool IsCompanyAccount { get; set; }
    
              /// <summary>
              /// Creates a customer model.
              /// </summary>
              /// <param name="customer">Customer details.</param>
              public CustomerViewModel(CustomerInfo customer)
              {
                  if (customer == null)
                  {
                      return;
                  }
    
                  FirstName = customer.CustomerFirstName;
                  LastName = customer.CustomerLastName;
                  Email = customer.CustomerEmail;
                  PhoneNumber = customer.CustomerPhone;
                  Company = customer.CustomerCompany;
                  OrganizationID = customer.CustomerOrganizationID;
                  TaxRegistrationID = customer.CustomerTaxRegistrationID;
                  IsCompanyAccount = customer.CustomerHasCompanyInfo;
              }
    
              /// <summary>
              /// Creates an empty customer model.
              /// Required by the MVC framework for model binding during form submission.
              /// </summary>
              public CustomerViewModel()
              {
              }
    
              /// <summary>
              /// Applies the model to a customer object.
              /// </summary>
              /// <param name="customer">Customer details to which the model is applied.</param>
              public void ApplyToCustomer(CustomerInfo customer)
              {
                  customer.CustomerFirstName = FirstName;
                  customer.CustomerLastName = LastName;
                  customer.CustomerEmail = Email;
                  customer.CustomerPhone = PhoneNumber;
                  customer.CustomerCompany = Company;
                  customer.CustomerOrganizationID = OrganizationID;
                  customer.CustomerTaxRegistrationID = TaxRegistrationID;
              }
          }
    
    
    
      
  • Billing addresses. The model will store select properties of the AddressInfo type (available in the CMS.Ecommerce namespace).

    
    
    
          public class BillingAddressViewModel
          {
              [Required]
              [DisplayName("Address line 1")]
              [MaxLength(100, ErrorMessage = "The maximum length allowed for the field has been exceeded.")]
              public string Line1 { get; set; }
    
              [DisplayName("Address line 2")]
              [MaxLength(100, ErrorMessage = "The maximum length allowed for the field has been exceeded.")]
              public string Line2 { get; set; }
    
              [Required]
              [MaxLength(100, ErrorMessage = "The maximum length allowed for the field has been exceeded.")]
              public string City { get; set; }
    
              [Required]
              [DisplayName("Postal code")]
              [MaxLength(20, ErrorMessage = "The maximum length allowed for the field has been exceeded.")]
              [DataType(DataType.PostalCode, ErrorMessage = "Please enter a valid postal code.")]
              public string PostalCode { get; set; }
    
              [DisplayName("Country")]
              public int CountryID { get; set; }
    
              [DisplayName("State")]
              public int StateID { get; set; }
    
              public int AddressID { get; set; }
    
              public SelectList Countries { get; set; }
    
              public SelectList Addresses { get; set; }
    
              /// <summary>
              /// Creates a billing address model.
              /// </summary>
              /// <param name="address">Billing address.</param>
              /// <param name="countryList">List of countries.</param>
              public BillingAddressViewModel(AddressInfo address, SelectList countries, SelectList addresses)
              {
                  if (address != null)
                  {
                      Line1 = address.AddressLine1;
                      Line2 = address.AddressLine2;
                      City = address.AddressCity;
                      PostalCode = address.AddressZip;
                      CountryID = address.AddressCountryID;
                      StateID = address.AddressStateID;
                      AddressID = address.AddressID;
                  }
    
                  Countries = countries;
                  Addresses = addresses;
              }
    
              /// <summary>
              /// Creates an empty BillingAddressModel object. 
              /// Required by the MVC framework for model binding during form submission.
              /// </summary>
              public BillingAddressViewModel()
              {
    
              }
    
              /// <summary>
              /// Applies the model to an AddressInfo object.
              /// </summary>
              /// <param name="address">Billing address to which the model is applied.</param>
              public void ApplyTo(AddressInfo address)
              {
                  address.AddressLine1 = Line1;
                  address.AddressLine2 = Line2;
                  address.AddressCity = City;
                  address.AddressZip = PostalCode;
                  address.AddressCountryID = CountryID;
                  address.AddressStateID = StateID;
              }
          }
    
    
    
      
  • Shipping options. The model will store select properties of the ShippingOptionInfo type (available in the CMS.Ecommerce namespace).

    
    
    
          public class ShippingOptionViewModel
          {
              public string ShippingOptionDisplayName { get; set; }
    
              [DisplayName("Shipping option")]
              public int ShippingOptionID { get; set; }
    
              public SelectList ShippingOptions { get; set; }
    
              /// <summary>
              /// Creates a shipping option model.
              /// </summary>
              /// <param name="shippingOption">Shipping option.</param>
              /// <param name="shippingOptions">List of shipping options.</param>
              public ShippingOptionViewModel(ShippingOptionInfo shippingOption, SelectList shippingOptions)
              {
                  ShippingOptions = shippingOptions;
    
                  if (shippingOption != null)
                  {
                      ShippingOptionID = shippingOption.ShippingOptionID;
                      ShippingOptionDisplayName = shippingOption.ShippingOptionDisplayName;
                  }
              }
    
              /// <summary>
              /// Creates an empty shipping option model.
              /// </summary>
              public ShippingOptionViewModel()
              {
              }
          }
    
    
    
      
  1. Add a view model (named DeliveryDetailsViewModel for the purpose of this series) aggregating all of the models created above.

    
    
    
         public class DeliveryDetailsViewModel
         {
    
             public CustomerViewModel Customer { get; set; }
    
             public BillingAddressViewModel BillingAddress { get; set; }
    
             public ShippingOptionViewModel ShippingOption { get; set; }
         }
    
    
    
     
  2. Implement new controller actions handling retrieval and display of the collected customer data.

    • To display the customer details step, implement a basic action that renders a form prompting users to enter the required information. The action loads available shipping options from the current site and calculates their price based on the contents of the current shopping cart and all currently active store promotions, for example, free shipping offers. The retrieved and calculated data is then used to fill the view model for display in a related view.

      
      
      
                /// <summary>
                /// Displays the customer details checkout process step.
                /// </summary>
                public ActionResult DeliveryDetails()
                {
                    // Gets the current user's shopping cart
                    ShoppingCartInfo cart = shoppingService.GetCurrentShoppingCart();
      
                    // If the shopping cart is empty, displays the shopping cart
                    if (cart.IsEmpty)
                    {
                        return RedirectToAction(nameof(CheckoutController.ShoppingCart));
                    }
      
                    // Gets all countries for the country selector
                    SelectList countries = new SelectList(CountryInfoProvider.GetCountries(), "CountryID", "CountryDisplayName");
      
                    // Creates a collection of shipping options enabled for the current site
                    SelectList shippingOptions = CreateShippingOptionList(cart);
      
                    // Loads the customer details
                    DeliveryDetailsViewModel model = new DeliveryDetailsViewModel
                    {
                        Customer = new CustomerViewModel(shoppingService.GetCurrentCustomer()),
                        BillingAddress = new BillingAddressViewModel(shoppingService.GetBillingAddress(), countries, null),
                        ShippingOption = new ShippingOptionViewModel(ShippingOptionInfoProvider.GetShippingOptionInfo(shoppingService.GetShippingOption()), shippingOptions)
                    };
      
                    // Displays the customer details step
                    return View(model);
                }
      
                // Prepares a shipping option select list together with calculated shipping prices
                private SelectList CreateShippingOptionList(ShoppingCartInfo cart)
                {
                    // Gets the shipping options configured and enabled for the current site
                    IEnumerable<ShippingOptionInfo> shippingOptions = ShippingOptionInfoProvider.GetShippingOptions(SiteContext.CurrentSiteID, true);
      
                    // Creates a collection of SelectListItems
                    IEnumerable<SelectListItem> selectList = shippingOptions.Select(shippingOption =>
                    {
                        // Calculates the shipping price for a given shipping option based on the contents of the current
                        // shopping cart and currently running store promotions (e.g., free shipping offers)
                        decimal shippingPrice = shoppingService.CalculateShippingOptionPrice(shippingOption);
      
                        // Gets the currency information from the shopping cart
                        CurrencyInfo currency = cart.Currency;
      
                        // Creates a select list item with shipping option name and a calculate shipping price
                        return new SelectListItem
                        {
                            Value = shippingOption.ShippingOptionID.ToString(),
                            Text = $"{shippingOption.ShippingOptionDisplayName} ({String.Format(currency.CurrencyFormatString, shippingPrice)})"
                        };
                    });
      
                    // Returns a new SelectList instance
                    return new SelectList(selectList, "Value", "Text");
                }
      
      
      
        
    • Add an HttpPost action processing the entered information. The action handles creation of new customer  and address objects (if none exist for the current user) and binds the acquired information to the customer’s shopping cart. Finally, it redirects the customer to the last step of the checkout process.

      
      
      
                /// <summary>
                /// Validates the entered customer details and proceeds to the order review step.
                /// </summary>
                /// <param name="model">View model with the customer details.</param>
                [HttpPost]
                public ActionResult DeliveryDetails(DeliveryDetailsViewModel model)
                {
                    // Gets the user's current shopping cart
                    ShoppingCartInfo cart = shoppingService.GetCurrentShoppingCart();
      
                    // Gets all enabled shipping options for the shipping option selector
                    SelectList shippingOptions = new SelectList(ShippingOptionInfoProvider.GetShippingOptions(SiteContext.CurrentSiteID, true).ToList(),
                                                                                                                                      "ShippingOptionID",
                                                                                                                                      "ShippingOptionDisplayName");
      
                    // If the ModelState is not valid, assembles the country list and the shipping option list and displays the step again
                    if (!ModelState.IsValid)
                    {
                        model.BillingAddress.Countries = new SelectList(CountryInfoProvider.GetCountries(), "CountryID", "CountryDisplayName");
                        model.ShippingOption.ShippingOptions = new ShippingOptionViewModel(ShippingOptionInfoProvider.GetShippingOptionInfo(shoppingService.GetShippingOption()), shippingOptions).ShippingOptions;
                        return View(model);
                    }
      
                    // Gets the shopping cart's customer and applies the customer details from the checkout process step
                    var customer = shoppingService.GetCurrentCustomer();
      
                    if (customer == null)
                    {
                        UserInfo userInfo = cart.User;
                        if (userInfo != null)
                        {
                            customer = CustomerHelper.MapToCustomer(cart.User);
                        }
                        else
                        {
                            customer = new CustomerInfo();
                        }
                    }
                    model.Customer.ApplyToCustomer(customer);
      
                    // Sets the updated customer object to the current shopping cart
                    shoppingService.SetCustomer(customer);
      
                    // Gets the shopping cart's billing address and applies the billing address from the checkout process step
                    var address = AddressInfoProvider.GetAddressInfo(model.BillingAddress.AddressID) ?? new AddressInfo();
                    model.BillingAddress.ApplyTo(address);
      
                    // Sets the address personal name
                    address.AddressPersonalName = $"{customer.CustomerFirstName} {customer.CustomerLastName}";
      
                    // Saves the billing address
                    shoppingService.SetBillingAddress(address);
      
                    // Sets the selected shipping option and evaluates the cart
                    shoppingService.SetShippingOption(model.ShippingOption.ShippingOptionID);
      
                    // Redirects to the next step of the checkout process
                    return RedirectToAction("PreviewAndPay");
                }
      
      
      
        
    • To handle the dynamic loading of states, add an HttpPost action to the controller. The action returns a serialized list of states in JSON format. This list is then processed by a JavaScript function that dynamically fills and shows a ‘States’ drop-down selector as needed. The action is called from a related view whenever a customer selects a different country from the ‘Country’ drop-down selector.

      
      
      
                /// <summary>
                /// Loads states of the specified country.
                /// </summary>
                /// <param name="countryId">ID of the selected country.</param>
                /// <returns>Serialized display names of the loaded states.</returns>
                [HttpPost]
                public JsonResult CountryStates(int countryId)
                {
                    // Gets the display names of the country's states
                    var responseModel = StateInfoProvider.GetStates().WhereEquals("CountryID", countryId)
                        .Select(s => new
                        {
                            id = s.StateID,
                            name = HTMLHelper.HTMLEncode(s.StateDisplayName)
                        });
      
                    // Returns serialized display names of the states
                    return Json(responseModel);
                }
      
      
      
        
    • Create a JavaScript file handling state selection for the selected country. More specifically, it fills and displays the state selector if the selected country contains states; otherwise, it leaves the selector hidden.

      
      
      
        (function () {
            'use strict';
      
            // Executes whenever a country is selected
            $('.js-country-selector').change(function () {
                var $countrySelector = $(this),
                    $countryStateSelector = $countrySelector.parent('.js-country-state-selector'),
                    $stateSelector = $countryStateSelector.find('.js-state-selector'),
                    $stateSelectorContainer = $countryStateSelector.find('.js-state-selector-container'),
                    selectedStateId = $countryStateSelector.data('stateselectedid'),
                    url = $countryStateSelector.data('statelistaction'),
                    postData = {
                        countryId: $countrySelector.val()
                    };
      
                $stateSelectorContainer.hide();
      
                if (!postData.countryId) {
                    return;
                }
      
                // Sends a POST request to the 'CountryStates' endpoint of the 'CheckoutController'
                $.post(url, postData, function (data) {
                    $countryStateSelector.data('stateselectedid', 0);
                    $stateSelector.val(null);
      
                    if (!data.length) {
                        return;
                    }
      
                    // Fills and shows the state selector element
                    fillStateSelector($stateSelector, data);
                    $stateSelectorContainer.show();
      
                    if (selectedStateId > 0) {
                        $stateSelector.val(selectedStateId);
                    }
                });
            });
      
            // Sets the default option for the state selector
            $('.js-country-state-selector').each(function () {
                var $selector = $(this),
                    $countrySelector = $selector.find('.js-country-selector'),
                    countryId = $selector.data('countryselectedid');
      
                if (countryId > 0) {
                    $countrySelector.val(countryId);
                }
      
                $countrySelector.change();
                $selector.data('countryselectedid', 0);
            });
      
            // Fills the state selector with retrieved states
            function fillStateSelector($stateSelector, data) {
                var items = '';
      
                $.each(data, function (i, state) {
                    items += '<option value="' + state.id + '">' + state.name + '</option>';
                });
      
                $stateSelector.html(items);
            }
        }());
      
      
        

      When working with JavaScript files in your project, we recommend installing the Microsoft.AspNet.Web.Optimization package. The package is used to improve web application performance by reducing the number of requests to the server when requesting assets and reducing the size of the requested assets (such as CSS and JavaScript files).

  3. Create a view for the customer details step. For example:

    
    
    
     <h2>Customer details step</h2>
     @using (Html.BeginForm(FormMethod.Post))
     {
         <div id="customerDetails">
             <h3>Customer details</h3>
             @Html.EditorFor(m => m.Customer)
         </div>
         <div id="billingAddress">
             <h3>Billing address</h3>
             <div>
                 @Html.LabelFor(m => m.BillingAddress.Line1)
                 @Html.EditorFor(m => m.BillingAddress.Line1)
                 @Html.ValidationMessageFor(m => m.BillingAddress.Line1)
             </div>
             <div>
                 @Html.LabelFor(m => m.BillingAddress.Line2)
                 @Html.EditorFor(m => m.BillingAddress.Line2)
                 @Html.ValidationMessageFor(m => m.BillingAddress.Line2)
             </div>
             <div>
                 @Html.LabelFor(m => m.BillingAddress.City)
                 @Html.EditorFor(m => m.BillingAddress.City)
                 @Html.ValidationMessageFor(m => m.BillingAddress.City)
             </div>
             <div>
                 @Html.LabelFor(m => m.BillingAddress.PostalCode)
                 @Html.EditorFor(m => m.BillingAddress.PostalCode)
                 @Html.ValidationMessageFor(m => m.BillingAddress.PostalCode)
             </div>
             <div class="js-country-state-selector" data-statelistaction='@Url.Action("CountryStates", "Checkout")' data-countryselectedid='@Model.BillingAddress.CountryID' data-stateselectedid='@Model.BillingAddress.StateID' data-countryfield='CountryID' data-statefield='StateID'>
                 @Html.LabelFor(m => m.BillingAddress.CountryID)
                 @Html.DropDownListFor(m => m.BillingAddress.CountryID, Model.BillingAddress.Countries, new { @class = "js-country-selector" })
                 <div class="js-state-selector-container">
                     @Html.LabelFor(m => m.BillingAddress.StateID)
                     @Html.DropDownListFor(m => m.BillingAddress.StateID, Enumerable.Empty<SelectListItem>(), new { @class = "js-state-selector" })
                 </div>
             </div>
         </div>
    
         <div id="shippingOption">
             <h3>Shipping option</h3>
             @Html.LabelFor(m => m.ShippingOption.ShippingOptionID)
             @Html.DropDownListFor(m => m.ShippingOption.ShippingOptionID, Model.ShippingOption.ShippingOptions)
         </div>
    
         <input type="submit" value="Continue" />
     }
     @Scripts.Render("~/Scripts/jquery-2.1.4.min.js")
     @Scripts.Render("~/Scripts/countryStateSelector.js")
    
    
    
     

    Optionally, you can offer your registered users the ability to auto-fill previously entered personal details (such as billing and shipping addresses). See Auto-filling addresses of existing customers for more information.

    The checkout process now allows your customers to provide information required for a successful checkout. In the last part of this series, we will continue with implementing the order review step.