CONTENTSTART
EXCLUDESTART EXCLUDEEND

Creating order from Orders

I love pretty much everything about Kentico, and by pretty much I mean everything except extending or hooking onto Orders.  Kentico has always been (in my opinion) the best CMS and website building tool around, but it has always also only been a mediocre Ecommerce management (this is partly why in Kentico version 11 they are partnering with a dedicated Ecommerce platform UCommerce, because Ecommerce is huge and very complex in itself).  It’s fine for small shops, but not so much for bigger.

One source of frustration that can occur with extending the Ordering system is often that, in order to integrate with 3rd party systems (Like Dynamics GP), certain information needs to be passed or calculated on Order Creation / Updates.  However this often poses a huge problem, which I’ll need to explain before we get into the solutions.  Along with that there are some general road blocks that this article will hopefully help you jump.

Also a quick apology for the scattered nature of this blog, tail end of a long project with a ton of customization.  Below are some quick anchors to what’s covered:

OrderInfo Insert/Update Hooks

Often in ecommerce sites you will want to perform special logic (such as calculating shipping tax and discount), and the most logical path is to hook onto the OrderInfo global events (such as order insert / update, or the EcommerceEvents.OrderPaid event).  But this may be the wrong path.  When an order is updated or paid, it doesn’t just call these methods one time, it often calls it many, many times.

For instance, if an Order is created with 2 items, and you modify the order and add an extra item, what the system does is it systematically removes each item from the cart, then adds all the items (plus the new one) back in.  Each time it updates the OrderInfo, which means that adding a cart looks like this:

  1. Add Item to Cart (from 2 items to 3)
    1. Order Update triggered (with 2 items)
    2. Order Update triggered, Item #2 removed (now has 1 item)
    3. Order Update triggered, Item #1 removed (now has 0 items)
    4. Order Update triggered, Item #1 re-added (now has 1 item)
    5. Order Update triggered, Item #2 re-added (now has 2 items)
    6. Order Update triggered, new Item #3 added (now has 3 items)
  2. Remove Item from Cart (From 3 items to 2)
    1. Order Update triggered (with 3 items)
    2. Order Update Triggered, Item #3 removed (now has 2 items)
    3. Order Update triggered, Item #2 removed (now has 1 item)
    4. Order Update triggered, Item #1 removed (now has 0 items)
    5. Order Update triggered, Item #1 re-added (now has 1 item)
    6. Order Update triggered, Item #2 re-added (now has 2 items)

In one of my recent projects, we needed to recalculate the shipping and tax and shipping discount and add them to custom fields we added to the Ecommerce Order class.  However, the only way to calculate the Shipping Tax and Discount is through the ShippingOptionInfoProvider’s Calculate methods, which requires a ShoppingCartInfo object.  These event triggers give you an OrderInfo, which there is no way to just "convert" an OrderInfo into a ShoppingCartInfo object.

You can build a ShoppingCartInfo object by passing an OrderID to the ShoppingCartInfoProvider.GetShoppingCartInfoFromOrder(int OrderID) method, but because of the above way of how the OrderInfo event is triggered, you would need to get the shopping cart at the LAST step (as this would have all the items).  Which although you can use RecursionControl class to make sure you only execute some logic on the first attempt, you can’t do it for the “Last” attempt (because you really don’t know what the last event will be).

All these things added up to a major headache!

Solution: ShoppingCartInfoProvider and Custom Data!

Since we cannot rely on the OrderInfo’s update events to run only once, we need to look to where this cascade of update events occurs.  After digging around, I finally found it!  The ShoppingCartInfoProvider.SetOrderInternal(ShoppingCartInfo cartObj, bool generateInvoice).

This method is called not only when the system converts a ShoppingCartInfo object into an OrderObject (like when you order and are sent to the payment screen), but it ALSO is responsible for updating existing orders!  And since it passes the CartObject as one of the parameters, you have the actual ‘finished’ cart that you will be doing your calculations on. 

Now in this method, there is no OrderInfo object (one may not even exist if it’s the creation of an order), so you cannot set an OrderInfo object’s fields directly.  You need a way to insert some extra information that would be passed to the OrderInfo’s Update/Insert Before hook, so I could then set (in my case) the Shipping Tax and Shipping Discount. This is where the Custom Data comes in.

