Hide menu Last updated: Oct 24 2016
Area: Episerver Commerce Applies to versions: 10 and higher

Taxes [Legacy]

Episerver Commerce includes a tax subsystem, incorporated into checkout workflows, that calculates a cart's totals, including discounts, shipping costs, and pricing applicable to a particular customer. The tax subsystem, configurable via Commerce Manager, lets you set rates for different locations and SKU tax category.

Classes in this topic are available in the following namespaces:

  • Mediachase.Commerce.Workflow.Activities.Cart. Contains CalculateTaxActivity.
  • Mediachase.Commerce.Workflow. Contains CartPrepareWorkflow.
  • Mediachase.Commerce.Catalog.Managers. Contains CatalogTaxManager.
  • Mediachase.Commerce.Orders. Contains OrderContext.

Key classes and files

  • CartPrepareWorkflow.xml. Executed after shipping addresses are provided. It includes a cart's tax calculation and other checkout activities.
  • CalculateTaxActivity.cs. Part of the CartPrepareWorkflow workflow, performs a cart's tax calculations.
  • CatalogTaxManager.cs. Provides tax category information for SKUs.
  • OrderContext.cs. Provides the GetTaxes() method to return the tax rate for items based on tax category and jurisdiction.

How it works

During checkout, Microsoft Workflow Foundation workflows calculate the cart total. One workflow, CartPrepare, is run prior to rendering the page where customers confirm their order.

This workflow performs these tasks:

  • Determines whether the item is still available, based on the Active status and the item's start and end dates. If the items is not available, it is removed from the cart and an error message is returned and displayed in the CartView.ascx.
  • Determines whether the cart items are still available based on remaining stock in inventory, reserved inventory stock, and whether backordering is permitted. If not available, the items are removed and an error message is returned.
  • The price for each cart item, based on tiered pricing. If pricing has changed, a message regarding the change is returned, which can be displayed to the user.
  • Calculates the extended price for each cart item, for instance multiplying the price by the quantity purchased.
  • Calculates discounts.
  • Splits line items into their respective shipments, if multiple shipments are created.
  • Adds shipping costs to the cart.
  • Adds applicable taxes to the cart.

The last action, adding applicable taxes, is done inside the CalculateTaxActivity activity.

Example: the CalculateTaxActivity activity

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using Mediachase.Commerce.Orders;
using System.Collections.Generic;
using System.Data;
using Mediachase.Commerce.Catalog;
using Mediachase.Commerce.Catalog.Dto;
using Mediachase.Commerce.Orders.Managers;
using System.Threading;
using Mediachase.Commerce.Catalog.Managers;

namespace Mediachase.Commerce.Workflow.Activities.Cart
{
	public partial class CalculateTaxActivity: Activity
	{
		public static DependencyProperty OrderGroupProperty = DependencyProperty.Register("OrderGroup", typeof(OrderGroup), typeof(CalculateTaxActivity));

		/// <summary>
		/// Gets or sets the order group.
		/// </summary>
		/// <value>The order group.</value>
		[ValidationOption(ValidationOption.Required)]
		[BrowsableAttribute(true)]
		public OrderGroup OrderGroup
		{
			get
			{
				return (OrderGroup)(base.GetValue(CalculateTaxActivity.OrderGroupProperty));
			}
			set
			{
				base.SetValue(CalculateTaxActivity.OrderGroupProperty, value);
			}
		}

		/// <summary>
		/// Initializes a new instance of the <see cref="CalculateTaxActivity"/> class.
		/// </summary>
		public CalculateTaxActivity()
		{
			InitializeComponent();
		}

		/// <summary>
		/// Called by the workflow runtime to execute an activity.
		/// </summary>
		/// <param name="executionContext">The <see cref="T:System.Workflow.ComponentModel.ActivityExecutionContext"/> to associate with this <see cref="T:System.Workflow.ComponentModel.Activity"/> and execution.</param>
		/// <returns>
		/// The <see cref="T:System.Workflow.ComponentModel.ActivityExecutionStatus"/> of the run task, which determines whether the activity remains in the executing state, or transitions to the closed state.
		/// </returns>
		protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
		{
			try
			{
				// Validate the properties at runtime
				this.ValidateRuntime();

				// Calculate tax
				this.CalculateTaxes();

				// Retun the closed status indicating that this activity is complete.
				return ActivityExecutionStatus.Closed;
			}
			catch
			{
				// An unhandled exception occured.  Throw it back to the WorkflowRuntime.
				throw;
			}
		}

