About this article
Implementing a SalesDiscountProvider
NOTE: the concepts presented in this article are now considered obsolete possibly because better alternatives are available.
This article is part of a series of articles on Dynamicweb CMS and Dynamicweb eCommerce. Topics discussed in this series include the new 7.1 release, Dynamicweb eCommerce in general and extensibility. For an introduction to the entire series and to Dynamicweb, take a look at Dynamicweb 7.1: New Features, eCommerce and Extensibility.
Although Dynamicweb ships with a number of predefined SalesDiscountProviders to provide discounts in the eCommerce environment, there's a fair change you also want to implement your own and apply customized rules to the discount calculation. Fortunately, implementing your own discount is pretty easy.
Introduction
The default sales discounts that ship with Dynamicweb are all accessible in the Management Center under eCommerce Settings | Product Catalog | Sales Discounts. Here you can enter one or more discounts that may apply to different users under different circumstances. Figure 1 shows the configuration for a very common discount: buy for more than 100 euros, and get a 5 percent discount on the total order (click for a larger version of the image):
Figure 1 - A common Sales Discount
Besides a percentage based discount, Figure 1 shows two other option: give away a fixed amount, or give away one or more prodcts (For example, buy a produduct from the category "soccer shoes" and get a soccer ball for free). Besides the type of dicsount, you can also configure when and to which users it applies.
When multiple discounts are applicable at the same time, the default behavior is to process them all. However, you can change this by choosing Management Cebter | Advanced Configuration | Sales Discounts and choosing Highest or Lowest from the Choose Decount drop down list.
For more information about the standard Sales Discounts, check out the Sales Discounts documentation in the Dynamicweb web site.
To implement your own sales discount provider, you need to inherit Dynamicweb.eCommerce.Orders.SalesDiscounts.SalesDiscountProvider which looks like this:
Figure 2 - The SalesDiscountProvider Class
At a minimum, you need to override ProcessOrder which looks like this
public virtual void ProcessOrder(Order order) {}
Inside that method, you can modify the order somehow to inject your discount value (a percentage, a fixed amount or one or more products). How you modify it depends on the type of discount and your own implementation. You could add a new product by inserting an OrderLine to the OrderLines collection if you want to give a free product based on some condition. Alternatively you can add an OrderLine with its Type set to OrderLineType.Discount to give a price based discount. How you write the logic to determine the type and value of the discount is completely up to you.
In the next section, I'll show you how to implement an IP and country based discount that enables discounts for users from specific countries.
The IP Based SalesDiscountProvider
In my SalesDiscountProvider I am using an external Web Service to get the country ID for the user's IP address. Then, based on this Country ID I check the discount's settings to see if the discount applies to the current user or not. Note that this is just a sample implementation. I could also look at the CustomerCountry or DeliveryCountry properties of the Order instance available in ProcessOrder to make the decision based on the actual billing or delivery addresses. However, by using a Web Service I can show you the concepts that are generally applicable to other systems or discount requirements as well, like accessing a backend CRM system with customer data.
Setting up the Solution
To make things more manageable and reusable, I typically put my discounts and other extensibility code in a separate Class Library that I then reference from my web project. If all you want to do is use the SalesDiscountProvider in an existing eCom solution, all you need to do is drop the assembly generated from the Class Library project in the application's bin folder and Dynamicweb will pick it up automatically. In my case, I am referencing my class library project from my Custom Modules web project. When the solution gets compiled, the Class Library's assembly is copied to my web project's bin folder , so any discount providers defined in it (or any other extensibility code) becomes available automatically. Once the two projects are added to the solution and the web project is referencing the Library project, the next step is to add a class that inherits SalesDiscountProvider. In my example project, I ended up with a Solution Explorer looking like this:
Figure 3 - The Solution Explorer
For now, the custom discount provider looks like this, but I'll add more code in the remainder of this article:
using System; using Dynamicweb.eCommerce.Orders.SalesDiscounts; namespace Dynamicweb.Samples.Lib { public class IPBasedDiscountProvider : SalesDiscountProvider { // TODO Implementation here } }
Referencing the External IP Service
Next, I need an external web service to do the translation from an IP address to a country code. I found a free one from http://www.webservicex.net/. The WSDL for the GeoIPService can be found here: http://www.webservicex.net/geoipservice.asmx?wsdl. To follow along with this article, add a Web Reference to this web service (In Visual Studio 2010, right-click Service References, then choose Add Service Reference and then click the Advanced button to add an ASP.NET Web Service reference). I put the service in the IPService namespace for easy access.
I can now get the country code (such as NL or GB) from the service by passing in an IP address with the following code:
using (IPService.GeoIPService myService = new IPService.GeoIPService()) { string ipAddress = HttpContext.Current.Request.ServerVariables["remote_addr"]; GeoIP result = myService.GetGeoIP(ipAddress); string countryCode = result.CountryCode; }
Note that for local addresses like 127.0.0.1 the service returns ZZ. This is useful if you're testing the implementation. Inside the sales discount provider class I have the following method that accesses the service, determines if the returned country code matches one of the the applicable country IDs and then returns either the defined discount percentage or zero:
private double GetPercentageFromWebService() { try { using (IPService.GeoIPService myService = new IPService.GeoIPService()) { string ipAddress = HttpContext.Current.Request.ServerVariables["remote_addr"]; GeoIP result = myService.GetGeoIP(ipAddress); bool isMatch = (from c in ApplicableCountryCodes.Split(new char[] { ',' }) where c.Trim().ToLower() == result.CountryCode.ToLower() select c).Count() > 0; return isMatch ? DiscountValue.Amount : 0; } } catch { return 0; } }
You'll see more about the ApplicableCountryCodes (which is a user defined list of country IDs to which the discount applies) and DiscountValue.Amount (which is the discount percentage given to the order) later. Note that the method returns a discount percentage of 0 when the service call fails. Depending on your requirements you could implement other logic here, like returning the configured percentage, or throwing an exception.
Since accessing the web service is a costly operation, and because this discount code is triggered any time the order is displayed, I am storing the discount result in the user's session. That way, I need to hit the service only once for each user. I have two helper methods that store and retrieve the item using session state:
private static double? GetPercentageFromSession() { return HttpContext.Current.Session[percentageSessionKey] as double?; } private static void StorePercentageInSession(double? discountPercentage) { HttpContext.Current.Session[percentageSessionKey] = discountPercentage; }
Both these methods use a nullable double to make storing it in and retrieving it from the session a little easier.
Implementing ProcessOrder
The next important method to look at is ProcessOrder which handles the actual discount implementation. In my sample project, the method looks as follows:
public override void ProcessOrder(eCommerce.Orders.Order order) { if (DiscountValue.Type == DiscountTypes.Percent) { double? discountPercentage = GetPercentageFromSession(); if (!discountPercentage.HasValue) { discountPercentage = GetPercentageFromWebService(); StorePercentageInSession(discountPercentage); } if (discountPercentage > 0) { double discountPrice = CalculateDiscountPrice(order, discountPercentage); OrderLine line = CreateOrderLine(order, discountPrice); // Add the order line order.OrderLines.Add(line); } } else { throw new NotImplementedException(@"Discounts for types other than Percent are not supported in this demo."); } }
Since most of the implementation relies on other method calls, the flow of ProcessOrder is easy to follow. First a check is made if this discount's Type is DiscountTypes.Percent which means a shop owner has defined a percentage of the total order as the discount value. Other options are a fixed amount or one or more products. You see more about this later. You could switch on the Type property and implement the other options as well. However, in my case I am only implementing the percentage option.
Next, the code tries to get the discount percentage from session state. If that fails (because it's the first time this code runs and the percentage hasn't been determined and stored in session state yet), I get it from the web service and then store it in session state for later retrieval.
The next step is to determine the actual discount amount. In this implementation, the amount is the percentage of the complete order before fees. So for example, when my order contains products with a total value of € 1000 and the discount percentage is 8 percent, my discount value is € 125. The calculation is done inside the CalculateDiscountPrice method:
private static double CalculateDiscountPrice (eCommerce.Orders.Order order, double? discountPercentage) { double discountPrice = (order.PriceBeforeFees.PriceWithoutVAT / 100) * discountPercentage.Value; discountPrice = discountPrice * -1; return discountPrice; }
Because the discount is added as an order line, its value must be negative. To accomplish this, the final result is multiplied by -1 before it is returned.
The final step is to create a new OrderLine and add it to the OrderLines collection. The method that creates the order line looks like this:
private OrderLine CreateOrderLine(eCommerce.Orders.Order order, double discountPrice) { OrderLine line = new OrderLine { Order = order, Quantity = 1, ProductName = DiscountName }; line.SetUnitPrice(discountPrice); line.Type = Base.ChkString(Base.ChkNumber(OrderLine.OrderLineType.Discount)); return line; }
And it's then added to the order like this:
order.OrderLines.Add(line, false);
When this discount is active and a product is added to the cart, the discount shows up as follows:
Figure 4 - The Cart with the Discount
Configuring the Discount in the Backend
To configure the discount in the backend, choose Management Center | eCommerce Settings | Product Catalog | Sales discounts. In versions of Dynamicweb prior to 7.1, the Sales Discount item was located in the main eCom section instead of in the Management Center. If you click New sales discount on the toolbar, the custom discount should appear under the Additional Sales Discount Types item. Figure 5 shows a fully configured IP based discount (click for a larger version of the image):
Figure 5 - The Configured SalesDiscountProvider
This image shows a few interesting options that are made available by the custom SalesDiscountProvider. First, there's the name of the discount: Discount for specific countries. This name is retrieved from the AddInName attribute. Likewise, the description of the provider in the Parameters section on the right comes from the AddInDescription attribute. Both attributes are applied at the class level as follows:
[AddInName("Discount for specific countries")] [AddInDescription(@"This discount applies when the user's IP is located in one of the assigned countries. Enter one or more comma separated country codes to which the discount applies.")] public class IPBasedDiscountProvider : SalesDiscountProvider { ... }
The next interesting thing is the Country Codes field in the Parameters section. This box is added by applying AddInParameter and AddInParameterEditor attributes to a property of your SalesDiscountProvider class as follows:
[AddInParameter("Country Codes"), AddInParameterEditor(typeof(TextParameterEditor), "")] public string ApplicableCountryCodes { get; set; }
You're not limited to text boxes alone. In fact, you can choose from a whole range of editors, and even create your own. The AddInParameterEditor must be of a type that inherits ParameterEditor. Figure 6 shows the complete list of editors that ship with Dynamicweb 7.1:
Figure 6 - ParameterEditors
Any value that a user enters in the Country Code box is assigned to the ApplicableCountryCodes property of the discount. That value is then used in the GetPercentageFromWebService method that determines the discount percentage for the user depending on whether the current user's country ID is listed in the country IDs to which the discount applies.
Caveats
When you use this discount, you need to be aware of an issue with how tax is calculated. Currently, the tax over this discount is based on the global tax set for the country. This means that if you have products with mixed tax rates (for example 6 and 19 percent for products in the Netherlands), you still end up with the global tax (for example 19%) over the total discount amount. To work around this, you can set the country tax to zero and then define sales tax groups for the various percentages. Then inside the CalculateDiscountPrice method, rather than accessing the PriceBeforeFees.PriceWithoutVat, you access PriceBeforeFees.Price, like this:
double discountPrice = (order.PriceBeforeFees.Price / 100) * discountPercentage.Value;
This gives you the requested percentage (through discountPercentage.Value) of the total order including tax.
Alternatively, you can loop over the OrderLines of the Order and calculate the discount value your self. Properties on OrderLine of interest include Price.VATPercent and Price.PriceWithoutVAT.
Conclusion
Creating your own SalesDiscountProvider is very useful and doesn't have to be very hard. At the minimum, you need to implement ProcessOrder and add discount related data to it. How you determine the discount value is up to you. You can use whatever techniques you have available in .NET to access external resources like web services or backend databases. Be aware that this code is called often, so a solid caching strategy is an absolute necessity.
Also be aware of the way tax is handled. It's easy to make a mistake here and assign a global tax percentage to a discount that is made up of products in various tax groups.
Downloads
Download the source of the custom SalesDiscount Provider (note: does not include the web service code)