The ShoppingCartInfo object has a property called ShoppingCartCustomData, with a GetValue and SetValue.  This adds any serializable object to the Custom Data.  When a shopping cart is converted to an order, that ShoppingCartCustomData becomes OrderCustomData (which also has a GetValue and SetValue). 

So here was my workflow:

  1. Override SetOrderInternal
  2. Using the cartObj that the method is passed, calculate the Shipping Tax and Discount (ShippingOptionInfoProvider.Calculate_____(cartObj))
  3. Pass the Shipping Tax and Shipping Discount in the ShoppingCartCustomData
  4. On OrderItem.Insert.Before and OrderItem.Update.Before, if it’s the first run (using RecursionControl) Get the Shipping Tax and Shipping Discount from the OrderCustomData
  5. Set the OrderInfo’s Custom ShippingTax and ShippingDiscount fields so external systems can properly calculate the tax.

Below is my code:

using System;
using CMS;
using CMS.Ecommerce;
using CMS.EventLog;
/// <summary>
/// Summary description for CustomShoppingCartInfoProvider
/// </summary>
[assembly: RegisterCustomProvider(typeof(CustomShoppingCartInfoProvider))]
public class CustomShoppingCartInfoProvider : ShoppingCartInfoProvider
{
    protected override void SetOrderInternal(ShoppingCartInfo cartObj, bool generateInvoice)
    {
        try { 
            // Set some Custom Order Data so the OrderInfo.Events.Insert.Before and OrderInfo.Events.Update.Before can latch onto them.
            double ShippingCost = ShippingOptionInfoProvider.CalculateShipping(cartObj);
            cartObj.ShoppingCartCustomData.SetValue("ShippingTax", ShippingOptionInfoProvider.CalculateShippingTax(cartObj));
            cartObj.ShoppingCartCustomData.SetValue("ShippingDiscount", ShippingOptionInfoProvider.CalculateShippingDiscount(cartObj, ShippingCost));
        } catch(Exception ex)
        {
            EventLogProvider.LogException("CustomShoppingCartInfoProvider", "ERRORSETTINGSHIPPINGVALUES", ex, cartObj.ShoppingCartSiteID, additionalMessage: "Could not set the Shipping Tax and Shipping Discount for the order due to an error calculating shipping.");
        }
        base.SetOrderInternal(cartObj, generateInvoice);
    }
}

using CMS;
using CMS.Base;
using CMS.DataEngine;
using CMS.DocumentEngine;
using CMS.Ecommerce;
using CMS.EventLog;
using CMS.Helpers;
using CMS.Membership;
using CMS.OnlineForms;
using CMS.SiteProvider;
using CMS.Synchronization;
using System;
using System.Data;

// Registers the custom module into the system
[assembly: RegisterModule(typeof(CustomInitializationModule))]

public class CustomInitializationModule : Module
{
    // Module class constructor, the system registers the module under the name "CustomInit"
    public CustomInitializationModule()
        : base("FeldmannCustomInit")
    {
    }
    // Contains initialization code that is executed when the application starts
    protected override void OnInit()
    {
	base.OnInit();
        // Add Custom Fields from Order Custom Field webpart to Order from Shopping Cart and handle getting the Shipping Taxes and Discount from the Custom Data (Set in the Custom Shopping Cart Info Provider)
        OrderInfo.TYPEINFO.Events.Insert.Before += OrderInsert_Before;
        OrderInfo.TYPEINFO.Events.Update.Before += OrderUpdate_Before;
    }
    private void OrderInsert_Before(object sender, ObjectEventArgs e)
    {
        try
        {
            OrderInfo OrderObject = (OrderInfo)e.Object;

            // Set shipping tax and discount fields
            if (OrderObject.OrderCustomData.ContainsColumn("ShippingTax"))
            {
                OrderObject.SetValue("OrderTotalShippingTaxesInMainCurrency", Convert.ToDouble(OrderObject.OrderCustomData.GetValue("ShippingTax")));
            }
            if (OrderObject.OrderCustomData.ContainsColumn("ShippingDiscount"))
            {
                OrderObject.SetValue("OrderTotalShippingDiscountInMainCurrency", Convert.ToDouble(OrderObject.OrderCustomData.GetValue("ShippingDiscount")));
            }
        }
        catch (Exception ex)
        {
            EventLogProvider.LogException("CustomLoader", "OrderInsert_Before", ex);
        }
    }
    private void OrderUpdate_Before(object sender, ObjectEventArgs e)
    {
        try
        {
            OrderInfo OrderObject = (OrderInfo)e.Object;
            RecursionControl FirstTimeSetTaxAndDiscount = new RecursionControl("OrderUpdate_" + OrderObject.OrderID);
            if(FirstTimeSetTaxAndDiscount.Continue) { 
                // Set shipping tax and discount fields set from the CustomShoppingCartInfoProvider.SetOrderInternal
                if (OrderObject.OrderCustomData.ContainsColumn("ShippingTax"))
                {
                    OrderObject.SetValue("OrderTotalShippingTaxesInMainCurrency", Convert.ToDouble(OrderObject.OrderCustomData.GetValue("ShippingTax")));
                }
                if (OrderObject.OrderCustomData.ContainsColumn("ShippingDiscount"))
                {
                    OrderObject.SetValue("OrderTotalShippingDiscountInMainCurrency", Convert.ToDouble(OrderObject.OrderCustomData.GetValue("ShippingDiscount")));
                }
            }
        } catch(Exception ex)
        {
            EventLogProvider.LogException("CustomLoader", "OrderUpdate_Before", ex);
        }
    }
}

