November Happy Hour will be moved to Thursday December 5th.
November Happy Hour will be moved to Thursday December 5th.
Note: This section explains how to work with tax calculations using the older APIs. Episerver recommends using the abstraction APIs to manage tax calculations, as described in Tax calculator.
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:
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:
The last action, adding applicable taxes, is done inside 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 the TaxManager.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:
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 matches all shipping addresses going to New Jersey, US.
Here is another scenario:
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:
To set up tax rates, add or import them through Commerce Manager or use the TaxImportExport class. For more information about configuring tax rates and jurisdictions, see Configuring Taxes in the Commerce User Guide.
Example: using TaxImportExport class to import Tax rates.
using Mediachase.Commerce.Orders.ImportExport;
public void ImportTaxes()
{
var taxImportExport= new TaxImportExport();
var csvFilePath = "[the file path of imported csv file]";
taxImportExport.Import(csvFilePath, ',');
}
The format of csv file is described below. Download a sample csv file.Order | Column name | Description |
1 | JurisdictionName | The jurisdiction name |
2 | StateProvinceCode | The state or province code |
3 | CountryCode | The country code |
4 | ZipPostalCodeStart | The zip/postal code start |
5 | ZipPostalCodeEnd | The zip/postal code end |
6 | City | The city |
7 | District | The district |
8 | County | The county |
9 | GeoCode | The geo code |
10 | JurisdictionCode | The jurisdiction code |
11 | JurisdictionGroupName | The jurisdiction group name |
12 | JurisdictionGroupCode | The jurisdiction group code |
13 | TaxNativeName | The tax native name |
14 | TaxName | The tax name |
15 | SortOrder | The sort order |
16 | LanguageCode | The language code |
17 | TaxCategory | The tax category |
18 | Percentage | The percentage |
19 | EffectiveDate | The effective date |
20 | TaxType | The tax type: SalesTax or ShippingTax |
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.
Last updated: Apr 01, 2021