		/// <summary>
		/// Calculates sale and shipping taxes.
		/// </summary>
		private void CalculateTaxes()
		{
			// Get the property, since it is expensive process, make sure to get it once
			OrderGroup order = OrderGroup;

			foreach (OrderForm form in order.OrderForms)
			{
				decimal totalTaxes = 0;
				foreach (Shipment shipment in form.Shipments)
				{
					var shippingTax = 0m;
					decimal shippingCost = shipment.ShipmentTotal - shipment.ShippingDiscountAmount;

					// Calculate sales and shipping taxes per items
					foreach (LineItem item in Shipment.GetShipmentLineItems(shipment))
					{
						// Try getting an address
						OrderAddress address = GetAddressByName(form, shipment.ShippingAddressId);
						if (address != null) // no taxes if there is no address
						{
							// Try getting an entry
							CatalogEntryDto entryDto = CatalogContext.Current.GetCatalogEntryDto(item.CatalogEntryId, new CatalogEntryResponseGroup(CatalogEntryResponseGroup.ResponseGroup.CatalogEntryFull));
							if (entryDto.CatalogEntry.Count > 0) // no entry, no tax category, no tax
							{
								CatalogEntryDto.VariationRow[] variationRows = entryDto.CatalogEntry[0].GetVariationRows();
								if (variationRows.Length > 0)
								{
									string taxCategory = CatalogTaxManager.GetTaxCategoryNameById(variationRows[0].TaxCategoryId);
									TaxValue[] taxes = OrderContext.Current.GetTaxes(Guid.Empty, taxCategory, Thread.CurrentThread.CurrentCulture.Name, address.CountryCode, address.State, address.PostalCode, address.RegionCode, String.Empty, address.City);

									if (taxes.Length > 0)
									{
										var quantity = Shipment.GetLineItemQuantity(shipment, item.LineItemId);
										var itemsPricesExcTax = item.PlacedPrice * quantity - (item.OrderLevelDiscountAmount + item.LineItemDiscountAmount);
										foreach (TaxValue tax in taxes)
										{
											if(tax.TaxType == TaxType.SalesTax)
											{
												// Calculate Sale Taxes
												totalTaxes += itemsPricesExcTax * ((decimal)tax.Percentage / 100);
											}
											else if (tax.TaxType == TaxType.ShippingTax)
											{
												// Calculate Shipping Taxes
												var lineItemShippingTax = (itemsPricesExcTax / shipment.SubTotal) * shippingCost * ((decimal)tax.Percentage / 100);
												shippingTax += lineItemShippingTax;
												totalTaxes += lineItemShippingTax;
											}
										}
									}
								}
							}
						}
					}

					shipment.ShippingTax = Math.Round(shippingTax, 2);
				}

				form.TaxTotal = totalTaxes;
			}
		}

		/// <summary>
		/// Gets the name of the address by name.
		/// </summary>
		/// <param name="form">The form.</param>
		/// <param name="name">The name.</param>
		/// <returns></returns>
		private OrderAddress GetAddressByName(OrderForm form, string name)
		{
			foreach (OrderAddress address in form.Parent.OrderAddresses)
			{
				if (address.Name.Equals(name))
					return address;
			}

			return null;
		}
	}
}

Tax calculations are performed in the CalculateTaxes method. For each shipment in a cart, applicable taxes are applied to the Orderform of the cart, based on whether the shipping address is in a jurisdiction with taxes and its tax category. You can apply multiple tax rates (for example, a state and city tax).

The CalculateTaxes method ultimately calls theTaxManager.GetTaxes() method, which executes a stored procedure called ecf_GetTaxes. This procedure retrieves the rows of data from the Tax table where shipping address properties (for instance country or state) and tax category match a tax entry's properties and tax category, and where a shipping address property is null in the shipping address or tax rate rows.

Tax rates are based on jurisdiction parameters (which designate a physical region) and SKU tax categories.

The parameters are:

  • Country
  • State
  • ZipPostalCodeStart
  • ZipPostalCodeEnd
  • District
  • County
  • City
  • TaxCategory

The stored procedure only matches tax rates for a shipping address, where the property of the rate matches the properties of the shipping address. Null/empty values do not prevent a match. For example, if a tax rate for the State of New Jersey had the following settings:

  • Country = US
  • State = NJ
  • ZipPostalCodeStart = empty
  • ZipPostalCodeEnd = empty
  • District = empty
  • County = empty
  • City = empty
  • TaxCategory = empty

This tax rate matches all shipping addresses going to New Jersey, US.

Here is another scenario:

  • Country = US
  • State = CO
  • ZipPostalCodeStart = 80101
  • ZipPostalCodeEnd = 80113
  • District = empty
  • County = empty
  • City = empty
  • TaxCategory = empty

This tax rate only applies to shipping addresses in Colorado, US with zip codes ranging between 80101 and 80113. It does not apply to the zip code 80115, for example. If the zip code range is invalid for the State of Colorado, it does not apply to any shipping addresses.

Conversely, you can apply a tax rate for all shipping addresses in UK for products with a tax category of "Soda" like this:

  • Country = UK
  • State = empty
  • ZipPostalCodeStart = empty
  • ZipPostalCodeEnd = empty
  • District = empty
  • County = empty
  • City = empty
  • TaxCategory = Soda

Configuring a tax system

To set up tax rates, add or import them through Commerce Manager or directly into the Tax database table. For more information about configuring tax rates and jurisdictions, see Tax Configuration in the Episerver Commerce User Guide.

Customizing taxes

To customize tax calculations, create a workflow that mirrors CartPrepareWorkflow, but substitute the CalculateTaxActivity activity with your own implementation.

Your implementation can access any internal or external tax calculation service. For the activity to work properly with other activities that calculate cart totals, your activity must set the TotalTax property of each OrderForm in the cart.

Related topic

Comments