Note on Extending Module Classes

In order to add custom fields to the OrderInfo class, or any other class, go to Modules > (Edit Module) > Classes > (Edit Class) > Fields > Add the fields.  You may need to hit the “Customize” button on the top to allow you to edit it.  For the OrderInfo, it’s Modules > E-Commerce > Classes > Order

Also you may need to alter the various “Alternative Forms” as by default many do not hide new fields, so your custom fields may show up on various screens otherwise!

Importance of RecursionControl

In my code I used something called the RecursionControl.  How this operates is you set the RecursionControl with a key name.  The first time it’s created, the “Continue” option is true, this signals it is the first time this thread this was called.

When it is created again on the same thread with the same key name, the Continue is false, signifying you have already run this code in this current thread.  This is to make sure stuff is only run once (on the first pass). 

Make sure to leverage this on the Order Paid event hook if you want to extend that, otherwise you may end up running your custom Order Paid logic (or other hooks) multiple times.

Making Sense of Pricing

Another issue with Kentico’s order system is that the values are a little cryptic in the OrderItem table.  You have some fields that contain the Items cost, the tax costs, etc, but it’s also missing things like the Shipping Tax and Shipping Discount (the OrderTotalShippingInMainCurrency contains the tax and discounts).  This means that if you need to send the shipping and item tax separately, you can’t by default.  That’s why I had to do the above customization to add them.

Below is a query I created that (leveraging the 2 new Shipping Fields) gives all the proper price points broken down from the COM_Order table.  This will help you in figuring out integration or displaying of values.  Remember, this query uses the 2 custom fields I added, so you may have to modify it if you don’t have those fields:

SELECT 
OrderID,
OrderTotalPriceInMainCurrency - OrderTotalShippingInMainCurrency - OrderTotalTaxInMainCurrency + OrderTotalDiscountInMainCurrency as 'Items',
OrderTotalTaxInMainCurrency as 'Items Tax',
OrderTotalPriceInMainCurrency - OrderTotalShippingInMainCurrency + OrderTotalDiscountInMainCurrency as 'Items Total',
OrderTotalDiscountInMainCurrency as 'Order Discount',
OrderTotalPriceInMainCurrency - OrderTotalShippingInMainCurrency as 'Items Total After Discount',
OrderTotalShippingInMainCurrency - OrderTotalShippingTaxesInMainCurrency + COALESCE(OrderTotalShippingDiscountInMainCurrency,0) as  'Shipping',
COALESCE(OrderTotalShippingDiscountInMainCurrency,0) as  'Shipping Discount',
OrderTotalShippingInMainCurrency - OrderTotalShippingTaxesInMainCurrency as  'Shipping After Discount',
OrderTotalShippingTaxesInMainCurrency as 'Shipping Tax',
OrderTotalShippingInMainCurrency as 'Shipping Total',
OrderDiscounts as 'Order Discount Text',
OrderTotalPriceInMainCurrency as 'Grand Total'
FROM COM_Order order by OrderID desc

Confusion in Discounts

Discounts should also be discussed.  There are multiple types of Discounts (Order Discounts, Shipping Discounts/Free Shipping, and Catalog Discounts, and variations of these).  They behave differently than one another:
  1. Order Discounts apply AFTER taxes, and take from the grand total (all items + taxes + shipping)\
  2. Shipping Discount/Free Shipping apply BEFORE shipping taxes are calculated.
  3. Catalog Discounts apply BEFORE taxes, and apply to individual items in the cart.
