allBlogsList

Custom Cart Provider for Insite 4-1 Persist By User

Our client required a cart that could be accessed from any browser and any computer, when the logged in. For example, if a user creates an order from their work computer, they'll be able to complete it from their home computer. Insite offers a few different cart providers, by default - Generic, ByCustomer, and ByShipTo. These out of the box providers are great! They push the state responsibility to the browser, freeing up the web server's CPU. However, they weren't what the client was looking for. So, I got the exciting opportunity to roll my own cart provider.

Insite Cart Providers

Generic

Provides unique carts per login.

ByCustomer

Provides unique carts for per user/customer.

ByShipTo

Provides unique carts for per user/shipment destination.

The implementation to retrieve the customer order for the current user is fairly straight forward. In process orders have the "Cart" status. We're leveraging the CreatedBy field in the CustomerOrder to be able to filter for the current user, since our cart provider ensures that only the user that created the cart will ever modify it.

order = UnitOfWork.GetRepository<CustomerOrder>()
		.GetTable()
		.FirstOrDefault(
			co => co.Status == CartStatusCart 
			&& co.Customer.Id == customer.Id 
			&& (co.CreatedBy == userName || co.ModifiedBy == userName));

However, there are a few catches to implementing a custom cart provider:

  • Need for caching: The cart provider can be called many times during a single request. IContextProvider.CurrentCustomerOrder uses this provider to get the current order - any time your code consumes this property, this provider is executed.
  • New carts' invalid Id: New carts will be initialized with an empty Guid ({00000000-0000-0000-0000-000000000000}) if they haven't been inserted into the database. The cart won't be in the database with that invalid id.
  • Need access to private method in base class: In the CartOrderProvider_Generic class, the ReplaceWithApprovalOrderIfNeeded() method needs to preempt our custom functionality, to properly handle order approval and the such. I had to copy the code from the de-compiled version of CartOrderProvider_Generic. Great news! Insite remedied this oversight in 4.2 - the method is now protected and virtual!
  • Moving magic strings to constants: The decompiled code contained magic strings. I decided to move these to constants in the class which could be easily modified to retrieve configuration values, if they need to change.
using System;
using System.Linq;
using InSite.Model;
using InSite.Model.Attributes.Dependency;
using InSite.Model.Interfaces;
using InSite.Model.Managers;

namespace InSiteCommerce.Web.Model
{
	/// <summary>
	/// InSite's "ByCustomer" cart order provider (<see cref="CartOrderProviderByCustomer"/>) does not retrieve the old
	/// customer order, if the user signs out and back in. This cart provider ensures that the current order will be
	/// retrieved regardless of the browser, or computer the user logs in from.
	/// 
	/// Requirements:
	/// <list type="number">
	///		<item><description>The same order is available to a user regardless of the computer used.</description></item>
	/// 	<item><description>The same order is available to a user regardless of the browser used</description></item>
	/// 	<item><description>The user can log out and log back in and still see their same order</description></item>
	/// 	<item><description>The user can be logged in on different browsers and/or computers at the same time and see the same cart</description></item>
	/// </list>
	/// </summary>
	[DependencyName("PersistByUserCustomer")]
	public class CartOrderProviderPersistByUserCustomer : CartOrderProviderByCustomer
	{
		#region Constants
		/// <summary>
		/// The <see cref="CustomerOrder.Status"/> if the order is awaiting approval.
		/// </summary>
		public const string AwaitingApproval = "AwaitingApproval";

		/// <summary>
		/// The format of the cookie key for customer orders for approval.
		/// </summary>
		public const string ApprovalOrderCookieNameFormat = "{0}_CustomerOrderForApproval";

		/// <summary>
		/// The <see cref="PerRequestCacheManager"/>'s cache key for the current order.
		/// </summary>
		public const string CurrentOrderPerRequestCacheKey = "ISC.DataStore.CurrentCustomerOrder";

		/// <summary>
		/// The name of the role for an Administrator.
		/// </summary>
		public const string AdministratorRole = "Administrator";
		#endregion Constants

