Sitecore Blog: Getting to Know Sitecore

Extending Sitecore E-Commerce - Pricing

By Adam Conn, January 11, 2011 | Rating:  | Comments (5)

Sitecore E-Commerce

Recently Sitecore released Sitecore E-Commerce, which is exciting because the benefits that Sitecore provides to websites as a WCM - such as providing personalized, relevant experiences for website visitors - are now available to web shops and online stores. 

Just like with WCM, as impressive as an e-commerce product's out of the box functionality may be, it will not solve every business requirement, so products that provide a flexible and extensible design have a great avantage over their less adaptable competition.  And just like with WCM, Sitecore's e-commerce product allows virtually every aspect of the e-commerce engine to be extended, customized, and replaced.

In this post I will demonstrate how to extend the pricing model used by Sitecore E-Commerce Fundamental Edition (SEFE).

If you are following along on your own system, make sure that you have Sitecore CMS, using SEFE 1.0.2 and the SEFE 1.0.2 Example Pages installed.  These are available in the SEFE section of SDN.

Step 1: Understand the requirements

The first step is to understand what I am trying to accomplish with this customization.  SEFE comes with a fairly rudimentary pricing system.  The following list describes how I will extend the pricing system:

  • A sale consists of a discount rate, start date, end date and an indication on whether the sale applies to all users or only members.
  • Multiple sales can be assigned to a single product.
  • If multiple sales are active for a single product, the largest discount rate should be used.
  • It should be possible to disable a sale.
  • If a sale price is available, the original price should also appear in the web shop.
  • A sale price may be calculated off the normal price or off the member price.  If the sale price is calculated off the normal price, which ever price is lower (sale price or the member price) is the price that should be used.

Step 2: Model the pricing rules

In order to satisfy the requirements, you basically need to define rules that are used to determine a product's price.  Data templates can be used to model these rules, and items can be used to configure these rules.

Create the following data template to store sale information:
* name: Product Sale
* location: /sitecore/templates/User Defined/Ecommerce/Extensions

Add the following fields to the data template:

* section: Product Sale
* name: Percent
* type: Integer
* shared: checked
* description: The amount by which the product price should be reduced.  For example, the value 50 would be used to indicate a discount of 50% should be applied.

* section: Product Sale
* name: Members Only
* type: Checkbox
* shared: checked
* description: A flag that indicates if the sale is only available to members.  A member is a user who is logged into the site.

* section: Product Sale
* name: Off Base Price Only
* type: Checkbox
* shared: checked
* description: A flag that idicates if the discount should be calculated from the base price.  If this value is selected, the non-member price is always used to calculate the discount.

* section: Product Sale
* name: Start Date
* type: Datetime
* shared: checked
* description: The date when the discount first takes effect.  This value can be left empty, meaning that the discount has always been available.

* section: Product Sale
* name: End Date
* type: Datetime
* shared: checked
* description: The date when the discount is not longer available.  This value can be left empty, meaning that the discount is available indefinitely.

* section: Product Sale
* name: Disabled
* type: Checkbox
* shared: checked
* description: A flag that indicates if the discount is disabled.  This allows a discount to be discontinued without being deleted entirely.

Step 3: Define the pricing rules

  1. Create the following folder.  It will store the specific pricing rules that are available on the site:
    /sitecore/system/Modules/Ecommerce/Extensions/Product Sales
  2. Set the following insert options on the folder:
    /sitecore/templates/Common/Folder
    * /sitecore/templates/User Defined/Ecommerce/Extensions/Product Sale
  3. A product needs to be able to have sales assigned to it, so the data template that represents a product's price must be extended.  Add the following field:
    * data template: /sitecore/templates/Ecommerce/Product/Product Base Price
    * field name:  Product Sales
    * type: Multilist
    * source: /sitecore/system/Modules/Ecommerce/Extensions/Product Sales
    * shared: checked

Step 4: Understand how SEFE determines the product price

