Global Office Directory
More Products
Share this page
Home > Community > Technical Blogs > Getting to Know Sitecore > Extending Sitecore E-Commerce - Pricing
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:
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
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='Sitecore.Pipelines.Loader.EnsureAnonymousUsers, Sitecore.Kernel']"> <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!):
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('|')) { 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:
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.
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: eCommerce, API
- Kasper Pagels January 22, 2011 at 1:08 PM
- Adam Conn January 26, 2011 at 12:09 PM
- Kevin Deenanauth August 13, 2012 at 9:08 AM
Adam is a technical architect on Sitecore's Product Marketing Team. He is responsible for spreading technical knowledge inside and outside of the company, with an emphasis on external systems and applications.
This website is designed to be fully functional with scripts disabled in browser. Please contact the webmaster for any suggestions