		/// <summary>
		/// Constructor.
		/// </summary>
		/// <param name="cookieManager"></param>
		/// <param name="perRequestCacheManager"></param>
		/// <param name="unitOfWorkFactory"></param>
		public CartOrderProviderPersistByUserCustomer(ICookieManager cookieManager, IPerRequestCacheManager perRequestCacheManager, IUnitOfWorkFactory unitOfWorkFactory)
			:base(cookieManager, perRequestCacheManager, unitOfWorkFactory)
		{
			// no op
		}

		/// <summary>
		/// Finds the current customer order for the context.
		/// </summary>
		/// <returns>The current customer order.</returns>
		protected override CustomerOrder LookupCustomerOrder()
		{
			var order = ReplaceWithApprovalOrderIfNeeded(PerRequestCacheManager.Get<CustomerOrder>(CurrentOrderPerRequestCacheKey));
			if (order != null)
			{
				return base.LookupCustomerOrder();
			}

			var idText = CookieManager.Get(CartCookieName);
			var userProfile = ContextProvider.CurrentUserProfile;
			var customer = ContextProvider.CurrentCustomer;
			if (customer == null) return null;
			var id = Guid.Empty;

			if ((idText == null || (Guid.TryParse(idText, out id) && id == Guid.Empty)) && userProfile != null)
			{
				var userName = userProfile.UserName;
				order = UnitOfWork.GetRepository<CustomerOrder>()
					.GetTable()
					.FirstOrDefault(co => co.Status == Orders.Constants.Statuses.CartStatusCart && co.Customer.Id == customer.Id && (co.CreatedBy == userName || co.ModifiedBy == userName));

				if (order != null)
				{
					PerRequestCacheManager.Add(CurrentOrderPerRequestCacheKey, order);
					return order;
				}
			}

			var returnValue = base.LookupCustomerOrder();
			// If we couldn't find a non-empty Id, then we need to clear out the previous cookie and per-request cache.
			if (id != Guid.Empty && returnValue == null)
			{
				SetCartOrder(null);
			}
			return returnValue;
		}

		/// <summary>
		/// Copied from CartOrderProvider_Generic in InSite Commerce v.4.1.0.19998. 
		/// It was added because <see cref="CartOrderProvider_Generic"/> class has this method set as private.
		/// </summary>
		/// <param name="customerOrder"></param>
		/// <returns></returns>
		protected virtual CustomerOrder ReplaceWithApprovalOrderIfNeeded(CustomerOrder customerOrder)
		{
			var name = string.Format(ApprovalOrderCookieNameFormat, ContextProvider.CurrentWebSite.Id);
			var id = CookieManager.Get(name);
			
			if (id == null || id.IsBlank())
				return customerOrder;

			customerOrder = UnitOfWork.GetRepository<CustomerOrder>().Get(id);
			var currentUserProfile = ContextProvider.CurrentUserProfile;
			if (customerOrder.Status != AwaitingApproval || currentUserProfile == null)
			{
				CookieManager.Remove(name);
				return null;
			}
			if (customerOrder.Approver.Id == currentUserProfile.Id)
				return customerOrder;
			
			if (!currentUserProfile.HasRole(AdministratorRole))
				return null;
			
			if (!customerOrder.Customer.UserProfiles.Contains(currentUserProfile))
				return null;
			
			if (customerOrder.Customer.Id.Equals(customerOrder.ShipTo.Id) || DefaultShipToProvider.GetAssignedShipToCount(ContextProvider.CurrentWebSite, currentUserProfile, customerOrder.Customer) == 0)
				return customerOrder;

			if (DefaultShipToProvider.GetAssignedShipTos(ContextProvider.CurrentWebSite, currentUserProfile, customerOrder.Customer).Any(s => s.Id == customerOrder.ShipTo.Id))
				return customerOrder;
			return null;
		}
	}
}

Once you have added the above class to your InSite 4.1 solution, rebuild the solution. Then, you can update the website settings to use the new custom CartOrderProvider, as shown below. If, you're using this post as a guide to create your own functionality, the name is determined by the DependencyNameAttribute at the top of the class.

Image of InSite console, showing that the CartOrderProvider setting should be updated to 'PersistByUser' to use the new custom cart provider.