In SEFE, product pricing information is stored on the product.  But each product does not have a single price.  Out-of-the-box, SEFE stores two prices for each product: normal price (the default price) and memberprice (the price offered to users who are logged in).  At runtime, SEFE determines which price applies to a visitor. 

A component called a "price manager" handles this logic.  A price manager is a .NET class.  The specific price manager that is used is defined in the Unity Application Block configuration file.  Unity is a vast subject that deserves its own post, so in this post I'm going to cover just enough to get you started. 

This is a gross over-simplication, but in this particular case, you can think of Unity as being a sort of dictionary of key-value pairs.  To use a specific example, any time SEFE needs to determine the price of a product, SEFE asks Unity for an object named "IProductPriceManager".  A configuration file specifies what Unity should return to SEFE.

The location of the Unity configuration file is specified in the Sitecore.Ecommerce.config file.  The following processor from the initialize pipeline sets the location:

<processor type="Sitecore.Ecommerce.Pipelines.Loader.ConfigureEntities, Sitecore.Ecommerce.Kernel" patch:after="processor[@type=&apos;Sitecore.Pipelines.Loader.EnsureAnonymousUsers, Sitecore.Kernel&apos;]">
  <UnityConfigSource>/App_Config/Unity.config</UnityConfigSource>
</processor>

In the Unity configuration file, the following tag is the starting point.  This tag tells Unity that, when an application asks for an object named "IProductPriceManager", Unity should expect to return an object of the type Sitecore.Ecommerce.DomainModel.Prices.IProductPriceManager, which is a part of the assembly Sitecore.Ecommerce.DomainModel:

<alias alias="IProductPriceManager" type="Sitecore.Ecommerce.DomainModel.Prices.IProductPriceManager, Sitecore.Ecommerce.DomainModel"/>

When Unity needs to retrieve an object named "IProductPriceManager", the following line tells Unity how Unity should handle a request for an object named "IProductPriceManager".  In this example, the specific type definition is configured under the name "ProductPriceManager".

<register type="IProductPriceManager" mapTo="ProductPriceManager">
  <lifetime type="perthread" />
</register>

Where is "ProductPriceManager" defined?  The following tag tells Unity that, when an application asks for an object named "ProductPriceManager", Unity should use the type Sitecore.Ecommerce.Prices.ProductPriceManager, which is a part of the assembly Sitecore.Ecommerce.Kernel:

<alias alias="ProductPriceManager" type="Sitecore.Ecommerce.Prices.ProductPriceManager, Sitecore.Ecommerce.Kernel"/>

So, after a little bit of back and forth, you can see exactly what SEFE uses to determine pricing.  It is in this file that you can change the price manager that SEFE uses.  But before you can do that, you need to create a custom price manager.

Step 5: Understand how prices are described by the SEFE API

A price manager is responsible for making pricing information available within SEFE.  In addition to the price, SEFE includes tax and discount information.  The Sitecore.Ecommerce.DomainModel.Prices.Totals class describes the pricing information.  The following is a description of the properties defined on this class.  Some of these properties may not make sense until you actually use them (which will happen in the next step!):

  • PriceExVat - the pre-tax product price
  • PriceIncVat - the post-tax product price
  • MemberPrice - price charged to members (by default, authenticated users are considered members)
  • TotalPriceExVat - PriceExVat multipled by quantity
  • TotalPriceIncVat - PriceIncVat multipled by quantity
  • DiscountExVat - total value of the discount, excluding tax
  • DiscountIncVat - total value of the discount, including tax
  • TotalVat - the total amount of tax
  • VAT - the tax rate that applies to the product

Step 6: Implement a custom price manager

Now that you've determined that the default price manager is Sitecore.Ecommerce.Prices.ProductPriceManager  and how prices are described using the SEFE API, the next step is to extend this class so it incorporates the pricing rules.

Create a new Class Library project in Visual Studio named "Sitecore.Marketing.SEFE.Extensions".

