Tutorial 3: Extending Ecommerce

In the previous tutorial we covered the template hierarchy, the rendering process, and the basics of frontend development in Dynamicweb – and in this tutorial we will get our hands dirty by actually extending an area of Dynamicweb – Ecommerce.

Ecommerce is a huge and important area in Dynamicweb, and can be extended in a multitude of places:

In this tutorial, we will be creating the following extensions:

  • A notification subscriber which hooks into the shopping cart and removes order lines which don’t fulfill a particular criteria
  • A custom price provider which reduces the price of a product in both the product catalog and the cart with 25% if it starts with a particular letter

We will also create a custom discount provider, to show how you use configurable add-ins to extend the Dynamicweb administration with custom input controls.

In this example, we have a business where products are sold only in batches by the dozen. To ensure this we will always add a quantity to make the order line quantity dividable by 12:

using Dynamicweb.Extensibility.Notifications; [Subscribe(Dynamicweb.Ecommerce.Notifications.Ecommerce.Cart.Line.Added)] public class LineAdded : NotificationSubscriber { public override void OnNotify(string notification, NotificationArgs args) { if (args == null || !(args is Dynamicweb.Ecommerce.Notifications.Ecommerce.Cart.Line.AddedArgs)) return; Dynamicweb.Ecommerce.Notifications.Ecommerce.Cart.Line.AddedArgs added = (Dynamicweb.Ecommerce.Notifications.Ecommerce.Cart.Line.AddedArgs)args; if (added.AddedLine.Quantity % 12 != 0) { double q = added.AddedLine.Quantity; added.AddedLine.Quantity = q + (12 - (q % 12)); } } }

A PriceProvider provides Dynamicweb Ecommerce with an algorithm for retrieving a specific price for a product.  It overrides the default algorithm of the system by returning a price.

It is up to you to decide how the price data is retrieved, cached and updated – and it’s perfectly possible to call external systems and query for price data.

The following rules apply for PriceProviders:

  • Multiple PriceProviders are allowed.
  • A PriceProvider should never returns a price if it does not have one for a particular situation. In the case that a PriceProvider cannot resolve a price, the system will call the next PriceProvider in the provider chain.
  • If no PriceProvider exists for a price, the system will fall back to the DefaultPriceProvider (Product Price Matrix)

A price is the cost per unit of a product sold in a specific context. When the system asks a PriceProvider for a price, it calls the PriceProvider with a set of parameters that define the context. As the developer of a PriceProvider, you can choose to use one or more of the parameters to resolve the price. You can also ignore parameters that are not relevant for resolving a price. For example, if you are not concerned with the quantity parameter (because the price is not dependent of quantity), you can choose to overlook this parameter by returning the same price no matter what quantity the system asks for.

The context of the price is determined by the following parameters:

  • Product – the product which the provider should resolve a price for
  • Quantity – the number of product items which the provider should return a price for
  • VariantID – the variant ID of the product variant which the provider should return a price for
  • Currency – the currency of the requested price. Only return a price in this currency – if no price exists return NULL instead. The provider will be asked again with the default currency of the solution, and will use the currency exchange rates from your Ecommerce to calculate the correct price
  • UnitID – the ID of the product unit which the provider should return a price for
  • User – the Extranet user which the provider should return a price for

Dynamicweb expects a PriceProvider to return a PriceRaw object – a price without a context, consisting only of an amount and a currency (e.g. 10 USD). The context used to retrieve the price will be remembered by the system.

All sales tax and VAT calculations are done by the system, in accordance with the system configurations. 
A PriceProvider simply needs to check whether the PricesInDbWithVAT bit is set in the configuration. If the bit is true, the PriceProvider always returns a price incl. VAT. If the bit is false, the PriceProvider always return prices without VAT.

The following is an example PriceProvider which reduces the price by half if the product name starts with ‘M’ and the user is logged in:

using Dynamicweb.Ecommerce.Prices; using Dynamicweb.Ecommerce.Products; using Dynamicweb.Ecommerce.International; using Dynamicweb.Security.UserManagement; namespace T3 { public class CustomPriceProvider : PriceProvider { public override PriceRaw FindPrice(Product product, double quantity, string variantID, Currency currency, string unitID, User user) { // Get the price from the DefaultPriceProvider DefaultPriceProvider defaultProvider = new DefaultPriceProvider(); PriceRaw rawPrice = defaultProvider.FindPrice(product, quantity, variantID, currency, unitID, user); if (product.Name.StartsWith("M") && User.IsExtranetUserLoggedIn()) { rawPrice.Price = rawPrice.Price / 2; return rawPrice; } return null; } } }

This provider returns a value, but it also supports methods for populating the cache, which is unique for the price provider type. This is because it is convenient to get all the prices you need in a single call to an external system, instead of calling the external system multiple times. To achieve this, implement the PreparePrices method. This method enables you to build a list of cached prices which you can use for making your custom calculation in FindPrice:

public override void PreparePrices(ProductCollection products) { //Cache prices here (mostly for product lists) } public override void PreparePrices(Dictionary<Product, Double> products) { //Cache prices here, depending on quantity (mostly for cart) } }

The PreparePrices method has two overloads: One that accepts a ProductCollection and another that accepts a dictionary of products and their quantities. The overload using a ProductCollection is typically called when dealing with a list of products as in the eCommerce Catalog. The overload using the dictionary with quantities is used in cases where you have a quantity, such as in the Ecommerce Cart.

Using the ConfigurableAddIn class, you can quickly extend certain areas of the Dynamicweb administration interface with custom input controls.

For instance, you may want to use custom input when calculating a discount – e.g. specify a country or an age group for which a discount should apply – and this is not possible using the standard functionality in Dynamicweb.

Configurable add-ins can be used in a number of providers, e.g.:

  • CheckoutHandler
  • FormSaveProviders
  • DiscountExtenderBase
  • The Live Integration add-in
  • Scheduled Task add-ins
  • Etc.

For a full list, please see the API documentation for the ConfigurableAddin class under Inheritance Hierarchy.

In this tutorial we will be creating a custom discount which awards a discount to users based on the day of their registration.

Using the DiscountExtenderBase you can extend on the default settings of a discount. It handles both product discounts and order discounts. This is the base implementation:

using System; using Dynamicweb.Ecommerce.Products; using Dynamicweb.Ecommerce.Orders; using Dynamicweb.Ecommerce.Orders.Discounts; using Dynamicweb.Extensibility.AddIns; using Dynamicweb.Extensibility.Editors; namespace Dynamicweb.Ecommerce.Examples.Orders.Discounts { /// <summary> /// The example of an extender which can be used with product and order discounts - if applied the discount will be valid on the specified day only /// </summary> public class CustomDiscountExtender : DiscountExtenderBase { [AddInParameter("My parameter"), AddInDescription("Description of parameter"), AddInParameterEditor(typeof(TextParameterEditor), "")] public string MyParameter { get; set; } /// <summary> /// Constructor - sets the favorite day to Tuesday /// </summary> public CustomDiscountExtender() { MyParameter = ""; } /// <summary> /// If this extender is used for a product discount, it will be checked here /// </summary> public override bool DiscountValidForProduct(Product product) { return CheckConditions(); } /// <summary> /// If this extender is used for an order discount, it will be checked here /// </summary> public override bool DiscountValidForOrder(Order order) { return CheckConditions(); } /// <summary> /// Checks if the conditions for this discount are valid. /// </summary> private bool CheckConditions() { // TO-DO: add your own logic here if (DateTime.Today.DayOfWeek.ToString() == "Monday") { return true; } else { return false; } } } }

You should notice

  1. public class that inherits from a base class.
  2. The override method DiscountValidForProduct()
  3. The override method DiscountValidForOrder
  4. The overall use of boolean to determine if the discount should be granted

In the following example you will find a discount that can be triggered on two occasions:

  1. If day/month of users registration date (CreatedOn) matches current day/month, the discount will be granted.
  2. If the weekday of today matches that of the selected, the discount will be granted.
using System; using System.Collections; using Dynamicweb.Security; using Dynamicweb.Ecommerce.Products; using Dynamicweb.Ecommerce.Orders; using Dynamicweb.Ecommerce.Orders.Discounts; using Dynamicweb.Extensibility.AddIns; using Dynamicweb.Extensibility.Editors; namespace T3 { /// <summary> /// The example of an extender which can be used with product and order discounts - if applied the discount will be valid on the specified day only /// </summary> public class SpecialDiscount : DiscountExtenderBase, IDropDownOptions { [AddInParameter ("Activate Yearly Anniversary"), AddInDescription("Based on users registration date. Specify whether or not discount type is active."), AddInParameterEditor(typeof(YesNoParameterEditor), "")] public bool YearlyAnniversaryActive { get; set; } [AddInParameter ("Activate Special Day"), AddInDescription("Specify whether or not discount type is active."), AddInParameterEditor(typeof(YesNoParameterEditor), "")] public bool SpecialDayActive { get; set; } [AddInParameter("Special day"), AddInDescription("Specify which day when this discount applies"), AddInParameterEditor(typeof(DropDownParameterEditor), "")] public string SpecialDay { get; set; } /// <summary> /// Constructor - set defaults /// </summary> public SpecialDiscount() { SpecialDay = DayOfWeek.Friday.ToString(); YearlyAnniversaryActive = false; SpecialDayActive = false; } /// <summary> /// If this extender is used for a product discount, it will be checked here /// </summary> public override bool DiscountValidForProduct(Product product) { return false; } /// <summary> /// If this extender is used for an order discount, it will be checked here /// </summary> public override bool DiscountValidForOrder(Order order) { if (YearlyAnniversaryActive) return YearlyAnniversaryDiscountEvaluate(); if (SpecialDayActive) return SpecialDayEvaluate(); return false; } private bool YearlyAnniversaryDiscountEvaluate() { if (!Dynamicweb.Security.UserManagement.User.IsExtranetUserLoggedIn()) return false; DateTime? CreatedDate = Dynamicweb.Security.UserManagement.User.GetCurrentExtranetUser().CreatedOn; if (!CreatedDate.HasValue) return false; DateTime myDate = Convert.ToDateTime(CreatedDate); if (myDate.Year < DateTime.Now.Year && myDate.Month == DateTime.Now.Month && myDate.Day == DateTime.Now.Day) return true; return false; } private bool SpecialDayEvaluate() { if(DateTime.Today.DayOfWeek.ToString() == SpecialDay) return true; return false; } Hashtable IDropDownOptions.GetOptions(string dropdownName) { switch (dropdownName) { case "Special day": var list = new Hashtable { { DayOfWeek.Monday, DayOfWeek.Monday.ToString() }, { DayOfWeek.Tuesday, DayOfWeek.Tuesday.ToString() }, { DayOfWeek.Wednesday, DayOfWeek.Wednesday.ToString() }, { DayOfWeek.Thursday, DayOfWeek.Thursday.ToString() }, { DayOfWeek.Friday, DayOfWeek.Friday.ToString() }, { DayOfWeek.Saturday, DayOfWeek.Saturday.ToString() }, { DayOfWeek.Sunday, DayOfWeek.Sunday.ToString() } }; return list; default: throw new ArgumentException($"Unknown dropdown name: '{dropdownName}'"); } } } }

With the above code implemented, the new discount is now available to be selected from the administration alongside with the standard discount option settings: (Figure 5.3).

Figure 5.3 Configurable add-in controls are rendered in the interface

In this tutorial, you’ve learned the following:

  • How to subscribe to a notification
  • How to create a custom price provider
  • How to use configurable add-ins to create input controls in the administration interface

In the next tutorial you will be learn about the flexible content type called items and how to work with them. In particular, you will be learning about defining items code-first – by creating a public .NET class which inherits from the ItemEntry base class.