Don't miss out Virtual Happy Hour this Friday (April 26).

Try our conversational search powered by Generative AI!

Loading...
ARCHIVED This content is retired and no longer maintained. See the latest version here.

Recommended reading 

EPiServer Commerce includes a tax subsystem that is incorporated into the checkout workflows which calculate the totals, including discounts, shipping costs, and the pricing applicable to a particular customer, for a cart. The tax subsystem is configurable via Commerce Manager and allows rates to be set for different locations and SKU tax category.

Classes referred to here are available in the following namespaces:

Key classes and files

  • CartPrepareWorkflow.xoml- executed after shipping address(es) are provided. It includes calculation of taxes for a cart as well as other checkout activities (see below).
  • CalculateTaxActivity.cs- part of the CartPrepareWorkflow workflow, performs the tax calculations for a cart.
  • CatalogTaxManager.cs- provides tax category information for SKUs.
  • OrderContext.cs - provides method, GetTaxes(), to return the tax rate for items based on tax category and jurisdiction.

How it works

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

This workflow performs the following tasks:

  • Determines whether the item is still available, based on the Active status and the start and end dates associated with each item. If they are not available, they are removed from the cart and an error message is returned which is displayed in the CartView.ascx.
  • Determines whether the items in the cart are still available based on remaining stock in inventory, reserved inventory stock, and whether backordering is permitted. If they are not available, they are removed and an error message is returned.
  • The price for each item in the cart, 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 an item in the cart, for instance multiplying the price by the quantity purchased.
  • Calculates the discounts that apply to the items in the cart.
  • Splits the 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 listed here, adding applicable taxes, is done inside the CalculateTaxActivity activity.

Example: the CalculateTaxActivity activity

C#
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;
		}
	}
}

The 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 based on its tax category. Multiple tax rates can be applied (as might occur in the instance of a state and city tax).

The CalculateTaxes method ultimately calls theTaxManager.GetTaxes() method, which executes a stored procedure called ecf_GetTaxes. This stored procedure retrieves the rows of data from the Tax table where either shipping address properties (for instance country or state), and tax category matches 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 setup 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 will match with 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 will not apply to the zip code 80115, for example. If the zip code range is invalid for the State of Colorado, it simply will 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 setup tax rates, they need to be added/imported through Commerce Manager or directly into the Tax database table. For more information about configuring tax rates and jurisdictions, refer to Tax Configuration in the EPiServer Commerce User Guide.

Customizing taxes

If you wish to customize the tax calculations, you will need to create your own workflow which mirrors the CartPrepareWorkflow workflow, but substitutes the CalculateTaxActivity activity with your own implementation.

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

Refer to Customizing Order Processing Workflow for more information.

Do you find this information helpful? Please log in to provide feedback.

Last updated: Oct 21, 2014

Recommended reading