Add references to the following assemblies:
Microsoft.Practices.Unity.dll
* Sitecore.Ecommerce.DomainModel.dll
* Sitecore.Ecommerce.Kernel.dll
* Sitecore.Kernel.dll

Create the class ProductSaleItem using the following code.  This class will make it easier to work with the product sale items from Sitecore:

using System;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;

namespace Sitecore.Marketing.SEFE.Extensions
{
    public class ProductSaleItem:CustomItem
    {
        public ProductSaleItem(Item innerItem) : base(innerItem) { }

        public static implicit operator ProductSaleItem(Item innerItem)
        {
            return new ProductSaleItem(innerItem);
        }
        private DateTime GetDateTime(string fieldName, DateTime defaultValue)
        {
            var dateField = InnerItem.Fields[fieldName];
            return dateField == null ? defaultValue : ((DateField)dateField).DateTime;
        }
        public DateTime StartDate
        {
            get
            {
                return GetDateTime("Start Date", System.DateTime.MinValue);
            }
        }
        public DateTime EndDate
        {
            get
            {
                return GetDateTime("End Date", System.DateTime.MaxValue);
            }
        }
        public bool MemberOnly
        {
            get
            {
                var field = (CheckboxField)InnerItem.Fields["Members Only"];
                return field != null && field.Checked;
            }
        }
        public bool OffBasePriceOnly
        {
            get
            {
                var field = (CheckboxField)InnerItem.Fields["Off Base Price Only"];
                return field != null && field.Checked;
            }
        }
        public int Percent
        {
            get
            {
                var value = 0;
                int.TryParse(InnerItem["Percent"], out value);
                return value;
            }
        }
        public bool Disabled
        {
            get
            {
                var field = (CheckboxField)InnerItem.Fields["Disabled"];
                return field != null && field.Checked;
            }
        }
    }
}

Create the class ProductPriceManager using the following code:  This class is responsible for applying the pricing rules defined in Sitecore at runtime:

using System.Collections.Generic;
using System.Linq;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Ecommerce.DomainModel.Products;
using Sitecore.Ecommerce.Prices;
using Sitecore.Ecommerce.Utils;

namespace Sitecore.Marketing.SEFE.Extensions
{
    public class SalePriceManager:ProductPriceManager
    {
        /// <summary>
        /// Gets a collection of the active product sale items that apply to the specified product item.
        /// </summary>
        protected virtual List<ProductSaleItem> GetSaleItems(Item productItem)
        {
            var items = new List<ProductSaleItem>();
            var salesField = productItem.Fields["Product Sales"];
            if (salesField == null || string.IsNullOrEmpty(salesField.Value))
            {
                return items;
            }
            foreach (var id in salesField.Value.Split(&apos;|&apos;))
            {
                var item = (ProductSaleItem)productItem.Database.GetItem(new ID(id));
                if (item == null)
                {
                    continue;
                }
                if (IsActiveSale(item))
                {
                    items.Add(item);
                }
            }
            return items;
        }

        /// <summary>
        /// Determines if the specified product sale item is eligible to be applied.
        /// </summary>
        protected virtual bool IsActiveSale(ProductSaleItem item)
        {
            if (item == null)
            {
                return false;
            }
            if (item.Disabled)
            {
                return false;
            }
            if ((item.StartDate > System.DateTime.Now) || (item.EndDate < System.DateTime.Now))
            {
                return false;
            }
            return true;
        }

        /// <summary>
        /// Gets the active product sale item for the specified product.
        /// </summary>
        protected virtual ProductSaleItem GetSaleItem<TProduct>(TProduct product) where TProduct: ProductBaseData
        {
            var productItem = ProductRepositoryUtil.GetRepositoryItem<TProduct>(product);
            var saleItems = GetSaleItems(productItem);
            if (saleItems == null || saleItems.Count == 0)
            {
                return null;
            }
            ProductSaleItem saleItem = null;
            if (Sitecore.Ecommerce.Utils.MainUtil.IsLoggedIn())
            {
                saleItem = (from ProductSaleItem item in saleItems
                            where (item.StartDate <= System.DateTime.Now) &&
                            (item.EndDate >= System.DateTime.Now)
                            orderby item.Percent descending
                            select item).First();
            }
            else
            {
                saleItem = (from ProductSaleItem item in saleItems
                            where (item.StartDate <= System.DateTime.Now) &&
                            (item.EndDate >= System.DateTime.Now) &&
                            !item.MemberOnly
                            orderby item.Percent descending
                            select item).First();
            }
            return saleItem;
        }