Sadly having post-tax discounts are often not allowed in certain order and tracking systems like Dynamics GP, which means you may have to disable this feature completely (in Wisconsin, USA, it’s actually illegal I was told to have discounts post tax).

Order Specific Discounts

Another downfall of the Ecommerce engine is you can’t really set Order-specific discounts (unless you generate a coupon and apply it, but there is only 1 coupon allowed per order so you would be out of luck if the user wanted to use a coupon and you wanted to give custom discounts).  There was the need for our client to be able to alter the order as they needed (give a discount to a specific order, include free shipping, etc).

Discounts do have a macro field that you can use to set Rules that make the discount apply.  Although you are only given the ShoppingCart and normal Macro Context to work with, so you do not have the OrderID anywhere that you can say “create a discount only for this order.”

If you need to create Order Specific discount, what I did is created a Custom User Interface page for the Order edit User Interface.

Adding a Custom Module Interface

Since you are editing an order, the OrderID is passed to each of the child user interface pages through the query string (OrderDiscounts.aspx?orderid=123).  So on my interface I programatically created a discount  using the DiscountInfo api, and set the DiscountCartCondition to:

{{% QueryString.OrderID == 123 && MembershipContext.AuthenticatedUser.CheckPrivilegeLevel(UserPrivilegeLevelEnum.Editor) %}}

DiscountInfo OrderDiscount = DiscountInfoProvider.GetDiscountInfo(OrderDiscountName, SiteContext.CurrentSiteName);
                if (OrderDiscount == null)
                {
                    OrderDiscount = new DiscountInfo();
                    OrderDiscount.ApplyFurtherDiscounts = true;
                    OrderDiscount.DiscountDisplayName = "Order Discount";
                    OrderDiscount.DiscountIsFlat = true;
                    OrderDiscount.DiscountApplyFurtherDiscounts = true;
                    OrderDiscount.DiscountApplyTo = DiscountApplicationEnum.Order;
                    OrderDiscount.DiscountCartCondition = string.Format("{{% QueryString.OrderID == {0} && MembershipContext.AuthenticatedUser.CheckPrivilegeLevel(UserPrivilegeLevelEnum.Editor) %}}", CurrentOrder.OrderID);
                    OrderDiscount.DiscountCustomerRestriction = DiscountCustomerEnum.All;
                    OrderDiscount.DiscountEnabled = true;
                    OrderDiscount.DiscountName = OrderDiscountName;
                    OrderDiscount.DiscountOrder = 100;
                    OrderDiscount.DiscountSiteID = SiteContext.CurrentSiteID;
                    OrderDiscount.DiscountUsesCoupons = false;
                    OrderDiscount.DiscountOrderAmount = 0;
                    OrderDiscount.DiscountValue = OrderDiscountAmt;
                    DiscountInfoProvider.SetDiscountInfo(OrderDiscount);
                }
                else
                {
                    OrderDiscount.DiscountValue = OrderDiscountAmt;
                    OrderDiscount.Update();
                }

Then I created the ShoppingCartInfoObject from the OrderID, called the ShoppingCartInfoProvider.EvaluateShoppingCart(OrderShopingCart) to evaluate the new discount, then lastly ShoppingCartInfoProvider.SetOrder(OrderShoppingCart) to save the order.

var OrderShoppingCart = ShoppingCartInfoProvider.GetShoppingCartInfoFromOrder(CurrentOrder.OrderID);
            ShoppingCartInfoProvider.EvaluateShoppingCart(OrderShoppingCart);
            ShoppingCartInfoProvider.SetOrder(OrderShoppingCart);

Since the orderID is in the query string, and the current user is at least an editor (to be able to edit an order) this discount will trigger and apply the discount properly.  And since the OrderID url parameter is on all the order editing, as long as you finish the order through the admin the discount will stick.

Conclusion

Hopefully my pain will result in less of yours, and will result in you being able to extend and leverage Kentico’s Ecommerce system with less headaches.  Next year as I get to work on some new projects that will leverage the UCommerce partnership in Kentico 11, I’ll try to blog about how to extend and work that system.  Until then, Happy Kentico-ing!
Comments
Blog post currently doesn't have any comments.
= two + four
CONTENTEND