You are here: Order System > Checkout Process > Taxes

Taxes

Overview

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

Key Classes And Files

CartPrepareWorkflow.xoml - Workflow that is 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:

The last action listed here, adding applicable taxes, is done inside the CalculateTaxActivity activity. Here is the code for the 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 sale tax
                this.CalculateSaleTaxes();
 
                // 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 the sale taxes.
        /// </summary>
        private void CalculateSaleTaxes()
        {
            // 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)
                {
                    List<LineItem> items = Shipment.GetShipmentLineItems(shipment);
 
                    // Calculate sales and shipping taxes per items
                    foreach (LineItem item in items)
                    {
                        // 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)
                                    {
                                        foreach (TaxValue tax in taxes)
                                        {
                                            if(tax.TaxType == TaxType.SalesTax)
                                                totalTaxes += item.ExtendedPrice * ((decimal)tax.Percentage / 100);
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
 
                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;
        }
 
        /// <summary>
        /// Validates the runtime.
        /// </summary>
        /// <returns></returns>
        private bool ValidateRuntime()
        {
            // Create a new collection for storing the validation errors
            ValidationErrorCollection validationErrors = new ValidationErrorCollection();
 
            // Validate the Order Properties
            this.ValidateOrderProperties(validationErrors);
 
            // Raise an exception if we have ValidationErrors
            if (validationErrors.HasErrors)
            {
                string validationErrorsMessage = String.Empty;
 
                foreach (ValidationError error in validationErrors)
                {
                    validationErrorsMessage +=
                        string.Format("Validation Error:  Number {0} - '{1}' \n",
                        error.ErrorNumber, error.ErrorText);
                }
 
                // Throw a new exception with the validation errors.
                throw new WorkflowValidationFailedException(validationErrorsMessage, validationErrors);
 
            }
 
 
            // If we made it this far, then the data must be valid.
            return true;
        }
 
        private void ValidateOrderProperties(ValidationErrorCollection validationErrors)
        {
            // Validate the To property
            if (this.OrderGroup == null)
            {
                ValidationError validationError = ValidationError.GetNotSetValidationError(CalculateTaxActivity.OrderGroupProperty.Name);
                validationErrors.Add(validationError);
            }
        }
 
    }
}

 

The tax calculations are performed in the CalculateSaleTaxes method. For each shipment in a cart, applicable taxes are applied to the Cart's Orderform 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 CalculateSalesTaxes method ultimately calls the TaxManager.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 (e.g. 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. 

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, go to Tax Configuration in the EPiServer Commerce User Guide.

Tax rates are setup based on jurisdiction parameters (which designate a physical region) and SKU tax categories. All of the parameters are:

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:

This tax rate will match with all shipping addresses going to New Jersey, US.

Here's another scenario :

This tax rate only applies to shipping addresses in Colorado, US with zip codes ranging between 80101 and 80113; it won't apply to the zip code 80115, for example. If the zip code range is invalid for the State of Colorado, it simply won't 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:

More information about workflows can be found here: Shopping Cart,  Customizing Order Workflow

Configuring Tax System

A guide to configuring the built-in tax subsystem can be found under Tax Configurationin 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.

For more information on how to customize checkout workflows, go here : Customizing Order Processing Workflow.

Namespaces

Mediachase.Commerce.Workflow.Activities.Cart - Contains CalculateTaxActivity

Mediachase.Commerce.Workflow - Contains CartPrepareWorkflow

Mediachase.Commerce.Catalog.Managers - Contains CatalogTaxManager

Mediachase.Commerce.Orders - Contains OrderContext

 


Version: EPiServer Commerce 1 R2 SP2| Last updated: 2012-06-29 | Copyright © EPiServer AB | Send feedback to us