        /// <summary>
        /// Get the discount amount (the difference between the list price 
        /// and the price a specific customer will be charged) for the 
        /// specified product.
        /// </summary>
        protected virtual decimal GetDiscountAmount<TProduct>(TProduct product) where TProduct:ProductBaseData
        {
            var saleItem = GetSaleItem(product);
            if (saleItem == null || saleItem.Percent == 0)
            {
                return 0;
            }
            var listPrice = this.GetPriceMatrixPrice<TProduct>(product, "Normalprice");
            var listPriceDiscount = listPrice * saleItem.Percent * 1 / (decimal)100;
            if (! Sitecore.Ecommerce.Utils.MainUtil.IsLoggedIn())
            {
                return listPriceDiscount;
            }
            var memberPrice = this.GetPriceMatrixPrice<TProduct>(product, "Memberprice");
            if (saleItem.OffBasePriceOnly)
            {
                if (memberPrice < (listPrice - listPriceDiscount))
                {
                    return (listPrice - memberPrice);
                }
                return listPriceDiscount;
            }
            var memberPriceDiscount = memberPrice * saleItem.Percent * 1 / (decimal)100;
            return ((listPrice - memberPrice) + memberPriceDiscount);
        }

        /// <summary>
        /// Overrides the default price manager method.
        /// </summary>
        public override TTotals GetProductTotals<TTotals, TProduct, TCurrency>(TProduct product, TCurrency currency, uint quantity)
        {
            var totals = base.GetProductTotals<TTotals, TProduct, TCurrency>(product, currency, quantity);
            var discountAmount = GetDiscountAmount<TProduct>(product);
            if (discountAmount == 0)
            {
                return totals;
            }
            var listPrice = this.GetPriceMatrixPrice<TProduct>(product, "Normalprice"); 
            var discountAmountWithTax = discountAmount * (1 + totals.VAT);
            var salePrice = listPrice - discountAmount;
            var salePriceWithTax = salePrice * (1 + totals.VAT);
            //
            //
            totals.PriceExVat = salePrice;
            totals.PriceIncVat = salePriceWithTax;
            //
            //values that consider quantity
            totals.DiscountExVat = discountAmount * quantity;
            totals.DiscountIncVat = discountAmountWithTax * quantity;
            totals.TotalPriceExVat = salePrice * quantity;
            totals.TotalPriceIncVat = salePriceWithTax * quantity;
            totals.TotalVat = (salePriceWithTax - salePrice) * quantity;
            return totals;
        }
    }
}

Step 7: Register the custom price manager

After the assembly containing the custom price manager is deployed, the next step is to configure Unity so the custom price manager is used.

Earlier you saw that the following line in the Unity configuration file specifies the price manager that SEFE should use:

<alias alias="ProductPriceManager" type="Sitecore.Ecommerce.Prices.ProductPriceManager, Sitecore.Ecommerce.Kernel"/>

Replace that line with the following:

<alias alias="ProductPriceManager" type="Sitecore.Marketing.SEFE.Extensions.SalePriceManager, Sitecore.Marketing.SEFE.Extensions"/>

Step 8: Configure pricing rules

In order to test this extension, you need to configure some sales.  I'm writing this post in January 2011, so you may want to change the dates and names to better match your situation.

  1. Add the following item to define a sale:
    * Item parent path: /sitecore/system/Modules/Ecommerce/Product Sales
    * Item name: Half-off Q1 2011
    * Percent: 50
    * Start Date: January 1, 2011
    * End Date: April 1, 2011
  2. Add the following item to define another sale:
    * Item parent path: /sitecore/system/Modules/Ecommerce/Product Sales
    * Item name: Quarter-off Q1 2011
    * Percent: 25
    * Start Date: January 1, 2011
    * End Date: April 1, 2011
  3. Assign the sales to the D3 camera product by setting the following field:
    * Item path: /sitecore/content/E-Commerce Examples/Product Repositories/Master/Cameras/D3
    * Field name: Product Sales

Step 9: Create XSL extension methods that display new pricing information

Since you're offering the customer a sale price, you probably want him to see the difference between the list price and the price he will be charged.  All of the pricing information is available through the SEFE API.  You need to write the presentation logic to display it.

The code from the SEFE Sample Pages uses XSLT to display product details.  In order to stay consistent with this example, create the following class.  It contains the logic to retrieve the sale price for a product:

using System.Xml.XPath;
using Microsoft.Practices.Unity;
using Sitecore.Diagnostics;
using Sitecore.Ecommerce.DomainModel.Carts;
using Sitecore.Ecommerce.DomainModel.Configurations;
using Sitecore.Ecommerce.DomainModel.Prices;
using Sitecore.Ecommerce.DomainModel.Products;
using Sitecore.Ecommerce.DomainModel.Currencies;
using Sitecore.Ecommerce.Xsl;

namespace Sitecore.Marketing.SEFE.Extensions
{
    public class ProductSaleXsl : XslExtensions
    {
        public virtual bool IsPriceReduced(XPathNodeIterator iterator)
        {
            var totals = GetProductTotals(iterator);
            return (totals.DiscountExVat > 0);
        }

        public virtual string GetSalePrice(XPathNodeIterator iterator)
        {
            var str = "-";
            var settings = Sitecore.Ecommerce.Context.Entity.GetConfiguration<GeneralSettings>();
            var totals = this.GetProductTotals(iterator);
            if ((totals != null) && (totals.PriceExVat != 0))
            {
                str = Sitecore.Ecommerce.Utils.MainUtil.FormatPrice(totals.PriceExVat, settings.DisplayCurrencyOnPrices);
            }
            return str;
        }

        protected virtual Totals GetProductTotals(XPathNodeIterator iterator)
        {
            var argument = this.GetItem(iterator);
            Assert.ArgumentNotNull(argument, "item");
            var productCode = argument["Product Code"];
            var repository = Sitecore.Ecommerce.Context.Entity.Resolve<IProductRepository>(new ResolverOverride[0]);
            var product = repository.Get<ProductBaseData>(productCode);
            if (product == null)
            {
                return null;
            }
            var cart = Sitecore.Ecommerce.Context.Entity.GetInstance<ShoppingCart>();
            var manager = Sitecore.Ecommerce.Context.Entity.Resolve<IProductPriceManager>(new ResolverOverride[0]);
            return manager.GetProductTotals<Totals, ProductBaseData, Currency>(product, cart.Currency);
        }
    }
}

This file must be registered with Sitecore in order for Sitecore to make the extensions available to XSLT renderings.  This is easiest done through a configuration file.  Create the file [sitecore path]\Website\App_Config\Include\Sitecore.Marketing.SEFE.Extensions.config and add the following:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <xslExtensions>
      <extension mode="on" type="Sitecore.Marketing.SEFE.Extensions.ProductSaleXsl, Sitecore.Marketing.SEFE.Extensions" namespace="http://www.sitecore.net/ecext" singleInstance="true" />
    </xslExtensions>
  </sitecore>
</configuration>

Step 10: Display the list and sale prices

Add the following namespace declaration to ProductInfo.xsl:
* xmlns:ecext="http://www.sitecore.net/ecext"

In the XSL file, find the div-tag that uses the "priceContainer" class.  Replace the code inside the div-tag with the following:

            <xsl:choose>
              <xsl:when test="ecext:IsPriceReduced(.)">
                <div class="priceMain">
                  <xsl:value-of select="ecext:GetSalePrice(.)"/>
                </div>
                <div class="priceOriginal">
                  <xsl:value-of select="ec:GetListPrice(.)"/>
                </div>
              </xsl:when>
              <xsl:otherwise>
                <div class="priceMain">
                  <xsl:value-of select="ec:GetListPrice(.)"/>
                </div>
              </xsl:otherwise>
            </xsl:choose>

Similar changes should also be made to the following XSL files:
* Global.xslt - "product-detail" template
* Column.xslt - "column" template.  In this template, do not use "." as the path.  You must use "$item", such as ec:GetListPrice($item).

Step 11: Test your pricing extension

If you haven't already published your changes and deployed your new assembly, do that now.  If you've followed all of these instructions, when you navigate to the D3 page on the published web shop site, you should see the following (because you are not a member so the 25% off sale applies to the non-member price):

If you log into the published web shop site, you should see the following (because you are a member so the 50% off sale applies to the member price):

Conclusion 

Now you have built your first SEFE extension.  If you followed all of the instructions you have likely noticed that most of the steps are Sitecore related (creating data templates, items, etc.) rather than SEFE-specific.  In case you didn't feel like following all the steps, but still want a working version of this extension, the source code is available as a Shared Source component.  The Sitecore installation package is available here.

Tags: e-commerce, API

Comments

  • Hi Adam
    Great article. Unity Application Block and SEFE really is a great combination. If you could make a example of how to ie add a extra property to the Order object itself? This could fx be internalOrderNumber from a external ERP system.
    And another question, is it recommended to change the contracts of SEFE Unity objects? Or just the implementations?

    - Kasper Pagels
    January 22, 2011 at 1:08 PM

  • Hi Kasper,

    I just posted an example that, while not exactly what you asked for, is similar. If you need a more focused nudge in the right direction, let me know.

    http://www.sitecore.net/en/Community/Technical-Blogs/Getting-to-Know-Sitecore/Posts/2011/01/Extending-Sitecore-Ecommerce-Order-Lines.aspx

    - Adam Conn
    January 26, 2011 at 12:09 PM

  • The custom product pricing manager is missing it's inheritance from Sitecore's ProductPriceManager ( public class SalePriceManager:ProductPriceManager .)

    See example in Trac - http://trac.sitecore.net/SefeExtensions/browser/Trunk/Sitecore.Marketing.SEFE.Extensions/Pricing/SalePriceManager.cs

    - Kevin Deenanauth
    August 13, 2012 at 9:08 AM

  • Hi Adam,
    My question just related a little bit to this subject. My problem is following:

    I would like to create a custom product template that is inherited from template Product of SES.
    I have added the unity registration to Unity.config, but somehow my newly create template would not be displayed on the repository. My code is as following:
    <alias alias="ProductExtension" type="GrapeGoods.Models.CommonModels.ProductExtension, GrapeGoods" />

    <register type="ProductBaseData" mapTo="ProductExtension" name="{93D0F359-6329-4406-A599-53938223BF49}"/>

    Did I miss any steps? How could I make the products created from my new template to be displayed in the repository.

    Thanks and appreciate your helps!

    - Nigel Tran
    October 09, 2013 at 1:25 AM

  • HI Adam,

    Is it possible to deal with a scenario like this : in one order, if total payable amount is greater than $500 , can we apply discount $10?

    Helen Peng

    - Helen Chang Ping Peng
    January 18, 2014 at 2:49 AM

*{0} must be filled in.
*{0} must be filled in.
*{0} must be filled in.
Newsletter

Enter your email address to see subscription options or manage your existing account

Boy Scouts of America | Read Case Study >

We have been impressed with Sitecore’s extensibility and ease of use. In addition, the time to market for new websites and web pages has decreased dramatically.

- Eric Brown, CMS Project Manager, Boy Scouts of America